speech-to-speech 是什么:一条能换掉每一段的语音代理流水线

2026-08-09

如果你只想知道 speech-to-speech 是什么,一句话:它是一条语音代理流水线,把 VAD、STT、LLM、TTS 四段串起来,对外暴露一个兼容 OpenAI Realtime 的 WebSocket 接口,而这四段里的每一段都能用一个命令行参数换掉。

仓库是 github.com/huggingface/speech-to-speech,GitHub 上的一句话描述原文是 “Build local voice agents with open-source models”,仓库标注 Apache-2.0,主语言 Python,2026-08-09 这天的快照是 11893 star、1463 fork、136 个 open issue。star 数只说明有人在看,不说明它适合你,下面这些结构性的东西才决定它适不适合。

一、四段级联,四条线程,中间是队列

README 的 “How it works” 把流水线拆成四级:

  1. VAD——用 Silero VAD v5 检测语音边界与轮次切换;
  2. STT——转写用户这一轮,可选实时局部转写;
  3. LLM——生成回复,流式输出文本与工具调用;
  4. TTS——合成音频并流式送回客户端。

关键不在这四个名词,而在它们的连接方式:每个组件跑在自己的线程里,用队列相连。这句话是理解后面一切行为的前提。因为四条线程是并发推进的,所以「用户忽然插话了,正在生成的那一段该怎么办」不是一个能靠 if 解决的问题——仓库为此专门做了一个带世代计数的 CancelScope,流水线线程在每个流式 token 上检查 cancel_scope.is_stale(gen)cancel() 时世代递增让旧世代立刻过期,另有一个 discarding 标志负责丢掉「在 cancel()response_done() 之间抵达」的残留输出。如果你把这套东西理解成「四个模块顺序调用」,看到 __RESPONSE_DONE__ 哨兵这类设计时就会一头雾水。

源码目录也是按这四段分的:src/speech_to_speech/ 下 96 个 Python 文件,VAD/STT/TTS/LLM/ 各自成组,服务端全部塞在 api/openai_realtime/ 里,参数定义单独放在 arguments_classes/ 的 18 个 dataclass 文件里,一共 131 个 CLI 参数。参数多到这个量级,本身就是「每段可替换」的代价。

本节延伸

二、「能换掉每一段」具体换的是什么

换的入口只有三个参数:--stt--llm_backend--tts。README 的 Supported Components 表给了各级可选后端:STT 侧除了默认的 Parakeet TDT,还有走 Transformers 的 Whisper、Faster Whisper、Apple Silicon 上的 Lightning Whisper MLX 与 MLX Audio Whisper、以及走 FunASR 的 Paraformer;TTS 侧除了默认的 Qwen3-TTS,还有 Kokoro-82M、Pocket TTS、ChatTTS、MMS TTS;LLM 侧是 OpenAI 兼容 API(responses-apichat-completions 两个后端)、本地 transformers、以及 macOS 上的 mlx-lm。有些后端内置,有些要装 extra,比如 pip install "speech-to-speech[faster-whisper]"pip install "speech-to-speech[paraformer]"

这里有个会咬人的行为,README 单独写明了:CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略,JSON 配置里多余的非激活后端键同理。翻译成人话——你把 Kokoro 的参数写在了跑 Qwen3-TTS 的命令里,程序不会报错停下,只会警告一声照跑,而你以为自己改的那个东西根本没生效。排查这类「参数改了没反应」时,第一件事是回去确认这个参数属不属于你当前激活的后端。

想看某个组合的真实默认值,README 给的办法是 speech-to-speech serve -h;要看另一种组合的参数,选择器要放在 -h 前面,例如 speech-to-speech serve --stt mlx-audio-whisper -h

本节延伸

三、三个开箱即用的反直觉

第一个:项目叫 local voice agents,但默认的 LLM 走的是云端。 README 给出了默认配置的等价完整命令,里面写着 --llm_backend responses-api--model_name gpt-5.4-mini,前面还要 export OPENAI_API_KEY=...。也就是说 pip install speech-to-speech 之后直接 speech-to-speech serve,本地跑的只有 STT 和 TTS,LLM 那一环打的是 OpenAI 的 Responses API。要真正全本地,得自己换——README 的 Fully Local 一节给的做法是另起一个 llama.cpp 进程,再用 --responses_api_base_url "http://127.0.0.1:8080/v1"--responses_api_api_key "" 把 LLM 指过去。密钥一律用环境变量占位,正文里、脚本里都别写死。

