# 3D 数字人驱动重构方案

## 1. 目标变更

原链路：

ASR -> LLM -> TTS -> MuseTalk/图片或视频驱动 -> WebRTC 视频轨 -> 前端播放

新链路：

ASR -> LLM -> TTS -> 音频特征/viseme/blendshape 控制数据 -> 前端 3D 渲染引擎 -> 浏览器展示

核心变化：
- 后端不再负责生成数字人视频帧
- 后端保留实时语音、时序仲裁、字幕、音频下行
- 后端新增 3D 控制流输出能力
- 3D 模型渲染放到前端或独立渲染端

## 2. 为什么要这样改

MuseTalk 这类方案适合“2D 说话视频合成”，但不适合 3D 模型系统：
- 输出形态是视频帧，不是骨骼或 blendshape 控制数据
- 一旦换 3D 模型，原本的视频生成结果无法直接复用
- 每轮都做视频推理，延迟、GPU 占用、链路复杂度都偏高
- 3D 系统通常需要的是时间序列控制参数，而不是渲染后的像素

因此，3D 化的正确边界是：
- 后端负责“说什么、怎么说、何时说”
- 渲染端负责“模型如何动、如何显示”

## 3. 本次代码重构内容

当前代码已调整为：
- WebRTC 下行仅保留音频轨
- 新增 `/ws/animation` WebSocket，下发 3D 动画控制数据
- `services/avatar.py` 改为音频到控制参数的驱动服务
- `/avatar/schema` 暴露控制协议说明
- 前端采用 Three.js + GLTFLoader + VRM loader，直接在浏览器渲染 3D 模型
- 支持加载本地 VRM/GLB 文件，也支持加载 URL 形式的 VRM/GLB/GLTF
- 若未加载模型，页面会回退到一个调试用的 3D 头像，便于联调口型和头部姿态

当前控制协议字段包括：
- `jawOpen`
- `mouthClose`
- `mouthFunnel`
- `mouthPucker`
- `viseme_aa`
- `viseme_ee`
- `viseme_oh`
- `headYaw`
- `headPitch`
- `headRoll`

输出包结构示例：

```json
{
  "type": "animation_chunk",
  "driver": "arkit-blendshape-v1",
  "mode": "blendshape_stream",
  "schema": "arkit",
  "chunk_index": 0,
  "total_chunks": 3,
  "sample_rate": 24000,
  "fps": 25,
  "text": "你好，欢迎使用系统。",
  "frame_count": 18,
  "duration_ms": 720,
  "frames": [
    {
      "seq": 102,
      "time_ms": 0,
      "speaking": true,
      "controls": {
        "jawOpen": 0.41,
        "mouthClose": 0.66,
        "mouthFunnel": 0.27,
        "mouthPucker": 0.19,
        "viseme_aa": 0.41,
        "viseme_ee": 0.24,
        "viseme_oh": 0.31,
        "headYaw": 0.004,
        "headPitch": 0.009,
        "headRoll": 0.001
      }
    }
  ]
}
```

## 4. 当前实现的定位

当前实现是“联调级骨架”，作用是：
- 先把系统边界改对
- 让前后端可以并行开发
- 让 3D 引擎接入方尽快有稳定协议可以消费

它不是最终口型算法，原因很明确：
- 目前控制值来自音频能量和频谱特征
- 没有做 phoneme 强制对齐
- 没有做语言相关 viseme 映射
- 没有结合 3D 模型自身 rig 约束做校准

所以这版适合：
- 打通链路
- 调接口
- 验证时序
- 校验驱动字段

不适合直接作为最终高保真口型方案。

## 5. 推荐的生产化方案

### 方案 A：前端 Three.js / React Three Fiber

适合：
- 浏览器直接渲染 WebGL 角色
- glTF/VRM 模型
- ARKit blendshape 或自定义 morph target

建议做法：
- 前端加载 3D 模型
- 建立 `controlsKey -> morphTargetInfluence` 映射表
- `/ws/animation` 按时间戳缓冲一小段再播放
- 音频继续使用 WebRTC 远端音频轨
- 前端用远端音频播放时间作为动画时钟基准

当前仓库已经按这个方向落地了第一版：
- 静态页面通过 import map 从 CDN 引入 `three`、`GLTFLoader`、`@pixiv/three-vrm`
- VRM 优先使用 `expressionManager` 写入 `aa/ee/oh/ou`
- 普通 glTF 则尝试把控制字段映射到 morph target
- 头部姿态优先驱动 `head/neck` 骨骼，找不到时回退调试头像

### 方案 B：Unity/Unreal 渲染端

适合：
- 更复杂的材质、灯光、骨骼、表情系统
- 要求更强的 3D 表现力

建议做法：
- 浏览器只做 UI 和音频交互
- Unity/Unreal 作为单独渲染客户端消费 `/ws/animation`
- 浏览器与渲染端通过流媒体或共享画面集成

### 方案 C：专用音频驱动服务

适合：
- 追求更准的嘴型
- 后端可以接受增加一个模型服务

可以考虑接入：
- phoneme 对齐模型
- viseme 预测模型
- NVIDIA Audio2Face 类服务
- 自研 `audio -> blendshape` 网络

推荐协议保持不变：
- 外部模型服务可以替换 `services/avatar.py`
- 对外仍输出统一 `frames[].controls`

## 6. 推荐的下一步实施顺序

1. 先确定 3D 模型和渲染端技术栈
2. 固定一份 blendshape 映射表
3. 把当前前端调试面板替换成真实 3D 场景
4. 增加时间同步与缓冲策略
5. 再替换掉启发式控制算法，接入更强的 viseme 模型

## 7. 接口约束建议

为了后续可替换，建议保持这些约束不变：
- 下行动画通道继续使用 `/ws/animation`
- 每条消息包含 `frame_count`、`fps`、`frames`
- 每帧保留 `seq` 和 `time_ms`
- `controls` 使用稳定字段名，不轻易改动
- 新增字段时只追加，不破坏已有字段

## 8. 风险与注意事项

- 真实 3D 模型的 blendshape 命名不一定和当前字段一致，需要做一次映射层
- 如果模型是骨骼嘴型而不是 morph target，需要把 `controls` 转成骨骼驱动参数
- 浏览器渲染时要注意音频时钟和动画时钟一致，否则会出现“听起来对，但嘴型慢半拍”
- 如果后续改成 phoneme/viseme 对齐，建议在包里增加 phoneme 片段和置信度字段

## 9. 结论

这次重构的重点不是“把 2D 数字人继续硬改下去”，而是把系统边界调整到适合 3D 模型的形态。

现在的系统已经从“视频合成型数字人”切到了“控制流型数字人”，后续无论接 Three.js、Unity、Unreal，还是专门的 Audio2Face/viseme 服务，都可以在这个边界上继续演进。
