跳到主要内容

创建日期:2026-10-13 | 最近更新:2026-10-13 事实核对基于 sherpa-onnx 1.13.8;实测素材是一期 12 分钟的 Vue3 教程和一条 17 分钟的金融口播。文中所有数字均为本机实测,脚本与原始结果留档于 source/sherpa-onnx-subtitle-lab/。

sherpa-onnx 101(第 7 篇 · 词表工程):你那份词表是怎么攒出来的

第 3 篇讲了词表的机制——热词的词表格式、同音词替换的 FST、三个坑。但机制只回答了"能怎么改", 没回答那个更要命的问题:左边那一列"模型听成的样子",你是从哪儿知道的?

这篇讲的就是这件事。起因是一位读者拿真实的 Vue3 教程试了转写,反馈说 "还不如 faster-whisper,专业术语 Vue3 识别不出"——而我把五个模型跑在同一期教程上之后发现: 五个模型全都没认对 Vue3,只是错得各不相同。

模型Vue3 被写成setup 被写成
SenseVoiceVI三SET UP(14 处)
faster-whisper large-v3Viu3 / View3setup ✓ / SiteUp(6 处)
sherpa-onnx Whisper large-v3View3setup ✓
Qwen3-ASRVue 三(只差数字格式)setup ✓

所以"换个模型"解决不了这件事——换个模型只是换一种错法。要收尾,只能靠词表; 而词表要能维护,就不能靠人肉逐条对。这篇讲怎么把它挖出来。

0. 先给结论​

#结论证据
1ASR 的错是长尾,而且因模型而异。同一个 Vue3,五个模型给出五种错法,没有一种是对的§1
2拼音是把"听错的"和"想要的"连起来的桥——但只对中文有效:玄界芯片/玄戒芯片 拼音完全相同(1.0),而 Vue3 和 VI三 只有 0.22§2
3英文/代码术语靠形态启发 + 最近词猜词:TILE→title、TUP→setup、PI→API 都能自动猜出来§3
4多模型分歧是最被低估的一招:两份输出对齐后不一致的位置,高概率就是错处§3.4
5实测:自动挖出的词表命中 106 处,手工精修的那份是 98 处——效果相当,但省掉人工逐条扫§4.2
6术语词表是外科手术式的:把 Vue 词表套到金融视频上,0 命中、零误伤§4.3
7六个坑,每个都会让词表「看起来写了却不起作用」:正则的 \b 在中英混排失效、行内注释被当替换内容、模型的空格不稳定、短碎片钻进正确词、空右侧会删词、正则规则不排序;外加一条盖不住的——词被 VAD 段边界切开,跨段的错怎么挖都挖不到§5

1. 为什么非得有词表​

1.1 错是长尾的​

通用 ASR 模型是在通用语料上训的。你的领域词——Vue3、props、玄戒芯片、北向资金—— 在训练语料里要么没有,要么比例极低,模型只能按音猜一个最像的常见词。

拿那期 Vue3 教程数一下(764 秒、约 5600 字):SenseVoice 裸跑的错处里, 术语和英文碎片占了大多数,而日常中文几乎没有错。这不是模型差,是长尾。

1.2 错法因模型而异(所以"换模型"不是解)​

同一个 Vue3:

SenseVoice VI三 ← 大写字母 + 中文数字
faster-whisper Viu3 / View3 ← 像个英文单词,但不对
sherpa-onnx Whisper View3 ← 同上
Qwen3-ASR Vue 三 ← 认对了词,错在数字写法

setup 也一样:SenseVoice 写 SET UP(拆成两个大写词),faster-whisper 大部分写对、 偶尔写 SiteUp。

这张表最重要的一行是"错法各不相同":它意味着

  • 换模型 ≠ 解决问题,只是换一种错;
  • 词表没法通用——左边的"错法"列必须针对你实际用的那个模型写;
  • 但也意味着多跑几个模型有额外价值:它们的分歧点就是错处的路标(§3.4)。

2. 音:那座桥​

词表左边的"错法"不是随机的——ASR 的错误绝大多数是"音近"。所以判断 "模型写的这个是不是想写那个词",最有效的依据不是字形,是读音。

