800、2000、7000:三个重开窗口的层级与钳制关系

2026-08-09

翻 speech-to-speech 的 VAD 参数列表时,最容易看花眼的是三个带毫秒的重开相关参数:--speculative_reopen_ms 默认 800、--smart_turn_max_wait_ms 默认 2000、--unanswered_reopen_ms 默认 7000。数字一大一小排在一起,很自然会让人以为是「先等 800,再等 2000,兜底 7000」这样一条时间轴。

不是。它们是三层管辖范围不同的窗口,其中两个之间还写死了钳制关系——配错了不会报错,只会静默失效。这篇就把这三个数字的层级关系摊开讲,顺便说清楚为什么这些毫秒数不能相加成「端到端延迟」。

先说清楚「重开」是什么

在讲窗口之前得先有一个概念:软结束的轮次(soft-ended turn)。

speech-to-speech 是 VAD → STT → LLM → TTS 的四段级联,README 里写明每个组件跑在自己的线程里、用队列相连。Silero VAD 判定「这一段语音结束了」之后,后面几级的活可以推测性地先开始干——不用等到百分之百确认用户说完了。

这就带来一个显而易见的风险:用户可能只是停顿一下,还没说完。所以这一轮在 VAD 侧不是立刻盖棺,而是保持一段时间的「可重开」状态。这段时间内如果语音恢复了,系统不会开一个新轮次,而是把同一轮重开为一个更新的修订版,累积的音频被重新发出,上一修订版已经干的活在到达用户之前就被丢掉。

三个窗口管的就是这个「可重开状态能维持多久」,而不是「用户要等多久才听到回复」。这个区别是本文所有结论的地基。

三个窗口各自管什么

按参数 help 原文整理,只列与本文直接相关的这三行:

参数默认值管辖范围
--speculative_reopen_ms800软结束的 Realtime 轮次保持可重开的基础窗口,除非有响应提交它
--smart_turn_max_wait_ms2000Smart Turn 判定「轮次不完整」时使用的推测重开宽限期
--unanswered_reopen_ms7000对「尚未收到任何助手输出」的软结束推测轮次,重开时长的兜底上限

三者的触发条件根本不同:

  • 800 是常态。任何软结束轮次都先落在这个基础窗里,直到有响应把它提交掉。
  • 2000 只在 Smart Turn 判不完整时出现。Smart Turn 默认是开的(--smart_turn 默认 True,要关得传 --no_smart_turn),它用 Smart Turn v3.2 这个外部 CPU ONNX 模型,拿当前轮次的内容与韵律去校验 Silero 那个「说完了」的判断。判完整的走 800 那条路;判不完整的,输出会被这个 2000 毫秒的宽限期门控住。
  • 7000 是给一类特定轮次兜底的:还没有收到过任何助手输出的推测轮次。help 原文用的词是 sanity cap——它防的是「这一轮一直没人管、却永远保持可重开」的极端情况,不是给所有轮次统一加长有效期。

顺带把另一个常被混进来的数字摘出来:--smart_turn_incomplete_delay_ms 默认 600,指 Smart Turn 报告轮次不完整后,延迟 STT 与 LLM 处理的时长,让恢复的语音有机会在昂贵计算开始前作废这个修订。help 里有一句关键限定:这段延迟跑在 smart_turn_max_wait_ms 之内。600 是 2000 里面的一段,不是 2000 之外再加的一段。

★ 两条钳制关系,配错了静默失效

--unanswered_reopen_ms 的 help 原文里带了两条约束,这是全篇最该记住的部分:

第一条:低于 speculative_reopen_ms 时无效。

7000 比 800 大,默认配置下这条约束看不出来。但一旦你手动把 unanswered_reopen_ms 调到比 speculative_reopen_ms 还小——比如想「收紧兜底」,把它设成 500——这个设置不产生任何效果。逻辑上也说得通:兜底上限比基础窗还短,那它就不叫上限了。麻烦在于,你不会收到一个明确的错误,参数照收,行为照旧。

顺便提醒一个同源的坑:README 明说 CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受、只是带警告地忽略,JSON 配置同理。这个项目的整体风格就是「宽进」,所以别指望配错参数会有人拦你。

第二条:启用 Smart Turn 时,被钳制到至少 smart_turn_max_wait_ms

README 的表述是让轮次在整个宽限期内都保持可重开。也就是说,Smart Turn 开着的时候,实际生效的兜底窗不会短于那 2000 毫秒的宽限期——unanswered_reopen_ms 与它是绑定的,你单独调 7000 这个值,未必能得到你以为的效果。

这两条合起来给出一个很实际的层级图:

speculative_reopen_ms (800)   ← 地板:所有软结束轮次的基础可重开窗

smart_turn_max_wait_ms (2000) ← Smart Turn 判不完整时的宽限期
                                (incomplete_delay 600 跑在它里面)

unanswered_reopen_ms (7000)   ← 仅对「零助手输出」的轮次生效的兜底上限
                                (低于 800 无效;开 Smart Turn 时被钳制到至少 2000)

判断依据落到这里就清楚了:如果你的诉求是「让用户的自然停顿更容易被接续」,该动的是 --speculative_reopen_ms 这个基础窗,而不是去把 7000 调得更大——后者只管那一类还没有任何助手输出的轮次。反过来,如果你在排查「某些轮次迟迟不结束」,先看 Smart Turn 是不是开着,因为开着的时候 2000 会参与钳制;关掉 Smart Turn(--no_smart_turn)之后,这条钳制路径就不存在了。

