docker compose up 起来的是两个服务,不是一个
speech-to-speech 是 Hugging Face 的开源语音代理流水线,仓库描述原文是 “Build local voice agents with open-source models”,2026-08-09 这天的 star 数是 11893(star 数只是个时间点快照,跟好不好用没有关系,这里只作为版本坐标)。它的 README 里 Docker 那一节短得几乎会被跳过:装好 NVIDIA Container Toolkit,然后 docker compose up。
问题就出在这三行字后面那句话上。README 说的是,compose 文件会启动一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露端口 8080 与 8765。
也就是说,你敲下 docker compose up 之后,桌面上不是一个服务,是两个。很多人第一次起完之后去 8080 找页面、或者只盯着 8765 看,都是因为没把这个拓扑先想清楚。
这两个端口分别是谁
先把端口和仓库里的其它说法对上号。下面这张表只列跟本篇直接相关的两行,参数的完整清单在别处讲:
| 端口 | 谁在听 | 在仓库其它地方长什么样 |
|---|---|---|
| 8080 | llama.cpp 提供的 OpenAI 兼容 LLM 服务 | README 的服务商对照表里,llama.cpp 那一行的 --responses_api_base_url 就是 http://127.0.0.1:8080/v1,--responses_api_api_key 填空字符串 |
| 8765 | speech-to-speech 的 Realtime 服务 | realtime_server_arguments.py 里 --port 默认就是 8765;默认服务地址是 ws://localhost:8765/v1/realtime |
对上号之后,这个 compose 的形状就一目了然了:它是 README「Fully Local」那一节两个终端的容器版本。那一节让你在终端 1 起 llama-server,在终端 2 起 speech-to-speech serve 并把 --responses_api_base_url 指向 http://127.0.0.1:8080/v1。compose 把这两步打成了一个编排单元。
这层对应关系是我们把 README 两节内容对照读出来的推断,不是 compose 文件的内容——我们只读了 README 里那句话,没有读过 docker-compose.yml 的正文,所以它具体给实时服务传了哪些参数、用的是哪个 Gemma 4 的 GGUF 检查点,本文一律不写。README 在这里只说了 “Gemma 4”,没有点名检查点。(仓库 Fully Local 那一节出现过 ggml-org/gemma-4-E4B-it-GGUF,但那是另一节的命令行示例,不能直接当成 compose 的内容。)
命令怎么写
Docker 这条路要先有源码,因为 compose 文件在仓库里:
git clone https://github.com/huggingface/speech-to-speech.git
cd speech-to-speech
docker compose up
前两行来自 README 的「从源码安装」一节,第三行来自 Docker 一节。每一行的作用:
git clone:Docker 路线跟pip install speech-to-speech不是一回事。pip 装的是包,拿不到docker-compose.yml、Dockerfile、Dockerfile.arm64这些编排文件,它们在仓库里。cd:docker compose默认在当前目录找 compose 文件。docker compose up:不加-d,日志直接打在前台。第一次起的时候强烈建议这样,因为两个服务的启动日志混在一起才看得出是哪一半没起来。
README 在 Docker 一节写明的硬前置是 NVIDIA Container Toolkit,原文给的链接指向 NVIDIA 官方安装指南。它不装,容器拿不到 GPU。
以上为按官方文档段落组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准。
起来之后能拿到什么
这里只写有依据的部分,我们没有运行过这套服务,所以不描述任何界面、日志行、启动耗时。
- 8765 上是一个 Realtime 服务,同一个 FastAPI / uvicorn 进程上暴露 WebSocket 与 WebRTC 两种传输,WebSocket 端点路径是
/v1/realtime。 - 8080 上是一个 OpenAI 兼容的 LLM 端点,按 README 的对照表,它的 base URL 形态是
http://127.0.0.1:8080/v1。 - 默认不会多出一个「LLM 代理」端点。
--enable_llm_proxy在模块级参数里默认是False。要开的话看下一节的安全说明。 - WebRTC 那条路要求装
speech-to-speech[webrtc]extra,走的是POST /v1/realtime/calls,Content-Type 是application/sdp。容器里装没装这个 extra,取决于镜像怎么构建的——我们没读过 Dockerfile 正文,不下结论。
注意后两条说的是 CLI 与源码层面的默认行为,不等于 compose 起出来的实例就一定是这个配置,因为 compose 完全可以在命令里覆盖它们。
怎么验收
第一处:8765 的协议层通不通。 仓库自带一个麦克风客户端,README 给的连法是:
speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime
如果你不想动麦克风,只想确认协议层握上了,README 里那段 Python 示例更适合做冒烟检查——它用官方 OpenAI SDK 连上去,把收到的事件类型逐条打出来:
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)
按协议文档,连接建立时服务端会发 session.created,一次成功的 session.update 会被 session.updated 确认。这两个事件名能打出来,8765 这一半就算走通了。那个 api_key="not-needed" 不是笔误——服务端自身不做认证,下一节会讲这件事的分量。
第二处:8080 那一半。 它就是个普通的 OpenAI 兼容端点,按 README 对照表里的形态(base URL http://127.0.0.1:8080/v1,API key 传空字符串)当作上游访问即可。具体怎么探活以 llama.cpp 官方文档为准,我们手上没有 llama.cpp 的一手资料,不编 curl 命令。
第三处:分清是哪一半没起来。 这是把两个服务当成一个时最常见的误判。8765 连得上但对话没有反应,跟 8765 根本连不上,是完全不同的两类问题——前者要往 8080 和上游模型那边查,后者才是实时服务本身的问题。前台跑 docker compose up 的价值就在这里。
最容易在哪一步出错
并发数默认是 1。 --num_pipelines 默认值是 1,help 原文说得很直接:一个 uvicorn 服务监听 --port,把每个进来的客户端路由到下一个空闲的流水线,每条流水线有自己的 VAD / STT / LM / TTS handler 和对话状态;最大并发 WebSocket 会话数等于 num_pipelines,再来的连接会被拒绝。协议错误码里也有对应的 session_limit_reached。
这条在容器化之后杀伤力更大:你把服务丢给同事试用,第二个人连上来就被拒,日志又在容器里,很容易被误判成「服务挂了」。做多人部署,这是第一个要动的参数。
绑定地址默认是环回。 --host 默认 127.0.0.1,help 原文写的是:显式传 0.0.0.0 才能把这个未认证的 API 暴露到网络上。如果你要照着官方 compose 改自己的编排,这是第一个坑——容器内绑在环回上,宿主机是访问不到的(这是容器网络的通用常识,不是本项目文档内容)。
而一旦你真的传了 --host 0.0.0.0,或者打开了 --enable_llm_proxy,README 的安全声明必须原样带上:**服务端自己不做任何认证,也不做任何限流。**只在受信网络上启用,或者部署在一个自己掌管访问控制的网关后面。README 举的例子是让计算副本充当这样的网关,只对用 token 创建了会话的客户端开放这些路径、按 token 校验 key、按用户限流。本文不给「这样配就安全了」的结论,也不给具体网关方案。
中文场景要换 STT。 默认 STT 是 Parakeet TDT,README 的多语言表里它覆盖的是 25 种欧洲语言。要做中文语音代理必须换成 Whisper 系或 Paraformer,Paraformer 还要装 paraformer extra。容器化之后换 STT 意味着改编排文件和镜像依赖,不是改一下本机命令行那么轻。
写错的后端参数不会报错。 README 明说,CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略,JSON 配置同理。在容器里日志刷得飞快,这条警告非常容易被冲走,然后你就会对着一个「参数明明写了却没生效」的服务发呆。
模型缓存。 README 的离线一节要求:断网前先在联网状态下用完全相同的配置启动一次,让 STT、LLM、TTS、Silero VAD、NLTK 和 Smart Turn 需要的资源都缓存好。容器每次重建是否还留着这些缓存,取决于你怎么挂载卷(同样是通用容器实践,不是本项目文档内容)。
什么情况别走这条路
- 手上不是 NVIDIA 卡,或者不打算装 NVIDIA Container Toolkit。README 的 Docker 前置就是它。仓库另有
Dockerfile.arm64,但我们没读过它的内容,不替它下结论。 - Apple Silicon。README 给 macOS 的路子是本机安装加
--mac-optimal-settings预设(STT 用 Parakeet TDT、LLM 用 MLX LM、TTS 用 Qwen3-TTS 走mlx-audio并默认取6bit变体),跟 NVIDIA Container Toolkit 这条容器路线不是同一条道。 - 只想快速试一下、不打算跑本地 LLM。那 8080 那一半就是多余的:直接
pip install speech-to-speech,export OPENAI_API_KEY=...之后speech-to-speech serve,LLM 走托管服务商即可。顺带说一句,这个开箱默认值本身就挺反直觉——项目名叫 “local voice agents”,但默认配置里只有 STT 和 TTS 在本地,LLM 默认打的是 OpenAI 的gpt-5.4-mini。docker compose 这条路的真正价值,恰恰是它把 LLM 那一环也搬到了本地。 - 要给多人用。先把
--num_pipelines想明白再谈部署,否则第二个用户就是你的第一个故障单。
延伸阅读
本文依据 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 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。