把汉字转成拼音再比,几组实测:

模型写的想要的拼音相似度是不是同一个词
玄界芯片玄戒芯片1.00是(同音字)
通过膨胀通货膨胀0.94是
完前不同完全不同0.92是
Vue3VI三0.22是,但拼音救不了

最后一行是关键:拼音只在纯中文场景有效。一旦掺进英文/代码术语,拼音就失效了—— Vue3 的拼音是 vue3,VI三 变成 visan,两者毫无关系。

所以工具里是两条腿:中文走拼音,英文/代码走形态与编辑距离。

为什么不用拼音直接生成词表? 因为"拼音相同但字不同"的同音字极多(是/事/市/式/试…), 全量替换会把正确的地方改坏。拼音只用来「发现候选」,不能用来「自动替换」——

拼音相同 → 候选规则 → 人工确认 → 进词表

3. 四种挖法​

工具是 scripts/09-mine-terms.py:喂给它模型输出 + 一份领域术语表(不需要精确, 把这条视频里可能出现的词列一遍就行),它吐出候选规则。

3.1 拿术语表反查(中文走拼音)​

对术语表里的每个词,在输出里滑动窗口取等长片段,算拼音相似度与编辑距离, 挑出"像它但不等它"的片段:

VI三=>Vue3 # conf=0.75 拼音
viu3=>Vue3 # conf=0.75 拼音
玄界=>玄戒 # conf=1.00 拼音

3.2 形态启发(英文走形状)​

代码术语被听错时,表面形态往往先崩掉。三类一眼可疑:

形态例子为什么会这样
全大写碎片SET UP、NAME、DATA、TILE中文模型的英文词表只有大写、没有子词
字母被空格拆开A PI、D IV、T UP逐字母解码时被切开
字母 + 中文数字VI三、VI二、VIO二数字被当成中文念

挖出碎片之后,脚本还会猜它本该是哪个词(按归一化编辑距离 + 拼音 + 子串关系):

TILE => title (0.80)
TUP => setup (1.00)
PI => API (1.00)
AG => age (1.00)
DV => div (0.67)

这一步的准确率高得超出预期——因为"TILE 是 title 掉了 i"这种事,机器算起来比人快。

3.3 歧义对齐(长句被听成两个样子)​

有些错不在词上,在整句。比如:

参照:长期会产生通货膨胀
模型:长期会一直通过膨胀

这种只能靠长片段对齐:把两份文本按句子切块、对齐、挑出"长度差不多但内容差很多"的块。 这类块通常就是错处,脚本只负责指路,具体怎么改要人看。

3.4 多模型分歧(最被低估的一招)​

同一段音频跑两个模型,它们不一致的地方,几乎就是错的地方。

理由很朴素:两个模型独立解码,如果都对,它们会一致;如果都错,往往错得不一样。 所以"分歧"是一个免费的、不需要参照稿的错处探测器。

实测那期 Vue3 教程、三份输出的分歧点:

SenseVoice : 如果之前有接触过 VI 二的同学呢就是你写过那些选项式 API
Qwen3-ASR : 那从这 1 小节开始呢咱们就来正式的学 Vue3 里边的组合式 API

一眼就看出来谁对。这招的价值在于:它不需要任何先验知识——不用术语表、不用参照稿, 只要两个模型就行。做视频转写的人手上通常不缺模型。

3.5 效果:自动挖 vs 手工写​

我那期 Vue3 的词表是手工攒的(逐条比对模型输出和术语,写了 16 条)。 用工具重挖一遍,拿手工那份当标准答案量一下:

指标数值
手工规则被自动挖到13 / 15 = 87%
自动产出的候选总数45 条
其中需人工剔除29 条
替换命中数自动 106 处 vs 手工 98 处

召回 87%、产出比手工还全——因为工具不会漏看。代价是候选人多了三倍, 但"从 45 条里剔掉 29 条"比"从 5600 字里找出 16 条错"轻松得多。

漏掉的那两条也值得记:D IV、VUE——前者是我自己的正则用了 \b(见 §5.1), 后者是脚本猜词阈值卡掉的。工具不完美,但它是起点。

4. 词表写出来之后​

4.1 左边的"错法"必须写成原文形态​

