资讯动态

浏览器实时语音对话实战:speech-to-speech 的 WebSocket/WebRTC 双传输 Demo 全解析

发布时间:2026/9/15 15:54:38 来源:尧图企业网站定制
浏览器实时语音对话实战speech-to-speech 的 WebSocket/WebRTC 双传输 Demo 全解析【免费下载链接】speech-to-speechBuild voice agents with open-source models项目地址: https://gitcode.com/GitHub_Trending/sp/speech-to-speech导读本文围绕 speech-to-speech 仓库中的demo/浏览器端实时语音对话示例系统讲解如何把它接到以 OpenAI Realtime GA 协议运行的后端服务上涵盖本地快速启动、WebSocket 与 WebRTC 两种传输方式的原理与差异、三种后端连接模式、用量限额、内置工具与完整的浏览器音频管线。读完本文你将能够独立启动并配置这套浏览器语音助手前端并理解其背后的适配器设计与源码实现细节。一、Demo 是什么demo/是一个纯浏览器端的语音聊天 UI面向 huggingface/speech-to-speech 后端通过WebSocket默认或WebRTC设置 → Transport仅限 env 固定部署使用 OpenAI RealtimeGA协议进行语音对话。两种传输方式共享同一个RealtimeSession适配器该适配器运行在官方openai/agents包的固定版本之上使用其原生stock传输类并将 demo 专属的队列、音频、可视化、设备、相机与计量逻辑全部隔离在协议实现之外保证协议层不被 demo 需求污染。从 demo/package.json 可以看到官方 SDK 被精确锁定在openai/agents0.14.3同时依赖hono4.13.0与zod4.4.3测试使用playwright/test1.62.1。另外仓库根目录下的 demo/Dockerfile 与 docker-compose.yml 提供了容器化运行方式demo/docs/adr/0001-docker-space-with-search-proxy.md 记录了为何需要一个小的 server 进程来代理搜索密钥的架构决策。二、本地快速启动1. 启动 speech-to-speech 后端在仓库根目录执行更多模型组合参考 后端 Realtime 引擎文档uv run speech-to-speech serve \ --stt parakeet-tdt \ --llm_backend transformers \ --tts kokoro \ --model_name Qwen/Qwen3-4B-Instruct-2507 \ --llm_device mps \ --llm_torch_dtype float16 \ --enable_live_transcription实时服务器默认监听ws://localhost:8765/v1/realtime可用--host/--port修改。其中--enable_live_transcription开启输入实时转写对应后端会下发conversation.item.input_audio_transcription.delta / completed事件。2. 安装 demo 依赖并启动npm ci --prefix demo uv pip install -r demo/requirements.txt export SPEECH_TO_SPEECH_URLws://localhost:8765/v1/realtime export SERPER_API_KEY... # 可选不设置则禁用 web search 工具 export STARTUP_GREETING... # 可选设为空字符串则禁用自动问候 uv run uvicorn --app-dir demo server:app --reload --port 78603. Docker 方式docker build -t s2s-demo demo/ docker run -p 7860:7860 -e SPEECH_TO_SPEECH_URLws://host.docker.internal:8765/v1/realtime s2s-demoDocker 宿主后端WebSocket 与 WebRTC 需要不同的 hostname。两种传输从不同的网络命名空间拨号到后端WebRTC 是服务端拨号——浏览器先把 SDP offer POST 给 demo 的/api/calls代理由代理在容器内部转发。容器里host.docker.internal能解析到宿主机因此上面的命令可行。WebSocket 是客户端拨号——demo 把 URL 直接交给浏览器由浏览器自己打开 socket。浏览器运行在宿主机上那里host.docker.internal并不是真实 DNS 名连接永远到不了后端且服务器日志里什么都不会有。所以单个 Docker 环境变量SPEECH_TO_SPEECH_URL一次只能让一种传输工作localhost:8765给 WebSockethost.docker.internal:8765给 WebRTC。想同时体验两者就不要用 Docker 运行 demo用上面的uvicorn命令让 host 与 container 命名空间合并——此时ws://localhost:8765/v1/realtime对两种传输都有效。4. 打开页面并冒烟测试打开 http://localhost:7860/点击 orb授权麦克风即可开始对话。浏览器要求HTTPS 或localhost才能使用getUserMedia()麦克风 相机。127.0.0.1和localhost都可以但纯http://192.168.x.y不行。还可以在 shell 中对后端做冒烟测试websocat ws://localhost:8765/v1/realtime # - 应立即收到 session.created 事件三、工作原理Demo README 给出了五步核心流程对应客户端适配器 demo/s2s-realtime-client.js 与后端 Realtime 引擎文档 中的调用链适配器用官方 Agents SDK 的RealtimeSession 原生 WebSocket transport连接配置好的/v1/realtimeURL。SDK 按 OpenAI RealtimeGAschema 发送session.update。SDK 把麦克风音频以PCM16 24 kHz 单声道 base64分块上传input_audio_buffer.append约每 40 ms 一帧。浏览器保留一份有界的内存副本配合服务端 VAD 边界把可回放的用户录音追加到会话历史中。录音不会单独上传或持久化。服务端推送response.output_audio.deltaPCM16 24 kHz 单声道 base64以及转写增量事件。从客户端源码可以看到会话配置固定使用server_vad服务端 VAD并开启interruptResponse: truedemo/s2s-realtime-client.jsSDK 内部以model: s2s-local建立会话仅作为占位符tracingDisabled: true。后端的会话模型是每个 pipeline unit 一个并发会话用--num_pipelines可增加服务并发数。后端接收input_audio_buffer.append后解码、重采样到 16 kHz、切分成 512 样本块送入 VADVAD 检测到语音边界后触发speech_started / speech_stoppedSTT 转写结果经TranscriptionNotifier变成transcription.delta / completed事件LLM 生成的内容由LMOutputProcessor排入统一队列TTS 再把音频与事件一并经由_send_loop编码成协议事件下发——session.update会深度合并进RuntimeConfig供 VAD打断阈值、LLMinstructions、tools、TTSvoice在处理时读取。兼容的协议事件面后端支持客户端事件input_audio_buffer.append、session.update、conversation.item.create、conversation.item.truncate对 SDK 中断场景作为无确认的 no-op、response.create支持 per-response 的instructions与tool_choice覆盖、response.cancel。服务端事件包括session.created、session.updated、error、input_audio_buffer.speech_started / speech_stopped、conversation.item.created、conversation.item.input_audio_transcription.delta / completed、response.created、response.output_audio.delta / done、response.output_audio_transcript.delta / done、response.function_call_arguments.done、response.done。CI 固定openai/agents0.14.3并对 SDK 原生OpenAIRealtimeWebSocket与OpenAIRealtimeWebRTC两个 transport 运行独立集成任务对应 demo/tests/agents-websocket.test.mjs、demo/tests/agents-webrtc.test.mjs、demo/tests/agents-adapter.test.mjs。已测试的能力矩阵包括session.updateinstructions、voice、tools、服务端 VAD、PCM 24 kHz 配置、麦克风输入WebSocket 走input_audio_buffer.appendWebRTC 走 RTP 音轨、助手音频response.output_audio.deltavs RTP 音轨、输入/输出转写事件、显式取消、服务端 VAD 抢话barge-in、函数调用执行与结果提交。需要注意该矩阵描述的是测试过的核心 GA 面不等于完整 API 等价。demo 适配器还补了一个固定 SDK 的坑openai/agents0.14.3在工具列表为空时会省略tools字段因此适配器通过原生 transport 的sendEvent钩子显式发送带tools: []的session.update见 demo/s2s-realtime-client.js确保服务端深度合并后的 session 配置不残留旧工具。四、WebRTC 传输在设置了SPEECH_TO_SPEECH_URL的情况下设置 → Transport会把 WebRTC 作为 WebSocket 的备选。同一段对话不同的底层管道SDK 的原生 WebRTC transport 把浏览器麦克风轨道及其 data channel 挂到RTCPeerConnection上并将 SDP offer POST 到同源的/api/calls代理代理转发到后端的POST /v1/realtime/callsOpenAI GA 握手。代理存在的原因在于 s2s 服务器没有 CORS 中间件——而且它只转发到 env 固定的 URL绝不转发客户端自带的地址因此不可能被当作开放代理使用。这也是为什么 URL 未固定用户手输 URL、LB 模式时切换按钮会被锁定为 WebSocket。只有握手经过代理协商好的音频双向 Opus RTP与 data channel 直接在浏览器 ↔ 后端之间流动。data channel 上的 JSON 事件与 WebSocket 是同一套 GA 协议只是不含音频麦克风音频走媒体轨道绝不发input_audio_buffer.append后端在 WebRTC 下会拒绝该事件助手声音以远端音频轨道到达绝无response.output_audio.delta。抢话barge-in清空在服务端完成。从 demo/server.py 的实现可以看到/api/calls只把Content-Type: application/sdp的请求体原样转发给由_webrtc_calls_url()从实时 URL 推导出的…/v1/realtime/calls地址ws://→http://wss://→https://并原样回传 answer 与Location头call id。后端要求需要webrtc额外依赖pip install speech-to-speech[webrtc]否则/v1/realtime/calls返回 501握手会以清晰的错误信息失败。与 WebSocket 的差异与注意点用户录音回放会话历史中的录音目前使用经由input_audio_buffer.append发送的原始 PCM 帧因此仅 WebSocket 传输可用。NAT默认只有 host ICE candidates——浏览器与后端同机/同局域网没问题。跨公网时需要在本应用上设置RTC_ICE_SERVERSJSON 形式的RTCIceServer字典列表或逗号分隔的 STUN/TURN URL 列表通过/api/config下发到浏览器并在后端设置SPEECH_TO_SPEECH_ICE_SERVERS。没有 TURN 中继兜底因此对称 NAT 场景仍可能连不上。后端文档给出的示例格式为export SPEECH_TO_SPEECH_ICE_SERVERS[{urls: stun:stun.example.com:3478}, {urls: turn:turn.example.com, username: u, credential: c}]demo/server.py 中的_parse_ice_servers()同时接受 JSON 列表、单个 dict 与逗号分隔的 URL 字符串。噪声门noise gate实现在 WebSocket 采集 worklet 中因此 WebRTC 下不可用——发送的是裸麦克风轨道仅带浏览器自身的noiseSuppression。相机快照会被重新编码以适配单条 data channel 消息约 60 KB模型看到的画面可能比 WebSocket 下更小。负载均衡模式目前仅支持 WebSocket。五、连接后端的三种模式由环境变量决定/api/config会告诉客户端当前处于哪种模式见 demo/server.pySPEECH_TO_SPEECH_URLLOAD_BALANCER_URLSPACE_ID连接方式URL 字段传输计量✅anyany直连 → 固定 URL可见、锁定WS 或 WebRTC关––any直连 → 用户 URL可编辑仅 WS关–✅✅LB 代理隐藏仅 WS开–✅–LB 代理隐藏仅 WS关SPEECH_TO_SPEECH_URLenv——本地使用首选优先级最高。浏览器直接连接该实时 WebSocket URL设置里以只读方式展示。设置它之后会彻底禁用负载均衡逻辑无/api/session代理、无队列、无计量、无登录。与 LB 地址不同它不是秘密。可接受完整的ws(s)://host/v1/realtimeURL也可只写 bare host如localhost:8765应用会自动补上/v1/realtime。两个 env 都没设置——在设置 → Speech-to-speech server URL里粘贴完整连接 URL 或 bare host浏览器直接连接。LOAD_BALANCER_URLenv——仅限多计算节点部署浏览器 POST 同源/api/session代理服务端转发到 LB浏览器再拨号 LB 交回的 per-session compute URL。LB 地址永远不落到浏览器Settings 的 URL 字段隐藏。在启用 OAuth 的 Space 上代理会把已登录用户的 HF access token 通过X-Reachy-Mini-Authorization头传给 LB见 demo/server.py用于后端用量归属token 只留在服务端匿名请求不带任何凭证。另外设置 → Restart会用当前的 voice、instructions 与 URL 重新连接。六、启动问候Startup Greeting默认情况下每次新连接都会创建一个隐藏的用户条目让模型给出简短问候然后请求一次回复。除了自然开场外这还会预热首个语音轮次所用的同一条 prompt 前缀。通过STARTUP_GREETING自定义隐藏提示词设为空字符串可禁用自动生成。默认提示词定义在 demo/server.pyStart the conversation now with a brief, spontaneous greeting in character. Keep it to one sentence, invite the user in naturally, and vary the wording each time.适配器在连接建立后通过新的RealtimeSession发送一次客户端代码见 demo/s2s-realtime-client.js 的_session.sendMessage(greeting)。七、内置工具助手可在对话中调用两个工具右上角Tools按钮切换Web 搜索——通过 Serper.dev 调 Google 结果服务端代理以保证 key 永不进入浏览器。设置SERPER_API_KEY为 env / Space secret。没有它时工具默认禁用除非用户在 Tools 面板里粘贴自己的 key。对应的/api/search代理在 demo/server.py 中实现服务端持有 key把结果裁剪到最多 5 条MAX_RESULTS并尽量提取 Google 的 direct answer 以省去模型一次往返。相机——启用后左下角显示实时自拍画面当模型调用该工具时当前帧会被发送给视觉语言模型让它看到你展示的东西。工具定义经session.update下发后端对本地 LLM 路径会把工具 JSON Schema 转成 Pythoninspect.Signature并注入系统提示词code包裹的函数调用对 OpenAI-compatible 路径则原生传tools。仓库还附带一个可直接复用的 Python 版搜索工具示例 examples/realtime_web_search_tool.py它使用与浏览器 demo 完全相同的 Serper API 与SERPER_API_KEY环境变量。八、用量限制仅部署的 Space对话时间按UTC 天、按登录等级计量见 demo/limiter.py 与 demo/auth.py但只在部署的 Space 上生效——只有当LOAD_BALANCER_URL和SPACE_ID由 HF Space 运行时自动注入同时存在时计量才开启。本地运行——即使导出了LOAD_BALANCER_URL——应用也不计量。可通过环境变量调整Env默认值说明LIMIT_ANON_SEC300匿名访客每日秒数5 分钟LIMIT_FREE_SEC600已登录非 PRO 用户每日秒数10 分钟UNLIMITED_ORGS追加到默认值额外 HF 组织名其成员享有无限额度同 PROUSAGE_HASH_SECRET随机HMAC 密钥哈希身份 key 给匿名 cookie 签名PRO 成员始终无限。cerebras、HuggingFaceM4、smolagents、pollen-robotics组织的成员开箱即用即无限界面显示为 Team 而非 PRO设置UNLIMITED_ORGSmy-team可继续追加。匹配时针对 HF OAuth 返回的用户组织不区分大小写。从 demo/limiter.py 可以看到更多内部可调参数RESERVE_CHUNK_SEC默认 10 秒预留粒度、HEARTBEAT_SEC默认 5 秒客户端心跳节奏、SESSION_REAP_SEC默认 15 秒静默回收阈值。实现上是服务端时钟的分块预留grant 时先扣一个 chunk客户端心跳每次把预留向后延长一个 chunk直到日预算耗尽返回expired正常结束时sendBeacon对账并退还未用部分崩溃场景由周期 sweep 回收至多损失一个 chunk。匿名用户按哈希 IP 与哈希签名 cookie id二者取大OR-match计量避免清掉单个标识符就重置额度。存储默认落在持久化的/data目录USAGE_DB_PATH可覆盖不可用时退回/tmp此时预算只按进程存活期有效。九、设置项存于localStorageKey说明Speech-to-speech server URL直接实时 WebSocket URL被 env 固定时隐藏/锁定TransportWebSocket默认或 WebRTC仅 env 固定 URL 时可选Microphone采集输入设备下次对话 / Restart 时生效Speakers助手音频输出设备。Chrome/Edge 可通过AudioContext.setSinkId实时切换其他浏览器保持系统默认VoiceQwen3-TTS 说话人名称Aiden、Ryan、Dylan、Eric、Ono_Anna、Serena、Sohee、Uncle_Fu、VivianInstructions连接建立后随session.update发送的系统提示词LocalStorage key 统一带s2s.ws.*前缀传输选择为s2s.transport设备为s2s.audio.inputId/s2s.audio.outputId。十、音频管线笔记输入getUserMedia({ echoCancellation, noiseSuppression, autoGainControl })以AudioContext采样率喂给mic-captureworkletdemo/worklets/mic-capture.js。worklet 重采样到 24 kHz——48→24 为精确 2:1走 boxcar 低通 抽取快路径奇数采样率走线性插值兜底——并打包为 Int16 LE。分块默认 40 ms960 样本 / 1920 字节后端会批量接收音频20–100 ms 都在甜区。可选的噪声门在 worklet 内实现按块 RMS 决定开合增益采用快速起音5 ms 保持250 ms 慢速释放80 ms包络避免吞掉词头、杜绝尾音咔哒声噪声门只影响发送的音频主线程可视化仍取原始麦克风信号见 demo/s2s-realtime-client.js 的 gate 消息与input-level事件。用户回放WebSocket 客户端只保留实际发送过的 PCM 的有界副本SentAudioRecorder默认 5 秒 preroll、最长 120 秒缓冲见 demo/ws/user-audio-recorder.js。用speech_started/speech_stopped时间戳切出每段话语包成内存 WAVpcm16ToWavBlob挂到用户消息行上。开始回放时会临时静音正在发送的麦克风音频以防回声。输出response.output_audio.delta解码为 Int16 → Float32 后投给audio-playbackworkletdemo/worklets/audio-playback.js。该 worklet 维护 per-context 环形缓冲把 24 kHz 线性插值到 48 kHz并在进出场施加 32 帧的短淡入淡出以抑制咔哒声发生 underrun 时输出静音不保持最后样本避免长间隔产生蜂鸣。base64 ↔ PCM 与response.done转写提取等纯函数工具集中在 demo/ws/codec.js。Barge-in抢话当服务端 VAD 在回复过程中检测到用户语音input_audio_buffer.speech_started且处于ai-speaking状态时客户端向播放 worklet 投递{ kind: clear }立即清空播放队列服务端本身则会取消进行中的响应。后端的取消机制基于共享CancelScopegeneration 计数 discard 标志LLM/TTS 线程逐 token 检查is_stale(gen)提前中止发送循环按cancel_generation丢弃过期输出。十一、文件结构文件角色index.html单页应用orb 设置弹窗与 WebRTC 应用 UI 一致main.js状态机、设置、工具、相机、噪声门 UI 接线ui/chat.jsChatView历史面板、临时气泡、转写/工具流、用户录音回放ui/account.jsAccountHF 登录徽章 弹窗、日限额弹窗ui/dom.js共享助手$、escHtml、truncateError、DEBUGauth.pyHF OAuth 每请求身份tier、哈希 keylimiter.pySQLite 按日对话时长预算服务端时钟分块预留s2s-realtime-client.js围绕单个 Agents SDKRealtimeSession的窄适配器覆盖原生 WebSocket/WebRTC 传输package.json / package-lock.json精确锁定的官方 Agents SDK 与浏览器测试依赖ws/codec.jsbase64 ↔ PCM 助手 转写提取纯函数ws/user-audio-recorder.js有界已发送 PCM 缓冲 VAD 切片 浏览器可播放 WAV 封装ws/orb-visualizer.jsOrbVisualiserFFT 频带 → orb CSS 自定义属性worklets/mic-capture.jsAudioWorklet48 kHz Float32 → 24 kHz Int16 PCM约 40 ms 分块worklets/audio-playback.jsAudioWorklet24 kHz Float32 环形缓冲 → 48 kHz线性插值 淡入淡出style.cssOrb 动画、布局、暗色主题十二、测试与验证demo 自带三个 Node 测试脚本demo/package.json验证适配器与两个原生传输的关键行为npm run test:agents:adapter—— tests/agents-adapter.test.mjsnpm run test:agents:ws—— tests/agents-websocket.test.mjsnpm run test:agents:webrtc—— tests/agents-webrtc.test.mjs这些测试覆盖session.update各项配置、麦克风/助手音频路径、输入输出转写事件、显式取消与 VAD 抢话、函数调用执行与结果提交等核心 GA 面。测试辅助代码位于 tests/helpers.mjs。十三、常见问题速查浏览器拿不到麦克风确保访问的是localhost或 HTTPS否则getUserMedia会被浏览器拒绝。WebSocket 连不上、服务器无日志检查SPEECH_TO_SPEECH_URL是否在浏览器所在命名空间可解析尤其 Docker 部署参见上文两种 hostname。WebRTC 握手失败确认后端已安装webrtcextra且SPEECH_TO_SPEECH_URL已固定/api/calls只转发固定 URL。跨公网 WebRTC 不通为本应用配置RTC_ICE_SERVERS、为后端配置SPEECH_TO_SPEECH_ICE_SERVERS对称 NAT 场景仍可能需要 TURN。本地似乎被限流计量只在LOAD_BALANCER_URL与SPACE_ID同时存在时开启本地默认不计量。工具开关无效Web 搜索工具默认依赖服务端SERPER_API_KEY若未配置可在 Tools 面板临时填入个人 key。【免费下载链接】speech-to-speechBuild voice agents with open-source models项目地址: https://gitcode.com/GitHub_Trending/sp/speech-to-speech创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价