2026-08-19 给豆包语音输入法加上专属热词,顺手修了一个自己埋的配置坑
背景与目标
豆包语音输入法(按住空格说话、松开出字的 Windows 小工具,走火山引擎 seedasr WebSocket 协议)用了一段时间,整体好用,但专属词汇识别不准。用户原话:
"有些时候识别准确率方面不是很准确,有些专属的词汇可能没那么准确。这个专属词汇功能能不能加上去?"
技术栈:Python + websockets + 火山引擎 SAUC 二进制协议 + PyInstaller 打包。
过程回顾
第一步:先查官方文档,不猜协议字段
翻火山引擎大模型流式语音识别 API 文档,确认 request.corpus.context 支持热词直传,格式是 JSON 字符串:
"corpus": {"context": "{\"hotwords\":[{\"word\":\"热词1\"},{\"word\":\"热词2\"}]}"}
关键信息:本项目用的流式输入模式(bigmodel_nostream)热词直传上限 5000 个词,完全够用,不用去控制台配词表。这一步避免了凭印象写字段名的风险。
第二步:最小侵入实现
asr_client.py:构造 full client request 时,有热词才加corpus.context,没有就不加(保持原行为)main.py:新增load_hotwords(),合并config.json的hotwords数组和hotwords.txt文件(每行一词、#注释、自动去重);托盘菜单加「重新加载热词」,改词表不用重启- 新建
hotwords.txt模板,更新config.example.json和 README
自测三条全过:无热词不带 corpus 字段、有热词格式正确、数组合并文件去重正常。
第三步:打包与部署
PyInstaller 用现有 spec 重新打包(约 28MB),把用户编辑好的 hotwords.txt(119 个词,大量东阳本地地名——这类词正是通用 ASR 最容易错的)复制到 exe 同目录。
然后发现用户实际用的是桌面上的一份旧 exe 拷贝。用户自己提出了更优方案:
"还是我把 Dist 里面的 EXE 文件发送到桌面快捷方式?"
对,快捷方式指向 dist/ 里的 exe,热词文件固定在 dist/ 维护一份,以后重新打包桌面自动指向最新版。删旧拷贝、建快捷方式、验证 TargetPath,一次到位。
第四步:bug 来了——空格失效
用户反馈:
"好像有 bug,我在终端里面按空格键,无法触发语音输入了?"
排查过程很关键:先看日志,不猜。dist/voice_input.log 里一行直接破案:
已启动:按住【f10】说话,轻点仍为原按键。
根因:项目根目录 config.json 之前被改成 f10(适配 Linux),而打包 spec 会把这个配置内嵌进 exe。这次重新打包,就把 f10 带进了 Windows 版,空格自然失效。旧桌面 exe 内嵌的是老配置(space),所以之前一直正常。
中间还有个插曲:进程列表里看到两个同名进程,一度以为重复运行,后来确认是 PyInstaller onefile 的引导进程 + 主程序,正常现象。
用户随后拍板定方向:
"按 F10 的版本是适配 linux 版本的,你 Windows 版本另外区别开来。"
关键踩坑
- 坑 1:内嵌配置导致"按键漂移"。现象:重新打包后空格不触发。根因:spec 里
datas = [('config.json', '.')]把当前 config 打进 exe,而 config 不知何时被改成了 Linux 的 f10。解决:先用外挂dist/config.json(外部配置优先)应急恢复 space,再做正式的双平台分离。 - 坑 2:单配置服务两个平台。根因:Windows 要 space(按住说话 + 轻点穿透),Linux 要 f10(keyboard 库在 Linux 不支持拦截,只能切换模式)。一份配置必然打架。
最终方案
双平台配置分离:
config.json→ Windows 专用,热键space(也是 Windows exe 的内嵌默认)config.linux.json→ 新建,热键f10,Linux 专用;已加入.gitignore(含 API Key)build_linux.sh→ 打包前临时交换配置,trap EXIT保证打完恢复:
restore_config() {
if [ -f config.json.linuxbak ]; then mv -f config.json.linuxbak config.json; fi
}
trap restore_config EXIT
cp config.json config.json.linuxbak
cp config.linux.json config.json
pyinstaller --clean --noconfirm doubao-voice-input-linux.spec
- 删掉应急用的
dist/config.json,恢复单一配置来源,重新打包验证:日志显示「已加载 119 个热词」+「按住【space】说话」
做得好的地方
- 先查文档再动手:热词字段写法、5000 词上限都是从官方文档确认的,没靠猜
- 排查先看日志:一行日志直接定位根因,全程没有瞎改代码
- 采纳用户的方案:快捷方式替代 exe 拷贝是用户自己提的,比"复制新 exe 到桌面 + 桌面放热词文件"更干净,直接采纳
- 热词热加载:托盘菜单「重新加载热词」让调词表的迭代成本降到零
复盘与改进
- 打包即快照,内嵌配置是隐患。任何"打进去就看不见"的东西,改的时候不会有人记得它。这次如果一开始就把平台相关配置外置或分离,bug 不会发生。
- 应急修复要留收尾动作。外挂
dist/config.json当时是对的(不用等几分钟打包),但它本身又会成为下一个"配置漂移"源——事后必须删掉并重新打包闭环,这次做到了。 - 跨平台项目的配置要显式分平台。
config.linux.json+ 构建脚本临时交换,比"记得打包前改回来"可靠得多。 - 下次可以考虑:spec 里根据平台自动选配置文件,连交换脚本都省掉。
素材点
- "一行日志破案":空格失效 → 日志显示 f10 → 内嵌配置串台,典型的"先看日志再动手"教学案例
- PyInstaller 内嵌配置导致按键漂移:打包即快照,看不见的配置最危险
- 119 个东阳本土地名热词:通用 ASR 的死角正是个人词表的价值点(村名、街道名识别)
- 用户自己提出快捷方式方案:人机协作里"用户拍板、AI 执行"的好样本
- onefile 双进程虚惊:两个同名进程不是重复运行,是引导进程 + 主程序