替换是在原文上做字面匹配的。所以规则左边必须写模型输出的原文:

D IV=>div ✅ 原文长这样
div=>div ❌ 归一化之后的样子,替换时永远匹配不到

我第一版工具就是拿归一化后的形式去重的,写出来的文件一条都不会生效。

4.2 短碎片会钻进正确词里​

PI=>API 这条规则看起来很合理(PI 是 API 被吃掉首字母),但它会钻进已经正确的 API:

组合式 API --[PI=>API]--> 组合式 AAPI ← 越修越错

修法:左边是某个正确词的子串时,必须加英文边界,写成正则:

re:(?<![A-Za-z])PI(?![A-Za-z])=>API

工具现在会自动检测这种"子串风险",并把这类规则输出成正则,还在注释里标出会误伤谁:

re:(?<![A-Za-z])PI\s*(?![A-Za-z])=>API # 6× 全大写碎片(原字面会误伤 ['api'])

4.3 误伤验证:术语词表是外科手术式的​

写词表最怕的是"改了不该改的"。一个简单的自检:把词表套到内容完全不同的视频上, 看它命中几处。

实测:拿 Vue3 那份词表去套 17 分钟的金融口播——

$ python 05-make-srt.py 金融.wav --terms terms/vue3-mined.txt
terms_replaced: 0

0 命中。 因为规则左边写的是"模型在这条视频上听成的具体样子",它足够具体, 不会在别的领域乱开火。(反过来,如果你写了 的=>得 这种通用规则,那就会到处开火—— 规则的左边越具体越安全。)

4.4 一份视频一份,还是一份通用?​

做法适合代价
一份视频一份系列教程、同一讲者的口播每期要花 10~20 分钟挖 + 校对
一份领域通用长期做同一领域(金融、编程)攒起来慢,但复用率高;要防误伤(§4.3)

实践上是两级:先把通用领域词(Vue、props、北向资金)攒成一份底表, 再针对每条视频补"这条视频特有的错法"(VI三、SiteUp)。底表管"认不认识这个词", 视频表管"这次它错成了什么"。

5. 五个坑​

按"踩到时有多懵"排序——它们的共同点是:词表看起来写了,但一点作用都没有。

5.1 \b 在中英混排上不成立​

>>> re.search(r"\bD IV\b", "个D IV呢我就不要了")
None # 匹配不上!
>>> re.search(r"(?<![A-Za-z])D\s*IV(?![A-Za-z])", "个D IV呢我就不要了")
<re.Match ...> # 这样才对

原因:Python 3 的 \w 默认匹配 Unicode 词字符,中文汉字也算。所以英文词紧挨中文时, 两侧都没有 \b。规则里的边界一律写 (?<![A-Za-z]) / (?![A-Za-z])——只看英文字母。

这个坑我在同一天踩了两次:先写在词表里,后来写挖词工具的正则时又踩了一遍 (SPACED 模式匹配不到 D IV呢),漏掉了两条本来能挖到的规则。

5.2 行内注释会被当成替换内容​

词表里写注释很方便:

VI三=>Vue3 # 8× 字母+中文数字

但如果解析时不剥掉 # 后面的内容,替换结果会变成:

老师我是第一次接触这个Vue3 # 8× 字母+中文数字,

