端侧模型选型:好模型为什么上不了手机
畅译里没有一个模型是因为「它最准」被选中的。
手机上跑的语音模型,是怎么挑出来的?
准确率只是入场券。真正决定用哪个的是包体积、内存峰值、加载耗时,以及它能不能和已有的运行时共存。畅译现在装着识别、标点、说话人分离三类模型,每加一个都要从同一个预算里扣。
- 光标点恢复模型 int8 量化后就有约 70MB,和 App 一起打包
- 说话人分离的调研结论推荐了另一个方案,最后没有采用
- 中英混说的录音里,英文块会跳过中文精修引擎,因为精修反而会把对的改错
- 模型加载失败必须能降级继续用,不能让 App 崩
推荐方案最后没用
做说话人分离时我先查了一圈,结论是苹果到 iOS 26 都没有原生 API,得自己引模型。当时调研写下的推荐是 FluidAudio,一个 Swift SDK,模型转成 CoreML 跑在神经网络引擎上,三十多兆,实时倍速能到 8 到 15 倍。指标很好看。
最后没用它。畅译的语音识别已经跑在 sherpa-onnx 上,如果说话人分离走 CoreML,App 里就会同时存在两套模型格式、两套加载和回退逻辑、两份资产包下载路径。这些东西平时看不出成本,出问题时会翻倍:一个模型加载失败的 bug,你要在两条完全不同的链路上各查一遍。
sherpa-onnx 自己带了离线说话人分离,换上 pyannote 和 CAM++ 的模型就能跑。识别、分离、声纹提取共用同一个运行时和同一套模型路径解析。单看性能指标它未必赢,但整个 App 只有一处需要维护。
两条引擎并行跑,按块决定信谁
录音时有两条识别链路同时在跑。流式 zipformer 负责实时出字,是双语模型;录完之后 SenseVoice 会对每一块重新识别一遍,做精修覆盖。SenseVoice 是离线全上下文,中文的同音字、成语、专名、数字规范化都明显更好。
问题出在英文上。SenseVoice 以中文为主,英文很差,实测里它能把一句完整的英文覆盖成半中半英的乱码。精修这一步在英文块上是负收益,粗稿本来是对的,精修完反而错了。
现在的做法是按块判断英文是否强主导,判定条件是拉丁字母数超过汉字数的三倍。达到这个条件的块直接采用流式结果,跳过精修。三倍这个数不是随手定的,中文句子里夹几个英文单词很常见,如果按过半来判,那些块会被误判成英文块,丢掉本来该有的中文精修。
加载失败不能是致命的
模型文件的查找有三级回退:先找按需下载的资产包,找不到就找 App 包里的子目录,再找不到就找包根目录。用户可能在资产包还没下完时就点了录音,也可能下载中断留下半个文件。
早期识别器创建失败时是直接 fatalError。当时的想法是模型都没有了程序也没法继续,不如崩得干脆。实际上调用方那一层本来就写好了降级分支,检测到创建失败会设一个模型加载失败的提示,让用户知道要重新下载。但那个分支永远等不到执行,因为进程已经在上一行没了。
改成可失败的初始化之后,模型缺失变成了一条提示,录音功能降级为只录不转,用户至少不会丢掉正在录的这一场。同样的处理还有热词:热词流创建失败时退化成普通的识别流,不值得为一个辅助功能崩掉整个识别会话。
能不能不加模型,先把输入弄干净
识别效果差的时候,第一反应总是换个更大的模型。但有一次的答案是输入本身就不对。
畅译早期的采集路径把麦克风输入重采样到 16k 之后什么都不做就送进识别器,没有高通滤波,没有增益控制,没有响度归一化。代码里留着当时的诊断记录,一条写着要很大声才录得到,另一条记着 RMS 大约 0.001,也就是采到的信号幅度只有满量程的千分之一。会议室里手机放在桌子中间,离说话人两三米,这个量级的信号喂给任何模型都不会好。
当时讨论过三个方向。只调采集端参数最省事,但治不了远场的音量方差和低频噪声,而且导入的音频文件根本走不到采集端,这条路对导入路径零收益。只做离线的响度归一化也不行,会议现场的实时转写拿不到任何改善,而且现场看到的文本和最后精修出来的会对不上,用户会觉得是转写变差了。
最后加的是一个共享的流式增强器,插在两条识别入口前面,高通滤波加自适应增益加软限幅。这里有个决定我一直觉得是对的:增强只作用于喂给识别器的样本,落盘的录音文件保持原始。录音是证据,回放要和当时听到的一致,不能因为算法觉得该响一点就把它改了。