Smart Turn:先干活再决定要不要认账
语音代理里最难的一个判断不是「用户说了什么」,而是「用户说完了没有」。
speech-to-speech(github.com/huggingface/speech-to-speech,仓库标注 Apache-2.0,star 11893,2026-08-09 快照)是一条 VAD → STT → LLM → TTS 的四段级联流水线,README 说每个组件跑在自己的线程里、用队列相连。第一级的 Silero VAD v5 负责检测语音边界与轮次切换,它给出的「这一段结束了」是个纯声学判断——静音够长就算完。
问题是人说话本来就带停顿。「帮我查一下……」后面那 0.5 秒,Silero 看到的和句子真的说完没有任何区别。
于是这个仓库在 Silero 之后又挂了一层:Smart Turn。它默认是开着的(--smart_turn 默认 True,关掉要传 --no_smart_turn),但它解决的问题跟大多数人以为的不一样。
它不是「再等一会儿」,它是「先干活,回头再决定认不认账」
按 README「Smart Turn endpointing」一节的机制描述,整个流程是这样的:
- Silero 先敲定语音段,此后 STT 与 LLM 的工作可以推测性地先开始
- 如果这一轮被判为完整:立刻开始处理,在提交输出之前使用
--speculative_reopen_ms(默认 800 ms) - 如果被判为不完整:先等
--smart_turn_incomplete_delay_ms(默认 600 ms)再启动 STT 与 LLM 的工作,而这一轮的输出继续被--smart_turn_max_wait_ms(默认 2 秒)门控 - 只要在上面任何一段延迟内语音恢复了:现有轮次被重开为一个更新的修订版,累积的音频被重新发出,上一个修订版的工作在到达用户之前就被丢弃
注意第 1 步的措辞:Silero 一敲定,活儿就可以开工了。也就是说这条流水线的默认姿态是「先干」——不等确认,先把 STT 和 LLM 跑起来。
Smart Turn 在这里的角色不是踩刹车,而是给「白干」这件事上一个成本可控的闸。它用当前轮次的内容与韵律去校验 Silero 那个纯声学判断,然后决定:这一轮的推测性输出要保持多久的「可反悔」状态,以及要不要在开工之前先晾一会儿。
这几个毫秒数字买的不是延迟,是误判成本。 这句话是理解后面所有调参的前提。
三个数字分别买到了什么
先把本篇要用到的几行参数摆出来。仓库 src/speech_to_speech/arguments_classes/vad_arguments.py 里跟 Smart Turn 直接相关的是这几个:
| 参数 | 默认值 | 管什么 |
|---|---|---|
--smart_turn | True | 总开关,默认开,关掉传 --no_smart_turn |
--smart_turn_model_path | None | 可选的 Smart Turn v3.x CPU ONNX 模型路径 |
--smart_turn_threshold | 0.5 | 判定「轮次完整」的概率阈值 |
--smart_turn_max_wait_ms | 2000 | 判不完整时的推测重开宽限期 |
--smart_turn_incomplete_delay_ms | 600 | 判不完整后延迟 STT 与 LLM 的毫秒数 |
--smart_turn_cpu_count | 1 | ONNX Runtime 每次推理可用的 CPU 线程数 |
--speculative_reopen_ms(默认 800)虽然不带 smart_turn 前缀,但它是「完整轮次」那条路上的提交窗,也要一起看。
把这几个默认值翻译成人话:
- 判完整 → 马上开工,输出在提交前留 800 ms 的可重开余地。赌的是「大概率真说完了」,代价是万一赌错,已经跑掉的那部分推理白费。
- 判不完整 → 先晾 600 ms 再开工。这 600 ms 里如果用户接着说了,昂贵的 STT 和 LLM 压根没启动,什么都没浪费。help 原文对这一条的解释很直白:让恢复的语音有机会在昂贵计算开始前作废这个修订版。
- 这 600 ms 跑在 2 秒之内。help 明说
smart_turn_incomplete_delay_ms的延迟是在smart_turn_max_wait_ms里面走的,不是叠加在外面。所以对不完整轮次,「先不开工」的那段和「开工了但先不提交」的那段是同一个 2 秒预算里的前后两截。
★ 这里最容易看漏的一层是:600 这个数字省的是算力,800 和 2000 省的是错误输出。 前者控制「白跑多少推理」,后者控制「说错的话有没有机会在到达用户之前被拦下来」。它们不是同一类成本,调参时也不该一起动。
被作废的修订版,输出去哪了
第 4 步那句「上一修订版的工作在到达用户之前就被丢弃」不是一句轻飘飘的描述,它背后是这个仓库里技术含量最高的一块代码。
按 src/speech_to_speech/api/openai_realtime/README.md 的 Interruption Handling 一节,VAD、_send_loop 与 LLM/TTS handler 共享一个 CancelScope 对象。它管两样东西:
- 世代计数器
cancel_scope.generation:流水线线程在每个响应开始时捕获当前世代,并在每个流式 token 上检查cancel_scope.is_stale(gen)。调用cancel()时世代递增,所有更早的世代立刻过期。原文强调这套做法的好处是 no timing games required——不需要猜「多久算晚」。 - 丢弃标志
cancel_scope.discarding:由cancel()置位,用来丢掉在cancel()与response_done()之间抵达的、来自被取代世代的输出。
所以 Smart Turn 触发的「重开为更新的修订版」,落到实现上就是一次世代递增:旧修订版的 token 在下一次 is_stale 检查时被判过期,已经在队列里的音频块和助手文本被 discarding 拦下。
★ 顺带一提原文专门解释过的一个边界条件,恰好就发生在这个场景:来自当前世代的输出永远放行,为的是新响应不会被残留的丢弃窗口吞掉——README 举的例子正是「一个被取代的推测轮次,它的 TTS 从来没发出过 __RESPONSE_DONE__ 哨兵」。哨兵丢了,守卫就没人清;靠「当前世代无条件放行」兜住,才不至于让新响应被静默吃掉。
理解这一层对调参有实际意义:推测性开工之所以敢这么激进,是因为丢弃机制是按世代号做的,不是靠时间窗猜的。 如果这套机制不可靠,默认值就不敢把「先干活」设成默认姿态了。
什么时候该动它,动哪个
给判断依据,不给推荐值——除了默认值本身,这个仓库的 help 文本没给别的推荐数字。
要不要关掉整个 Smart Turn。 它是个外部模型(pipecat-ai/smart-turn-v3,CPU ONNX),不是这个仓库自己训的。--smart_turn_model_path 默认是 None,help 说省略时会从 pipecat-ai/smart-turn-v3 下载最新支持的 v3.2 CPU 模型。这意味着两件事:一是内网、离线或不方便出网的机器上,你要么提前把模型准备好并用 --smart_turn_model_path 指过去,要么就得面对一个额外的下载依赖;二是它会占 CPU,--smart_turn_cpu_count 默认只有 1。如果你的部署场景根本不需要处理「说到一半停顿」这类输入——比如输入侧是固定话术、或者上游已经有别的端点判定——--no_smart_turn 是一个正当选择,而不是偷懒。
阈值往哪调。 --smart_turn_threshold 默认 0.5,help 的语义是:越高越倾向在含糊停顿处继续等待。所以方向很清楚——用户说话习惯性带长停顿、被截断的抱怨多,就往上调;反过来,如果问题是「明明说完了还在等」,往下调。但注意这个阈值不改变延迟结构,它只改变走「完整」那条路还是「不完整」那条路的比例。
别忘了它会钳制别的参数。 --unanswered_reopen_ms(默认 7000)是针对「还没有收到任何助手输出的软结束推测轮次」的兜底上限,help 里写明了两条约束:它低于 speculative_reopen_ms 时无效;启用 Smart Turn 时,它被钳制到 smart_turn_max_wait_ms。也就是说你在开着 Smart Turn 的前提下调 unanswered_reopen_ms,效果可能跟你写的值不一样——这是排查「参数改了没反应」时的第一个怀疑对象。
要确认你这套后端组合下这些参数的实际默认值,回到 -h:
speech-to-speech serve -h
想看某个具体组合的参数,把选择器放在 -h 前面。关掉 Smart Turn 的写法就是加一个反向开关:
speech-to-speech serve --no_smart_turn
以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准。
★★ 一条不能越的线:这些数字不能相加
写到这儿最容易顺手干的一件蠢事,就是把 800、600、2000 加起来,得出一个「端到端首响应就是这么多毫秒」的结论。
不能这么算。 这些毫秒值是流水线自己引入的、可配置的等待,它们不包含 STT 推理、LLM 首 token、TTS 首包这三段真正吃算力的时间。而那三段取决于你选的模型和你的硬件——换个 STT 后端、换张卡、换个本地还是云端的 LLM,量级完全不同。
真实的首响应延迟是这么串起来的:用户停止说话 → VAD 判定静音(受 --min_silence_ms 影响,默认 64)→ Smart Turn 判定与其延迟(完整/不完整两条路)→ STT 转写 → LLM 首 token → TTS 首包 → 网络传输。上面这篇讲清楚的只是第二环的行为,前后各环的耗时得你自己在自己的机器上测。
顺带把几个容易记串的相关默认值也回一下卡:新轮次与抢话(barge-in)始终要求 --min_speech_ms(默认 384)的活跃语音,而接续一个可重开轮次只要求 --min_speech_continuation_ms(默认 192);--speech_pad_ms 默认 500,是 VAD 触发前保留并前置拼上去的那段音频。这三个跟 Smart Turn 不是一回事,但经常被混在同一句「延迟」里说,那是另一篇的题目了。
延伸阅读
本文依据 speech-to-speech 官方仓库(github.com/huggingface/speech-to-speech)的 README、
src/speech_to_speech/arguments_classes/ 下的参数定义与 Realtime Engine 架构文档整理,核对日 2026-08-09。
本文内容为仓库源码与文档口径,我们没有安装、部署或调用过该服务,文中毫秒值均为参数默认值而非实测延迟。
参数与默认值随版本变动,请以 speech-to-speech serve -h 的实际输出为准。