"""WS 输出:裸 WebSocket 音频流(第二种输出方式)。 比 WebRTC 简单——没有 SDP/ICE/STUN/TURN,一条 WS 直接收发音频帧。 适合:服务端对接、话务网关、自定义客户端、调试。 约定:连接建立后,**第一条文本消息**发 JSON 启动参数: {"assistant_id": "asst_xxx"} # 推荐:key 服务端解析 {"inline_config": {...AssistantConfig}} # 调试:内联 之后的二进制消息即音频帧(protobuf,与 transports.py serializer 对应)。 """ import json from db.session import SessionLocal from fastapi import APIRouter, WebSocket from loguru import logger from models import AssistantConfig from services.config_resolver import resolve_runtime_config from starlette.websockets import WebSocketDisconnect # pipecat 重依赖,惰性导入(见 voice_webrtc.py 说明) router = APIRouter() async def _resolve_start_config(raw: str) -> AssistantConfig: data = json.loads(raw) if data.get("assistant_id"): async with SessionLocal() as session: return await resolve_runtime_config(session, data["assistant_id"]) if data.get("inline_config"): return AssistantConfig(**data["inline_config"]) raise ValueError("启动参数缺少 assistant_id 或 inline_config") @router.websocket("/ws/stream") async def voice_stream(websocket: WebSocket): from services.pipecat.pipeline import run_pipeline from services.pipecat.transports import build_ws_transport await websocket.accept() try: cfg = await _resolve_start_config(await websocket.receive_text()) transport = build_ws_transport(websocket) # 直接 await:管线持续读这条 WS 的音频帧,直到对端断开 await run_pipeline(transport, cfg) except WebSocketDisconnect: logger.info("WS 音频流断开") except Exception as e: logger.error(f"WS 音频流出错: {e}")