2026-08-19 语音输入法复盘:生僻字纠正、用量统计与欠费排查
背景与目标
豆包语音输入法(Python + 火山引擎 SAUC 大模型语音识别 WebSocket 协议,Windows 托盘 exe + Linux 版)今天集中处理四件事:
- 用户加了热词「谌朴」、也点了重新加载,识别结果仍然是「陈朴」,要查原因;
- 加本地替换纠正功能,并支持手动收录错词(热词 / 替换纠正二选一);
- 账户出现一毛钱欠费,用户怀疑与当天的热词改动有关,要排查;
- Linux 版曾录出 4 小时的卡死录音,要把交互机制改掉。
本期工作清单(全景)
| 工作线 | 处理 |
|---|---|
| 热词「谌朴」不生效诊断 | 详写 |
| 本地替换纠正 + 收录错词弹窗 | 详写 |
| 重新打包 exe(进程占用坑) | 略写 |
| 历史用量统计 | 详写 |
| 欠费成因排查(计费规则查证) | 详写 |
| Linux 按住说话改造 | 详写 |
| 资源包购买指引 | 一句话带过 |
过程回顾
1. 热词不生效?先看日志再下结论
用户原话:「我添加了热词,然后尝试语言输入'谌朴',依然无法准确识别?我重新加载过热词的?」
查 dist/voice_input.log 发现三条硬证据:
- 日志有「已重新加载 121 个热词」,重载机制没坏;
- 「谌朴」加的是
dist/hotwords.txt(exe 同目录那份),正是程序读取的文件; - 加热词前识别结果是「陈普」,之后变成「陈朴」:朴字被热词纠正过来了,热词确实传到了服务端并生效。
再对照火山引擎官方协议文档,corpus.context 热词直传的 JSON 格式与代码完全一致。结论:热词机制是加权而非强制替换,「谌」是极生僻姓氏字,语言模型对高频姓「陈」的先验太强,加权压不过去。这不是配置问题,是机制天花板。
2. 本地替换纠正 + 收录弹窗
用户原话:「按本地替换处理,顺便再优化下功能,如果我发现有些词经常识别错误,可以手动提交收录到本地热词,或者替换纠正两个选择的功能。」
实现:
- 新建
corrector.py:加载corrections.txt(每行错误词=正确词,长词优先),识别完成后强制替换;去重追加;tkinter 收录弹窗。 main.py:识别结果先过纠正表再注入;托盘新增「收录识别错误的词…」,弹窗自动预填最近一次识别原文,两个按钮「加入热词」(写 hotwords.txt)和「加入替换纠正」(写 corrections.txt),立即生效。- 预置两条:
陈朴=谌朴、陈普=谌朴。
验证方式值得一提:Windows 控制台是 GBK,直接 print 中文全是乱码,改用断言验证替换结果,一次通过。
3. 打包坑:exe 被运行中的进程占用
PyInstaller 最后一步报 PermissionError: [WinError 5],原因是旧版托盘程序正在运行,exe 写不进去。taskkill 结束两个进程后重建成功,启动冒烟测试,日志确认「已加载 121 个热词」「已加载 2 条替换纠正」。
4. 用量统计:5.4 小时、1600 次
awk 统计三份日志(桌面 + 项目根目录 + dist):
- 桌面主日志:1486 次,5.16 小时;
- 项目根目录日志里有一条 14810 秒(4.11 小时)的卡死录音,占历史总用量将近一半,真实使用约 0.23 小时;
- 合计真实用量 ≈ 5.4 小时。
5. 欠费排查:相关性不等于因果
用户原话:「肯定跟我们这个热词的调整或者是这个操作有关系。」
这个判断有事实基础——日志显示热词功能确实是当天 18:51 才第一次启用,欠费也当天出现。但逐条查证后结论是否定:
- 官方计费文档:流式语音识别只按语音时长计费,费用构成里没有热词相关计费项;热词只是请求头里几百字节 JSON,不是音频;
- 新增的替换纠正是纯本地处理,不碰 API;
- 当天总用量只有约 4.5 分钟,按后付费 1 元/小时算正好一毛多,与欠费金额吻合;
- 免费试用额度是 20 小时 / 半年,历史真实用量 5.4 小时远没用尽。
结论:欠费与代码改动无关,最可能是试用额度到期或控制台变更导致后付费生效。建议用户看账单详情里的计费项验证。用户随后拍板:「这个先不纠结,反正我用两天看一下这个后台的账单还会不会出来。」接受观察方案,这事到此打住。
顺带算出真实成本:2.0 模型后付费 1 元/小时,用户一个月 4~5 小时 ≈ 四五块钱;资源包 30 小时 28 元,折算单价差不多,这个用量买不买都行。
6. Linux 卡死录音修复:toggle 改按住
根因:Linux 版原是「按一下 F10 开始、再按一下结束」的切换模式,4 小时卡死录音就是结束那一下没按。
用户原话:「我按住 F4 开始说话,松开就结束发送,跟那个按空格是一样的这个机制。改成这个机制就不会浪费时间了。」
改造 hotkey.py:Linux 换成按住模式,首次按下开始(自动重复只算第一次),松开即结束。物理上不可能再「忘了按结束」。用模拟事件测了状态机,包括一个边界情况:录音中途在托盘停用再松开,依然正常结束、不会卡死。配置文件热键 f10 → f4。Linux 包需在 Kali 上跑 build_linux.sh 重新构建。
关键踩坑
- 热词不是万能的:热词直传只是候选加权,生僻字对抗高频先验时拉不动;确定性的错词要用「替换纠正」(本地替换或官方自学习平台 correct_table)。
- GBK 控制台乱码误导调试:print 验证不可靠,用断言验证内容本身。
- awk 字符串比较陷阱:
"7.20s" > 60按字典序为真(「7」>「6」),过滤条件里$2带单位字符时必须$2+0强制转数值,否则分组统计全错。 - 打包前确认旧进程退出:Windows 上运行中的 exe 文件被锁定,PyInstaller 最后一步才报错,白等一次构建。
最终方案(核心代码)
替换纠正核心:
def apply_corrections(text: str, pairs: list) -> str:
for wrong, right in pairs: # pairs 已按错误词长度降序
text = text.replace(wrong, right)
return text
Linux 按住模式状态机:
def _on_press_hold(self, event):
if self._held: # 自动重复,忽略
return
self._held = True
if not self._enabled:
return
self._holding = True
self.on_hold_start()
def _on_release_hold(self, event):
if not self._held:
return
self._held = False
if self._holding:
self._holding = False
self.on_hold_end()
做得好的地方
- 凡事先要证据:识别结果前后对比(陈普→陈朴)直接证明热词已生效,避免了「重新加载是不是没生效」的空转排查;
- 官方文档当裁判:协议格式、计费规则都查文档原文,不靠记忆和猜测;
- 敢于纠正用户的因果判断:用户认定欠费与热词改动有关,用「计费项只有语音时长 + 当天 4.5 分钟 ≈ 1 毛」两条证据反驳,同时承认时间相关性确实存在,不硬顶也不迎合;
- 顺藤摸瓜找到真凶:欠费讨论中牵出 4 小时卡死录音,直接促成 Linux 交互机制修复,消除了一个会持续烧钱(后付费时代 4 小时 ≈ 4 元)的隐患。
复盘与改进
- 收录弹窗的 tkinter 交互只做了逻辑验证,没在真实托盘环境里人工点过一遍,下次发版前应手动过一遍完整路径;
- Linux 改造无法在本机实测,依赖用户在 Kali 上重建验证,状态机测试覆盖了主要分支但没有真机事件;
- 欠费根因仍是推断(额度到期/控制台变更),等用户观察两天账单后才能最终确认,这次把验证方法(看账单计费项和时长)交代清楚了,留了验证出口,但不算完全跑完;
- 历史日志分散在三处(桌面、项目根目录、dist),统计口径容易漏,以后日志路径应统一。
素材点
- 「热词只是加权不是替换」:一个生僻姓氏字「谌」让语音识别热词机制现了原形,最后靠本地强制替换表解决;
- 一毛钱欠费引发的排查:用户笃定是新功能惹的祸,证据链算下来是 4.5 分钟 × 1 元/小时,相关性不等于因果的典型样本;
- 忘记按停止键,一段录音录了 14810 秒(4.11 小时),占历史总用量近一半——免费时代是笑话,付费时代是真金白银;
- 39 岁项目部征迁岗自己攒的语音输入法:121 个热词全是村名、互通枢纽、征迁术语,垂直场景的 AI 工具改造样本;
- awk 统计日志翻车:「7.20s」> 60 按字典序成立,一个 +0 解决,命令行统计的经典坑。