注释被原样灌进了字幕。而且更坏的是:后续规则还会继续破坏这段注入的注释 (setup # 2×空格拆字里面)。

修法:读词表时先 line.split("#", 1)[0].strip()。

5.3 模型的空格是不稳定的​

同一条音频,同一次输出,模型会时而写 VI三、时而写 VI 三:

好嘞各位那从这一小节开始呢咱们就来正式的学习 VI 三里边的这个组合式 A PI

我的规则写的是 VI三(没有空格),于是在这一句上完全失效——而这正是最需要修的那句。

修法:字面规则匹配不到时,自动退化成"允许中间夹空白"的正则再试一次 (\s{0,3} 连接每个字符)。这一条改动让替换数从 93 涨到 113。

5.4 右边为空的规则会把词删掉​

挖词工具经常猜不出碎片该是什么(ZIS、MESS),于是输出 ZIS=>。 这条规则一旦生效,会把所有 ZIS 删掉。

修法:工具把这类写成注释掉的 # TODO ZIS=>{正确的词},绝不输出可直接生效的空规则。

5.5 顺序:长规则必须先跑,正则也一样​

规则是按左边长度从长到短应用的。否则 Vue 会先把 Vue3 里的 Vue 换掉,剩下个 →3。

更隐蔽的是正则之间也要排。实测踩到的:

原文:…你是不能在 SEET UP 函数里边用这个 ZIS 关键字的
规则:UP=>setup(短) SEET\s*UP=>setup(长,人工补的变体)

短的 UP=>setup 先跑,把 SEET UP 里的 UP 吃掉 → 变成 SEET setup, 那条更长的规则再也没得匹配。结果是把一个错法变成了一个更怪的错法。

修法:正则规则同样按模式长度倒序排。这条和 §4.2 的短碎片问题是同一个根: 短规则会先开火,把长规则要用的东西吃掉。

5.6 词会被 VAD 段边界切开——跨段的错,词表管不到​

词表是逐段应用的,而 VAD 的段边界经常切在英文词中间。实测:

段 A:…你是不能在 SEET
段 B:UP 函数里边用这个 ZIS 关键字的

规则看到的是两段独立文本——SEET UP 这个错法在任一段里都不完整, 所以怎么挖都挖不到。更糟的是段 B 开头的 UP 会被 UP=>setup 命中, 拼出 SEET + setup 这种怪物。

两个缓解办法:

  1. 给边界加哨兵:应用规则时,如果相邻两段在边界上都是英文字母数字(说明词被切断了), 就在段首段尾塞一个字母,让 (?<![A-Za-z]) 在断口处不成立,规则就不会在断口上开火。 哨兵必须真的是字母——我第一版塞了 NUL,(?<![A-Za-z]) 照样成立,等于没加;
  2. 变体靠人工补:既然挖不到,就靠人看上下文写规则(SEET UP=>setup、 BEFORE\s*CR[A-Z]*=>beforeCreate)——同一个错法往往有好几种变体, 工具只会按出现次数挖「最常见的那一种」。

这类残留最后只剩 1 处(356 条字幕里),接受它比继续加工程更划算。知道边界在哪,比假装没有边界重要。

6. 给一条视频攒词表的完整流程​

1. 跑一遍裸模型 bash video2srt.sh 视频.mp4 -o 裸.srt
2. 列一份领域术语表 这条视频大概会说什么词,写 20~50 个,不用精确
3. 多跑一个模型(可选但值) 另一个模型的分歧点是最好的错处路标
4. 挖 python 09-mine-terms.py --hyp 裸.srt 另一份.srt \
--terms 术语表.txt --out 候选.txt
5. 过一遍候选 剔掉明显不对的(比如 VALUE=>Vue),补上 TODO 那批
6. 套上去看效果 bash video2srt.sh 视频.mp4 --terms 候选.txt
7. 自检误伤 套到别的视频上,应该 0 命中

第 4~5 步是唯一需要人的地方,也是这条流水线的价值所在:把"从 5600 字里找 16 个错" 变成"从 45 条候选里剔掉 29 条"。

工具在 source/sherpa-onnx-subtitle-lab/scripts/09-mine-terms.py, 那期 Vue3 的成品词表是 terms/vue3-mined.txt(自动挖)与 terms/vue3-sensevoice.txt(手工), 两份的产出逐条一致。术语表模板见 terms/vue3-vocab.txt。

7. 收尾​

把这一篇和前面几篇连起来看:

  • 第 3 篇说"词表能救术语",这篇说"词表本身也能被造出来";
  • 第 6 篇说"选对模型比换大模型划算",这篇补上后半句:选对模型之后,用词表收尾;
  • 而词表的左边一列,是模型的错误指纹——它记录了这个模型在这类内容上的具体失效方式。 攒过几条视频之后你会发现,同一个模型的错法是重复的(SenseVoice 总是把英文写成大写碎片、 总是把数字念成中文)。到那时,词表就从"每条视频一份"变成"一份管一年"。

那才是它真正的形态:不是补丁,是你对自己那套流水线的认识。