speech-to-speech 语音代理流水线
一条四段级联的语音代理流水线:VAD 检测你说完没有,
STT 转成文字,LLM 生成回复,TTS 合成语音送回去。每一段都能换掉,
对外则是一个兼容 OpenAI Realtime 的 WebSocket 与 WebRTC 服务。
本专题共 40 篇,不复述功能清单,而是回答四件事:
这个参数到底管什么、这条命令什么时候用、装不上怎么判定、该选哪个后端。
内容依据
官方仓库
的 README、arguments_classes/ 下的参数定义与 Realtime Engine 架构文档整理,核对日 2026-08-09。
本专题内容为仓库源码与文档口径,我们没有安装、部署或调用过该服务,
文中毫秒值均为参数默认值而非实测延迟;
参数随版本变动,请以 speech-to-speech serve -h 的实际输出为准。
speech-to-speech 是什么:一条能换掉每一段的语音代理流水线
speech-to-speech 是 Hugging Face 的开源语音代理流水线:VAD、STT、LLM、TTS 四段级联,每段后端都能用一个 CLI 参数换掉,对外暴露兼容 OpenAI Realtime 的 WebSocket 接口。本文按仓库 README 与参数源码讲清它的结构、三个反直觉的默认值,以及那些毫秒参数到底买的是什么。
架构与概览
VAD、STT、LLM、TTS 四个组件各跑一个线程、用队列相连——这是理解后面一切延迟与打断行为的前提。也包括那个反直觉的事实:项目叫 local voice agents,默认配置的 LLM 却打在云上。
`--num_pipelines` 默认是 1:多人用之前必须先动它
speech-to-speech 的实时服务默认只开一个流水线实例,也就是只承载一个并发会话,多出来的连接会被直接拒绝。这篇讲清 `--num_pipelines` 的语义、池化单元里装了什么、两种传输为什么共用一个池,并给出「该设多大、什么时候动它没用」的判断依据。
96 个 Python 文件怎么读:按目录找到你要改的地方
speech-to-speech 的 src 下有 96 个 Python 文件、131 个 CLI 参数,从头读一遍不现实。这篇按「你想改什么」倒推该开哪个目录:调回合判定去 VAD、换中文识别去 STT、改工具调用去 LLM、改线上事件去 api/openai_realtime,另给参数名与事件名两条反查线索。
名字里写着 local,默认配置的 LLM 却在云上
speech-to-speech 的仓库描述写着 Build local voice agents,但官方给出的默认等价命令里,LLM 那一环走的是 OpenAI Responses API 的 gpt-5.4-mini,真正跑在本机的只有 VAD、STT 和 TTS 三段。这篇把这套默认配置逐段拆开对号入座,给出四个「怎么确认自己这套到底连没连外网」的判定动作,再按你的处境倒推该走独立 llama.cpp 进程、进程内本地后端还是 macOS 预设,最后说清中文支持、并发数量与暴露服务这三个最容易连带踩的坑。
VAD 到 STT 到 LLM 到 TTS:四个线程、三道队列
speech-to-speech 把语音代理拆成 VAD、STT、LLM、TTS 四段级联,每段跑在自己的线程里、用队列相连。这篇按官方 README 与源码口径讲清三道队列各自搬的是什么、六步数据流在哪一步分叉、世代计数为什么非有不可,以及排查问题时怎么按队列边界把现象定位到具体某一段,而不是笼统地抱怨"响应慢"。
安装与部署
默认装了什么、七个 pip extras 该装哪些,以及两个真会卡住人的坑:Qwen3-TTS 的 wheel 默认盯着 CUDA 12.8,DeepFilterNet 和 Pocket TTS 的 numpy 版本互相排斥。另有 Docker 与断网离线运行。
从 pip install 到第一次 serve:默认装了些什么
speech-to-speech 一行 pip install 就装上了,但默认拼出来的是 Parakeet TDT 做转写、responses-api 把请求打向 OpenAI 兼容接口、Qwen3-TTS 负责出声这一套组合。本文按官方 README 与 arguments_classes 目录下的参数定义,逐条拆开默认安装究竟装了什么、Linux 上 CUDA wheel 错配与 numpy 版本互斥这两个坑该怎么绕过去、第一次 serve 之后应该用哪条命令来验收结果,以及哪几类场景下这套默认组合从一开始就不适合你,需要提前换掉。
断网也能跑:HF_HUB_OFFLINE=1 之前要先做的事
speech-to-speech 的 README 用几句话交代了离线运行,但真照着做很容易只离线了一半——环境变量只管模型资产的拉取,默认的 responses-api 后端照样往远程打。这篇把断网前要做的准备拆成可复制的命令:预热缓存、把 LLM 换成本地 llama.cpp、单独安置 Smart Turn 的 ONNX 检查点,并给出逐条验收动作与四类不适用场景。
七个 pip extras:哪些该装,哪些别碰
speech-to-speech 的 README 在可选组件小节列了一串 pip extras,装错的代价不是报错而是静默走回默认路径。这篇按「你的平台是什么、要不要中文、跑不跑 GPU」倒推出决策路径,逐个说清七个模型侧 extra 各自换掉流水线的哪一级、哪些在你的平台上根本是内置的不用装、哪两个之间存在 numpy 版本硬冲突,并给出装完之后怎么用 -h 验收、以及哪几个默认值会让你以为换了后端其实没换。
DeepFilterNet 要 numpy 小于 2,Pocket TTS 要大于等于 2
speech-to-speech 的 README 单列了一条依赖互斥:DeepFilterNet 要 numpy<2,Pocket TTS 要 numpy>=2,装进同一环境必然打架。按排查顺序讲透:用哪条命令看环境在哪一侧、文档给出的处置、改完怎么验证,以及哪些现象其实是 CUDA wheel 错配而非 numpy。
docker compose up 起来的是两个服务,不是一个
speech-to-speech 的 docker compose up 起来的不是一个服务:它同时拉起跑 Gemma 4 的 llama.cpp 服务与实时服务,暴露 8080 与 8765 两个端口。本文讲清两个端口各自对应仓库里哪段配置、怎么做协议层冒烟检查,以及并发数默认为 1、绑定地址默认走环回这两个最容易翻车的默认值。
Qwen3-TTS 装不上:那个 wheel 默认盯的是 CUDA 12.8
speech-to-speech 默认 TTS 是 Qwen3-TTS,非 macOS 平台默认走 GGML 后端,而它依赖的 qwentts-cpp-python 在 PyPI 上的默认 wheel 面向 CUDA 12.8。本文按官方 README 的口径,把这条安装期陷阱拆成怎么判定、怎么处置、处置后怎么验证、以及什么情况说明根本不是它,顺带列出容易被误认成同一个问题的几种情形。
命令与配置
serve、talk、local 三个命令的分工,一条默认 serve 背后的 14 个参数,macOS 预设做了什么又被什么覆盖,以及那个不报错的坑:非激活后端的参数只会带个警告被忽略。
`--mac-optimal-settings` 做了什么,又被什么覆盖
speech-to-speech 在 Apple Silicon 上提供了一个一键预设开关,官方 README 说它只做四件事,而且优先级排在所有显式参数之后。这篇把预设的四项内容、能覆盖它的六类参数、可直接复制的命令写法、验收时该看哪一行输出,以及哪些场景根本不该指望这个开关,按仓库文档与参数导出的口径逐条讲清楚。
参数写错了不报错:非激活后端的选项只会带个警告被忽略
speech-to-speech 的 CLI 只为当前选中的后端构造配置,未激活后端的已知选项仍会被接受、只带一个警告就忽略掉。这篇按排查顺序讲清楚:怎么用带选择器的 -h 判定某个参数是不是根本没进配置、参数前缀和 arguments_classes 下的定义文件如何对应、处置后拿什么验证,以及哪些症状其实是钳制规则、平台级忽略或废弃开关造成的,不该往这个方向查。
老教程里的 --mode 为什么跑不通了
网上大量 speech-to-speech 的旧文都在用 `--mode realtime` 和 `--mode local` 起服务,照抄下来轻则打印一行警告,重则直接退出。这篇按排查的路子走一遍:先给出三种典型现象,再给可执行的判定动作(看 `-h` 输出、核对参数导出、看启动首行有没有警告),然后按官方迁移语义把老命令逐条改写成 `serve` / `talk` / `local`,最后单列一节说明哪些失败其实与 `--mode` 毫无关系(安装依赖冲突、并发上限、离线缓存、组件已下架),免得你把力气花错了地方。
默认绑 127.0.0.1 是有道理的:改成 0.0.0.0 之前想清楚
speech-to-speech 的 serve 命令默认把 Realtime 服务绑在 127.0.0.1,官方 --host 的帮助文本里直接写着「显式传 0.0.0.0 才会把未认证的 API 暴露到网络上」。这篇按官方参数语义梳理 8765 端口上到底有哪些入口、改 host 之前必须确认什么、端口转发与显式暴露两条路各自怎么配、事后逐项怎么验收,以及哪些场景根本不该动这个默认值。文中毫秒与端口均为参数默认值,非实测数据。
一条 serve 背后的 14 个默认参数,逐个说清楚
`speech-to-speech serve` 看着是一条裸命令,README 却给出了它等价的 14 个显式参数。这篇逐个拆开讲:默认 STT 只覆盖 25 种欧洲语言、默认 LLM 走的是云端 Responses API、六个 Qwen3-TTS 旋钮在不同平台各有几个不生效,再补上绑定地址、端口与并发数这三个没写进命令、却常决定成败的默认值。
serve、talk、local:三个命令分别在什么场景
speech-to-speech 的命令行只有 serve、talk、local 三个入口,但它们分的不是「运行方式」,而是「谁托管流水线」和「谁推音频」这两件事。本文按官方 README 与参数定义梳理三者的绑定地址、并发上限、预设优先级差异,给出一条可以自己走完的选择路径,顺带说清 --mode 弃用后老教程为什么全跑不通。
VAD 与回合判定
19 个 VAD 参数、Smart Turn 的推测式回合、384 与 192 的迟滞设计、三个重开窗口的钳制关系。这一组还讲清一件事:那些毫秒值买的不是延迟,是「误判成本」。
19 个 VAD 参数逐个讲:默认值背后的意图
speech-to-speech 的 vad_arguments.py 里有 19 个参数,光看 --help 很难判断该动哪个。这篇按「基础切分、迟滞、重开窗口、Smart Turn、实时转写」五组把默认值拆开,讲清 min_silence_ms 为什么只有 64 毫秒、384 与 192 为什么要分家、三个重开窗口谁钳制谁,并说明这些毫秒值是可配置等待而非真实链路耗时,不能相加当作端到端结论。
384 与 192:为什么接续一轮的门槛只有开新轮的一半
speech-to-speech 的 VAD 参数里有一对反直觉的默认值:判定「开一个新轮次」需要 384 毫秒活跃语音,而「接续一个还能重开的旧轮次」只要 192 毫秒。这篇把这套迟滞机制拆开讲:可重开轮次是怎么来的、两个门槛为什么不对称、参数被钳制在什么区间、什么情况下你调它根本不会生效,以及遇到切分太碎时真正该动的是哪个参数。
800、2000、7000:三个重开窗口的层级与钳制关系
speech-to-speech 里三个重开窗口的参数默认值分别是 800、2000、7000 毫秒,它们不是可以相加的延迟,而是三层管辖范围不同、彼此还带钳制关系的轮次可重开有效期。本文按官方参数 help 原文讲清三者的层级、两条会导致设置静默失效的钳制条件,以及想改变接续行为时到底该动哪一个参数。
两个长得很像的实时转写开关,别调错
speech-to-speech 里有两个名字几乎一样的实时转写开关:模块级的 --enable_live_transcription 默认开着,VAD 侧的 --enable_realtime_transcription 默认关着。很多人以为自己开了实时转写,其实动的是另一个。连它们各自的间隔参数和最小静音参数都撞脸。这篇按现象、判定动作、参数语义给出的处置、处置后怎么验证、以及什么情况说明不是这个原因五步走,把这两组参数的归属、默认值和作用范围一次拆开讲清楚。
切得太碎怎么办:--short_segment_merge_ms 的补救逻辑
speech-to-speech 的 VAD 默认用 64 毫秒静音就切一刀,而短于 384 毫秒的语音段又不算有效语音,两个默认值撞在一起就会把一句话切成一堆被丢掉的碎片。本文按官方参数 help 的语义,拆解怎么判定自己撞上的是不是这个问题、--short_segment_merge_ms 这个默认关着的开关到底缝的是什么、开完之后从哪几个协议事件上验证,以及哪几种情况说明根本不该动它。
语音对话的延迟预算怎么拆:哪几段是配置,哪几段是算力
语音代理"说完半天不回话",有的是参数里写死的等待,有的是模型推理在跑,两者的处置方式完全相反。本文按 speech-to-speech 仓库的参数默认值与 Realtime 架构文档,把一次回合拆成配置等待段与算力段,说明每段由谁决定、能从哪个协议事件划出边界、以及为什么这些毫秒数绝对不能相加。
Smart Turn:先干活再决定要不要认账
speech-to-speech 在 Silero 判定「说完了」之后又挂了一层 Smart Turn v3.2 端点判定,很多人把它当成延迟旋钮,于是要么关掉要么乱调阈值。这篇按仓库参数定义与 README 的机制描述,讲清推测性开工、完整轮次的提交窗、不完整轮次的两段延迟、修订版作废后输出怎么丢弃。
Realtime 协议与传输
5 个上行、15 个下行事件怎么读,WebRTC 模式的三处不一样,什么情况下非得自架 TURN,以及 LLM 代理那条官方自己写明的安全边界:不认证、不限流。
--enable_llm_proxy:不认证、不限流,官方自己说的
speech-to-speech 的 --enable_llm_proxy 把服务端配好的远端 LLM 也暴露成一个 OpenAI 兼容端点,供摘要、起标题这类侧边任务并发调用。但官方 README 写得很直白:服务端自己不做认证,也不做限流。本文讲清它的开启条件、命令怎么组、两条路径分别对应哪个后端、怎么验收、什么情况别开。
5 个上行、15 个下行:Realtime 事件表怎么读
speech-to-speech 的 Realtime 协议只有 20 个事件,难的不是名字是时序:response.created 要等第一个音频块才发,被打断时三个终态事件排在 speech_started 前面,助手转写已从块级 done 改成 delta 拼接。本文按上行 5 个、下行 15 个拆表,讲清每类事件该绑客户端哪个状态,以及两种传输下哪几条不通用。
从一段 base64 PCM 到一段合成语音:六步数据流
speech-to-speech 的实时服务把客户端发来的一段 base64 PCM 变成一段合成语音,中间要经过重采样切块、VAD 定边界、STT 出转写、LLM 生成、TTS 回流、会话配置深合并六个环节。这篇按仓库内 Realtime Engine 架构文档的口径逐步拆开每一步的输入输出与交接队列,重点讲三个最容易把客户端写错的时机差异:`response.created` 的发出时点、助手转写为什么必须改到 delta 上渲染、以及注入上下文为什么不会触发生成,并给出 WebSocket 与 WebRTC 两种传输的选择依据。
老客户端要改:转写从 done 迁到 delta
speech-to-speech 的 Realtime Engine 文档单开一节说明了一个破坏性变更:助手转写以 response.output_audio_transcript.delta 流式发出,而终态 done 一个响应只发一次。老客户端如果仍把每个块级 done 当成渲染事件,升级之后字幕就会整轮憋到最后才刷出来,而且不报任何错。本文按排查五步走,给出可执行的确认动作、文档语义给出的处置方式、改完之后的验证办法,以及哪几种情况说明你遇到的问题根本不在这里。
什么情况下你非得自己架一台 TURN
speech-to-speech 同时提供 WebSocket 与 WebRTC 两种传输,只有走 WebRTC 才需要考虑 ICE 打洞。这篇按官方文档口径讲清楚:怎么确认自己在 WebRTC 这条路上、不配 ICE 时的默认行为、哪两种部署形态官方点名必须架 TURN,以及环境变量怎么写、按三段怎么验收。
session.update 深合并进 RuntimeConfig:配置是处理时现读的
speech-to-speech 的 Realtime 服务把客户端发来的 session.update 深合并进一个共享的 RuntimeConfig,VAD、LLM、TTS 在处理时现读它,而不是重启才生效。本文按官方仓库的 Realtime Engine 架构文档梳理这条配置链路:深合并意味着只发要改的子树、生效与否以 session.updated 回读为准、哪些改动该走 response.create 的按响应覆盖、WebRTC 下 session.created 时机不同带来的客户端写法差异,以及会话级配置与进程级 CLI 参数之间那条容易踩空的边界。
WebRTC 模式的三处不一样,尤其那个被拒绝的事件
speech-to-speech 的 serve 命令同时暴露 WebSocket 与 WebRTC 两种传输,官方文档说两者协议相同,却又单独列出了三处差异:上行的 append 事件会被服务端直接拒绝、output_audio_buffer.clear 只有 WebRTC 一侧支持、session.created 的发送时机从连接时挪到了数据通道打开时。本文按仓库文档与源码口径,讲清这三处差异各自的成因、对现有客户端代码的具体影响,以及什么处境下该选 WebRTC、什么部署必须先准备好 TURN 服务器再上线。
工具调用与打断
同一个功能,云端 API 是协议原生能力,本地模型要靠提示词约定加正则抠。以及 CancelScope 用世代计数做取消——一套可以直接搬进自己项目的并发取消范式。
工具结果回流四步:为什么第 2 步不触发生成
把工具执行结果发回 speech-to-speech 之后助手一声不吭,不是服务挂了:conversation.item.create 只负责把结果注入上下文,这一步不触发生成。本文按仓库内 Realtime Engine 文档拆开工具结果回流的四步,讲清什么时候必须补发 response.create、什么时候可以就此收手,以及怎么从事件流判断卡在哪一步。
三条可以搬走的并发取消经验
speech-to-speech 的打断处理把「取消一个正在流式输出的响应」这件事拆成了世代计数、队列保留清单和当前世代放行三块。这三条不绑语音场景,任何有流式输出、又允许中途叫停的系统都能照着搬。本文按官方 README 的机制原文讲清每条的做法、它替掉了什么旧写法,以及你手上的系统要不要跟着改的判断依据。
同一个功能,云端是协议原生,本地靠正则抠
speech-to-speech 的工具调用在两个 LLM 后端上走的是完全不同的两条路:OpenAI API 侧由 client.responses.create 直接返回结构化 function_call,本地 transformers / mlx-lm 侧要把 JSON Schema 转成 Python 函数签名注入系统提示词,再用正则从 code 块里把调用抠出来。本文按官方 README 与源码口径拆开这两条路、它们汇合的位置,以及选后端时真正该看的几条硬约束。
用户一开口,服务端这八步依次发生
speech-to-speech 处理抢话不是「把音频停掉」这一个动作,而是 Realtime Engine 文档里写死的八个步骤:VAD 事件入队、终态事件补齐、世代递增与冲队列、打断门控、handler 自查过期、丢弃守卫、客户端主动取消、伪取消保护。本文逐步拆开这条链路,说明每一步在防哪类故障,以及你写客户端时该按什么顺序渲染这些事件。
用世代计数取消,比取消标志加时间窗健壮在哪
speech-to-speech 的 Realtime Engine 用一个 CancelScope 对象同时管住世代计数器与丢弃标志,取代了旧的双信号模式。本文按官方架构文档拆开这套取消机制:世代号怎么让旧响应瞬间过期、丢弃守卫在哪三种情况下清除、「当前世代永远放行」为什么能防住静默吞响应,以及你的流水线该不该照抄。
后端选型
六个 STT、五个 TTS、四种 LLM 后端怎么选。特别提醒中文读者:默认 STT 只覆盖 25 种欧洲语言,想说中文得先换掉它。
六个 STT 后端怎么选:先看平台,再看语言
speech-to-speech 的 STT 这一级有六个可换后端,默认的 Parakeet TDT 只覆盖 25 种欧洲语言,中文场景一上来就得换。这篇不罗列参数,而是给一条从你的处境倒推的决策路径:先按操作系统砍掉一半选项,再按语言定下具体后端,然后确认权重从哪来、要不要进生产,最后交代参数前缀不统一、未激活后端参数被静默忽略这两个选型期最容易踩的坑。
默认 STT 只覆盖 25 种欧洲语言:想说中文得先换它
speech-to-speech 的默认 STT 是 Parakeet TDT,其语言参数 help 写明只支持 25 种欧洲语言,中文这条链第一环就断了。本文按平台、语种、联不联网、是否进生产四个问题给出换 STT 的决策路径,列出 Whisper 系与 Paraformer 的默认检查点与语言参数写法,并说明哪些维度官方没给数据。
五个 TTS 后端怎么选:默认那个不一定适合你
speech-to-speech 的 TTS 这一级挂了五个可换后端,装完默认走 Qwen3-TTS。但默认设备写死 cuda、GGML wheel 挑 CUDA 版本、语言覆盖各不相同,纯 CPU 机器和中文场景往往都不该用默认。这篇不罗列参数,而是从你的机器、语言、安装成本、是否要克隆音色、是否进生产这五个处境倒推到结论,并说清哪些维度官方没给数据、我们不比。
LLM 后端选型:托管、自托管、进程内三条路
speech-to-speech 的 LLM 那一环有三种接法:走服务商 API、指向自己机器上的 vLLM 或 llama.cpp、把模型直接跑进流水线进程。这篇不罗列参数,而是从平台、要不要中文、联不联网、是否进生产这四个处境倒推到结论,并说清 responses-api 与 chat-completions 该怎么选。
想把语音代理真正接进自己的产品,而不只是跑个 demo?
从 AI 编程实践到 Agent 工程落地,站内有成体系的教程与课程。