数据截至 (上游 commit b21e54d6a845)
断句、Smart Turn 与投机回合
这一章讲什么: 整个项目最原创、也最绕的部分。分三层讲:先讲 Silero 怎么切语音段,再讲 Smart Turn 怎么判断「这句话是不是说完了」,最后讲当判断错了时,
turn_id/turn_revision这套机制怎么把已经跑出来的回答无声无息地作废。
1. 先认识问题:停顿 ≠ 说完
人说话会停顿:「帮我订一张……嗯……明天去上海的票」。传统 VAD 只看有没有声音,遇到中间那个停顿就会认为你说完了,于是助手抢话。
三种应对,本项目全都用了:
| 层次 | 手段 | 代价 |
|---|---|---|
| 声学层 | 要求静音持续 min_silence_ms 才收尾 | 静音阈值调大 = 每句都变慢 |
| 语义层 | Smart Turn 模型判断「这段听起来完整吗」 | 每次收尾多一次推理 |
| 系统层 | 先当作说完了跑,留一个反悔窗口 | 白跑的算力 + 一整套作废机制 |
第三条是本项目的核心赌注:用算力换延迟。下面依次拆。
2. Silero 层:怎么切出一段语音
迭代器的状态机
VADIterator.__call__(src/speech_to_speech/VAD/vad_iterator.py:111-170)每次吃一块音频,返回 None(还没完)或一个 tensor 列表(一段说完了)。
未触发(triggered=False)
│ 概率 ≥ threshold
▼
已触发(triggered=True) ──── 概率 ≥ threshold-0.15 ──► 继续累积,重置 temp_end
│
│ 概率 < threshold-0.15
▼
静音计时中(temp_end 记录起点)
│ 静音时长 ≥ min_silence_samples
▼
收尾:返回整段 buffer,清空,回到未触发
三个值得抄的细节:
- 迟滞(hysteresis):进入用
threshold,维持用threshold - 0.15(vad_iterator.py:147与153)。避免概率在阈值附近抖动导致反复切段。 - 前置缓冲:未触发时的音频存进
_pre_speech_buffer环形缓冲,触发时把最近speech_pad_ms的内容当作prefix_buffer前置进去(vad_iterator.py:131-143)。没有这个,每句话开头的辅音都会被吃掉。 默认 CLI 值是 500 ms(arguments_classes/vad_arguments.py:42-47),注意比VADIterator自身的默 认 30 ms 大得多。 - 区分「时长」和「有效语音时长」:
active_speech_samples只在概率高于维持阈值时累加,和 buffer 总长度是两个数。判断「这段够不够算一句话」用的是前者。
handler 层的额外判定
VADHandler.process(src/speech_to_speech/VAD/vad_handler.py:543-614)在 Silero 之上加了几道:
| 判定 | 参数(默认) | 作用 |
|---|---|---|
| 有效语音下限 | min_speech_ms=384 | 短于此的段直接丢,防噪声触发 |
| 续接下限 | min_speech_continuation_ms=192 | 接着一个可重开的回合说话时,门槛减半 |
| 段长上限 | max_speech_ms=inf | 超长段丢弃(默认不启用) |
| 碎片缝合 | short_segment_merge_ms=0 | >0 时把相邻短段拼起来再判 |
「续接门槛更低」是有意的:你刚说完一句、助手还没答,你补一句「哦对了」——那是同一个回合的续接,不该按新回合的严格门槛丢掉。实现见 _active_speech_min_ms(vad_handler.py:239-243)。
碎片缝合的防滥用:低于 _SHORT_SEGMENT_MIN_FRAGMENT_MS = 100(vad_handler.py:40)的碎片永远不缓存,否则一串亚阈值噪声累加起来就能凑够 min_speech_ms 触发假打断。
延迟发出的 speech_started
注意 speech_started 不是 Silero 一触发就发,而是等有效语音累积够门槛才发(vad_handler.py:562-594)。因为这个事件在协议层会触发「取消当前回答」,发早了等于被咳嗽一声打断。
3. Smart Turn 层:这句话听起来完整吗
它是什么
一个 ONNX 分类器(pipecat-ai/smart-turn-v3,v3.2 CPU 版),吃最多 8 秒音频,吐一个「这是完整回合」的概率(src/speech_to_speech/VAD/smart_turn.py:20-24、130-153)。
关键设计:它不是每块音频都跑,只在 Silero 已经判定收尾之后跑一次(文件头 docstring 明确说明,smart_turn.py:1-7)。所以它对每块音频的开销是零。
输入预处理:重采样到 16 kHz、在左边补零到固定 8 秒、超长则取最后 8 秒(smart_turn.py:123-127)。也就是说它只看结尾——这符合「判断结尾语调/语义是否完整」的直觉。