speech-to-speech
如果学习只在于模仿,那么我们就不会有科学,也不会有技术。——高尔基
https://github.com/huggingface/speech-to-speech
当语音助手不再神秘:speech-to-speech 试着把整条语音链路交回你手里
项目地址:https://github.com/huggingface/speech-to-speech
很多人一提到语音助手,脑子里先冒出来的往往不是“可控”,而是“黑盒”。你听见一句话被识别、被理解、被回答、再被说出来,可中间发生了什么,通常像一条被幕布遮住的流水线。speech-to-speech 的迷人之处就在于,它把这条流水线一段段拉到台前,告诉你:这里是 VAD,这里是 STT,这里是 LLM,这里是 TTS,而且每一段都可以替换。
从仓库 description 来看,它的定位很直接:用开源模型构建本地语音代理。而 README 则把这个想法完整落地成一句更有画面感的描述:这是一个低延迟、完全模块化的语音代理管线,流程是 VAD -> STT -> LLM -> TTS,并通过一个兼容 OpenAI Realtime 的 WebSocket API 暴露出来。
这句话非常关键。它几乎把整个项目的灵魂都说透了:低延迟、模块化、语音全链路、兼容标准接口、并且组件可替换。它不是在做一个只能跑某种固定模型的演示,而是在搭一条开放、可交换、可部署的语音基础设施。
一条会“开口说话”的四段式流水线
README 的 “How it works” 部分把整个系统拆成了四个线程化的组件,每个组件通过队列连接:
- VAD
- STT
- LLM
- TTS
这种结构非常朴素,但也非常有力量。因为一旦你把语音助手拆成这样的四段,就会发现它不再只是一个模糊的“AI 对话能力”,而是一个能被明确配置、明确替换、明确调优的系统。
VAD:先判断你是不是在说话
第一层是 Voice Activity Detection。README 指出默认使用的是 Silero VAD v5,用于检测语音边界和轮次切换。
这一步很像整条链路的门卫。它决定什么时候该认真听,什么时候一句话已经讲完,什么时候用户可能正在打断。语音交互是否自然,很多时候并不只取决于模型多强,也取决于这道“开关”够不够灵敏、够不够稳。
STT:把声音变成文字
第二层是 Speech to Text。README 的描述很克制:它负责转写用户说的话,并且支持可选的实时部分转录。
这里有个细节很重要:不是单纯“转写”,而是允许 live partial transcripts。这意味着它在意实时感,而不是等整段话结束后才慢慢回放一次理解结果。
LLM:真正开始思考并生成回答
第三层是 Language Model。README 说它会生成响应,并支持流式文本与工具调用。
这就把语音助手和普通文本 agent 的关系搭了起来:前面两层负责“听见你说什么”,这一层才开始“知道该怎么答”,而且不仅能回答,还能流式输出,并可能发起工具调用。它没有把语音系统和通用 agent 世界切断,而是明确把二者接到了一起。
TTS:最后把回答重新说出来
第四层是 Text to Speech。这一层负责把文本响应合成为语音,再流回客户端。
如果说前面三层像在后台做理解和推理,那么 TTS 就是整套系统真正“露面”的那一刻。用户最终感受到的是一段会说话的回答,而 speech-to-speech 把这一步也做成可替换模块,而不是固定死的终点。
每一段都可以换,这才是它真正的个性
README 非常强调一点:每个阶段都有多个可互换的后端,并且通过 CLI 参数选择。甚至连 LLM 这一槽位,也不是绑定某一家服务,而是可以接本地模型、自建服务器、或兼容 OpenAI API 的提供方。
这会让整个项目有一种非常鲜明的“乐高感”。你不是在被迫接受一套焊死的方案,而是在拿着一盒组件,自己决定:
- 用哪种 STT
- 用哪种 LLM 后端
- 用哪种 TTS
- 跑在哪种模式下
- 接什么样的客户端
而 README 还特别提到,代码设计上就以易于修改为目标,并重点围绕 Transformers 和 Hugging Face Hub 上可用的模型来组织。这种表达很像在说:我不只是允许你替换,我甚至欢迎你动手去改。
快速开始非常直接
README 的 Quickstart 只有几行:
1 | pip install speech-to-speech |
运行后,它会在 ws://localhost:8765/v1/realtime 启动一个兼容 OpenAI Realtime 的服务器,默认使用:
- Parakeet TDT 作为本地 STT
- OpenAI-compatible LLM
- Qwen3-TTS 作为本地语音输出
这一组默认组合很有代表性。它不是“什么都内置在一个模型里”,而是明确拼接了一条可运行的默认路径,让用户先把语音链路跑起来,再逐步替换各个部分。
如果从源码运行,README 还给出了第二个终端中的对话方式:
1 | python scripts/listen_and_play_realtime.py --host 127.0.0.1 --port 8765 |
这类示例非常实用,因为它不只告诉你“服务起来了”,也顺手给你一条与它说话的路径。
它甚至考虑到了“我想把 LLM 也放在自己机器上”
README 在 Quickstart 紧接着给了一个很现实的分支:如果你更希望 LLM 也运行在本地机器上,可以用 llama.cpp 启动 Gemma 4:
1 | llama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full |
然后把 speech-to-speech 指向这个 OpenAI-compatible 的本地 LLM 服务:
1 | speech-to-speech \ |
这几行命令很能体现这个项目的气质:它不是在问“你愿不愿意全部用默认配置”,而是在给你一条明确路线,把最耗算力、也最核心的一段推理链路,尽量拉回自己的机器。
支持的组件非常丰富,而且被整理成了清楚的矩阵
README 用表格列出 Supported Components,这一段几乎像一张菜单。
VAD
- Silero VAD v5
STT
- Parakeet TDT
- Whisper(通过 Transformers)
- Faster Whisper
- Lightning Whisper MLX
- MLX Audio Whisper
- Paraformer
LLM
- OpenAI-compatible API
- Transformers
- mlx-lm
TTS
- Qwen3-TTS
- Kokoro-82M
- Pocket TTS
- ChatTTS
- MMS TTS
这种列法很好,因为它不是泛泛地说“支持多种后端”,而是把每一类组件的可选项、平台和安装方式都摊在读者面前。你可以很直观地感受到这个项目不是一个单一路径的 demo,而是一块可以不断替换零件的底板。
安装不只是一条 pip install,还给了很多现实场景的依赖路径
README 中 Installation 一节写得非常细。
它首先说明需要 Python 3.10+,然后指出默认安装会覆盖标准 realtime 路径,包括:
- Parakeet TDT
- OpenAI-compatible LLM API
- Qwen3-TTS
- 本地音频和 realtime server 模式
接着,它又讨论了更具体的依赖情境,比如 Linux 下 Qwen3-TTS 的 GGML 后端在 CUDA 版本上的问题,并分别给出了 CUDA 13.x、CUDA 12.4 和 CPU-only 的安装方式:
1 | # CUDA 13.x |
这种说明方式很有“真正在服务用户”的味道。它不是停留在理想环境,而是直接承认不同机器上依赖可能不一样,并把替代路径准备好。
可选后端也可以按 extra 安装
README 同样给出了 extras 安装方式:
1 | pip install "speech-to-speech[kokoro]" |
这种写法很利落:想要什么,就装什么。一个语音代理系统本来就很容易在依赖上变得臃肿,而把后端做成 extras,恰好让它保持了灵活性。
它不只有一种运行方式,而是把不同部署姿势分得很明白
README 的 Run Modes 一节非常重要。它列出了四种模式:
realtimelocalwebsocketsocket
realtime:标准化的语音 API 入口
默认模式是 realtime,通过 WebSocket,在 /v1/realtime 提供 OpenAI Realtime 协议。
README 明确说:当你要为应用或设备对接一个标准语音 API 时,就用这个模式。这个定位非常准确,它意味着项目不仅能自己说话,也能作为后端去服务别的客户端。
local:直接对着本机说话
local 模式走的是机器本身的麦克风和扬声器。适合你想不接任何外部客户端,直接和整条管线对话的场景。
这个模式非常像一个实验台:不必先搭应用,不必先接设备,先听一听整条链路会如何回应。
websocket:更轻量的原始音频 WebSocket
如果你不需要完整 Realtime 协议,只想要一个更简单的自定义客户端,那么可以使用 websocket 模式,传原始 PCM。
socket:最简 TCP 音频流
还有一个非常极简的 socket 模式。README 也写得很坦率:它只提供原始 PCM 音频流,不具备完整的 Realtime API 特性,例如打断处理、实时转录事件、工具调用等。
这个说明很有分寸。它没有把每种模式都说成“通用解法”,而是清楚地告诉你每条路的适用边界。
默认实时服务配置也被完整展开了
README 甚至把默认的 speech-to-speech 启动参数完整列了出来,包括:
- 阈值
- STT 为 parakeet-tdt
- LLM backend 为 responses-api
- TTS 为 qwen3
- Qwen3-TTS 的模型、speaker、语言、后端、量化
- 默认模型名为
gpt-5.4-mini - 启用 live transcription
- 运行模式为 realtime
这种展开很有价值。因为很多“开箱即用”的系统会把默认行为藏在内部,而这里恰恰相反:README 把默认值公开出来,让你知道现在这套链路到底是怎样拼起来的。
macOS 用户还有一套专门的优化入口
README 里有个很贴心的参数:--local_mac_optimal_settings。
1 | speech-to-speech --local_mac_optimal_settings |
它会自动:
- 使用 MPS
- 选择 Parakeet TDT 做 STT
- 选择 MLX LM 做 LLM backend
- 选择 Qwen3-TTS 做 TTS,并默认用
mlx-audio - 切换到
local模式
这个设计非常讨喜,因为它不是要求用户先读完半篇 README 再自己拼参数,而是把一套适合本地 Mac 的组合预先打包成一个入口。项目在这里显得非常体贴:它知道你也许不是来研究参数矩阵的,你只是想先跑起来。
它兼容 OpenAI Realtime 协议,这让接入变得非常自然
README 的 Realtime API 一节明确说明:realtime 模式通过 WebSocket 使用 OpenAI Realtime protocol,并且任何兼容 OpenAI Realtime 的客户端都可以连接。
还给出了 Python 示例:
1 | from openai import OpenAI |
这段示例非常具有说服力,因为它把“兼容性”从一句口号变成了实际调用方式。对很多已有 OpenAI Realtime 客户端或设备的人来说,这意味着切换后端的门槛会低很多。
它还带了一个 LLM Proxy 功能
README 提到,启用 --enable_llm_proxy 后,realtime server 还能把自己配置好的远程 LLM 暴露成普通的 OpenAI compatible endpoint。这样客户端就可以在同一后端上做总结、标题生成或其他旁路任务。
支持的路径包括:
POST /v1/chat/completionsPOST /v1/responses
但 README 也非常明确地提醒:这个代理本身不做认证,也不做限流,所以只能放在可信网络中,或者部署在具备访问控制的网关之后。
这种写法再次体现了它的成熟:它不仅告诉你“能做什么”,也同时提醒你“别在哪些环境里乱开”。
LLM 后端选择被讲得很细,几乎像一份实战指南
README 把 LLM 说成整条链路里最耗算力、延迟最高的部分,这判断很现实。因此,它也花了很大篇幅来解释 LLM Backends。
它支持三类来源:
- 本地推理
- 自建服务器
- 提供商 API
而 responses-api 和 chat-completions 两种后端又共用一套 --responses_api_* 参数,只是分别对应不同接口路径。
README 还列出了不同 provider / server 的 base URL 与 API key 形式,比如:
- OpenAI
- HF Inference Providers
- OpenRouter
- vLLM
- llama.cpp
这会让项目显得非常“接地气”。它并没有假装所有人都在同一套基础设施上工作,而是明确承认现实中的部署方式千差万别,于是提供了一套能接上这些世界的参数桥梁。
多语言支持不是单点承诺,而是取决于你选的组件组合
README 的 “Multi-Language Support” 部分写得很诚实:语言覆盖取决于你选的 STT 和 TTS 后端,而不是取决于这条管线本身。
它列举了:
- Parakeet TDT 默认支持 25 种欧洲语言
- Whisper 系列覆盖更广,但依赖具体 checkpoint
- Paraformer 默认更偏中文
- Qwen3-TTS 默认是多语言,且
--qwen3_tts_language auto - Kokoro、ChatTTS、MMS TTS 各有不同语言范围
接着 README 还给出了两种常见使用方式:
- 单语言:设置
--language - 自动语言切换:设置
--language auto
例如自动检测语言:
1 | speech-to-speech \ |
或者明确指定中文:
1 | speech-to-speech \ |
这类说明很有价值,因为语音项目最容易被一句“支持多语言”含糊带过,而这里反而把“依赖于你怎么配”讲得清清楚楚。
Pocket TTS 也被单独拿出来讲
README 还专门写了一节 Pocket TTS,说明它支持流式 TTS 和 voice cloning,用法例如:
1 | speech-to-speech \ |
并列出了可用的 voice presets,包括:
albamariusjavertjeanfantinecosetteeponineazelma
这部分虽然不长,但很有趣,因为它让这个系统不只是“能说话”,而是开始有了“怎么说”的具体层次。
它已经被用于真实生产环境
README 里有一句很有分量的话:这条管线已经在生产环境中运行,作为成千上万台 Reachy Mini 机器人的对话后端。
这句话本身就很能说明问题。它让整个项目从“开源实验管线”一下子变成了“已经被实际设备使用的后端系统”。而这也解释了为什么 README 在实时协议、延迟、可替换后端、客户端连接方式等方面写得这么细——这些不是抽象设想,而是已经被用在真实对话系统里的能力。
仓库里还带了一个浏览器语音聊天 Demo
除了主 README,仓库中还有 demo/README.md。这份文档描述的是一个 Realtime Voice Demo:一个浏览器中的语音聊天 UI,连接 speech-to-speech 后端,通过 WebSocket,或在某些场景下通过 WebRTC。
这份 README 展示了另一个维度:speech-to-speech 不只是后端命令行工具,它还已经具备浏览器侧演示形态,包括:
- 本地 quick start
- Docker 运行 demo
- WebSocket 与 WebRTC 的差异
/api/calls代理握手逻辑- Web search 工具
- Camera 工具
- 已部署 Space 下的 usage limits
- Settings、localStorage 项
- 浏览器侧音频管线说明
这让整个项目显得更完整:后端负责语音处理,前端 demo 则把它变成一个可以直接上手体验的语音聊天界面。
它连归档模型都交代得很清楚
仓库中还有 archive/README.md,说明一些已 sunset 的模型实现仍保留在仓库里,但已经不再接入 s2s_pipeline.py。其中包括:
- STT 的
moonshine - TTS 的
parler - TTS 的
melo - 对应的旧参数定义
这种安排很体面。它既没有假装这些实现不存在,也没有继续把它们暴露成默认选项,而是给了一个明确的归档位置。这种“让历史有地方可放”的处理方式,也是一种项目成熟度的体现。
开发与贡献路径同样很清楚
README 的 Contributing 部分写得简洁直接:
- 欢迎 issue 和 PR
- 对于较大的变更,建议先开 issue 讨论
- 本地开发使用
uv sync - 测试用
pytest - 代码检查用
ruff check
命令如下:
1 | uv sync |
这类说明不花哨,但很实用。它说明这个项目在工程协作上并没有故作神秘,开发者想参与进来时,入口是清楚的。
这是一个把语音代理拆开给你看的项目
speech-to-speech 最吸引人的地方,可能不是“它能说话”,而是“它把会说话这件事拆解成了一套你能理解、能替换、能部署、能接管的模块系统”。
它把语音交互这件事从一个混沌整体中拆了出来:
- 用 VAD 判断轮次
- 用 STT 识别语音
- 用 LLM 做推理与工具调用
- 用 TTS 生成回答
- 用 Realtime 协议暴露能力
- 用可选后端让每段都能替换
- 用多种运行模式适配不同接入方式
- 用 demo 把它变成可直接体验的对话界面
这种感觉很像把一台原本封死外壳的机器拆开,把齿轮、传动轴和接口一个个摆在桌面上。你终于不只是一个“使用语音助手的人”,你也可以成为“配置语音助手的人”“部署语音助手的人”“重新组合语音助手的人”。
而这,正是开源项目最迷人的地方之一:它不是只给你一个答案,它把生成答案的路径,也一起交到了你手里。