第二个:默认 STT 不支持中文。 README 的多语言矩阵写得很清楚,默认 STT nvidia/parakeet-tdt-0.6b-v3 覆盖的是 25 种欧洲语言。想做中文语音代理,必须换 STT,选 Whisper 系或 Paraformer(README 说 Paraformer 默认那个检查点偏中文)。README 给的中文示例是 --stt whisper-mlx --stt_model_name large-v3 --language zh。另外 --language 默认是 en,要做语言切换传 --language auto--enable_lang_prompt 默认 False,README 的判断依据是大模型通常能从上下文推断语言,但对小模型这条显式指令可能有帮助。

第三个:--num_pipelines 默认是 1。 服务端每个会话从池里认领一个 PipelineUnit,池子默认只有一个单元——默认状态下只承载一个并发会话。你要做多用户部署,这是第一个要动的参数。它属于模块级参数(module_arguments.py);和它常常一起改的 --host(默认 127.0.0.1)与 --port(默认 8765)则来自服务端参数文件 realtime_server_arguments.py——两组参数不在一个文件里,翻源码时别找错地方。

本节延伸

四、那些毫秒数字,买的不是延迟,是误判成本

VAD 那层一共 19 个参数,其中大半带 _ms 后缀,也有不是毫秒的(触发阈值 --thresh 默认 0.6,采样率 --sample_rate 默认 16000)。那一串毫秒数很容易让人误以为是性能指标。它们不是。

--min_silence_ms 默认 64,是切分语音的最小静音间隔;--min_speech_ms 默认 384,是被视为有效语音的最小段长;--speech_pad_ms 默认 500,是触发前保留并前置拼上去的音频。真正体现设计思路的是这一组:

参数默认管什么
--min_speech_continuation_ms192 ms接续一个可重开轮次所需的活跃语音时长
--speculative_reopen_ms800 ms软结束轮次保持可重开的基础窗口
--smart_turn_max_wait_ms2000 msSmart Turn 判轮次不完整时的宽限期
--smart_turn_incomplete_delay_ms600 ms判不完整后延迟启动 STT/LLM 的时长
--unanswered_reopen_ms7000 ms对「尚未有任何助手输出」的轮次的兜底上限

Silero 判定「用户说完了」之后,STT 和 LLM 可以推测性地先开始干活——省时间,但用户如果只是停顿,这活就白干了。Smart Turn v3.2(--smart_turn 默认 True,关掉用 --no_smart_turn,阈值 --smart_turn_threshold 默认 0.5)做的就是给「白干」上一个成本可控的闸:判为完整的轮次立刻开工,提交前留 800 ms 的重开窗;判为不完整的先晾 600 ms 再启动昂贵计算,其输出继续被 2 秒宽限期门控。任一段延迟内语音恢复,轮次会被重开为一个更新的修订版,上一版的工作在到达用户之前就被丢弃。

同样的思路也体现在迟滞设计上:开新轮次和抢话要求满 384 ms 的活跃语音,而接续一个还没提交的旧轮次只要 192 ms——因为接续的风险小,只是把同一轮延长;开新轮或打断助手代价大,门槛就保持高。这个参数被硬钳制在 [100, min_speech_ms] 区间,你设一个比 min_speech_ms 还大的值不会生效。--unanswered_reopen_ms 也有约束:低于 speculative_reopen_ms 时无效,启用 Smart Turn 时被钳制到 smart_turn_max_wait_ms

必须说死的一点:上面这些毫秒值全部是流水线自己引入的、可配置的等待不包含 STT 推理、LLM 首 token、TTS 首包这三段真正吃算力的时间。把它们加起来去得出一个「端到端延迟是多少毫秒」的结论,是彻底错误的读法。那三段取决于你的模型和硬件,本文没有任何测量数据。README 自己的定性判断是 LLM 是流水线里计算最重、延迟最高的组件,所以它才专门给了 --responses_api_reasoning_effort none 这种「为压语音延迟而关推理」的旋钮——这是 README 的定性判断,本站没有任何测量数据可以佐证或反驳它。

