默认绑 127.0.0.1 是有道理的:改成 0.0.0.0 之前想清楚
很多人跑通 speech-to-speech 的第一件事,是想让同事、想让另一台机器上的前端、想让办公室里那台树莓派也能连上来试试。于是搜到 --host 这个参数,顺手改成 0.0.0.0,服务起来了,能连了,事情就算完了。
这一步走得太快了。这个默认值不是随便定的,官方在参数的帮助文本里已经把话说得很直白。
一、先把那行 help 原文读完
src/speech_to_speech/arguments_classes/ 下 realtime_server_arguments.py 只有三个参数,--host 是其中之一,类型 str,默认值 "127.0.0.1",帮助文本原文是:
Host interface for the Realtime server. Default is 127.0.0.1. Pass 0.0.0.0 explicitly to expose the unauthenticated API on the network.
注意 unauthenticated 这个词——它不是我加的形容,是写在 --help 输出里的。同一个文件里 --port 默认 8765。README 侧的说法与之一致:serve 默认绑定 127.0.0.1,要暴露到网络必须显式传 --host 0.0.0.0;而 local 命令永远绑定环回,并把自带的麦克风/扬声器客户端连到 ws://127.0.0.1:<port>/v1/realtime。
也就是说,官方在这里做了一个很明确的分工:local 是「只给这台机器上的你自己用」,压根没给你改的余地;serve 给了你改的余地,但把后果直接写进了 help。
再看 Realtime Engine 那份架构文档里的 Python 客户端示例,api_key 那一行写的是:
client = OpenAI(
base_url="http://localhost:8765/v1",
websocket_base_url="ws://localhost:8765/v1",
api_key="not-needed",
)
not-needed —— 官方示例自己就承认服务端不校验这个字段。这三处线索指向同一件事:这个服务的访问控制不在服务里,在你的网络边界上。
二、改 host 之前,先摆清楚 8765 上到底有什么
绑到 0.0.0.0 之后暴露出去的不是「一个端口」,而是一组入口。按事实卡口径,与本篇直接相关的有这几条:
| 入口 | 传输 | 出现的前提 |
|---|---|---|
/v1/realtime | WebSocket | 默认存在 |
POST /v1/realtime/calls(Content-Type: application/sdp) | WebRTC | 需要装 speech-to-speech[webrtc] extra |
POST /v1/chat/completions | HTTP | --enable_llm_proxy 且运行在 --llm_backend chat-completions |
POST /v1/responses | HTTP | --enable_llm_proxy 且运行在 --llm_backend responses-api |
WebRTC 那条的握手是客户端 POST 一个 SDP offer,服务端返回 SDP answer(201,带 Location: /v1/realtime/calls/{call_id} 头),之后音频走 RTP 媒体轨、事件走 oai-events 数据通道。
后两条要单独说。README 关于 --enable_llm_proxy 的安全声明必须完整带出,不许弱化:服务端自己不做任何认证,也不做任何限流。 官方给的建议是只在受信网络上启用该代理,或者把服务部署在一个自己掌管访问控制的网关后面;README 举的例子是 s2s-endpoint 的计算副本充当这样的网关——只对「用 HF token 创建了会话的客户端」开放这些路径,按该 token 校验 API key,并按用户限流。
代理开启后的几条行为细节,正好说明为什么它比语音端点更敏感:请求是无状态的(每次发完整消息列表);代理到上游用的是服务端持有的 key,该 key 永不到达客户端;model 字段总是被覆写成服务端配置的 --model_name;服务端忽略客户端传的 API key。翻译成人话就是——谁能连上这个端口,谁就能用你的 key 打你配置的那个模型,而且你在服务端设的 --model_name 保证了他打的是哪个模型,却拦不住他打多少次。--llm_proxy_connect_timeout_s 默认 10.0,且官方注明读取没有超时(生成可能要几分钟),所以也别指望超时能替你兜底。
代理默认关闭,并且要求远端后端(chat-completions 或 responses-api),否则返回 501 并附原因。默认关闭这一点值得庆幸,但它是一个你随手就能打开的开关。
顺带一提那个更容易被忽略的账单问题:默认配置下 LLM 那一环走的是 --llm_backend responses-api + --model_name gpt-5.4-mini,也就是打 OpenAI。所以就算你一个代理都没开,只要有人能连上 /v1/realtime 并说话,这一路请求同样是拿你的 $OPENAI_API_KEY 出去的。这是按默认配置的参数语义推出来的后果,不是实测账单。
三、方案一:别动 --host,把服务接到你面前来
大多数「想让别的机器连上来」的需求,其实是「想在另一台机器上调试」。这种情况下更省心的做法是保持默认绑定,用 SSH 端口转发把远端的环回口接到本地:
# 通用运维做法,非 speech-to-speech 官方文档内容
ssh -L 8765:127.0.0.1:8765 <user>@<your-server>
保持服务端一侧原样启动:
export OPENAI_API_KEY=...
speech-to-speech serve
然后在本地这台机器上,客户端连的仍然是官方 Quickstart 里的那个地址:
speech-to-speech talk --url ws://127.0.0.1:8765/v1/realtime
每个选项为什么在这:-L 8765:127.0.0.1:8765 把本地 8765 的流量隧道到远端的环回 8765,服务端的监听地址完全没变、依旧只接受环回连接;talk --url 后面必须是完整的 realtime url,路径 /v1/realtime 不能省。默认服务地址是 ws://localhost:8765/v1/realtime,端口跟着 --port 走,你改了 --port 这里也要跟着改。
端口转发是通用运维做法,不是该项目官方文档里的内容,它把「谁能访问」这个问题交给了 SSH 的账号体系,而不是交给一个不认证不限流的服务。
四、方案二:确实要暴露时,把暴露面压到最小
有些场景绕不开,比如你要给一台没有 SSH 通道的嵌入式设备提供服务。那就把这几件事一起做完再改 host:
speech-to-speech serve \
--host 0.0.0.0 \
--port 8765 \
--llm_backend responses-api \
--model_name "ggml-org/gemma-4-E4B-it-GGUF" \
--responses_api_base_url "http://127.0.0.1:8080/v1" \
--responses_api_api_key ""
逐条解释这里为什么这么写:
--host 0.0.0.0是这次要做的事,同时也是把unauthenticated API放到网络上的那一步;--port 8765显式写出来,是为了后面验收时你知道该盯哪个端口,不用去猜默认值;- 后四行把 LLM 指向本机 llama.cpp(
http://127.0.0.1:8080/v1,--responses_api_api_key传空字符串),出自 README 的「Fully Local」示例。这一步的意义在于:万一有人连上来对着你的服务说了一整天话,被消耗的是你自己那张卡的算力,而不是一个云端 API 的额度。 - 注意这里没有
--enable_llm_proxy。它默认就是关的,只要你不主动开,/v1/chat/completions与/v1/responses这两条 HTTP 入口就不会出现。要暴露到网络就别顺手开代理,两件事同时做等于把「无认证的通用 LLM 端点」挂到网上。
以上为按官方参数语义组合的示例,未逐项实测,以官方文档与 --help 的实际输出为准。
关于访问控制本身,官方给的方向只有一句:只在受信网络上启用,或者放在一个你自己掌管访问控制的网关后面。至于具体用防火墙只放行某个网段、用 nginx 或别的网关做鉴权和限流——这些都是通用运维做法,不属于该项目的官方文档内容,需要你结合自己的环境评估。 这篇不会告诉你「这样配就安全了」,因为服务本身没有提供任何可以让人下这个结论的机制。
五、产出物长什么样:只说有依据的那部分
改完之后,能确定的变化其实很少,而且都可以事先说清楚:
- 监听地址由
--host决定,端口由--port决定,默认8765。这是一个 FastAPI / uvicorn 服务,同时暴露 WebSocket 与 WebRTC 两种传输。 - 并发上限是硬的:
--num_pipelines默认1,官方 help 写得很清楚——一个 uvicorn 服务监听--port,把每个进来的客户端路由到下一个空闲的 pipeline,最大并发 websocket 会话数等于num_pipelines,再多的连接会被拒绝。所以默认配置下第二个连接进不来。请把它理解成「资源池就这么大」,不要把它当限流或访问控制用——它挡不住一个人反复连、也不区分连的人是谁。 local不受影响:它永远绑环回,--host在这条路径上不是给你留的口子。- 代理没开就没有那两条 HTTP 入口;开了但后端不是远端后端,会拿到 501 并附原因。
- Docker 那条路要单独看:README 说
docker compose up会启动一个跑 Gemma 4 的 llama.cpp 服务和实时服务,暴露端口 8080 与 8765。8080 是 llama.cpp,8765 是 Realtime——两个端口的归属别记反了,你在编排文件里做端口映射时放行的是哪一个,直接决定了外面能碰到什么。
我们没有部署过这个服务,上面每一条都是仓库源码与文档口径,不含任何抓包或连接测试的结论。
六、怎么验收:四步,别跳
第一步,确认监听地址真的变了(或真的没变)。 这是通用运维命令,不是项目自带的:
# Linux
ss -ltnp | grep 8765
# Windows PowerShell
netstat -ano | findstr :8765
你要看的只有一件事:绑的是 127.0.0.1 还是 0.0.0.0。这一步最容易出错的地方是改了 --port 却还在查 8765,或者上一次启动的进程没退干净,你查到的是旧进程。
第二步,确认 --enable_llm_proxy 的状态与你以为的一致。 最直接的办法是回头看你实际敲的那条启动命令有没有这个 flag,以及看 speech-to-speech serve -h 里它的默认值仍是 False。要提醒一句 README 里另一个很咬人的行为:CLI 只为选中的后端构造配置,未激活后端的已知选项仍然被接受,但会带警告地忽略。也就是说你把某个参数写错了位置、写到了没激活的后端上,程序不会报错停下,只会警告后照跑——所以「命令没报错」不等于「参数生效了」,得去 -h 里对默认值。
第三步,确认 LLM 那一环打的是谁。 如果你按方案二把 --responses_api_base_url 指向了本机 llama.cpp,就该确认 8080 上那个服务确实起着;如果你没指,默认的 responses-api 后端会去调远程服务。README 在讲离线运行时明确点了这一条:不加本地 base URL 覆盖,默认后端就是往外打的。
第四步,确认 --num_pipelines 与你的用途匹配。 默认 1 意味着只能有一个会话;多台设备要连,第二台会被拒绝。这不是 bug,是 help 里写明的行为。
七、什么情况说明你不该改这个默认值
- 你只是想自己对着麦克风说话。 用
local,它永远绑环回,这个问题根本不存在。 - 你只是想在另一台机器上调试。 走第三节的端口转发,服务端监听地址不用动。
- 你打算开着
--enable_llm_proxy一起暴露。 官方对这个组合的措辞是受信网络或自管网关后面,没有第三种说法;你手上没有网关,就别开这个开关。 - 你的 LLM 还指着云端 API。 先把 LLM 那一环换成自己掌控的服务再谈暴露,否则暴露的不只是一个语音接口。
- 客户端和服务端之间隔着对称 NAT,或者服务跑在没暴露 UDP 的容器里。 这种情况下 WebRTC 直连本来就通不了,README 明确指出需要 TURN 服务器(ICE 配置走
SPEECH_TO_SPEECH_ICE_SERVERS环境变量,不设则用 aiortc 默认的 host candidates 加 Google STUN)。这时你要解决的是媒体通路问题,不是把 host 改成0.0.0.0就能了事的。
这个仓库在 2026-08-09 的快照里有 11893 个 star,但 star 数说明不了任何关于安全性或稳定性的事。真正值得记住的只有 help 里那句话:默认是 127.0.0.1,改成 0.0.0.0 意味着你在网络上放了一个 unauthenticated 的 API。项目把选择权交给了你,也把它的边界一并交代清楚了——剩下的部分,得你自己在网络那一层补上。
延伸阅读
本文依据 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 的实际输出为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。