断网也能跑:HF_HUB_OFFLINE=1 之前要先做的事

2026-08-09

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 四段级联,每段跑在自己的线程里、用队列相连。断网会影响的是两类完全不同的网络行为:

  1. 模型资产的获取:STT、TTS、Silero VAD、NLTK、Smart Turn 各自要的权重与数据文件,默认从 Hugging Face Hub 取。这一类由 HF_HUB_OFFLINE=1 兜住——前提是本地缓存里真的已经有了。
  2. 推理请求的去向: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.1README 的绑定行为说明
默认端口 8765,服务地址 ws://localhost:8765/v1/realtimeREADME Quickstart
自带客户端连法speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime
local 命令永远绑定环回README 原文
Docker 路线暴露 8080 与 8765docker compose up 会起一个跑 Gemma 4 的 llama.cpp 服务与实时服务

至于终端上会打印哪几行日志、界面上出现什么提示、声音听起来如何——这些我们一个字都不写,因为没有跑过。

怎么验收

断网之后再排查是最难受的,所以验收动作要放在断网之前做完:

  1. 确认参数真的落在你以为的那个组合上。speech-to-speech serve -h 看当前组合的默认值;要看另一种组合的参数,把选择器放在 -h 前面,比如 speech-to-speech serve --stt mlx-audio-whisper -h。这个顺序很反直觉,写错了看到的就不是你要的那份帮助。
  2. 盯住警告行。 前面说过,写错了非激活后端的专属参数,程序不报错、只警告后照跑。预热阶段如果你把参数拼错,缓存下来的就是另一套东西。
  3. 预热与正式启动的配置逐字比对。 最稳的做法是把那条命令存成脚本,预热和离线跑用同一个文件,只在外面套 HF_HUB_OFFLINE=1
  4. 确认 LLM 出口已经改掉。 检查 --responses_api_base_url 是不是指向了 http://127.0.0.1:8080/v1;漏了这个参数,responses-api 会按默认走远程。
  5. Smart Turn 的处置选一条并写进脚本:靠缓存、指死路径、或者 --no_smart_turn,别留在「应该有吧」的状态。

什么情况不适用

一、你依赖的是服务商 API。 只要模型标识带 :cerebras:groq:together 这类后缀,或 base URL 指的是 https://router.huggingface.co/v1https://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 上想省事可以先看仓库自带的 DockerfileDockerfile.arm64docker-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 的实际输出为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。