真实片段上的词汇提升¶
状态: 测量于 2026-10-28;整段片段的 RNN-T 运行已于 2026-10-29 移到解码器偏置(下文)。于 2026-10-29 默认发布(未做实验)。延续 custom-vocabulary.md、vocabulary-real-clips.md 以及未发布的 spike/parakeet-boost 分支。
问题: Parakeet 能否在听错时写出用户的 Vocabulary 词条(“Cappuccino”、“OpenCode”、“Scrawl”),而不会在用户并未说出的地方写出它们?
答案: 可以,在默认强度(Normal,每词片 2.5 nats)下即可。当列表中包含 “Cappuccino” 时,Hex 在 Kit 的真实片段上正确写出它的次数为 19 次中的 12 次(原先为 4 次),流式时为 19 次中的 13 次(原先为 6 次)。对所有列出的词条,召回率从 157 次中的 136 次提高到 157 次中的 146 次。任何组别的 WER 都没有变差。不含任何词条的片段保持不变,而且提升写出的每一个新词条都是正确的。代价是时间:当片段包含候选时,重新打分会使片段完成时间增加约 0.05–0.15 秒(中位数);没有候选时则不增加。
工作原理¶
改动位于 transcribe.cpp 分支(anomalyco/transcribe.cpp,分支 hex/parakeet-vocabulary,9badd2a)下的 src/arch/parakeet/vocabulary.cpp。Streams 和 TDT 模型仍然使用它。
greedy decode, unchanged ─▶ words with their frames
└─ candidate finder: spans of 1–4 words that look like a term
├─ same letters, a sounds-like spelling, or close letters
├─ or one consonant sound off, only in names with 5+ sounds
└─ skip: case-only, inflections, short terms without a sounds-like,
addresses and paths (. / @ : \ _)
└─ for each candidate: transducer forward score on the same frames,
from the same predictor state
└─ replace when log P(term) + boost × pieces ≥ log P(heard)
Hex 在每次运行或流式会话开始时传入词表(transcription::gguf_session::with_vocabulary),只传给接受它的 Parakeet 模型,且仅在未设置 Whisper 风格的提示(hint)族时。transcription/local_transcriber 共享这条路径,因此 macOS、Linux 和 Windows 都能获得该功能。词表最多 64 个词条,每个词条最多 12 个词片;每次解码最多 16 个候选跨度。重新打分在解码后的转写引擎线程上运行,绝不会在音频线程上运行。
为什么不在解码时提升 Token¶
该 spike 分支在每一步贪心解码时为词条词片增加奖励,并受一个 margin 门控约束。在真实语音上,这个门控要么过紧,要么过松。Parakeet 对 “Cappuccino” 开头的 “a” 非常确信(log-prob 接近 0),因此一个能保护普通语音的门控永远不会让词条开始:在麦克风数据上 Cappuccino 从 0/11 变成 1/11,而 “Capuchin” 仍然是 Capuchin。更宽松的门控则会被普通词触发,使流式 WER 上升 0.9 个百分点。重新打分在决定前会看到整个跨度。在 Kit 说 Cappuccino 而 Parakeet 写出 “capuchin” 的情况下,这些帧作为 “capuchin” 的得分为 −0.2,作为 “Cappuccino” 的得分为 −9.2。Cappuccino 由 5 个词片组成,因此 2.5 的提升(合计 12.5)可以覆盖这个差距,词条胜出。在别处,“capuchin” 的得分是 −0.1,而 “Cappuccino” 是 −13.7。这个差距比提升更大,所以保留了原词。做决定的是音频,而不是拼写。
已否决:减去内部语言模型先验¶
减去预测器自身的文本先验(ILM,λ = 0.3–0.5)会让罕见名称的代价更低。但它过度触发:“about” → Cappuccino、“right” → Parakeet。因此提升保持为每个词片的简单常数值。
方法¶
- rve(real-voice-expansion,真实语音扩展):Kit 的真实片段 168 条:20 条听写、78 条会议麦克风、70 条会议系统音频。其中 19 条包含 “Cappuccino”,43 条不包含任何词条。
- tc(transcription-corpus,转写语料库):274 条片段:51 条合成词条片段、18 条合成近似读音片段、200 条普通合成句子、5 条私有片段。
- Vocabulary: Hex, Cappuccino, OpenCode, Rune, Scrawl, Effect, Anomaly, Dax, Aiden, Kit, Parakeet, Moonshine, GPUI, Sparkle, Argon, Astra, HiveMind.
- Model:
parakeet-unified-en-0.6b-Q8_0,Metal,在 MacBook Pro 上。每条片段分别以离线和流式方式运行,带词表和不带词表各跑一次。 - WER 经过归一化处理;“term-normalized”(词条归一化)还会在两侧合并 “open code”、“GP UI”、“hive mind” 和 “moon shine”。如果不这样做,正确的 “OpenCode” 对参考文本 “open code” 会被计为两个错误,导致原始 WER 上升,即使每个改动都是修正。
- False insertions(错误插入): 在参考文本没有该词条的地方写出的词条。每一条都经过人工核查。
结果¶
召回率统计的是词条被正确写出的次数。WER 采用词条归一化。
| rve,离线 | 关闭 | Normal(2.5) | Strong(4.0)¹ |
|---|---|---|---|
| Cappuccino | 4/19 | 12/19 | 14/19 |
| 全部词条 | 136/157 | 146/157 | 148/157 |
| WER,全部 | 5.05 | 4.90 | 4.88 |
| WER,听写麦克风 | 3.32 | 3.32 | |
| WER,会议麦克风 | 4.85 | 4.60 | |
| WER,会议系统音频 | 5.63 | 5.59 | |
| WER,不含词条片段 | 3.14 | 3.14 | 3.24 |
| 错误插入 | — | 0 | 1(“Capuchin”) |
| rve,流式 | 关闭 | Normal(2.5) |
|---|---|---|
| Cappuccino | 6/19 | 13/19 |
| 全部词条 | 139/157 | 146/157 |
| WER,全部 | 6.00 | 5.91 |
| WER,听写麦克风 | 0.26 | 0.26 |
| WER,会议麦克风 | 6.39 | 6.30 |
| WER,会议系统音频 | 6.41 | 6.32 |
| WER,不含词条片段 | 2.26 | 2.26 |
| 错误插入 | — | 0 |
| tc | 离线关闭 | 离线 Normal | 流式关闭 | 流式 Normal |
|---|---|---|---|---|
| Cappuccino | 2/5 | 4/5 | 2/5 | 4/5 |
| 全部词条 | 47/56 | 53/56 | 47/56 | 53/56 |
| WER,全部 | 1.82 | 1.69 | 1.65 | 1.54 |
| WER,不含词条片段 | 1.63 | 1.63 | 1.61 | 1.61 |
| 错误插入 | — | 0 | — | 0 |
¹ Strong 档位是按每词片 3.0 测量的。Hex 的 Strong 档位是 4.0,是为需要它的用户刻意上调的。
在 Normal 强度下,评分器在 rve 上统计到 4 个“假”插入,在 tc 上统计到 1 个;没有一个是真正的错误。其中两个是 “Scrawl”:参考文本写的是 “scroll”,但 Kit 说的是 Scrawl。还有两个(“Kit”、“Scrawl”)在关闭提升时本来就会出现。tc 上的那一个是 “Parakeet”,出现在一条粗略参考文本将其拼作 “parake” 的片段中。在 3.0 强度下,字面词 “Capuchin” 有一次变成了 Cappuccino。这就是 Normal 作为默认档位的原因。
延迟¶
在 MacBook Pro 上于最终 fork 提交处测量,当时它还在运行其他构建任务,所以绝对数值带有噪声;重要的是差异。
| 每 10 秒音频 | Off | Normal |
|---|---|---|
| rve 离线,中位数 / p90 | 363 / 670 ms | 356 / 698 ms |
| tc 离线,中位数 / p90 | 695 / 957 ms | 763 / 1111 ms |
| rve 流式结束时,中位数 / p90 | 175 / 298 ms | 215 / 439 ms |
使用这个 17 个词条的列表,168 个 rve 片段中有 81 个至少有一个候选可供重新评分。没有任何候选的片段无需付出额外代价。在有候选的情况下,重新评分离线时中位数为 62 ms,在流结束时为 122 ms,最多不超过 0.55 s。先前一个更宽松的候选查找器能为 146 个片段评分并得到相同结果,但代价约为前者的两倍。
解码器偏置(整段 RNN-T)¶
测量日期为 2026-10-29,使用相同的片段和列表,仅离线。Fork 分支 hex/parakeet-vocabulary-tree,提交 7045d1c(src/arch/parakeet/decoder.cpp 中的 decode_rnnt_tree)。现在,RNN-T 模型上的整段运行会在解码时进行偏置,而不是事后重新评分:
greedy decode ─▶ the words, exactly as before
└─ where a term's first piece is plausible at a word start
└─ shadow hypotheses spell the term through the vocabulary trie
├─ boost per piece: the full boost if the greedy words sound like
│ the term (classify_vocabulary_span), else 0.6 × boost
├─ same skips as rescoring: case-only, inflections, addresses
└─ at the word boundary: the term replaces the greedy span only
if it scores higher; otherwise the shadows are dropped
最初尝试了使用字典树的普通 RNN-T 束搜索(浅融合),但不如重新评分:该模型给 blank 的权重太高,导致路径求和时会丢掉整个词组,而且运行速度比贪心解码慢约 2.5 倍。保留贪心路径、只在词条内部进行分支则同时解决了这两个问题。
| Off | 重新评分,Normal | 解码器,Light (2.0) | 解码器,Normal (2.5) | 解码器,Strong (4.0) | |
|---|---|---|---|---|---|
| rve Cappuccino | 4/19 | 12/19 | 11/19 | 14/19 | 18/19 |
| rve 所有词条 | 136/157 | 146/157 | 148/157 | 151/157 | 155/157 |
| rve WER,全部 | 5.05 | 4.90 | 4.87 | 4.80 | 4.80 |
| rve WER,无词条片段 | 3.14 | 3.14 | 3.14 | 3.14 | 3.24 |
| tc 所有词条 | 47/56 | 53/56 | 53/56 | 54/56 | 55/56 |
| tc WER,全部 | 1.82 | 1.69 | 1.69 | 1.67 | 1.65 |
| tc WER,无词条片段 | 1.63 | 1.63 | 1.63 | 1.63 | 1.63 |
| 解码器耗时 vs 贪心解码 | 1× | + 重新评分遍历 | 1.07–1.09× | 1.04–1.08× | 1.17–1.20× |
Strong 再次在无词条片段中把一个普通词变成词条,就像重新评分在 3.0 时所做的那样。重新评分在同一运行中的基线是 147/157 和 4.88;表格保留了已发布的 2026-10-28 数据。实时流继续使用重新评分:在流结束时再次解码整个话语,会消耗掉流式处理本来要节省的延迟。
设置¶
词汇表 › Boost:Off (0)、Light (2.0)、Normal (2.5,默认)、Strong (4.0)。CLI:hex settings set vocabulary_boost normal。
后续¶
- 重新评分的成本随每个片段的候选数增加而增长。如果流结束时的延迟很重要,就用一次批量联合遍历来对候选评分,而不是逐个进行。
- 对于 Cappuccino,剩余的未命中大多是 “about”/“other”,这些音频听起来确实像常见词。用户提供的“听起来像”的拼写(如 “a bat war”)无需更强 boost 即可修复这些情况。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex