如果学习只在于模仿,那么我们就不会有科学,也不会有技术。——高尔基

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” 部分把整个系统拆成了四个线程化的组件,每个组件通过队列连接:

  1. VAD
  2. STT
  3. LLM
  4. 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
2
3
pip install speech-to-speech
export OPENAI_API_KEY=...
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
2
3
4
speech-to-speech \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key ""

这几行命令很能体现这个项目的气质:它不是在问“你愿不愿意全部用默认配置”,而是在给你一条明确路线,把最耗算力、也最核心的一段推理链路,尽量拉回自己的机器。

支持的组件非常丰富,而且被整理成了清楚的矩阵

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
2
3
4
5
6
7
8
9
10
11
12
13
# CUDA 13.x
pip install "qwentts-cpp-python==0.3.1+cu130" \
-f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cu130

# CUDA 12.4
pip install "qwentts-cpp-python==0.3.1+cu124" \
-f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cu124

# CPU-only fallback
pip install "qwentts-cpp-python==0.3.1+cpu" \
-f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cpu

pip install speech-to-speech

这种说明方式很有“真正在服务用户”的味道。它不是停留在理想环境,而是直接承认不同机器上依赖可能不一样,并把替代路径准备好。

可选后端也可以按 extra 安装

README 同样给出了 extras 安装方式:

1
2
3
4
5
6
7
8
pip install "speech-to-speech[kokoro]"
pip install "speech-to-speech[pocket]"
pip install "speech-to-speech[chattts]"
pip install "speech-to-speech[facebook-mms]"
pip install "speech-to-speech[faster-whisper]"
pip install "speech-to-speech[whisper-mlx]"
pip install "speech-to-speech[paraformer]"
pip install "speech-to-speech[mlx-lm]"

这种写法很利落:想要什么,就装什么。一个语音代理系统本来就很容易在依赖上变得臃肿,而把后端做成 extras,恰好让它保持了灵活性。

它不只有一种运行方式,而是把不同部署姿势分得很明白

README 的 Run Modes 一节非常重要。它列出了四种模式:

  • realtime
  • local
  • websocket
  • socket

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from openai import OpenAI

client = OpenAI(
base_url="http://localhost:8765/v1",
websocket_base_url="ws://localhost:8765/v1",
api_key="not-needed",
)

with client.realtime.connect(model="local") as conn:
conn.send(
{
"type": "session.update",
"session": {
"type": "realtime",
"instructions": "You are a helpful assistant.",
"audio": {
"input": {
"turn_detection": {
"type": "server_vad",
"interrupt_response": True,
}
}
},
},
}
)

for event in conn:
print(event.type)

这段示例非常具有说服力,因为它把“兼容性”从一句口号变成了实际调用方式。对很多已有 OpenAI Realtime 客户端或设备的人来说,这意味着切换后端的门槛会低很多。

它还带了一个 LLM Proxy 功能

README 提到,启用 --enable_llm_proxy 后,realtime server 还能把自己配置好的远程 LLM 暴露成普通的 OpenAI compatible endpoint。这样客户端就可以在同一后端上做总结、标题生成或其他旁路任务。

支持的路径包括:

  • POST /v1/chat/completions
  • POST /v1/responses

但 README 也非常明确地提醒:这个代理本身不做认证,也不做限流,所以只能放在可信网络中,或者部署在具备访问控制的网关之后。

这种写法再次体现了它的成熟:它不仅告诉你“能做什么”,也同时提醒你“别在哪些环境里乱开”。

LLM 后端选择被讲得很细,几乎像一份实战指南

README 把 LLM 说成整条链路里最耗算力、延迟最高的部分,这判断很现实。因此,它也花了很大篇幅来解释 LLM Backends。

它支持三类来源:

  • 本地推理
  • 自建服务器
  • 提供商 API

responses-apichat-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
2
3
4
5
speech-to-speech \
--stt parakeet-tdt \
--language auto \
--llm_backend mlx-lm \
--model_name "mlx-community/Qwen3-4B-Instruct-2507-bf16"

或者明确指定中文:

1
2
3
4
5
6
speech-to-speech \
--stt whisper-mlx \
--stt_model_name large-v3 \
--language zh \
--llm_backend mlx-lm \
--model_name mlx-community/Qwen3-4B-Instruct-2507-bf16

这类说明很有价值,因为语音项目最容易被一句“支持多语言”含糊带过,而这里反而把“依赖于你怎么配”讲得清清楚楚。

Pocket TTS 也被单独拿出来讲

README 还专门写了一节 Pocket TTS,说明它支持流式 TTS 和 voice cloning,用法例如:

1
2
3
4
speech-to-speech \
--tts pocket \
--pocket_tts_voice jean \
--pocket_tts_device cpu

并列出了可用的 voice presets,包括:

  • alba
  • marius
  • javert
  • jean
  • fantine
  • cosette
  • eponine
  • azelma

这部分虽然不长,但很有趣,因为它让这个系统不只是“能说话”,而是开始有了“怎么说”的具体层次。

它已经被用于真实生产环境

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
2
3
uv sync
pytest
ruff check

这类说明不花哨,但很实用。它说明这个项目在工程协作上并没有故作神秘,开发者想参与进来时,入口是清楚的。

这是一个把语音代理拆开给你看的项目

speech-to-speech 最吸引人的地方,可能不是“它能说话”,而是“它把会说话这件事拆解成了一套你能理解、能替换、能部署、能接管的模块系统”。

它把语音交互这件事从一个混沌整体中拆了出来:

  • 用 VAD 判断轮次
  • 用 STT 识别语音
  • 用 LLM 做推理与工具调用
  • 用 TTS 生成回答
  • 用 Realtime 协议暴露能力
  • 用可选后端让每段都能替换
  • 用多种运行模式适配不同接入方式
  • 用 demo 把它变成可直接体验的对话界面

这种感觉很像把一台原本封死外壳的机器拆开,把齿轮、传动轴和接口一个个摆在桌面上。你终于不只是一个“使用语音助手的人”,你也可以成为“配置语音助手的人”“部署语音助手的人”“重新组合语音助手的人”。

而这,正是开源项目最迷人的地方之一:它不是只给你一个答案,它把生成答案的路径,也一起交到了你手里。