8.9 KiB
8.9 KiB
DPM 实时语音对话整合方案(复用 live-avatar 云化栈)
目标:砍掉数字人/口型/wav2lip 等全部视觉部分,只保留「语音识别 + 对话 + 实时交互」链路; 前端在 DPM React 应用中新增对话页(复用 live-avatar demo 前端的样式与逻辑); 后端并入 DPM
backend/pyproject.toml。 基线:live-avatar 云化栈已在/Volumes/Pine/mycode/opc/live跑通(s2s v0.2.11 + cloud-migration 补丁 + demo 前端)。
0. 现状对比
| live-avatar(已跑通) | DPM(现状) | |
|---|---|---|
| 前端 | 原生 JS demo(index.html / main.js / s2s-ws-client.js / 两个 worklet) | React 19 + Vite + Tailwind;/ai 页已有 AiChatPanel(文字 + 录音上传式语音) |
| 后端 | s2s 独立进程 :8765(VAD 本地 + 云 ASR/LLM/TTS) | FastAPI :8000;已有 app/asr.py(dashscope paraformer)、app/llm.py(qwen + 工具)、SSL_CERT_FILE=certifi 同款处理 |
| 语音体验 | 实时全双工:本地 VAD → 流式云 ASR → 流式 LLM → 流式 TTS → 音频回放 | 录音 → 上传 → 转写 → 文字回复(无流式、无 TTS 回音) |
DPM 缺的正是 live-avatar 栈的实时交互能力;两边的云凭据/CA 处理已一致(dashscope + certifi),合并成本低。
1. 两个关键决策点
1.1 后端:内嵌进 DPM FastAPI(推荐) vs 独立 s2s 进程
- 推荐:内嵌。s2s v0.2.11 的
api/openai_realtime/websocket_router.create_app(pool, stop_event)本身就返回一个 FastAPI 应用, 可在 DPMapp.main里构建管线池后app.mount("/v1", s2s_app),与现有/api/*共存、单进程单端口。 Tauri 桌面打包只需一个后端进程,无子进程管理。 - 备选:s2s 独立跑 :8765,DPM 负责拉起/守护。改动最小,但两进程两端口,打包与部署更重。
1.2 前端:iframe 内嵌(推荐 v1) vs React 组件移植(v2)
- 推荐 v1:iframe 内嵌。把 live-avatar
demo/目录拷入 DPMpublic/voice-demo/(去掉数字人 iframe/视觉元素), React 新增VoiceAssistant.jsx(路由/voice)里<iframe src="/voice-demo/">全屏。 100% 复用现有样式与逻辑、零 React 改写、风险最低;WS 地址由voice-demo/config.js按VITE_API_BASE派生。 - v2:React 组件移植。把
s2s-ws-client.js+ 两个 worklet 拷入src/voice/,用 hooks 驱动、JSX 重绘圆球/气泡, 与 DPM 布局/Tailwind 完全融合;工作量较大,建议先 v1 跑通再决定。
2. 后端合并清单
2.1 backend/pyproject.toml
dependencies = [
# ...现有依赖(fastapi/uvicorn/dashscope/certifi 已覆盖)...
# s2s 云化栈(path 依赖,指向补丁后的 s2s-cloud;或 vendored 到 backend/vendor/)
"speech-to-speech @ file:///Volumes/Pine/mycode/opc/live/s2s-cloud",
# s2s 运行所需(base 依赖里 Darwin 专用的 mlx/transformers 等不要全装)
"torch>=2.4", # VAD(silero) 本地必需;CPU 亦可
"numpy>=1.26",
"scipy>=1.10",
"httpx>=0.28",
"websockets>=12",
"nltk==3.9.4",
"openai==2.28.0",
"soundfile>=0.13",
]
安装用
pip install -e "backend[voice]" --no-deps+ 手动补依赖,避免 s2s base 依赖把 mlx/transformers 等大件拖进来。torch是唯一的大依赖(VAD 本地必需);若部署机无 GPU,CPU torch 跑 silero 足够。
2.2 配置(app/config.py + backend/.env)
DASHSCOPE_API_KEY=sk-... # 已有(与 app/asr.py、app/llm.py 共用)
S2S_ENABLED=1
S2S_NUM_PIPELINES=4 # 并发会话数
S2S_WS_PATH=/v1/realtime
S2S_STT_MODEL=qwen3-asr-flash-realtime
S2S_LLM_MODEL=qwen-plus
S2S_TTS_MODEL=qwen3-tts-flash-realtime
S2S_TTS_VOICE=Cherry
2.3 代码(新 app/s2s_bridge.py + 挂载)
s2s_bridge.py:读取 Settings → 用与start_backend.sh等价的参数构建管线池 (--stt dashscope-asr --llm_backend chat-completions --tts qwen3-cloud --enable_live_transcription false), 返回websocket_router.create_app(pool, stop_event)。app/main.pylifespan:ENABLED时启动池 →app.mount(settings.S2S_WS_PATH, s2s_app);关闭时 stop。- 现有
/api/ai/chat、/api/ai/asr保留(旧 AiChatPanel 流程不破坏)。
3. 前端合并清单
src/pages/VoiceAssistant.jsx(新):- 全屏
<iframe src="/voice-demo/" />(v1),或未来 React 移植版。 - 加入
App.jsx路由:<Route path="/voice" element={<VoiceAssistant />} />(建议放在 ScreenLayout 内或独立全屏)。
- 全屏
public/voice-demo/:拷贝 live-avatardemo/(index.html / main.js / style.css / ws/ / worklets/ / textchat.html), 并删除数字人 iframe(index.html里的#avatar-frame→ 背景改纯色/园区图)。public/voice-demo/config.js:暴露window.VOICE_WS_URL(从VITE_API_BASE派生ws://<host>:<port>/v1/realtime)。- 麦克风权限:
- 浏览器开发模式:
localhost直接可用。 - Tauri v2 打包:
src-tauri/tauri.conf.json需app.macOSPrivateApi: true(macOS 麦克风)+ 系统隐私授权说明; Windows 需在 manifest/权限描述中声明 microphone。
- 浏览器开发模式:
4. 实施步骤
| 步骤 | 内容 | 工作量 |
|---|---|---|
| P1 | 后端内嵌:pyproject 加依赖 → s2s_bridge.py → mount → 冒烟(WS 连得上、说话→回复→出声) | 0.5–1 天 |
| P2 | 前端:public/voice-demo 拷贝去视觉化 → VoiceAssistant.jsx + 路由 → WS 指向 DPM 后端 | 0.5 天 |
| P3 | 联调:DPM 页面语音对话全链路 + 并发验证 | 0.5 天 |
| P4(可选) | React 原生移植(hooks 驱动 client + worklets,Tailwind 重绘 UI) | 1–2 天 |
5. 风险与注意
- torch 体积:VAD 本地必需(s2s 管线结构固定 VAD 本地),CPU 版可接受;若部署机极简可评估 VAD 上云(不推荐,延迟差)。
- 单管线槽:
S2S_NUM_PIPELINES默认 1 → 并发对话会拒连;按需调到 4。 - WS 鉴权:内网/桌面场景可无鉴权;如需,在 session.update 里带 token 校验。
- iframe 限制:v1 iframe 内样式隔离、无法用 DPM 主题;v2 移植可解决。
- Tauri 麦克风:打包前务必配置 macOSPrivateApi + 权限描述,否则桌面版无声/无权限。
附录 A · 视觉触发子系统(USB 摄像头 + 桌面端检测)
目标:大屏前 USB 摄像头拍观众;检测有人正对且停留 ≥10 秒 → 主动问候 + 全屏联动;问候后静默 60 秒。
A.1 架构与数据通路
大屏机(Tauri 应用,fullscreen)
└─ USB 摄像头(大屏前,拍观众)
└─ webview getUserMedia(一路视频流)
├─ 持续检测:MediaPipe FaceMesh 1~2fps(本机、不出设备)
│ ├─ 头姿 yaw/pitch 各 ±15° = 正对
│ └─ 连续 10s 正对 → 触发(窗口内 ≥80% 帧正对,防抖)
└─ 按需抓帧:LLM 决定时 canvas 取当前帧 → input_image → 云 qwen3-vl(人数/人物)
触发动作:
① 主动问候:client.sendUserText("检测到有人正对摄像头…请主动问候") + requestResponse()
→ s2s 走 LLM 生成问候语 → TTS 播报(零后端改动,复用现有 WS 协议)
② 全屏联动:调后端 → event_bus / mqtt.publish → 各屏响应
静默窗口:问候触发后 60s 冷却,期内检测照跑但不触发
A.2 Tauri 摄像头权限(必须配置)
- macOS:
tauri.conf.json→app.macOSPrivateApi: true;Info.plist 加NSCameraUsageDescription/NSMicrophoneUsageDescription(麦克风语音对话也需要)。 - Windows(NSIS 打包目标):WebView2 的 getUserMedia 跟随系统隐私设置 (设置 → 隐私和安全性 → 相机/麦克风 → 允许该应用);安装包建议在应用清单声明设备能力。
- 大屏机摄像头常开:确认系统隐私里该应用允许相机,否则检测与抓帧同时失效。
A.3 隐私与合规
- 持续检测帧仅在本机内存处理、即用即弃,不上云;只有 LLM 决定抓的那一帧发往百炼 VLM。
- 大屏前采集观众人脸属个人信息处理 → 大屏旁需公示"影像识别用于互动服务"。
A.4 落地清单(并入 P2/P3)
| 项 | 说明 |
|---|---|
前端新增 useCameraDetect hook |
getUserMedia + MediaPipe FaceMesh + 状态机(IDLE→FACING→TRIGGER→SILENT) |
| 依赖 | @mediapipe/tasks-vision(face_detector + face_landmarker) |
| 参数化 | 停留 10s / 静默 60s / 角度 ±15° / 频率 1~2fps → 配置文件或设置页 |
| 触发动作 | 主动问候(sendUserText+create)+ MQTT 联动(后端 API) |
| 首对话抓帧 | 与检测共用同一路视频流,canvas 取帧 → input_image → qwen3-vl |
注:视觉子系统是可选增强,不影响主语音对话方案;摄像头/检测不可用时语音链路照常工作。