还有第四道门槛:时长迟滞

窗口只决定「这一轮还能不能被重开」,能不能触发重开还要过语音时长这一关,这是另一对参数:

  • 新轮次、抢话(barge-in):始终要求满 --min_speech_ms(默认 384 毫秒)的活跃语音
  • 接续一个可重开轮次(软结束、未提交、还在重开窗口内):只要求 --min_speech_continuation_ms(默认 192 毫秒)

门槛为什么差一倍?因为风险不对等。接续已有轮次只是把同一轮延长,代价小;开新轮或者打断助手正在播的音频,代价大得多,所以门槛保持高。help 里还给了推荐搭配:192 配 384,这也是默认值。

这个参数被硬钳制在 [100, min_speech_ms] 区间。设一个大于 min_speech_ms 的值不会生效——又是一个不报错的静默失效点。设成 0 则是禁用这个拆分、回落到统一用 min_speech_ms

配套还有一个碎片处理开关 --short_segment_merge_ms,默认 0(关)。大于 0 时,相邻的、每段都短于 min_speech_ms 的 VAD 段会被暂存并缝合这么多毫秒再丢弃;活跃语音短于 100 毫秒的碎片永不暂存。help 明说它在 min_silence_ms 取值很低时有用——而 min_silence_ms 默认才 64 毫秒,本来就切得碎,所以这是给那种场景准备的补救手段。

★★ 这几个数字买的不是延迟,是误判成本

必须把话说死:上面这些毫秒值是流水线自己引入的、可配置的等待,不包含 STT 推理、LLM 首 token、TTS 首包这三段真正吃算力的时间。那三段取决于你选的模型和硬件,本文不提供任何估计值,我们也没有任何测量数据。

所以,严禁把 800 + 2000 + 7000 或者 600 + 2000 之类相加,得出「端到端首响应大概多少毫秒」这类结论。这是读这套参数最常见的翻车方式,而且加出来的数字看着很像那么回事,特别有欺骗性。

那这些数字到底在买什么?回到 Smart Turn 的设计动机:Silero 判定说完就马上开工,能省时间,但可能白干(用户只是停顿)。Smart Turn 的作用是给「白干」上一个成本可控的闸——判完整的轮次少等,走 800 毫秒的提交窗;判不完整的先晾 600 毫秒再启动 STT 与 LLM,输出最多被门控到 2000 毫秒。--smart_turn_threshold 默认 0.5,help 语义是这个概率阈值越高,越倾向在含糊停顿处继续等待。

换句话说,你调的是「宁可多等一点也别白干」与「宁可白干也别让用户等」之间的比例,不是在调一个延迟总量。

重开之后,之前那份活去哪了

窗口生效、轮次被重开为新修订版之后,上一版已经产出的东西怎么办?这条线索接到服务端的取消机制上。

流水线线程(LLM、TTS)在每个响应开始时捕获当前的 cancel_scope.generation,并在每个流式 token 上检查 cancel_scope.is_stale(gen);调 cancel() 时世代递增,所有更早的世代立刻过期。同时 cancel_scope.discarding 标志被置位,让异步的 _send_loop 丢掉那些来自被取代世代的输出。流水线的输出是带世代标签的——AudioOutput 块与 AssistantTextEvent 都有 cancel_generation 字段。

这里有个原文专门解释过的边界条件值得记一笔:来自当前世代的输出永远放行,这样新响应不会被残留的丢弃窗口吞掉。README 举的例子恰恰就是「一个被取代的推测轮次,它的 TTS 从来没发出过 __RESPONSE_DONE__ 哨兵」。推测执行加上重开修订,天然会制造这种「清除信号没来」的状态,所以这条兜底不是可有可无的。

理解这一层的意义在于:重开窗口开得越大,被作废的修订版就可能越多,取消与丢弃这条路径被走的次数也越多。它不是零成本的宽容。

动手前的核对清单

调这几个参数之前,建议按顺序过一遍:

  1. --smart_turn 现在是开还是关?默认开。开着的话,unanswered_reopen_ms 存在被 smart_turn_max_wait_ms 钳制的关系。
  2. 你想改的行为,属于「所有软结束轮次」还是「零助手输出的轮次」?前者动 speculative_reopen_ms,后者才轮到 unanswered_reopen_ms
  3. 新设的 unanswered_reopen_ms 是否低于 speculative_reopen_ms?低于就是白设。
  4. 新设的 min_speech_continuation_ms 是否超过 min_speech_ms?超过会被钳制回去。
  5. 单位有没有搞混?带 _ms 后缀的都是毫秒,但同一组里的 --realtime_processing_pause 单位是(默认 0.5)。

最后一步,也是最该做的一步:-h 去核对你这套组合下的真实默认值

speech-to-speech serve -h

README 特别提到,要看另一种后端组合的参数,得把选择器放在 -h 前面,例如:

speech-to-speech serve --stt mlx-audio-whisper -h

以上命令抄自官方文档口径,未逐项实测,以官方文档与 --help 的实际输出为准。默认值随版本变动,本文列的是 2026-08-09 快照下参数定义里的值,别把它当成常量记在脑子里。

延伸阅读


本文依据 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 的实际输出为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。