断网也能跑:HF_HUB_OFFLINE=1 之前要先做的事
speech-to-speech 是 Hugging Face 的语音代理流水线(github.com/huggingface/speech-to-speech,2026-08-09 快照 star 11893),仓库描述那句话是 “Build local voice agents with open-source models”。看到 local 这个词,很多人的第一反应是:那我拔网线应该也能跑吧。
理论上可以。README 专门有一节 Offline Operation 说明了这件事。但那一节只有短短几句,而这几句里藏着一个特别容易只做一半的地方——HF_HUB_OFFLINE=1 管的是模型资产从 Hub 拉取那一环,它管不着 LLM 后端往哪里发请求。默认配置下 LLM 那一环走的是云端,环境变量设了也白设。
这篇就把「断网前要做的事」拆成能直接复制的几步,并给出每一步怎么验收。
先把「离线」这个词拆成两件事
流水线是 VAD → STT → LLM → TTS 四段级联,每段跑在自己的线程里、用队列相连。断网会影响的是两类完全不同的网络行为:
- 模型资产的获取:STT、TTS、Silero VAD、NLTK、Smart Turn 各自要的权重与数据文件,默认从 Hugging Face Hub 取。这一类由
HF_HUB_OFFLINE=1兜住——前提是本地缓存里真的已经有了。 - 推理请求的去向:LLM 那一环走的是 OpenAI 兼容协议。默认后端是
responses-api,默认模型是gpt-5.4-mini。README 把这句话说得很直白:不加本地 base URL 覆盖,默认的responses-api后端会去调远程服务——断网就断在这里。
所以离线运行不是「加个环境变量」,是两件事都要做:缓存要齐,出口要改。
第一步:联网状态下用完全相同的配置预热一次
README 对这一步的要求是明确的:依赖与所选组件的模型资产都装到本地后,流水线可以断网运行;断网前先在联网状态下用完全相同的配置启动一次,让它缓存好 STT、LLM、TTS、Silero VAD、NLTK 和 Smart Turn 需要的资源。
「完全相同的配置」这五个字是重点,不是客套话。这里有两件独立的事会咬人。
第一件很直白:换后端就等于换了要缓存的那份权重。预热时用 --tts qwen3、正式跑时改成 --tts kokoro,程序当然不会拦你——它们本来就是 --tts 的两个合法取值——但 Kokoro 那份权重压根没在预热阶段被拉下来,断网后才发现。
第二件更隐蔽,来自 CLI 自己的一条行为:它只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带着警告被忽略(JSON 配置同理,多余的非激活后端键也会被忽略)。也就是说,你在一条选了 --tts qwen3 的命令里顺手写上 --kokoro_voice bm_fable,程序不报错、只警告后照跑。预热阶段这么写一次,你以为自己热的是 Kokoro,实际缓存下来的是另一套东西。
如果你是中文场景,这一步之前还得先做一个决定:默认 STT 是 Parakeet TDT(nvidia/parakeet-tdt-0.6b-v3),README 的多语言表里写的是它覆盖 25 种欧洲语言。要做中文语音代理必须换 STT,Whisper 系或 Paraformer 都在支持矩阵里。这个决定必须在预热之前定下来,因为换 STT 就等于换了要缓存的那份权重。
第二步:把 LLM 挪到本地进程
README 给出的最省事的全本地做法,是把 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
逐个说这几个选项为什么在这:
--llm_backend responses-api是默认后端,打的是/v1/responses。这里没换后端,换的是它的目的地。--responses_api_base_url "http://127.0.0.1:8080/v1"——这是整套离线方案的命门。README 的服务商对照表里,llama.cpp 那一行给的就是http://127.0.0.1:8080/v1,对应上面llama-server默认起的端口。--responses_api_api_key ""——对照表里 llama.cpp 这一行写的是空字符串。打本地服务不需要密钥。顺带提醒:任何时候正文、脚本、截图里都别出现真实密钥,云端服务商那一栏该用$OPENAI_API_KEY、$HF_TOKEN这种形式。--model_name "ggml-org/gemma-4-E4B-it-GGUF"与 llama-server 那侧取的是同一个标识。
需要注意 llama-server 那条命令里的 -hf,按这个选项的字面语义是从 Hugging Face 取模型。README 没有单独交代 llama.cpp 这一侧在断网时怎么拿权重,所以这一侧的模型文件同样得提前备好,具体做法以 llama.cpp 官方文档为准。
想在托管这台机器上直接说话,还有一条路是不走独立服务、用进程内的本地后端:Apple Silicon 用 --llm_backend mlx-lm,CUDA / CPU 用 --llm_backend transformers。
第三步:断网后的启动命令
README 给出的离线启动形式是这样的:
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 ""
环境变量前置,加上那三个把 LLM 拽回本地的参数。README 同时提醒:每个选中的模型都必须已缓存,或通过其后端支持的本地路径提供。
Smart Turn 是这里唯一需要单独处理的组件。 它默认是开着的(--smart_turn 默认 True),用的是外部的 CPU ONNX 检查点,省略路径时从 pipecat-ai/smart-turn-v3 下载。README 给了三条处置:
- 已缓存的检查点配合
HF_HUB_OFFLINE=1可用; - 要做显式的、不依赖缓存的配置,传
--smart_turn_model_path /path/to/smart-turn-v3.2-cpu.onnx; - 检查点确实拿不到时,传
--no_smart_turn关掉它。
第二条对做镜像、做交付包的人价值最大:缓存目录是个隐性依赖,换台机器、换个用户、清一次缓存就没了;把 ONNX 文件放进你自己的目录并用参数指死,这份依赖才是显式的。
以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准。
产出物:能确定的只有这些
我们没有安装、部署或调用过这个服务,所以只说有依据的部分:
| 事项 | 依据 |
|---|---|
serve 默认绑定 127.0.0.1 | README 的绑定行为说明 |
默认端口 8765,服务地址 ws://localhost:8765/v1/realtime | README Quickstart |
| 自带客户端连法 | speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime |
local 命令永远绑定环回 | README 原文 |
| Docker 路线暴露 8080 与 8765 | docker compose up 会起一个跑 Gemma 4 的 llama.cpp 服务与实时服务 |
至于终端上会打印哪几行日志、界面上出现什么提示、声音听起来如何——这些我们一个字都不写,因为没有跑过。
怎么验收
断网之后再排查是最难受的,所以验收动作要放在断网之前做完:
- 确认参数真的落在你以为的那个组合上。 用
speech-to-speech serve -h看当前组合的默认值;要看另一种组合的参数,把选择器放在-h前面,比如speech-to-speech serve --stt mlx-audio-whisper -h。这个顺序很反直觉,写错了看到的就不是你要的那份帮助。 - 盯住警告行。 前面说过,写错了非激活后端的专属参数,程序不报错、只警告后照跑。预热阶段如果你把参数拼错,缓存下来的就是另一套东西。
- 预热与正式启动的配置逐字比对。 最稳的做法是把那条命令存成脚本,预热和离线跑用同一个文件,只在外面套
HF_HUB_OFFLINE=1。 - 确认 LLM 出口已经改掉。 检查
--responses_api_base_url是不是指向了http://127.0.0.1:8080/v1;漏了这个参数,responses-api会按默认走远程。 - Smart Turn 的处置选一条并写进脚本:靠缓存、指死路径、或者
--no_smart_turn,别留在「应该有吧」的状态。
什么情况不适用
一、你依赖的是服务商 API。 只要模型标识带 :cerebras、:groq、:together 这类后缀,或 base URL 指的是 https://router.huggingface.co/v1、https://openrouter.ai/api/v1,那这条链路天生要联网,离线无从谈起。
二、你用的是直接音频输入模式。 --stt none --llm_backend chat-completions 把 VAD 切好的完整音频段直接送给支持音频输入的模型,这条路径依赖的就是服务商侧的模型能力(README 明确说 responses-api 不支持直接音频模式,并要求显式设置接受音频的 --model_name,默认的 gpt-5.4-mini 接受文本与图像但不接受音频)。这套东西没法搬到断网环境里。
三、机器从头到尾就没联过网。 预热这一步没有替代方案——README 的前提就是「先在联网状态下用完全相同的配置启动一次」。真要做完全封闭的交付,你得在一台能联网的同架构机器上完成预热,再把缓存和模型文件搬过去,并把能显式指路径的组件(比如 Smart Turn)都改成显式路径。这属于通用运维做法,不是该项目官方文档的内容。
四、平台没对上。 --mac-optimal-settings 这个预设做的是 MPS 默认值、Parakeet TDT、MLX LM、Qwen3-TTS 走 mlx-audio 并默认取 6bit MLX 变体这四件事,只对 Apple Silicon 有意义;而 Linux 上 Qwen3-TTS 的 GGML 后端还有 CUDA wheel 版本错配那个坑要先趟过去。README 的依赖解析与 wheelhouse 说明也是按 macOS / 非 macOS 的平台标记组织的,这套划分里没有单独的 Windows 一档——我们不替它补,Windows 上想省事可以先看仓库自带的 Dockerfile、Dockerfile.arm64 与 docker-compose.yml。
两句必须说清楚的提醒
别把参数默认值当成延迟。 离线部署常常伴随「这样是不是更快」的联想。这条流水线里那些带 _ms 后缀的参数是流水线自己引入的、可配置的等待,不包含 STT 推理、LLM 首 token、TTS 首包这三段真正吃算力的时间。把它们加起来得出一个「端到端多少毫秒」的结论,是错的。
离线不等于可以随便暴露。 内网离线部署很容易顺手加上 --host 0.0.0.0,或者为了让别的程序共用模型而打开 --enable_llm_proxy(默认 False)。这里必须把官方自己的声明带出来:服务端自身不做认证、不做限流,只应在受信网络或网关之后启用。另外 --num_pipelines 默认是 1,也就是默认只承载一个并发会话——内网多人用之前,这是第一个要动的参数。
延伸阅读
本文依据 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 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。