两个长得很像的实时转写开关,别调错
speech-to-speech 的参数表里有一对特别阴的东西:--enable_live_transcription 和 --enable_realtime_transcription。名字翻成中文都是「实时转写」,但它们来自两个不同的参数文件,默认值一个是 True 一个是 False,管的事情也不是一回事。更糟的是,它们各自还带了一个「间隔 0.5」的伙伴参数和一个「最小静音毫秒」的伙伴参数,四个数字两两撞脸。
这篇按排查的路子走一遍:现象、怎么确认是这个问题、参数语义给出的处置、处置后怎么验证、以及什么情况说明根本不是这个原因。
一、现象长什么样
典型的三种描述:
- 「我压根没配实时转写,日志里却一直在刷用户说话的中间结果。」
- 「我明明把实时转写打开了,客户端就是收不到局部转写,只有一整段最终结果。」
- 「我想让中间结果出得快一点,把那个 0.5 改小了,一点变化都没有。」
这三句话的共同点是:说话人心里只有「实时转写」这一个概念,而仓库里有两个开关、两条路径。改错一个,看起来就是「参数不生效」。
二、怎么确认是不是这一对参数在打架
判定动作 1:先确认参数属于哪个文件
这是最快的一刀。两个开关在源码里的归属是:
| 参数 | 所属文件 | 默认值 | help 原文要点 |
|---|---|---|---|
--enable_live_transcription | module_arguments.py | True | 在用户说话时启用实时转写显示,help 里注明「works with parakeet-tdt」 |
--enable_realtime_transcription | vad_arguments.py | False | 启用说话过程中的渐进式音频释放,以支持实时转写 |
一个在模块级(和 --stt、--tts、--llm_backend、--num_pipelines 挤在同一个文件里),一个在 VAD 侧(和 --thresh、--min_silence_ms、--speech_pad_ms 挤在一起)。
模块级那个默认就是开的,所以「我没配它却在刷中间结果」不是玄学,是默认值。VAD 侧那个默认是关的,所以「我以为我开了」多半是把模块级那个又写了一遍。
判定动作 2:跑一次 -h,按分组看,别按名字看
speech-to-speech serve -h
看输出时不要盯着关键字搜 transcription,要看这一项落在哪一组参数里。这两个开关名字的区分度太低,肉眼扫过去很容易读成同一个;分组和默认值才是可靠的区分特征。
判定动作 3:把伙伴参数一起摆出来
真正让人改错的不是开关本身,是它们各自的两个伙伴:
| 意图 | 模块级(module_arguments.py) | VAD 侧(vad_arguments.py) |
|---|---|---|
| 开关 | --enable_live_transcription(默认 True) | --enable_realtime_transcription(默认 False) |
| 更新/释放间隔 | --live_transcription_update_interval(默认 0.5,单位秒) | --realtime_processing_pause(默认 0.5,单位秒) |
| 最小静音 | --live_transcription_min_silence_ms(默认 500) | --min_silence_ms(默认 64) |
两个「0.5 秒」正对着,这就是「我把 0.5 改小了没反应」的来源——改的那个跟你想要的那条路径不在一起。
而两个「最小静音」差了将近一个数量级:--live_transcription_min_silence_ms 的 help 说的是「启用实时转写时,结束语音之前的最小静音时长」,默认 500 ms;--min_silence_ms 是 VAD 用来切分语音段的最小静音间隔,默认只有 64 ms。你要是把它们当成同一个东西,一改就会连带影响整条回合切分。
顺带说一句单位陷阱:这一堆参数里只有带 _ms 后缀的是毫秒,--live_transcription_update_interval 和 --realtime_processing_pause 都是秒。
三、按参数语义该怎么处置
分三种意图对号入座。
意图 A:我要的是用户说话过程中就有局部转写往外发。 需要的是 VAD 侧那个——它的 help 讲得很直白,作用是「渐进式音频释放」,也就是不等整段话说完就先把音频块放出去。它默认是关的,得显式加:
speech-to-speech serve \
--enable_realtime_transcription \
--realtime_processing_pause 0.5
(以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 -h 的实际输出为准。--realtime_processing_pause 0.5 写的就是默认值,放在这里是为了让你知道该改哪一个。)
意图 B:我不想要说话过程中的实时转写显示。
那你要动的是模块级的 --enable_live_transcription,因为它默认是 True。这里有个必须如实说明的地方:这个仓库里我们有明确依据的布尔取反写法只有 Smart Turn 那一个——help 原文写的是「pass --no_smart_turn to disable it」。其它布尔参数的取反写法官方 help 里没有逐个写出来,请以你本机 -h 的实际输出为准,别照着 --no_smart_turn 的样子自己拼一个出来。
意图 C:我要调「多久算说完一句」。
先想清楚你要调的是哪一层。--live_transcription_min_silence_ms(500)按 help 的说法是实时转写启用时结束语音的门槛;--min_silence_ms(64)是 VAD 切分语音段的基础参数。后者默认值本来就很低,切得碎是预期内的;仓库为此另给了一个补救开关 --short_segment_merge_ms(默认 0,即关闭),它的 help 明说「在 min_silence_ms 取值很低时有用」——大于 0 时,相邻的、每段都短于 --min_speech_ms(默认 384)的 VAD 段会被暂存并缝合这么多毫秒,且活跃语音短于 100 ms 的碎片永远不会被暂存。
四、处置之后怎么验证
第一,回读 -h 里的默认值。 改完之后重新看一次分组输出,确认你写在命令行上的那个名字,和你想要的那一组落在同一个文件里。这一步听起来废话,但这对参数的翻车率高就高在肉眼读名字。
第二,看客户端收到的事件名。 用户侧的局部转写走的是 conversation.item.input_audio_transcription.delta,最终结果是 conversation.item.input_audio_transcription.completed。这两个事件名是判断「实时转写这条路径有没有通」的直接依据:delta 在流、就是通了;只有 completed、没有 delta,说明局部转写没往外发。
第三,把日志级别调上去。
speech-to-speech serve --log_level debug
--log_level 默认是 info,help 原文举的例子就是 --log_level debug。
第四,别用「感觉快了没有」来验收。 这一步是这篇文章里最想强调的一条:这些参数默认值是流水线自己引入的、可配置的等待,它们不包含 STT 推理、LLM 首 token、TTS 首包这三段真正吃算力的时间。你把几个毫秒数加起来得到的任何「端到端延迟」都是假的。验收就看事件有没有按预期的名字出现,不要看体感。
五、什么情况说明不是这两个参数的问题
这一节比前面四节都重要——改错了参数固然常见,但下面这五种情况改到天亮也没用。
情况一:你缺的是助手侧的转写,不是用户侧的。
助手输出的转写走的是完全另一组事件:response.output_audio_transcript.delta 是当前音频输出项的助手转写增量后缀,response.output_audio_transcript.done 是输出项关闭时发一次的完整转写。仓库 README 为这件事单开了一节讲兼容性变更:以前消费每个块级 done 事件的客户端,必须把实时渲染改到 delta 上、把 done 只当作定稿。如果你的客户端只监听 done,那问题出在客户端的事件消费方式上,跟这两个开关没有半点关系。
情况二:你换了 STT 后端。
--enable_live_transcription 的 help 里注明的是「works with parakeet-tdt」。换成别的 STT 之后官方 help 没有给出说法,我们也不替它下结论——但这至少意味着,当你不在默认 STT 上时,「开关没生效」的可能原因就不止参数写错这一个了。
情况三:你说的是中文,而且什么转写都没有。
这多半根本不是开关问题,是选型问题。默认 STT 是 parakeet-tdt(nvidia/parakeet-tdt-0.6b-v3),README 的多语言表里写明它覆盖的是 25 种欧洲语言;--language 默认值是 en。要做中文语音代理必须换 STT,README 给出的中文示例是这样的:
speech-to-speech serve \
--stt whisper-mlx \
--stt_model_name large-v3 \
--language zh \
--llm_backend mlx-lm \
--model_name mlx-community/Qwen3-4B-Instruct-2507-bf16
在这种情况下,你先要解决的是「模型认不认得中文」,转写开关是后面的事。
情况四:第二个客户端根本连不上。
--num_pipelines 默认是 1,help 原文说得很清楚:一个 uvicorn 服务监听 --port(默认 8765),把每个进来的客户端路由到下一个空闲流水线,最大并发 WebSocket 会话数等于 num_pipelines,再多的连接会被拒绝。连接被拒和转写不出来是两码事,别在参数表里找错方向。
情况五:转写在来,只是你觉得「等得久」。
那是回合判定那一层的事,跟这两个开关无关。默认开着的 Smart Turn(--smart_turn 默认 True)会在 Silero 敲定轮次之后决定助手输出保持推测状态多久:判为完整的轮次用 --speculative_reopen_ms(默认 800 ms)的提交窗;判为不完整的轮次先等 --smart_turn_incomplete_delay_ms(默认 600 ms)再启动 STT 与 LLM 工作,其输出继续被 --smart_turn_max_wait_ms(默认 2000 ms)门控;另有 --unanswered_reopen_ms(默认 7000 ms)作为「还没有任何助手输出的轮次」的兜底上限,它低于 --speculative_reopen_ms 时无效,启用 Smart Turn 时被钳制到 --smart_turn_max_wait_ms。这些数字买的是误判成本,不是延迟——再说一次,不要把它们相加。
延伸阅读
把这一整套压成一句能记住的话:名字里带 live 的在模块级、默认开着;名字里带 realtime 的在 VAD 侧、默认关着。 改参数之前先确认自己站在哪一层,比记住哪个数字是多少有用得多。
本文依据 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 的实际输出为准。