# 开发参考 ## 1. 系统边界 这个项目当前采用单进程后端 + 浏览器本地渲染 3D 数字人的模式。 明确边界如下: - 服务端不再输出视频流,只输出音频流和动画控制帧。 - 浏览器负责 Three.js/VRM 渲染、模型加载、动画缓冲和最终口型展示。 - 浏览器和服务端之间的语音收发都通过 `WS /ws/audio` 完成。 - 字幕、状态和动画控制通过 HTTP/SSE/WebSocket 传输。 这意味着后续如果你要替换数字人渲染方案,优先改的是浏览器侧;如果你要替换识别、大模型或语音合成,优先改的是 `services/` 和 `core/`。 ## 2. 运行时主链路 ### 2.1 语音链路 1. 浏览器通过 `getUserMedia` 获取麦克风。 2. 浏览器连接 `WS /ws/audio`,上传 `pcm_s16le` 音频块,并接收服务端回传的回复语音片段。 3. 服务端在 [main.py](../main.py) 中接收二进制 PCM 并进入 `_process_user_audio_chunk()`。 4. 音频被重采样为 16k 单声道后送入 VAD。 5. VAD 检测到用户语音结束后,触发 `ChatPipeline.process_turn()`。 6. `ASRService` 输出用户文本。 7. `LLMService` 调用线上 DeepSeek 兼容接口生成回答。 8. `TTSService` 按句子分段合成。 9. 每段 TTS 音频一方面通过 `WS /ws/audio` 下发给浏览器播放;另一方面经 `AvatarService` 转成动画帧,通过 `/ws/animation` 发给浏览器。 10. 浏览器在播放语音的同时,从动画缓冲队列中取帧,驱动 VRM 表情、GLTF morph target 和头部骨骼。 ### 2.2 文本链路 1. 浏览器调用 `POST /chat/text`。 2. 后端直接执行 `LLM -> TTS -> 动画控制`。 3. 字幕通过 `/ws/subtitles` 下发,语音通过 `WS /ws/audio` 下发。 文本链路主要用于调试 LLM/TTS/动画,不依赖麦克风、VAD 和 ASR。 ## 3. 后端模块职责 ### 3.1 入口层 [main.py](../main.py) 负责: - 应用初始化。 - 管理全局单例服务。 - 维护音频、字幕、动画客户端集合。 - 维护 `RuntimeState`,给 `/health` 和 `/events` 提供实时指标。 当前实现假设一次只保留一个主音频 WebSocket 连接。新连接到来时会主动关闭旧连接。这是刻意简化,不是 bug。 现在这个约束已经扩展到三个浏览器通道: - `WS /ws/audio` - `WS /ws/subtitles` - `WS /ws/animation` 也就是说,服务端当前只允许一个浏览器页面完整接管数字人会话。新页面连上后,旧页面会被主动断开,关闭码为 `1012 replaced`。 ### 3.2 会话状态机 [core/state_machine.py](../core/state_machine.py) 的状态很轻: - `idle` - `user_speaking` - `thinking` - `avatar_speaking` 它目前主要服务于两个目标: - 打断时取消数字人播报。 - 给前端和监控面板提供可观测状态。 如果后面要做更复杂的并发控制,比如排队、多会话或多路媒体编排,这里需要升级成更明确的 session controller,而不只是一个枚举状态机。 ### 3.3 流水线层 [core/pipeline.py](../core/pipeline.py) 是主编排器。 当前设计特点: - `process_turn()` 处理语音输入。 - `process_text_turn()` 处理文本输入。 - TTS 会按句或短片段切分,以降低首包时延。 - 每段合成结果都会回调到上层,用于同步推送音频和动画。 后续如果要接入流式 LLM 或更细粒度的流式 TTS,这个文件是首选改造点。 ### 3.4 服务层 `services/` 下的实现都偏适配器风格: - [services/asr.py](../services/asr.py): 本地 ASR 模型封装。 - [services/llm.py](../services/llm.py): DeepSeek/OpenAI 兼容接口封装,并维护短历史上下文。 - [services/tts.py](../services/tts.py): 本地 TTS 模型封装。 - [services/vad.py](../services/vad.py): 语音起止检测。 - [services/avatar.py](../services/avatar.py): 从音频提取能量和频谱亮度,再映射到嘴型和头部姿态。 这里最大的工程价值是“可替换性”。后续更换供应商或模型时,尽量保持 `core/pipeline.py` 的接口不变,只替换 `services/` 内部实现。 ## 4. 浏览器侧实现 [web/app.js](../web/app.js) 同时承担了以下职责: - 建立音频 WebSocket。 - 订阅 `/events`、`/ws/subtitles`、`/ws/animation`。 - 维护聊天消息面板和诊断面板。 - 用 Three.js + VRM 加载和渲染模型。 - 把动画控制帧缓存约 `120ms` 后再播放,尽量和音频保持一致。 - 处理刷新后的语音恢复:如果浏览器拦截自动恢复,会等待下一次用户手势再恢复麦克风和播放上下文。 前端当前兼容三种形态: - VRM expression manager。 - 通用 GLTF morph target。 - 没有表情 rig 时回退到调试头像。 当前前端待机动画状态: - 已启用随机眨眼。 - 更激进的身体 idle motion 已回退,因为部分 VRM 模型会出现 T pose 或姿态异常。 如果以后想接入真实业务数字人模型,先确认模型是否提供: - 标准 VRM 表情。 - 或稳定可映射的 morph target 名称。 - 或至少可控制的头骨/颈骨。 ## 5. 动画协议 服务端通过 `/ws/animation` 发送 JSON,关键字段包括: - `type`: `animation_ready` / `animation_state` / `animation_reset` / `animation_chunk` - `driver`: 当前驱动器名称。 - `mode`: 驱动模式。 - `schema`: 控制 schema,当前默认是 ARKit 风格 blendshape。 - `fps`: 帧率。 - `frame_count`: 当前消息内帧数。 - `frames`: 每帧控制数据。 `frames[*]` 典型结构: ```json { "seq": 101, "time_ms": 80, "speaking": true, "controls": { "jawOpen": 0.42, "mouthClose": 0.66, "mouthFunnel": 0.21, "mouthPucker": 0.18, "viseme_aa": 0.42, "viseme_ee": 0.27, "viseme_oh": 0.31, "headYaw": 0.01, "headPitch": -0.02, "headRoll": 0.01 } } ``` 注意:当前口型驱动是“基于音频特征估算”,不是基于音素或强制对齐的精确 viseme,所以它更适合演示验证,不适合直接当作高精度唇形同步方案。 ## 6. 字幕协议 `/ws/subtitles` 发送的消息结构比较简单: - `role`: `user` 或 `ai` - `text`: 文本内容 - `source`: `voice` 或 `text` - `partial`: 是否是分段中间结果 - `final`: 是否已经结束 - `ts_ms`: 时间戳 前端对 AI 文本采用“局部追加”策略,所以如果以后改成 token streaming,要继续保证消息顺序和终止标记一致。 ## 7. 状态与观测 后端通过 `/health` 和 `/events` 暴露运行状态。推荐排查问题时按这个顺序看: 1. `state`: 当前状态机是否符合预期。 2. `audio_clients`: 语音通道是否真正建连。 3. `vad_start_count` / `vad_end_count`: 是否检测到语音边界。 4. `last_asr_latency_ms` / `last_llm_latency_ms` / `last_tts_latency_ms`: 性能瓶颈在哪一段。 5. `llm.ready` / `asr.ready` / `tts.ready` / `vad.ready`: 组件有没有降级。 6. `last_animation_frame_count`: 是否真正产生了控制帧。 ## 8. 当前约束 这些约束需要后续开发明确知晓: - 当前是进程内单例服务,不是多租户、多 session 设计。 - 浏览器侧只允许一个页面完整接管服务端语音输入输出与字幕/动画订阅。 - 模型推理基本都直接跑在应用事件循环附近,适合研发验证,不适合高并发。 - 当前动画驱动依赖音频能量和谱质心,不具备严格唇音级别精度。 - 语音链路高度依赖 VAD;如果要做电话式长连接交互,建议补更稳的 turn management。 - 浏览器刷新后不保证零手势恢复语音;这是浏览器媒体策略约束,不是后端故障。 ## 9. 推荐扩展方向 按收益排序,后续建议优先做这些: 1. 把 `RuntimeState`、音频缓冲和历史上下文下沉到真正的 session 对象,摆脱全局单例。 2. 把 ASR、TTS、动画生成从主事件循环中隔离出去,避免阻塞 FastAPI/WebSocket。 3. 为 `/ws/subtitles` 和 `/ws/animation` 增加明确版本号和协议文档,避免前后端演进时互相踩踏。 4. 如果追求更真实口型,改成音素/viseme 驱动而不是纯音频特征驱动。 5. 为关键链路补自动化回归,至少覆盖文本链路、语音链路和打断流程。 ## 10. 常用调试入口 - `GET /health`: 看整体运行状态。 - `GET /events`: 看持续状态流。 - `POST /chat/text`: 快速验证 LLM/TTS/动画,不依赖麦克风。 - `WS /ws/audio`: 看语音上传和语音回放主通道。 - `GET /avatar/schema`: 看控制字段列表。 - [scripts/run_all_checks.sh](../scripts/run_all_checks.sh): 统一检查入口。 脚本使用建议: - [scripts/model_probe.py](../scripts/model_probe.py): 快速确认 ASR/TTS/VAD 是否能加载。 - [scripts/qa_check.py](../scripts/qa_check.py): 直接压文本链路,适合看 LLM/TTS 延迟。 - [scripts/smoke_test.py](../scripts/smoke_test.py): 输出 `wav + mp4`,其中 `mp4` 是基于动画控制帧绘制的调试视频,不是最终 3D 渲染结果。 如果你刚接手这个项目,建议先按下面顺序理解: 1. [README.md](../README.md) 2. [main.py](../main.py) 3. [core/pipeline.py](../core/pipeline.py) 4. [services/avatar.py](../services/avatar.py) 5. [web/app.js](../web/app.js)