顺带一个单位陷阱:--realtime_processing_pause 默认 0.5,单位是,其余带 _ms 后缀的才是毫秒。

本节延伸

五、对外那一层:为什么是 Realtime 协议

服务端是一个 FastAPI/uvicorn 应用,同时暴露 WebSocket 与 WebRTC 两种传输(WebRTC 需要装 speech-to-speech[webrtc] extra)。选择兼容 OpenAI Realtime 的意义在于:客户端侧的事件语义是现成的,session.update 改配置、response.create 触发生成、response.cancel 取消,服务端回 response.output_audio.delta 送音频块、response.output_audio_transcript.delta 送助手转写增量。

两个细节值得记住。一是 session.update 的配置是深合并进一个共享的 RuntimeConfig,由 VAD、LLM、TTS 在处理时读取——不是重启生效,是运行中现读。二是 response.created 不是在请求发起时发的,是在第一个出站音频块出来时才发,你写客户端「正在思考」状态时得考虑这个时机差。助手转写这条流还有个迁移点:以前消费块级 done 的客户端要把实时渲染改到 delta 上,把 done 只当作定稿。

local 命令则把服务和自带的麦克风客户端组合在一个进程里,走环回。三个命令的分工是:serve 起服务,talk --url <完整 realtime url> 起自带客户端连上去说话,local 两者合一。顺便提醒,--mode 已废弃且很快会停止工作,网上不少旧教程还在用它——迁移期内 --mode realtime 等价于 serve--mode local 等价于 local,都会打警告,其余取值已被移除。

本节延伸

六、你该从哪一环开始改

按处境倒推,比按参数表选省事得多:

  • 要中文——第一件事换 STT,其它先别动;
  • 要全本地——第一件事换 LLM 后端,因为默认那一环在云上;
  • 要多人同时用——第一件事调 --num_pipelines
  • 只是想先看看它长什么样——什么都别改,README 的 Quickstart 就三条命令,装完 speech-to-speech serve,另开一个终端 speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime

有两类情况这套东西可能不适合你。一是你只要「一段音频转成一段文字」,那你要的是 STT 库,不是一条带轮次管理和打断处理的流水线,这一整套复杂度是为对话准备的。二是你打算把它直接开到公网上:serve 默认绑 127.0.0.1,要暴露必须显式传 --host 0.0.0.0;而 --enable_llm_proxy(默认关闭)会把配置好的远端 LLM 也作为普通 OpenAI 兼容端点暴露出来,README 的原话是服务端自己不做任何认证,也不做任何限流,只应在受信网络上启用,或者把服务部署在一个自己掌管访问控制的网关后面。这句声明本文原样带出,不做任何「这样配就安全了」的引申。

至于装的时候会遇到什么坑——Qwen3-TTS 的 CUDA wheel 版本错配、DeepFilterNet 与 Pocket TTS 的 numpy 版本互斥,README 各自开了小节,那是另一篇的事。

本节延伸

专题全部内容

本专题共 40 篇,按下面七组读。想直接动手的从「安装与部署」开始,想搞清楚它为什么这么设计的从「VAD 与回合判定」和「工具调用与打断」开始。

架构与概览

四个线程三道队列,这是理解后面一切延迟与打断行为的前提。

安装与部署

两个真会卡住人的坑都在这组:CUDA wheel 版本与 numpy 版本互斥。

命令与配置

三个命令的分工、14 个默认参数,以及那个不报错的坑。

VAD 与回合判定

那些毫秒数买的不是延迟,是误判成本——这一组讲透这句话。

Realtime 协议与传输

要对着这个 API 做客户端或设备,从这组开始。

工具调用与打断

这一组的并发取消范式可以直接搬进你自己的项目。

后端选型

中文场景务必先看「默认 STT 只覆盖 25 种欧洲语言」那篇。


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

许可条款请以官方 LICENSE 原文为准,本文不构成法律意见。各模型权重另有各自许可,请以各模型页面为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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