创建日期: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 被写成 |
|---|---|---|
| SenseVoice | VI三 | SET UP(14 处) |
| faster-whisper large-v3 | Viu3 / View3 | setup ✓ / SiteUp(6 处) |
| sherpa-onnx Whisper large-v3 | View3 | setup ✓ |
| Qwen3-ASR | Vue 三(只差数字格式) | setup ✓ |
所以"换个模型"解决不了这件事——换个模型只是换一种错法。要收尾,只能靠词表; 而词表要能维护,就不能靠人肉逐条对。这篇讲怎么把它挖出来。
0. 先给结论
| # | 结论 | 证据 |
|---|---|---|
| 1 | ASR 的错是长尾,而且因模型而异。同一个 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 | 是 |
Vue3 | VI三 | 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 这种怪物。
两个缓解办法:
- 给边界加哨兵:应用规则时,如果相邻两段在边界上都是英文字母数字(说明词被切断了),
就在段首段尾塞一个字母,让
(?<![A-Za-z])在断口处不成立,规则就不会在断口上开火。 哨兵必须真的是字母——我第一版塞了 NUL,(?<![A-Za-z])照样成立,等于没加; - 变体靠人工补:既然挖不到,就靠人看上下文写规则(
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 总是把英文写成大写碎片、 总是把数字念成中文)。到那时,词表就从"每条视频一份"变成"一份管一年"。
那才是它真正的形态:不是补丁,是你对自己那套流水线的认识。