名字里写着 local,默认配置的 LLM 却在云上
speech-to-speech 的 GitHub 仓库描述只有一行英文:Build local voice agents with open-source models。看到 local 这个词,多数人默认理解成「装完就断网也能用」。
但你照着 README 的 Quickstart 敲三行命令起来的那套流水线,LLM 那一环打的是 OpenAI。
这不是什么阴谋,README 自己把话说得很清楚——它给出了默认配置的等价完整命令,一眼就能看出来。问题在于绝大多数人不会去看那段等价命令,装完对着麦克风说话有反应,就以为一切都在本机跑。这篇就把这套默认值逐段拆开,重点不是复读文档,而是给你几条能自己下判断的动作:怎么确认你这台机器到底连没连外网,以及按你的处境该改哪一个参数。
先把默认值摊开
README 的 Quickstart 是这三行:
pip install speech-to-speech
export OPENAI_API_KEY=...
speech-to-speech serve
中间那行不是装饰。README 明确给出了 speech-to-speech serve 的等价完整命令:
speech-to-speech serve \
--thresh 0.6 \
--stt parakeet-tdt \
--llm_backend responses-api \
--tts qwen3 \
--qwen3_tts_model_name Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--qwen3_tts_speaker Aiden \
--qwen3_tts_language auto \
--qwen3_tts_backend ggml \
--qwen3_tts_non_streaming_mode True \
--qwen3_tts_mlx_quantization 6bit \
--model_name gpt-5.4-mini \
--chat_size 30 \
--responses_api_stream \
--enable_live_transcription
--llm_backend responses-api、--model_name gpt-5.4-mini——默认模型是通过 OpenAI Responses API 的 gpt-5.4-mini。你可以用 --model_name 覆盖,用 --responses_api_base_url 指向其他 OpenAI 兼容服务商或服务,但不改的话,它就是走 OpenAI 默认地址。
哪一段在本地,哪一段在外面
这条流水线是四段级联:VAD → STT → LLM → TTS,每个组件跑在自己的线程里,用队列相连。按默认值对号入座:
| 环节 | 默认后端 | 跑在哪 |
|---|---|---|
| VAD | Silero VAD v5 | 本机,内置 |
| STT | parakeet-tdt(CUDA / CPU 取 nvidia/parakeet-tdt-0.6b-v3,Apple Silicon 取 mlx-community/parakeet-tdt-0.6b-v3) | 本机 |
| LLM | responses-api + gpt-5.4-mini | 远端 |
| TTS | qwen3(Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice) | 本机 |
判断一个环节在不在本地,看它选的后端属于哪一类。 responses-api 和 chat-completions 这两个 LLM 后端本身不含推理,它们是 HTTP 客户端:--llm_backend responses-api 打 /v1/responses,--llm_backend chat-completions 打 /v1/chat/completions,两者共用同一组 --responses_api_* 连接参数。--responses_api_base_url 省略,就用 OpenAI 默认。真正在本机做推理的 LLM 后端是另外两个:transformers(CUDA / CPU)和 mlx-lm(Apple Silicon)。
作者为什么这么默认
README 自己给了理由:LLM 是流水线里计算最重、延迟最高的组件,一次大模型前向就可能主导端到端响应时间,所以按硬件与延迟预算选后端很重要。把最重的那一段丢给远端,pip install 完配个 key 就能说话,这是发行默认值的取舍。
理解这一点比记住参数更重要:「开箱即用」和「完全本地」在这个项目里就是两套不同的配置,不存在一套通吃。你要哪个,得自己选。
怎么确认你这套到底连没连外网
四个可执行的判定动作,从便宜到彻底:
一、看你有没有传本地 base URL。 命令行里既没有 --responses_api_base_url,--llm_backend 又是默认的 responses-api,那就是在打远端。这一条其实已经能定性。
二、-h 看实际生效的默认值。 用 speech-to-speech serve -h 查看当前组合的默认值与专属参数;要看另一种组合,把选择器放在 -h 前面,例如 speech-to-speech serve --stt mlx-audio-whisper -h。整套 CLI 有 131 个参数,只靠记忆对不上。
三、★ 别看你写了什么,看 --llm_backend 的实际取值。 这是最阴的一条:README 说明,CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略;JSON 配置同理,多余的非激活后端键也会被忽略。也就是说,你兴冲冲加了 --llm_device cuda --llm_torch_dtype float16,以为把模型摁在本地显卡上了,可只要 --llm_backend 还是 responses-api,这些参数就只是被警告一句然后照跑。程序不会报错停下——没有红字,不等于你改对了。
四、断网法。 依赖与模型资产都在本地之后,流水线可以断网运行,但 README 有一句关键提醒:不加本地 base URL 覆盖,默认的 responses-api 后端会去调远程服务,离线就断了。所以把网拔了跑一次,是最不容易骗自己的验证。
换成真本地,三条路
路 A:独立起一个 llama.cpp 进程
README 的「Fully Local」一节说,最省事的全本地做法是把 LLM 放到独立的 llama.cpp 进程里:
# 终端 1:llama.cpp 提供 Gemma 4
llama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full
# 终端 2:speech-to-speech 指向这个本地服务
speech-to-speech serve \
--stt parakeet-tdt \
--llm_backend responses-api \
--tts qwen3 \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key "" \
--responses_api_stream \
--enable_live_transcription
注意两个端口别搞反:8080 是 llama.cpp 的口,8765 才是 Realtime 服务自己的口(--port 默认 8765,默认服务地址 ws://localhost:8765/v1/realtime)。仓库自带的 docker compose up 起的也正是这两个:一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露 8080 与 8765。--responses_api_api_key 在 llama.cpp 这一栏官方写的就是空字符串;vLLM 那一栏是 http://localhost:8000/v1,key 可省略或填任意字符串。
这条路的好处是 LLM 进程和语音流水线解耦,重启一边不影响另一边。
路 B:进程内本地后端
不想多起一个进程,就让 LLM 在同一个进程里跑:Apple Silicon 用 --llm_backend mlx-lm,CUDA / CPU 用 --llm_backend transformers。这时候前面被忽略的那组参数才真正生效,它们的默认值是:--llm_device cuda、--llm_torch_dtype float16、--llm_gen_max_new_tokens 1024、--llm_gen_temperature 0.0、--llm_gen_do_sample False。
后两个值得留意:本地后端默认是确定性贪心解码。你如果觉得同一句话回来的答复一模一样、缺少变化,那不是模型坏了,是默认就这么设的。
路 C:macOS 预设
speech-to-speech local --mac-optimal-settings
也可以指定 LLM:
speech-to-speech local \
--mac-optimal-settings \
--model_name mlx-community/Qwen3-4B-Instruct-2507-bf16
这个预设做四件事:对支持的模型组件使用 MPS 默认值、STT 设为 Parakeet TDT、LLM 后端设为 MLX LM、TTS 设为 Qwen3-TTS 并使用 mlx-audio、默认取 6bit 的 MLX 变体。
优先级很容易搞反:预设只提供默认值。显式的 --device、组件级设备参数(如 --qwen3_tts_device),以及 --stt、--llm_backend、--model_name、--tts 全部优先于预设。另外,想暴露服务而不启动麦克风客户端时,把这个开关用在 serve 上而不是 local 上——local 是把 serve 和 talk 在同一进程里用环回组合起来的。
还想用云,只是不想用 OpenAI
那就只换连接参数。官方给的服务商对照表:HF Inference Providers 用 https://router.huggingface.co/v1 配 $HF_TOKEN,OpenRouter 用 https://openrouter.ai/api/v1 配 $OPENROUTER_API_KEY,OpenAI 则省略 base URL、用 $OPENAI_API_KEY。
什么时候该从默认的 responses-api 切到 chat-completions?README 给了两个具体理由:一是服务商在 Responses 路径上忽略 chat_template_kwargs.enable_thinking,你需要 reasoning_effort 这个旋钮来抑制推理;二是该服务的 Responses 流式工具调用路径不可靠,而它的 Chat Completions 工具调用流式是稳的——README 点名这在某些 vLLM 构建上有用,并指向 issue #312(我们没有读过这个 issue 的正文)。对应的参数是 --responses_api_reasoning_effort none。
关掉推理这件事本身就是个权衡:语音对话里模型多想几秒,用户听到的就是干等。
按处境倒推
- 只想先听个响、手上正好有 key:什么都别改,接受 LLM 在云上。
- 数据不能出内网:路 A,独立 llama.cpp,
--responses_api_base_url指到本机。 - Apple Silicon 单机玩:路 C 加
--llm_backend mlx-lm。 - 有 CUDA 机器、不想多起进程:路 B,
--llm_backend transformers。 - 要做中文:无论走哪条路,都得先换 STT。默认的 Parakeet TDT 只覆盖 25 种欧洲语言,
--language默认是en。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,也就是默认只承载一个并发会话。这是多用户部署第一个要动的参数。
真要离线,还有一步
断网前先在联网状态下用完全相同的配置启动一次,让它缓存好 STT、LLM、TTS、Silero VAD、NLTK 和 Smart Turn 需要的资源。之后:
HF_HUB_OFFLINE=1 speech-to-speech serve \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key ""
Smart Turn 用的是单独的 ONNX 检查点:已缓存的检查点配合 HF_HUB_OFFLINE=1 可用;要做不依赖缓存的显式配置,传 --smart_turn_model_path /path/to/smart-turn-v3.2-cpu.onnx;检查点拿不到就传 --no_smart_turn 关掉它。
三个连带的坑
暴露服务这件事得单独说。 serve 默认绑定 127.0.0.1,要暴露到网络必须显式传 --host 0.0.0.0——官方 help 的原话就是「显式传 0.0.0.0 才会把这个未认证的 API 暴露到网络上」。local 则永远绑环回,并把自带客户端连到 ws://127.0.0.1:<port>/v1/realtime。
--enable_llm_proxy 默认 False,开启后服务会把它配置的远端 LLM 也作为普通 OpenAI 兼容端点暴露出来。官方对这条的安全声明必须原样带出:服务端自己不做任何认证,也不做任何限流;只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面。 换个绑定地址、换个端口都不构成安全措施,这里不存在「这样配就安全了」的说法。
老教程跑不通多半是 --mode。 这个参数已废弃且很快将停止工作。迁移窗口内 --mode realtime 等价于 serve、--mode local 等价于 local,两者都会打印警告;其余所有 mode 取值已被移除,会带着「改用新命令」的提示直接退出。网上不少旧文还在用它。
别把参数默认值当成延迟。 这套 CLI 里散落着一堆毫秒值,它们是流水线自己引入的可配置等待,不含 STT / LLM / TTS 的推理耗时,把它们加起来当成「端到端延迟」是错的。仓库确实提供了对比 MLX 量化变体的 scripts/benchmark_tts.py,但那是给你自己跑的。
最后一句关于口碑:这个仓库在 2026-08-09 的快照里 star 是 11893,README 也提到它作为数千台 Reachy Mini 机器人的对话后端在生产环境运行。这些是事实,但它们推不出延迟、稳定性或者「适合你的场景」的任何结论——那得你自己在自己的机器上跑一遍。
延伸阅读
本文依据 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 原文为准,本文不构成法律意见。