save code

This commit is contained in:
xsl
2026-03-29 20:50:53 +08:00
parent f24de38e94
commit 36e838caec
17 changed files with 381 additions and 2048 deletions
-189
View File
@@ -1,189 +0,0 @@
# 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` 骨骼,找不到时回退调试头像
### 方案 BUnity/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 服务,都可以在这个边界上继续演进。
-250
View File
@@ -1,250 +0,0 @@
# 可视化语音聊天系统(初版)说明文档
本文档覆盖:
- 资源位置(代码、模型、环境)
- 本机启动与部署
- 局域网/远程访问
- 常用运维命令
- 验收与排障
---
## 1. 项目概览
当前项目实现的是单机部署的可视化聊天系统(WebRTC):
- 用户语音上行(VAD + ASR
- 文本调用在线 LLM
- 本地 TTS 合成语音
- WebRTC 音频下行 + 3D 数字人控制流下行(WebSocket
- 前端字幕与指标面板(SSE + WebSocket
- 单工仲裁(支持 barge-in 打断)
代码根目录:
- `/home/xsl/product`
---
## 2. 代码结构(关键目录)
- `main.py`:后端入口(FastAPI + WebRTC + 全链路编排)
- `config.py`:配置加载(读取 `.env`
- `core/`
- `state_machine.py`:会话状态机(单工仲裁)
- `pipeline.py`VAD/ASR/LLM/TTS/字幕/时延逻辑
- `services/`
- `asr.py`SenseVoice
- `llm.py`:在线模型 + fallback
- `tts.py`Kokoro
- `vad.py`Silero VAD
- `avatar.py`:音频转 3D 控制数据(blendshape / 头部姿态)
- `webrtc/`
- `tracks.py`:实际音频轨道实现
- `gateway.py``audio_track.py`:结构化封装
- `web/`
- `index.html``app.js``style.css`
- `scripts/`
- 启停:`service.sh``start-public.sh`
- 验收:`run_all_checks.sh``qa_check.py``smoke_test.py``model_probe.py``vad_check.py``tts_stress.py``llm_fallback_check.py`
- 监控:`gpu_monitor.sh`
---
## 3. 环境与模型资源位置
### 3.1 Conda 环境
当前实际运行环境:
- `/home/xsl/miniconda3/envs/MuseTalk`
系统启动脚本固定使用该 Python:
- `scripts/start-public.sh``PY=/home/xsl/miniconda3/envs/MuseTalk/bin/python`
### 3.2 主要模型路径
MuseTalk 模型目录:
- `/home/xsl/work/MuseTalk/models/`
关键文件:
- UNet`/home/xsl/work/MuseTalk/models/musetalkV15/unet.pth`
- 配置:`/home/xsl/work/MuseTalk/models/musetalkV15/musetalk.json`
- Whisper`/home/xsl/work/MuseTalk/models/whisper`
数字人源视频(写实底片 + MuseTalk 驱动源):
- `/home/xsl/work/MuseTalk/data/video/yongen.mp4`
MuseTalk 推理脚本:
- `/home/xsl/work/MuseTalk/scripts/inference.py`
### 3.3 其他环境记录
完整环境与模型清单参考:
- `environments.md`
---
## 4. 配置文件
主配置文件:
- `/home/xsl/product/.env`
当前关键项(示例):
- `LLM_API_KEY`
- `LLM_BASE_URL=https://api.deepseek.com`
- `WEBRTC_HOST=0.0.0.0`
- `WEBRTC_PORT=8080`
- `STUN_URL=stun:stun.l.google.com:19302`
- `SSL_CERTFILE=/home/xsl/product/certs/dev-cert.pem`
- `SSL_KEYFILE=/home/xsl/product/certs/dev-key.pem`
说明:
- `LLM_MODEL` 为空时会自动按 `base_url` 推断(DeepSeek 默认 `deepseek-chat`)。
---
## 5. 启动与部署
### 5.1 本机启动(推荐)
```bash
bash /home/xsl/product/scripts/service.sh start
bash /home/xsl/product/scripts/service.sh status
```
重启:
```bash
bash /home/xsl/product/scripts/service.sh restart
```
停止:
```bash
bash /home/xsl/product/scripts/service.sh stop
```
日志:
```bash
bash /home/xsl/product/scripts/service.sh logs
```
### 5.2 访问地址
- HTTPS(推荐):`https://<服务器IP>:8080/`
- 本机:`https://127.0.0.1:8080/`
如果你使用自签证书,浏览器首次需要手动信任。
### 5.3 远程/跨机器访问
见:
- `REMOTE_ACCESS.md`
重点:
- 跨机器麦克风通常要求 HTTPS
- WSL2 场景可能需要 Windows 端口转发/防火墙放行
---
## 6. 一键验收与调试命令
### 6.1 一键全检查
```bash
bash /home/xsl/product/scripts/run_all_checks.sh
```
覆盖:
- 服务状态
- 文本链路压测
- LLM fallback
- VAD 检查
- TTS 20 句稳定性
- smoke 媒体输出(`wav + mp4`
### 6.2 单项脚本
```bash
conda run -n MuseTalk python /home/xsl/product/scripts/model_probe.py
conda run -n MuseTalk python /home/xsl/product/scripts/qa_check.py --base-url "https://127.0.0.1:8080" --rounds 20
conda run -n MuseTalk python /home/xsl/product/scripts/llm_fallback_check.py
conda run -n MuseTalk python /home/xsl/product/scripts/vad_check.py
conda run -n MuseTalk python /home/xsl/product/scripts/tts_stress.py
conda run -n MuseTalk python /home/xsl/product/scripts/smoke_test.py --text "全链路验收"
```
### 6.3 长稳与GPU监控
1小时长稳:
```bash
conda run -n MuseTalk python /home/xsl/product/scripts/longrun_test.py --base-url "https://127.0.0.1:8080" --minutes 60 --interval-sec 30
```
GPU采样(CSV):
```bash
bash /home/xsl/product/scripts/gpu_monitor.sh /home/xsl/product/outputs/gpu-1h.csv 3600 5
```
---
## 7. 前端功能说明
网页包含:
- Three.js 3D 数字人舞台
- 文本输入框
- 字幕消息区(WebSocket
- 指标面板(SSE
当前前端能力:
- 浏览器通过 import map 直接加载 Three.js、GLTFLoader、three-vrm
- 可加载本地 `VRM`/`GLB` 文件
- 可加载 URL 形式的 `VRM`/`GLB`/`GLTF` 模型
- 页面默认会自动尝试加载官方 `three-vrm` 示例模型:`VRM1_Constraint_Twist_Sample.vrm`
- 未加载真实模型时会显示调试头像,仍可联调动画控制流
当前已支持:
- 页面刷新自动重连
- 单连接策略(新连接踢掉旧连接)
- 句级字幕流式下发
- 打断耗时等指标展示
---
## 8. 当前已知边界
- 当前后端输出的是可直接对接 3D 引擎的控制数据,不负责最终 3D 渲染。
- 当前控制数据基于音频能量和频谱特征做启发式推断,适合作为联调骨架,不等同于生产级口型模型。
- 若要达到更高口型精度,建议后续接入 phoneme/viseme 对齐模型或 Audio2Face 类推理服务。
---
## 9. 快速排障
1) 页面无声/无画面
- 先看 `service.sh status`
-`/health` 是否正常
- 浏览器是否授权麦克风、是否信任 HTTPS 证书
2) 连接冲突
- 系统是单连接模式,新连接会断开旧连接(预期)
3) 口型不动
- 查看 `/health``avatar.last_frame_count``avatar.last_error`
- 查看前端 `/ws/animation` 是否已连接,控制流预览是否持续刷新
- 若模型已加载但不动,先检查该模型是否带有 VRM expression 或 morph target
4) 模型异常
-`scripts/model_probe.py` 快速定位 ASR/TTS/VAD
---
## 10. 版本建议
如果要做“生产化下一步”,建议优先:
- 增加 TURN(公网复杂 NAT
- 增加多会话隔离(当前默认单会话)
- 增加 Prometheus/结构化日志
- 增加模型切换开关(Kokoro/Qwen3-TTS
+119
View File
@@ -0,0 +1,119 @@
# 3D 数字人语音聊天系统
这个项目提供一个浏览器侧渲染的 3D 数字人语音交互原型:
- 浏览器通过 WebRTC 上传麦克风音频,并接收服务端回传的 AI 语音。
- 浏览器通过 WebSocket 订阅字幕和口型/头部控制数据。
- 服务端执行 VAD -> ASR -> DeepSeek LLM -> TTS,并把音频特征转换为驱动 3D 数字人的控制帧。
当前形态更接近单机演示和研发验证环境,不是已经拆分完毕的生产化架构。
## 当前架构
语音链路:
```text
Browser Mic
-> WebRTC audio upstream
-> FastAPI / aiortc
-> VAD
-> ASR
-> LLM
-> TTS
-> WebRTC audio downstream
-> browser audio playback
```
控制链路:
```text
LLM reply text / TTS audio
-> AvatarService
-> /ws/animation
-> browser animation buffer
-> VRM or GLTF morph targets / head bones
```
字幕链路:
```text
ASR text / LLM segmented reply
-> /ws/subtitles
-> browser message panel
```
## 关键目录
- [main.py](main.py): FastAPI 入口,聚合 WebRTC、WebSocket、健康状态和文本对话接口。
- [core/pipeline.py](core/pipeline.py): 语音/文本对话流水线,负责 ASR、LLM、TTS 编排。
- [core/state_machine.py](core/state_machine.py): 简单会话状态机,处理用户说话、思考、数字人说话之间的切换。
- [services/asr.py](services/asr.py): ASR 封装,优先使用 FunASR,失败时进入降级模式。
- [services/llm.py](services/llm.py): 在线 LLM 封装,默认兼容 DeepSeek OpenAI-style API。
- [services/tts.py](services/tts.py): TTS 封装,优先使用 Kokoro,失败时返回静音兜底。
- [services/vad.py](services/vad.py): VAD 封装,基于 silero-vad。
- [services/avatar.py](services/avatar.py): 把音频转换成 blendshape 和头部控制帧。
- [web/app.js](web/app.js): 浏览器端 3D 舞台、WebRTC 建连、字幕和动画消费逻辑。
- [scripts](scripts): 启动、证书生成、巡检和服务管理脚本。
## 快速启动
安装依赖:
```bash
pip install -r requirements.txt
```
准备环境变量,至少包括:
```bash
export LLM_API_KEY="..."
export LLM_BASE_URL="https://api.deepseek.com/v1"
export LLM_MODEL="deepseek-chat"
```
本地启动:
```bash
python main.py
```
或使用脚本:
```bash
bash scripts/start-public.sh
```
启动后访问:
- `http://127.0.0.1:8080/`
- `http://127.0.0.1:8080/health`
如果浏览器与服务端跨机器访问,麦克风采集通常需要 HTTPS。可以先生成自签证书:
```bash
bash scripts/gen-self-signed-cert.sh
```
再在环境变量或 `.env` 中配置 `SSL_CERTFILE``SSL_KEYFILE`
## 核心接口
- `GET /`: 前端页面。
- `POST /webrtc/offer`: 建立 WebRTC 音频连接。
- `POST /chat/text`: 文本输入链路,跳过 ASR/VAD。
- `POST /chat/reset`: 清空上下文和运行时状态。
- `GET /health`: 返回后端运行指标和组件健康状态。
- `GET /meta`: 返回 STUN、HTTPS、端口和动画协议配置。
- `GET /events`: Server-Sent Events 运行状态流。
- `WS /ws/subtitles`: 字幕流。
- `WS /ws/animation`: 数字人控制流。
- `GET /avatar/schema`: 当前控制协议 schema。
## 后续开发建议
优先阅读 [docs/development-guide.md](docs/development-guide.md)。这个文档说明了:
- 语音与文本两条链路的完整时序。
- 前后端之间的协议约定。
- 当前实现的边界和适合继续扩展的位置。
- 已知的工程化约束。
-72
View File
@@ -1,72 +0,0 @@
# 跨机器访问说明
## 1) 服务监听
已默认监听 `0.0.0.0:8080`,同网段机器可访问:
- `http://<你的机器IP>:8080/`
推荐用服务脚本管理(避免旧进程占端口):
```bash
bash /home/xsl/product/scripts/service.sh start
bash /home/xsl/product/scripts/service.sh status
bash /home/xsl/product/scripts/service.sh logs
bash /home/xsl/product/scripts/service.sh restart
bash /home/xsl/product/scripts/service.sh stop
```
查询本机 IP:
```bash
ip addr
```
## 2) 浏览器麦克风权限(关键)
跨机器访问时,浏览器通常要求 **HTTPS** 才允许 `getUserMedia`
### 方案 A(推荐):启用 HTTPS
```bash
bash /home/xsl/product/scripts/gen-self-signed-cert.sh
```
然后编辑 `.env`:
```env
SSL_CERTFILE=/home/xsl/product/certs/dev-cert.pem
SSL_KEYFILE=/home/xsl/product/certs/dev-key.pem
```
启动:
```bash
bash /home/xsl/product/scripts/service.sh restart
```
访问:
- `https://<你的机器IP>:8080/`
首次需要在浏览器信任自签证书。
### 方案 BHTTP 测试
如果仅做 UI 验证,可先用 HTTP;但麦克风可能被浏览器拒绝。
## 3) WSL2 网络注意事项
如果服务跑在 WSL2,其他机器无法直接访问时,需要在 Windows 做端口转发与防火墙放行。
最简单做法:在 Windows 上运行同样服务,或配置端口代理到 WSL IP。
## 4) WebRTC 联通
系统已配置 STUN`.env` 里的 `STUN_URL`),用于获取可达候选地址:
```env
STUN_URL=stun:stun.l.google.com:19302
```
若跨公网且 NAT 复杂,需增加 TURN(后续可加)。
-278
View File
@@ -1,278 +0,0 @@
# 3D Digital Human System Technical Documentation
## 1. Project Summary
This project implements a real-time conversational digital human system with these core capabilities:
- Text and voice interaction
- LLM response generation (DeepSeek online, with fallback)
- TTS synthesis for reply audio
- Audio-driven 3D animation control stream
- Browser-side 3D rendering (Three.js + VRM/GLTF)
- WebRTC audio transport and WebSocket animation transport
Current workspace root:
- `/home/xsl/code/product`
## 2. Runtime Architecture
### 2.1 High-Level Pipeline
1. User input enters through `/chat/text` or voice pipeline.
2. LLM generates reply text.
3. TTS synthesizes reply waveform.
4. Avatar service converts audio chunks to animation control frames.
5. Backend pushes:
- reply audio to WebRTC audio track
- animation frames to `/ws/animation`
6. Frontend renders 3D model and applies mouth/head/body motions.
### 2.2 Transport Split
- WebRTC: audio only
- WebSocket (`/ws/animation`): animation control data
- SSE (`/events`): system metrics and runtime status
This split removes server-side video generation and makes rendering native in the browser.
## 3. Core Modules
### 3.1 Backend Entry
- `main.py`
Responsibilities:
- FastAPI app and route registration
- WebRTC offer/peer management
- Animation WebSocket broadcasting
- Runtime metrics and `/health`
- Session reset and chat endpoints
### 3.2 Pipeline Orchestration
- `core/pipeline.py`
- `core/state_machine.py`
Responsibilities:
- Turn-based orchestration (input -> LLM -> TTS -> output)
- State transitions (idle, thinking, speaking)
- Latency accounting and result metadata
### 3.3 Services
- `services/llm.py`
- `services/tts.py`
- `services/asr.py`
- `services/vad.py`
- `services/avatar.py`
Responsibilities:
- `LLMService`: online model calls via OpenAI-compatible client
- `TTSService`: speech synthesis
- `AvatarService`: audio-to-controls mapping for mouth/viseme/head motion
### 3.4 Frontend
- `web/index.html`
- `web/app.js`
- `web/style.css`
Responsibilities:
- Three.js scene setup and camera controls
- VRM/GLTF model loading (URL or local file)
- Animation buffering and frame application
- Idle body pose + talking expressions
- WebRTC audio playback
## 4. Animation Data Contract
Backend pushes animation chunks shaped like:
- `type`: `animation_chunk` / `animation_reset` / `animation_state`
- `fps`, `duration_ms`, `frame_count`
- `frames[]` with:
- `seq`
- `time_ms`
- `controls`
Typical controls include:
- `jawOpen`
- `viseme_aa`, `viseme_ee`, `viseme_oh`
- `mouthPucker`
- `headYaw`, `headPitch`, `headRoll`
Frontend behavior:
- buffers frames for stable playback
- applies VRM expressions when available
- falls back to morph targets or bone motion
## 5. Configuration
Configuration file:
- `.env` (root)
### 5.1 LLM Settings
Supported API key aliases:
- `LLM_API_KEY`
- `DEEPSEEK_API_KEY`
- `OPENAI_API_KEY`
Recommended DeepSeek settings:
- `LLM_BASE_URL=https://api.deepseek.com/v1`
- `LLM_MODEL=deepseek-chat`
### 5.2 Server Settings
- `WEBRTC_HOST` (default `0.0.0.0`)
- `WEBRTC_PORT` (default `8018`)
- `SSL_CERTFILE`
- `SSL_KEYFILE`
### 5.3 Avatar Settings
- `AVATAR_DRIVER_MODE=blendshape_stream`
- `AVATAR_CONTROL_PROTOCOL=ws`
- `AVATAR_BLENDSHAPE_SCHEMA=arkit`
## 6. Startup and Process Control
New process script:
- `scripts/service-8018.sh`
Usage:
```bash
bash scripts/service-8018.sh start
bash scripts/service-8018.sh status
bash scripts/service-8018.sh logs
bash scripts/service-8018.sh restart
bash scripts/service-8018.sh stop
```
Behavior:
- Loads environment from `.env`
- Starts uvicorn from project venv if available
- Supports HTTPS cert/key from `.env`
- Writes pid and log to `.run/`
- Cleans stale python listeners on target port
Runtime files:
- `.run/visual-chat-8018.pid`
- `.run/visual-chat-8018.log`
## 7. API and Web Endpoints
### 7.1 Health and Metadata
- `GET /health`
- `GET /meta`
### 7.2 Chat and Session
- `POST /chat/text`
- `POST /chat/reset`
### 7.3 Streaming Channels
- `GET /events` (SSE)
- `GET /ws/subtitles` (WebSocket)
- `GET /ws/animation` (WebSocket)
### 7.4 WebRTC
- `POST /webrtc/offer`
## 8. Validation Checklist
### 8.1 Service Up
- `bash scripts/service-8018.sh status` should show running
- `GET /health` should return valid JSON
### 8.2 LLM Online
Health fields should indicate:
- `llm.ready = true`
- `llm.error = null`
- `last_llm_source = online` after at least one chat turn
### 8.3 Frontend Motion and Audio
- Model loads successfully
- Mouth motion follows replies
- Body remains in non-T-pose idle stance
- Remote audio is audible via WebRTC
## 9. Troubleshooting Guide
### 9.1 LLM stays fallback
Symptoms:
- `last_llm_source = fallback`
- `llm.ready = false`
Checks:
1. Confirm `.env` exists in workspace root.
2. Confirm key value is set in `LLM_API_KEY` (or alias).
3. Restart service after any `.env` update.
4. Verify `llm.base_url` and `llm.model` in `/health`.
### 9.2 Model load errors
Symptoms:
- Web UI shows model fetch failures
Checks:
1. Use reachable URL or local `.vrm` file.
2. Confirm browser can access external CDN.
3. Prefer VRM full-body models for body-pose control.
### 9.3 Mouth not moving
Checks:
1. Confirm animation websocket connected.
2. Confirm animation frame count increases.
3. Confirm TTS is ready and not in silence fallback.
### 9.4 HTTPS/microphone issues on LAN
Checks:
1. Use valid cert paths in `.env`.
2. Trust self-signed cert on browser/device.
3. Ensure firewall allows target port.
## 10. Security and Operations Notes
- Keep `.env` out of source control.
- Rotate API keys regularly.
- Restrict exposed port by network policy if internet-facing.
- Consider reverse proxy and managed TLS for production.
## 11. Suggested Next Production Steps
1. Add structured request tracing (request id, turn id).
2. Add persistent metrics export (Prometheus/OpenTelemetry).
3. Add automatic reconnect and health watchdog.
4. Add integration tests for LLM online/fallback branches.
5. Add model asset mirror to avoid public CDN instability.
+6 -4
View File
@@ -90,12 +90,14 @@ class ChatPipeline:
for i, seg in enumerate(segments):
if should_interrupt and should_interrupt():
break
if on_text_segment is not None:
await on_text_segment(seg, i, len(segments))
seg_audio, seg_sr = await self.tts.synthesize(seg)
if should_interrupt and should_interrupt():
break
sr = seg_sr
if i == 0:
first_chunk_ms = int((time.perf_counter() - t0) * 1000)
if on_text_segment is not None:
await on_text_segment(seg, i, len(segments))
if on_audio_chunk is not None:
await on_audio_chunk(seg_audio, seg_sr, seg, i, len(segments))
audio_chunks.append(seg_audio)
@@ -122,13 +124,13 @@ class ChatPipeline:
if on_reply_text is not None:
await on_reply_text(reply_text)
llm_latency_ms = int((time.perf_counter() - llm_t0) * 1000)
await self.arbitrator.on_avatar_start()
audio, sr, tts_first_chunk_ms, tts_latency_ms = await self._synthesize_sentence_first(
reply_text,
on_audio_chunk=on_audio_chunk,
on_text_segment=on_reply_segment,
should_interrupt=lambda: self.arbitrator.cancel_avatar_event.is_set(),
)
await self.arbitrator.on_avatar_start()
playback_s = max(0.2, min(5.0, float(audio.shape[0]) / float(sr)))
# Do not block request completion on playback duration.
asyncio.create_task(self._finish_avatar_after_playback(playback_s))
@@ -157,13 +159,13 @@ class ChatPipeline:
if on_reply_text is not None:
await on_reply_text(reply_text)
llm_latency_ms = int((time.perf_counter() - llm_t0) * 1000)
await self.arbitrator.on_avatar_start()
audio, sr, tts_first_chunk_ms, tts_latency_ms = await self._synthesize_sentence_first(
reply_text,
on_audio_chunk=on_audio_chunk,
on_text_segment=on_reply_segment,
should_interrupt=lambda: self.arbitrator.cancel_avatar_event.is_set(),
)
await self.arbitrator.on_avatar_start()
playback_s = max(0.2, min(5.0, float(audio.shape[0]) / float(sr)))
asyncio.create_task(self._finish_avatar_after_playback(playback_s))
payload = {
+2 -1
View File
@@ -32,4 +32,5 @@ class Arbitrator:
async def on_avatar_done(self) -> None:
async with self._lock:
self.state = SessionState.IDLE
if self.state == SessionState.AVATAR_SPEAKING:
self.state = SessionState.IDLE
+209
View File
@@ -0,0 +1,209 @@
# 开发参考
## 1. 系统边界
这个项目当前采用单进程后端 + 浏览器本地渲染 3D 数字人的模式。
明确边界如下:
- 服务端不再输出视频流,只输出音频流和动画控制帧。
- 浏览器负责 Three.js/VRM 渲染、模型加载、动画缓冲和最终口型展示。
- WebRTC 只承载音频。
- 字幕、状态和动画控制通过 HTTP/SSE/WebSocket 传输。
这意味着后续如果你要替换数字人渲染方案,优先改的是浏览器侧;如果你要替换识别、大模型或语音合成,优先改的是 `services/``core/`
## 2. 运行时主链路
### 2.1 语音链路
1. 浏览器通过 `getUserMedia` 获取麦克风。
2. 浏览器调用 `POST /webrtc/offer`,建立音频上行和下行连接。
3. 服务端在 [main.py](../main.py) 中接收音轨并进入 `_consume_user_audio()`
4. 音频被重采样为 16k 单声道后送入 VAD。
5. VAD 检测到用户语音结束后,触发 `ChatPipeline.process_turn()`
6. `ASRService` 输出用户文本。
7. `LLMService` 调用线上 DeepSeek 兼容接口生成回答。
8. `TTSService` 按句子分段合成。
9. 每段 TTS 音频一方面入 `AudioBus`,通过 WebRTC 下发给浏览器播放;另一方面经 `AvatarService` 转成动画帧,通过 `/ws/animation` 发给浏览器。
10. 浏览器在播放语音的同时,从动画缓冲队列中取帧,驱动 VRM 表情、GLTF morph target 和头部骨骼。
### 2.2 文本链路
1. 浏览器调用 `POST /chat/text`
2. 后端直接执行 `LLM -> TTS -> 动画控制`
3. 字幕通过 `/ws/subtitles` 下发,语音仍通过 WebRTC 下发。
文本链路主要用于调试 LLM/TTS/动画,不依赖麦克风、VAD 和 ASR。
## 3. 后端模块职责
### 3.1 入口层
[main.py](../main.py) 负责:
- 应用初始化。
- 管理全局单例服务。
- 维护 WebRTC peer 集合、字幕客户端集合、动画客户端集合。
- 维护 `RuntimeState`,给 `/health``/events` 提供实时指标。
当前实现假设一次只保留一个主 WebRTC 连接。新连接到来时会主动关闭旧连接。这是刻意简化,不是 bug。
### 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) 同时承担了以下职责:
- 创建 WebRTC 连接。
- 订阅 `/events``/ws/subtitles``/ws/animation`
- 维护聊天消息面板和诊断面板。
- 用 Three.js + VRM 加载和渲染模型。
- 把动画控制帧缓存约 `120ms` 后再播放,尽量和音频保持一致。
前端当前兼容三种形态:
- VRM expression manager。
- 通用 GLTF morph target。
- 没有表情 rig 时回退到调试头像。
如果以后想接入真实业务数字人模型,先确认模型是否提供:
- 标准 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. `peers`: WebRTC 是否真正建连。
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 设计。
- 浏览器侧只允许一个主 WebRTC 会话接管服务端音频输出。
- 模型推理基本都直接跑在应用事件循环附近,适合研发验证,不适合高并发。
- 当前动画驱动依赖音频能量和谱质心,不具备严格唇音级别精度。
- 语音链路高度依赖 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/动画,不依赖麦克风。
- `GET /avatar/schema`: 看控制字段列表。
- [scripts/run_all_checks.sh](../scripts/run_all_checks.sh): 统一检查入口。
如果你刚接手这个项目,建议先按下面顺序理解:
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)
+28 -5
View File
@@ -18,7 +18,7 @@ from pydantic import BaseModel
from config import settings
from core.pipeline import ChatPipeline
from core.state_machine import Arbitrator
from core.state_machine import Arbitrator, SessionState
from services.asr import ASRService
from services.avatar import AvatarService
from services.llm import LLMService
@@ -52,6 +52,7 @@ audio_bus = AudioBus(sample_rate=48000)
pcs: set[RTCPeerConnection] = set()
subtitle_clients: set[WebSocket] = set()
animation_clients: set[WebSocket] = set()
MAX_BUFFERED_AUDIO_SAMPLES = 16000 * 45
@dataclass
@@ -73,7 +74,9 @@ class RuntimeState:
last_animation_mode: str = settings.avatar_driver_mode
last_animation_frame_count: int = 0
busy: bool = False
pending_voice_turn: bool = False
buffer_16k: list[np.ndarray] = field(default_factory=list)
buffered_16k_samples: int = 0
runtime = RuntimeState()
@@ -155,6 +158,19 @@ def _runtime_payload() -> dict:
}
def _clear_buffered_audio() -> None:
runtime.buffer_16k.clear()
runtime.buffered_16k_samples = 0
def _append_buffered_audio(chunk: np.ndarray) -> None:
runtime.buffer_16k.append(chunk)
runtime.buffered_16k_samples += int(chunk.shape[0])
while runtime.buffered_16k_samples > MAX_BUFFERED_AUDIO_SAMPLES and runtime.buffer_16k:
dropped = runtime.buffer_16k.pop(0)
runtime.buffered_16k_samples -= int(dropped.shape[0])
def _resample_mono(audio: np.ndarray, from_sr: int, to_sr: int) -> np.ndarray:
if from_sr == to_sr:
return audio.astype(np.float32, copy=False)
@@ -202,15 +218,16 @@ async def _enqueue_audio_and_animation(
async def _run_pipeline_from_buffer() -> None:
if runtime.busy or not runtime.buffer_16k:
if runtime.busy or not runtime.buffer_16k or not runtime.pending_voice_turn:
return
runtime.busy = True
runtime.pending_voice_turn = False
start_t = time.perf_counter()
try:
runtime.last_input_mode = "voice"
audio_16k = np.concatenate(runtime.buffer_16k, axis=0)
runtime.buffer_16k.clear()
_clear_buffered_audio()
async def on_user_text_now(user_text: str) -> None:
runtime.last_user_text = user_text
@@ -245,6 +262,8 @@ async def _run_pipeline_from_buffer() -> None:
await _broadcast_animation(await avatar_service.build_controls_from_audio(audio, sr, text=result["reply_text"]))
finally:
runtime.busy = False
if runtime.pending_voice_turn and runtime.buffer_16k:
asyncio.create_task(_run_pipeline_from_buffer())
async def _consume_user_audio(track) -> None:
@@ -256,7 +275,7 @@ async def _consume_user_audio(track) -> None:
layout = getattr(frame, "layout", None)
channels = len(layout.channels) if layout and layout.channels else 1
mono_16k = _resample_to_16k_mono(pcm, sample_rate, channels)
runtime.buffer_16k.append(mono_16k)
_append_buffered_audio(mono_16k)
vad_buffer = np.concatenate([vad_buffer, mono_16k], axis=0)
while vad_buffer.shape[0] >= 512:
@@ -269,7 +288,7 @@ async def _consume_user_audio(track) -> None:
event = None
if event and "start" in event:
barge_t0 = time.perf_counter()
was_avatar_speaking = str(arbitrator.state) == "avatar_speaking"
was_avatar_speaking = arbitrator.state == SessionState.AVATAR_SPEAKING
await arbitrator.on_speech_start()
await audio_bus.clear()
await _broadcast_animation(await avatar_service.build_reset_payload(reason="speech-start"))
@@ -280,6 +299,7 @@ async def _consume_user_audio(track) -> None:
if event and "end" in event:
await arbitrator.on_speech_end()
runtime.vad_end_count += 1
runtime.pending_voice_turn = True
asyncio.create_task(_run_pipeline_from_buffer())
@@ -400,6 +420,8 @@ async def chat_text(req: TextChatRequest):
@app.post("/chat/reset")
async def chat_reset():
llm_service.clear_history()
_clear_buffered_audio()
runtime.pending_voice_turn = False
runtime.last_user_text = ""
runtime.last_reply_text = ""
runtime.pipeline_runs = 0
@@ -415,6 +437,7 @@ async def chat_reset():
runtime.vad_start_count = 0
runtime.vad_end_count = 0
runtime.last_animation_frame_count = 0
await audio_bus.clear()
await _broadcast_animation(await avatar_service.build_reset_payload(reason="chat-reset"))
return {"ok": True}
-517
View File
@@ -1,517 +0,0 @@
# 数字人视频生成 — 研究与验证记录
> 本文档记录了从零开始探索「单图 + 音频 → 说话视频」的完整过程,
> 包括工具选型、环境踩坑、最终跑通的完整流程,以及对产品化的建议。
>
> 日期:2026-03-25
> 硬件:RTX 5090 (Blackwell, sm_120), CUDA 12.9, WSL2
---
## 一、目标
给定一张人物图片和一段音频,生成该人物"说话"的视频:
- 嘴型与音频内容同步
- 头部有自然动作(非僵硬的照片)
- 可扩展至实时/直播场景
---
## 二、工具选型过程
### 2.1 最初方向:LivePortrait
- **项目**https://github.com/KlingTeam/LivePortrait(快手)
- **本质**:视频驱动的人脸动画,需要一个"驱动视频"来控制人物表情和头部运动
- **误区澄清**:网上有描述称 LivePortrait 支持"音频驱动",这是**不准确的**。它只能从驱动视频中提取音频附加到输出,无法用音频生成嘴型
- **结论**LivePortrait 适合生成头部动作底片,不能单独完成任务
### 2.2 调研其他工具
| 工具 | 厂商 | 核心能力 | 实时性 | 结论 |
|------|------|---------|--------|------|
| LatentSync 1.6 | 字节跳动 | 扩散模型唇形同步,512x512 | ❌ 约4fps | 质量好,但太慢 |
| MuseTalk 1.5 | 腾讯 | 单步推理唇形同步 | ⚠️ 约7fps | 速度可接受,质量好 |
| Live Avatar | 阿里 | 140B 扩散模型 | ❌ 需集群 | 单卡不可行 |
| SadTalker | 开源 | 音频驱动全脸动画 | ❌ | 未测试 |
| Sonic | 腾讯 | 音频驱动 | 未知 | 项目在 `/home/xsl/work/Sonic`,未测试 |
### 2.3 最终方案
**两阶段流水线:**
```
hairstyle-result.jpg
[LivePortrait] ← 驱动视频 (d3.mp4, 自然头部动作)
头动底片视频 (hairstyle-result--d3.mp4, 11.8s)
[MuseTalk] ← 音频 (test_speech_10s.wav)
最终视频 (嘴型同步 + 头部运动)
```
---
## 三、环境配置(重要踩坑记录)
### 3.1 RTX 5090 的核心问题
RTX 5090 是 Blackwell 架构(sm_120),**PyTorch 稳定版(2.6 及以下)不支持**。
所有项目都必须使用 PyTorch nightly cu128
```bash
pip install --pre torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/nightly/cu128
# 已验证版本:torch-2.12.0.dev20260324+cu128
```
**这是最先要做的事,否则所有 CUDA 推理都会报错:**
```
CUDA error: no kernel image is available for execution on the device
```
### 3.2 HuggingFace CLI 命令名
在某些 conda 环境中,命令名是 `hf` 而不是 `huggingface-cli`
```bash
which hf # /home/xsl/miniconda3/envs/MuseTalk/bin/hf
hf --version # 1.7.2
```
下载需要 Token
```bash
export HF_TOKEN=<your_token>
hf download <repo> <file> --local-dir <dir>
```
---
## 四、LivePortrait 部署
### 4.1 环境
```bash
conda create -n LivePortrait python=3.10 -y
conda activate LivePortrait
pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu128
cd /home/xsl/work/LivePortrait
pip install -r requirements.txt
```
### 4.2 权重
```bash
# 约 1.1GB
huggingface-cli download KlingTeam/LivePortrait \
--local-dir pretrained_weights \
--exclude "*.git*" "README.md" "docs"
```
目录结构:
```
pretrained_weights/
├── insightface/ (~21MB)
├── liveportrait/ (~608MB)
└── liveportrait_animals/ (~500MB)
```
### 4.3 生成头动底片
```bash
conda activate LivePortrait
cd /home/xsl/work/LivePortrait
python inference.py \
-s /path/to/portrait.jpg \
-d assets/examples/driving/d3.mp4 \
--flag_crop_driving_video \
-o /home/xsl/work/head_motion_base
```
**驱动视频参考**(位于 `assets/examples/driving/`):
| 文件 | 时长 | 特点 |
|------|------|------|
| d3.mp4 | 11.8s | 自然说话动作,与10s音频匹配 |
| d6.mp4 | 33.6s | 长片段,适合循环 |
| d10.mp4 | 15.0s | 较长 |
**技巧**:选择时长接近音频时长的驱动视频;`--flag_crop_driving_video` 会自动裁剪驱动视频。
### 4.4 已知警告(可忽略)
```
[E:onnxruntime] Failed to load library libonnxruntime_providers_cuda.so
```
onnxruntime-gpu 依赖 CUDA 11.x,与系统 CUDA 12.9 不兼容,人脸检测自动回退 CPU,不影响结果。
---
## 五、LatentSync 部署(已验证,非推荐方案)
> **结论**:质量好,但速度太慢(4fps),不适合产品化。作为备选方案保留。
### 5.1 环境
```bash
conda create -y -n latentsync python=3.10.13
conda activate latentsync
conda install -y -c conda-forge ffmpeg
pip install -r /home/xsl/work/LatentSync/requirements.txt
# RTX 5090 必须卸载稳定版再装 nightly
pip uninstall torch torchvision torchaudio -y
pip install --pre torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/nightly/cu128
```
### 5.2 权重(约 4.9GB
```bash
cd /home/xsl/work/LatentSync
huggingface-cli download ByteDance/LatentSync-1.6 whisper/tiny.pt --local-dir checkpoints
huggingface-cli download ByteDance/LatentSync-1.6 latentsync_unet.pt --local-dir checkpoints
```
### 5.3 输入准备
LatentSync 需要**视频**(非图片)+ **WAV 音频**
```bash
# 图片 → 视频
ffmpeg -loop 1 -i portrait.jpg \
-t <duration> \
-vf "scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2" \
-r 25 -c:v libx264 -pix_fmt yuv420p input_video.mp4 -y
# mp3 → wav
ffmpeg -i audio.mp3 -ar 16000 -ac 1 input_audio.wav -y
```
### 5.4 推理
```bash
conda activate latentsync
cd /home/xsl/work/LatentSync
python -m scripts.inference \
--unet_config_path "configs/unet/stage2_512.yaml" \
--inference_ckpt_path "checkpoints/latentsync_unet.pt" \
--inference_steps 20 \
--guidance_scale 1.5 \
--enable_deepcache \
--video_path "assets/input_video.mp4" \
--audio_path "assets/input_audio.wav" \
--video_out_path "video_out.mp4"
```
---
## 六、MuseTalk 部署(推荐方案)
### 6.1 项目路径
```
/home/xsl/work/MuseTalk
```
### 6.2 环境搭建(完整步骤,含所有踩坑修复)
```bash
conda create -y -n MuseTalk python=3.10
conda activate MuseTalk
conda install -y -c conda-forge ffmpeg
# 1. PyTorch nightlyRTX 5090 必须)
pip install --pre torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/nightly/cu128
# 2. 项目依赖
cd /home/xsl/work/MuseTalk
pip install -r requirements.txt
# 3. 修复 chumpy 构建问题
pip install --no-build-isolation chumpy
# 4. MMLab(版本必须严格匹配,不可随意升降)
pip install mmengine
MMCV_WITH_OPS=1 pip install mmcv==2.1.0 --no-build-isolation # 需要系统 nvcc
pip install "mmdet>=3.3.0" # ≥3.3.0 才兼容 mmcv 2.1.0
pip install "mmpose>=1.3.0" # ≥1.3.0 才兼容 mmcv 2.1.0
pip install xtcocotools json-tricks munkres
# 5. 更新 transformers(旧版与 huggingface-hub 1.x 冲突)
pip install "transformers>=4.45.0"
```
### 6.3 MMLab 版本兼容矩阵(关键)
```
mmcv 2.1.0 ←→ mmdet ≥3.3.0 ←→ mmpose ≥1.3.0
```
| 踩坑 | 原因 | 修复 |
|------|------|------|
| mmcv 构建失败 | nightly PyTorch 没有预编译轮子 | `MMCV_WITH_OPS=1 pip install mmcv==2.1.0 --no-build-isolation`(需系统 nvcc |
| `MMCV==2.1.0 is used but incompatible` | mmdet 3.1.0 要求 mmcv < 2.1.0 | 升级到 mmdet ≥3.3.0 |
| `mmpose 1.1.0 incompatible` | 同上 | 升级到 mmpose ≥1.3.0 |
| `transformers ImportError` | huggingface-hub 1.x 与旧 transformers 冲突 | 升级 transformers ≥4.45.0 |
### 6.4 权重下载
```bash
cd /home/xsl/work/MuseTalk
export HF_TOKEN=<your_token>
# 注意:此环境中命令是 hf,不是 huggingface-cli
hf download TMElyralab/MuseTalk \
"musetalkV15/musetalk.json" "musetalkV15/unet.pth" \
--local-dir models
hf download stabilityai/sd-vae-ft-mse \
config.json diffusion_pytorch_model.bin \
--local-dir models/sd-vae
hf download yzd-v/DWPose \
dw-ll_ucoco_384.pth \
--local-dir models/dwpose
hf download openai/whisper-tiny \
config.json pytorch_model.bin preprocessor_config.json \
--local-dir models/whisper
hf download ByteDance/LatentSync \
latentsync_syncnet.pt \
--local-dir models/syncnet
# face-parse 权重(不在 HuggingFace
pip install gdown
gdown --id 154JgKpzCPW82qINcVieuPH3fZ2e0P812 -O models/face-parse-bisent/79999_iter.pth
curl -L https://download.pytorch.org/models/resnet18-5c106cde.pth \
-o models/face-parse-bisent/resnet18-5c106cde.pth
```
权重总大小约 **5GB**
```
models/
├── musetalkV15/
│ ├── unet.pth (3.2GB)
│ └── musetalk.json
├── sd-vae/
│ ├── diffusion_pytorch_model.bin (320MB)
│ └── config.json
├── dwpose/
│ └── dw-ll_ucoco_384.pth (389MB)
├── whisper/
│ └── pytorch_model.bin (145MB)
├── syncnet/
│ └── latentsync_syncnet.pt (1.4GB)
└── face-parse-bisent/
├── 79999_iter.pth (51MB)
└── resnet18-5c106cde.pth (45MB)
```
### 6.5 PyTorch 2.6+ 兼容性补丁(必须)
PyTorch 2.6+ 将 `torch.load``weights_only` 默认值改为 `True`,导致 MuseTalk 加载旧格式权重失败。
已在 `scripts/inference.py` 中加入 monkey-patch
```python
# 在 inference.py 开头的 import torch 后面加入:
_orig_torch_load = torch.load
def _patched_torch_load(f, *args, **kwargs):
kwargs.setdefault('weights_only', False)
return _orig_torch_load(f, *args, **kwargs)
torch.load = _patched_torch_load
```
同样的补丁也需要加到 `scripts/realtime_inference.py`(如果使用实时模式)。
### 6.6 推理配置文件
创建 YAML 配置文件(`configs/inference/my_task.yaml`):
```yaml
task_0:
video_path: "/path/to/head_motion_video.mp4"
audio_path: "/path/to/audio.wav"
bbox_shift: 0 # 可选:调整嘴部区域位置,负值下移,正值上移
```
### 6.7 运行推理
```bash
conda activate MuseTalk
cd /home/xsl/work/MuseTalk
python -m scripts.inference \
--inference_config configs/inference/my_task.yaml \
--result_dir results/output \
--unet_model_path models/musetalkV15/unet.pth \
--unet_config models/musetalkV15/musetalk.json \
--version v15
```
输出路径:`results/output/v15/<video_name>.mp4`
---
## 七、完整流水线(已验证示例)
### 输入
- 图片:`/home/xsl/work/LivePortrait/hairstyle-result.jpg`1080x1920,正面人像)
- 音频:`/home/xsl/work/LivePortrait/test_speech_10s.mp3`11.8秒,中文语音)
### Step 1:生成头动底片
```bash
conda activate LivePortrait
cd /home/xsl/work/LivePortrait
python inference.py \
-s /home/xsl/work/LivePortrait/hairstyle-result.jpg \
-d assets/examples/driving/d3.mp4 \
--flag_crop_driving_video \
-o /home/xsl/work/head_motion_base
```
输出:`/home/xsl/work/head_motion_base/hairstyle-result--d3.mp4`11.8s, 2.1MB
### Step 2:音频转 WAV
```bash
ffmpeg -i /home/xsl/work/LivePortrait/test_speech_10s.mp3 \
-ar 16000 -ac 1 /tmp/input_audio.wav -y
```
### Step 3MuseTalk 嘴型同步
```bash
conda activate MuseTalk
cd /home/xsl/work/MuseTalk
cat > configs/inference/hairstyle.yaml << 'EOF'
task_0:
video_path: "/home/xsl/work/head_motion_base/hairstyle-result--d3.mp4"
audio_path: "/tmp/input_audio.wav"
EOF
python -m scripts.inference \
--inference_config configs/inference/hairstyle.yaml \
--result_dir results/hairstyle \
--unet_model_path models/musetalkV15/unet.pth \
--unet_config models/musetalkV15/musetalk.json \
--version v15
```
输出:`/home/xsl/work/MuseTalk/results/hairstyle/v15/hairstyle-result--d3_input_audio.mp4`
### 性能数据
| 阶段 | 耗时 | 帧数 | 等效速度 |
|------|------|------|---------|
| LivePortrait 头动生成 | ~30s | 295帧 | ~10fps |
| MuseTalk 嘴型合成 | ~50s | 354帧 | ~7fps |
| 总计 | ~80s | — | 生成12s视频 |
---
## 八、产品化建议
### 8.1 当前方案的局限
1. **非实时**:总体约 7fps 生成速度,12秒视频需要 80 秒处理
2. **两套环境**LivePortrait 和 MuseTalk 分别用不同 conda 环境,pipeline 不够整洁
3. **头动单调**:驱动视频固定,长时间使用会重复
4. **图片分辨率**MuseTalk 处理区域限定在 256x256 嘴部区域
### 8.2 提速方向
- **MuseTalk 实时模式**`scripts/realtime_inference.py` 支持流式处理,延迟更低
- **头动循环库**:预先用 LivePortrait 生成多种头动视频(点头、摇头、思考等),运行时随机选取
- **TensorRT 量化**:对 MuseTalk UNet 做 TRT 优化,预计可提速 2-3x
### 8.3 架构建议(产品级)
```
音频输入
[ASR/VAD] → 静音检测、分段
[MuseTalk 实时流] ← 头动视频循环
[后处理:美颜/超分](可选)
视频输出流
```
### 8.4 替代方案调研建议
在正式产品开发前,建议调研以下工具:
- **Sonic**`/home/xsl/work/Sonic`):已有项目,未测试,可能支持更好的实时性
- **EchoMimic**:阿里的音频驱动方案,支持半身动作
- **AniPortrait**:同时驱动头部和嘴型
---
## 九、目录结构总览
```
/home/xsl/work/
├── LivePortrait/ # 头部动作驱动
│ ├── pretrained_weights/ (~1.1GB)
│ ├── hairstyle-result.jpg # 测试图片
│ ├── test_speech_10s.mp3 # 测试音频
│ └── DEPLOYMENT.md # 详细部署笔记
├── LatentSync/ # 扩散模型唇形同步(备用)
│ ├── checkpoints/ (~4.9GB)
│ └── video_out.mp4 # 测试输出
├── MuseTalk/ # 单步推理唇形同步(推荐)
│ ├── models/ (~5GB)
│ └── results/hairstyle/v15/hairstyle-result--d3_input_audio.mp4
├── head_motion_base/ # LivePortrait 生成的头动底片
│ └── hairstyle-result--d3.mp4
└── Sonic/ # 待调研
```
### Conda 环境
```bash
conda env list
# LivePortrait /home/xsl/miniconda3/envs/LivePortrait
# latentsync /home/xsl/miniconda3/envs/latentsync
# MuseTalk /home/xsl/miniconda3/envs/MuseTalk
```
---
## 十、快速复现命令(Cheat Sheet
```bash
# === 生成一个说话视频(完整流程)===
# 1. 生成头动底片
conda activate LivePortrait && cd /home/xsl/work/LivePortrait
python inference.py -s portrait.jpg -d assets/examples/driving/d3.mp4 \
--flag_crop_driving_video -o /tmp/head_motion
# 2. 音频转 WAV
ffmpeg -i input.mp3 -ar 16000 -ac 1 /tmp/audio.wav -y
# 3. 嘴型同步
conda activate MuseTalk && cd /home/xsl/work/MuseTalk
python -m scripts.inference \
--inference_config <(echo "task_0:\n video_path: /tmp/head_motion/*.mp4\n audio_path: /tmp/audio.wav") \
--result_dir /tmp/output \
--unet_model_path models/musetalkV15/unet.pth \
--unet_config models/musetalkV15/musetalk.json \
--version v15
```
+5 -3
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
cd /home/xsl/product
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
mkdir -p certs
openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
@@ -10,6 +12,6 @@ openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-subj "/C=CN/ST=Local/L=Local/O=VisualChat/OU=Dev/CN=localhost"
echo "Generated:"
echo " /home/xsl/product/certs/dev-cert.pem"
echo " /home/xsl/product/certs/dev-key.pem"
echo " $ROOT/certs/dev-cert.pem"
echo " $ROOT/certs/dev-key.pem"
echo "Set SSL_CERTFILE and SSL_KEYFILE in .env to enable HTTPS."
+2 -1
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
OUT="${1:-/home/xsl/product/outputs/gpu-metrics.csv}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="${1:-$ROOT/outputs/gpu-metrics.csv}"
SECONDS_TOTAL="${2:-3600}"
INTERVAL="${3:-5}"
+3 -1
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
cd /home/xsl/product
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PY="/home/xsl/miniconda3/envs/MuseTalk/bin/python"
echo "[1/6] service status"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="/home/xsl/product"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PID_FILE="$ROOT/.run/visual-chat.pid"
LOG_FILE="$ROOT/.run/visual-chat.log"
START_CMD="$ROOT/scripts/start-public.sh"
+3 -1
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
cd /home/xsl/product
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PY="/home/xsl/miniconda3/envs/MuseTalk/bin/python"
+3 -1
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd /home/xsl/product
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
python -m uvicorn main:app --host 0.0.0.0 --port 8080
-724
View File
@@ -1,724 +0,0 @@
# 智能可视化语音聊天系统 — 详细任务清单(本机实时版)
> **目标**:在本机(RTX 5090 / WSL2)实现"浏览器访问 → WebRTC 传输 → 单工语音对话 → 数字人口型同步"的**实时**可用系统。
> **基础**`environments.md` 与 `research-notes.md` 中已验证的环境、模型和踩坑修复全部复用。
> **最后更新**2026-03-25 — 经过逐组件可行性硬核验证后修订(第三版)。
---
## 0. 逐组件可行性验证报告(执行前必读)
### 0.1 组件全景审计表
| 组件 | 选型 | 许可证 | 纯本地 | RTX 5090 验证 | 实时性证据 | 风险等级 |
|------|------|--------|--------|---------------|-----------|---------|
| **VAD** | Silero VAD 6.x | MIT | 是(CPU) | N/A(纯CPU) | 流式 VADIterator32ms 帧级延迟 | 无 |
| **ASR** | SenseVoice-Small (FunASR) | Apache 2.0 | 是(GPU) | 已有用户确认 cu128 可跑 | 中文 CER 4.2%CPU 14x实时,GPU 更快 | 低 |
| **LLM** | 在线 API (OpenAI 兼容) | N/A | 否(设计如此) | N/A | 流式,首 token ~0.5-1.5s | 中(网络) |
| **TTS** | Kokoro-82M-v1.1-zh | Apache 2.0 | 是(GPU) | 确认支持 CUDA 12.8 RTX 50 系 | 210x实时(4090)<2GB VRAM | 低 |
| **TTS 升级** | Qwen3-TTS 0.6B | Apache 2.0 | 是(GPU) | 确认可跑,需 torch.compile 优化 | 97ms 首音频,RTF<0.25(优化后) | 中 |
| **口型** | MuseTalk v1.5 | MIT | 是(GPU) | 已在你机器验证通过 | 30fps+(V100)RTX 5090 更快 | 中(需改流式) |
| **头动底片** | LivePortrait | MIT | 是(离线预处理) | 已在你机器验证通过 | 离线,无实时要求 | 无 |
| **WebRTC** | aiortc | BSD-3-Clause | 是 | N/A(纯Python) | asyncio 原生,H.264 已优化 | 低 |
**结论:全部组件均为开源许可(MIT / Apache 2.0 / BSD-3),全部可本地运行,全部有 RTX 5090 兼容路径。**
### 0.2 两个致命问题及修正
#### 问题一:faster-whisper 中文识别率灾难
| | faster-whisper large-v3-turbo | SenseVoice-Small |
|---|---|---|
| 中文 WER/CER | **WER 77%**(几乎不可用) | **CER 4.2%**(优秀) |
| 中文方言 | 不支持 | 7 大方言 + 26 种口音 |
| 推理速度(GPU) | ~50x 实时 | RTF 0.0076130x 实时) |
| 模型大小 | ~1.5GB | ~234MB |
| 许可证 | MIT | Apache 2.0 |
**决定:ASR 从 faster-whisper 改为 SenseVoice-Small。** faster-whisper 的 turbo 模型砍掉了 28/32 层 decoder,导致中文(需要更复杂解码的语言)准确率暴跌,完全不可接受。
#### 问题二:CosyVoice2 实际延迟远超宣传
| | 宣传值 | 开源版实测值(社区) |
|---|---|---|
| 流式首块延迟 | 150ms | **1 - 4.5 秒** |
| 原因 | 阿里云预训练音色,无特征提取 | 开源 zero-shot 需提取 token/embedding |
| 流式实现 | 优化的 chunk 流 | 累积式 chunk1A, 2A, 3A...),无 KV cache |
**决定:TTS 从 CosyVoice2 改为 Kokoro-82M(快速启动) + Qwen3-TTS 0.6B(质量升级路径)。**
### 0.3 修正后的端到端延迟预算
| 阶段 | 选型 | 预估延迟 | 数据来源 |
|------|------|----------|---------|
| VAD 尾部检测 | Silero VAD | ~300ms | 默认 silence_duration_ms=300 |
| ASR 转写(3s) | SenseVoice-Small GPU | ~50-150ms | RTF 0.00763s × 0.0076 ≈ 23ms + 开销 |
| LLM 首 token | 在线流式 | ~500-1500ms | 取决于模型/网络 |
| TTS 首音频块 | Kokoro-82M | ~20-50ms | 210x 实时 on 4090 |
| MuseTalk 首帧批 | MuseTalk batch=4 | ~100-200ms | 30fps+ → 4帧 ≈ 130ms |
| WebRTC 编码传输 | aiortc H.264 | ~50-100ms | 软编码 |
| **合计** | | **~1.0 - 2.3s** | |
> 比上版预算更优,因为 SenseVoice 比 faster-whisper 更快,Kokoro 比 CosyVoice2 快一个数量级。
### 0.4 GPU 显存预算(RTX 5090 = 32GB
| 模型 | 精度 | VRAM | 常驻 |
|------|------|------|------|
| MuseTalk (UNet + VAE + Whisper) | fp16 | ~3-4GB | 是 |
| SenseVoice-Small | fp16 | ~0.8GB | 是 |
| Kokoro-82M | fp16 | ~1.5GB | 是 |
| PyTorch CUDA 上下文 | — | ~1GB | 固定 |
| **合计** | | **~6-7GB** | |
> 32GB VRAM 用不到 1/4,若后续升级 Qwen3-TTS 0.6B+3GB)仍有充裕余量。
### 0.5 "开源可控"确认清单
| 组件 | 许可证 | 可商用 | 代码仓库 | 离线可用 |
|------|--------|--------|----------|---------|
| Silero VAD | MIT | 是 | github.com/snakers4/silero-vad | 是 |
| SenseVoice-Small | Apache 2.0 | 是 | github.com/FunAudioLLM/SenseVoice | 是 |
| Kokoro-82M | Apache 2.0 | 是 | github.com/hexgrad/kokoro | 是 |
| Qwen3-TTS | Apache 2.0 | 是 | github.com/QwenLM/Qwen3-TTS | 是 |
| MuseTalk v1.5 | MIT | 是 | github.com/TMElyralab/MuseTalk | 是 |
| LivePortrait | MIT | 是 | github.com/KlingTeam/LivePortrait | 是 |
| aiortc | BSD-3 | 是 | github.com/aiortc/aiortc | 是 |
**零云端依赖(LLM 除外,且 LLM 有本地兜底),零非开源组件。**
### 0.6 "稳健落地"保障策略
| 风险 | 发生时 | 保底方案 | 切换成本 |
|------|--------|----------|---------|
| SenseVoice 在 nightly PyTorch 编译失败 | M0 | 用 MuseTalk 自带 whisper-tiny 做 ASR(精度低但能跑) | 改一行 import |
| Kokoro 中文质量不达标 | M1 | 切 Qwen3-TTS 0.6B(已验证 RTX 5090 可跑) | 换一个 TTS 服务封装 |
| Qwen3-TTS RTX 5090 推理慢 | M4 | torch.compile + CUDA graphs 优化(社区已有方案,RTF<0.25 | 加 2 行代码 |
| MuseTalk 流式改造困难 | M1 | 先用"分段文件处理"TTS 输出完整 WAV → MuseTalk 批量合成 → 整段播放 | 延迟增加 2-3s 但可用 |
| 在线 LLM 不可用 | 运行时 | 本地预设话术兜底,8s 超时自动触发 | 已内建 |
| aiortc 软编码瓶颈 | M2 | 改用 ffmpeg + NVENC 硬编码管道 | 约 1 天工作量 |
---
## 1. 总体架构
### 1.1 系统拓扑
```
浏览器 (Chrome)
│ WebRTC (ICE/DTLS/SRTP)
┌─────────────────────────────────────────────────────┐
│ webrtc-gateway (aiortc + FastAPI) │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ 上行音频 │ │ 下行视频 + 音频轨 │ │
│ └────┬─────┘ └──────────▲───────────┘ │
└───────┼───────────────────────────────┼─────────────┘
│ │
▼ │
┌──────────────┐ ┌──────┴──────┐
│ Silero VAD │ │ mixer │
│ (CPU,MIT) │ │ (音视频合流) │
└──────┬───────┘ └──▲───────▲──┘
│ speech.end │ │
▼ │ │
┌──────────────┐ ┌──────┴──┐ ┌──┴──────────┐
│ SenseVoice │ │MuseTalk │ │ TTS 音频 │
│ ASR (GPU) │ │ 口型渲染│ │ (Kokoro) │
│ Apache 2.0 │ │ (GPU) │ │ Apache 2.0 │
└──────┬───────┘ │ MIT │ └──────▲──────┘
│ text └────▲────┘ │
▼ │ wav │ text
┌──────────────┐ ┌─────┴──────┐ │
│ LLM Client │──text──▶│ Kokoro TTS ├───────┘
│ (在线 API) │ │ (GPU) │
└──────────────┘ └────────────┘
```
### 1.2 单工状态机
```
speech.start speech.end
┌────┐ ──────────▶ ┌──────────────┐ ────────▶ ┌──────────┐
│IDLE│ │USER_SPEAKING │ │THINKING │
└──▲─┘ ◀────────── └──────────────┘ └────┬─────┘
│ barge-in │ tts.ready
│ (用户抢话) ▼
│ ┌─────────────────┐ ┌────────────────┐
└───────────────│ │◀────────│AVATAR_SPEAKING │
playback.done │ (回到 IDLE) │ └────────────────┘
└─────────────────┘
```
**状态规则**
- `IDLE`:数字人播放闭嘴循环视频,下行静音
- `USER_SPEAKING`:数字人立即切闭嘴循环(如正在说话则中断),录音开始
- `THINKING`:录音结束,ASR → LLM → TTS 管线启动,数字人继续闭嘴循环
- `AVATAR_SPEAKING`MuseTalk 输出帧 + TTS 音频同步下发
- 任何状态下检测到 `speech.start` → 立即跳转 `USER_SPEAKING`barge-in
### 1.3 进程内通信(不用消息中间件)
单机单进程,所有模块通过 `asyncio.Queue``asyncio.Event` 通信,延迟最低:
```python
audio_chunk_queue: asyncio.Queue[bytes] # VAD → ASR
text_queue: asyncio.Queue[str] # ASR → LLM
tts_chunk_queue: asyncio.Queue[bytes] # LLM→TTS → mixer
avatar_frame_queue: asyncio.Queue[np.ndarray] # TTS→MuseTalk → mixer
state_event: asyncio.Event # 状态变更通知
```
---
## 2. 环境与依赖
### 2.1 创建统一 conda 环境
```bash
conda create -n digital-human python=3.10 -y
conda activate digital-human
conda install -y -c conda-forge ffmpeg
# PyTorch nightlyRTX 5090 必须,已验证版本)
pip install --pre torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/nightly/cu128
```
### 2.2 安装各模块依赖
```bash
# MuseTalk 依赖链(按 research-notes.md 已验证)
cd /home/xsl/work/MuseTalk
pip install -r requirements.txt
pip install --no-build-isolation chumpy
pip install mmengine
MMCV_WITH_OPS=1 pip install mmcv==2.1.0 --no-build-isolation
pip install "mmdet>=3.3.0" "mmpose>=1.3.0"
pip install xtcocotools json-tricks munkres
pip install "transformers>=4.45.0"
# ASR - SenseVoice
pip install funasr
# TTS - Kokoro
pip install kokoro soundfile
# TTS 升级备选 - Qwen3-TTSM4 阶段再装)
# pip install qwen3-tts
# VAD
pip install silero-vad
# WebRTC + Web 服务
pip install aiortc aiohttp uvicorn fastapi
# 工具库
pip install pydantic python-dotenv tenacity numpy opencv-python-headless
```
### 2.3 模型权重
| 模型 | 路径 | 状态 | 大小 |
|------|------|------|------|
| MuseTalk v1.5 全套 | `/home/xsl/work/MuseTalk/models/` | 已有 | 5.5GB |
| LivePortrait 权重 | `/home/xsl/work/LivePortrait/pretrained_weights/` | 已有 | 1.2GB |
| SenseVoice-Small | 首次调用自动下载 | **新增** | ~234MB |
| Kokoro-82M-v1.1-zh | 首次调用自动下载 | **新增** | ~180MB |
| Silero VAD | 首次调用自动下载 | **自动** | ~2MB |
```bash
# SenseVoice 模型会在首次 AutoModel() 调用时自动下载
# Kokoro 模型会在首次 import kokoro 时自动下载
# 也可以手动预下载:
python -c "from funasr import AutoModel; AutoModel(model='FunAudioLLM/SenseVoiceSmall', device='cuda:0')"
python -c "import kokoro; kokoro.KPipeline(lang_code='z')"
```
### 2.4 MuseTalk 补丁(必须,已验证)
`torch.load(weights_only=False)` monkey patch — 保持 research-notes.md 中已验证的方案。
### 2.5 验收清单
- [x] `python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name())"` → True, NVIDIA GeForce RTX 5090
- [x] SenseVoice 加载模型 + 转写一段中文音频成功
- [x] Kokoro 合成一句中文语音成功
- [x] MuseTalk 单样例推理输出视频成功
---
## 3. VAD 语音活动检测(Silero VAD, MIT
### 3.1 实现
```python
from silero_vad import load_silero_vad, VADIterator
model = load_silero_vad()
vad_iterator = VADIterator(
model,
threshold=0.5,
sampling_rate=16000,
min_silence_duration_ms=300, # 尾部静音判定
speech_pad_ms=30,
)
# 流式处理 WebRTC 上行音频帧
for audio_chunk in webrtc_audio_stream:
speech_dict = vad_iterator(audio_chunk)
if speech_dict:
if 'start' in speech_dict:
arbitrator.on_speech_start()
if 'end' in speech_dict:
arbitrator.on_speech_end(recorded_audio)
```
### 3.2 验收
- [x] 静音环境不误触发
- [x] 正常说话稳定触发
- [x] 说话结束后 ~300ms 内检测到 speech.end
---
## 4. ASR 语音识别(SenseVoice-Small, Apache 2.0, 本地 GPU
### 4.1 模型加载
```python
from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess
asr_model = AutoModel(
model="FunAudioLLM/SenseVoiceSmall",
device="cuda:0",
hub="hf",
)
```
### 4.2 推理接口
```python
async def transcribe(audio_array: np.ndarray) -> str:
"""16kHz mono float32 numpy array → 文本"""
res = asr_model.generate(
input=audio_array,
cache={},
language="zh",
use_itn=True,
)
return rich_transcription_postprocess(res[0]["text"])
```
### 4.3 性能预期
- AISHELL-1 中文 CER: 4.2%(比 whisper-turbo 的 77% WER 好一个量级)
- 3 秒语音推理延迟: ~50-150ms (GPU)
- VRAM: ~0.8GB
### 4.4 验收
- [ ] 10 条中文测试语音,转写准确率主观 > 90%
- [ ] 单段 3 秒语音延迟 < 300ms
---
## 5. LLM 对话(在线 API,流式输出)
### 5.1 客户端实现
```python
import openai
client = openai.AsyncOpenAI(
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"),
)
async def chat_stream(messages: list[dict]) -> AsyncIterator[str]:
response = await client.chat.completions.create(
model=os.getenv("LLM_MODEL", "gpt-4o-mini"),
messages=messages,
stream=True,
max_tokens=150,
temperature=0.7,
)
async for chunk in response:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
```
### 5.2 流式分句送 TTS
- LLM 流式输出 token → 累积到句子边界(。!?,\n)→ 立即送 TTS
- 系统提示词要求"用简短口语回复,每句不超过 30 字"
### 5.3 降级策略
- 超时 8s 或网络错误 → 返回预设兜底话术
-`tenacity` 做 1 次快速重试(2s 超时)
### 5.4 验收
- [x] 正常首 token < 2 秒
- [x] 断网 3 秒内返回兜底话术
- [x] 20 轮对话上下文连贯
---
## 6. TTS 语音合成(Kokoro-82M, Apache 2.0, 本地 GPU
### 6.1 模型加载
```python
import kokoro
pipeline = kokoro.KPipeline(lang_code='z') # z = 中文
```
### 6.2 合成接口
```python
async def synthesize(text: str) -> tuple[np.ndarray, int]:
"""文本 → (audio_array, sample_rate)"""
generator = pipeline(text, voice='zf_xiaoxiao') # 中文女声
audio_chunks = []
for _, _, chunk in generator:
audio_chunks.append(chunk)
audio = np.concatenate(audio_chunks)
return audio, 24000 # Kokoro 输出 24kHz
```
### 6.3 音频格式对齐
- Kokoro 输出 24kHz → 需重采样到 16kHz 供 MuseTalk whisper
- 下行音频可保持 24kHzWebRTC 支持)
### 6.4 升级路径(Qwen3-TTS
M4 阶段如果 Kokoro 中文质量不满意,切换到 Qwen3-TTS 0.6B
- 97ms 首音频延迟
- 更好的中文自然度
- 需额外 ~3GB VRAM(总计仍 <10GB
- RTX 5090 需 `torch.compile` + CUDA graphs 优化(社区已验证方案)
### 6.5 验收
- [ ] 40 字中文文本合成延迟 < 500ms
- [ ] 音质清晰,无明显机器感
- [x] 连续合成 20 句无崩溃
---
## 7. 数字人渲染(MuseTalk v1.5, MIT, 本地 GPU
### 7.1 离线预处理:底片库(一次性)
**Step 1**: 用 LivePortrait 生成闭嘴自然动作循环视频
```bash
conda activate digital-human
cd /home/xsl/work/LivePortrait
for drv in d3.mp4 d6.mp4 d10.mp4; do
python inference.py \
-s /path/to/avatar_photo.jpg \
-d assets/examples/driving/$drv \
--flag_crop_driving_video \
-o /home/xsl/product/assets/avatar_loops/
done
```
**Step 2**: 用 MuseTalk 预处理(提取 latents/coords/masks → 缓存)
```bash
cd /home/xsl/work/MuseTalk
python -m scripts.realtime_inference \
--inference_config configs/inference/realtime.yaml \
--version v15 \
--unet_model_path models/musetalkV15/unet.pth \
--unet_config models/musetalkV15/musetalk.json
```
预处理产物缓存后,运行时直接加载,不再需要人脸检测和解析。
### 7.2 实时口型合成:改造 MuseTalk
**核心**:将 `Avatar.inference()` 从完整音频文件改为流式音频 chunk。
```python
class RealtimeAvatar:
def __init__(self, prepared_data_path):
# 加载预处理缓存
self.latents = torch.load(f"{prepared_data_path}/latents.pt")
self.coords = pickle.load(open(f"{prepared_data_path}/coords.pkl", 'rb'))
self.frames = [...] # 预加载原始帧
self.masks = [...] # 预加载 mask
self.frame_idx = 0
async def process_audio_chunk(self, pcm_16k: np.ndarray) -> list[np.ndarray]:
"""接收 TTS PCM 块 → 返回口型帧列表"""
whisper_features = audio_processor.get_audio_feature(pcm_16k)
whisper_chunks = audio_processor.get_whisper_chunk(whisper_features, ...)
output_frames = []
for batch in datagen(whisper_chunks, self.latents, batch_size=4):
pred = unet.model(batch_latent, timesteps, encoder_hidden_states=audio_feat)
recon = vae.decode_latents(pred)
for frame in recon:
blended = blend_frame(frame, self.frame_idx)
output_frames.append(blended)
self.frame_idx = (self.frame_idx + 1) % len(self.frames)
return output_frames
def get_idle_frame(self) -> np.ndarray:
"""闭嘴循环帧(不过 UNet"""
frame = self.frames[self.frame_idx % len(self.frames)]
self.frame_idx = (self.frame_idx + 1) % len(self.frames)
return frame
```
### 7.3 帧率与同步
- 25fps(与底片一致),每帧 40ms
- TTS 音频时长 → 计算帧数 → MuseTalk 生成对应数量帧
- asyncio 定时器控制帧发送节奏
### 7.4 验收
- [ ] 闭嘴循环 60 秒无卡顿
- [ ] 5 秒 TTS 音频口型帧 200ms 内开始输出
- [ ] 口型同步主观无 >200ms 错位
---
## 8. WebRTC 网关与前端
### 8.1 后端(aiortc + FastAPI, BSD-3
```python
@app.post("/webrtc/offer")
async def webrtc_offer(request: Request):
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
pc = RTCPeerConnection()
video_track = AvatarVideoTrack() # 从 MuseTalk 帧队列读取
audio_track = AvatarAudioTrack() # 从 TTS 音频队列读取
pc.addTrack(video_track)
pc.addTrack(audio_track)
@pc.on("track")
def on_track(track):
if track.kind == "audio":
asyncio.ensure_future(process_incoming_audio(track))
await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
```
### 8.2 前端页面(纯 HTML/JS,无框架依赖)
```
┌──────────────────────────────┐
│ 数字人视频 │
│ (WebRTC video 标签) │
│ │
├──────────────────────────────┤
│ 🎤 正在倾听... │ ← 状态指示
├──────────────────────────────┤
│ 你: "今天天气怎么样?" │ ← ASR 字幕
│ AI: "今天天气不错呢..." │ ← LLM 回复字幕
└──────────────────────────────┘
```
- 页面加载 → 自动建 WebRTC 连接 → 请求麦克风权限
- 下行视频+音频 → `<video>` 标签
- WebSocket 接收字幕和状态更新
- 断连 3 秒自动重连
### 8.3 验收
- [ ] Chrome 打开 3 秒内看到数字人视频
- [x] 说话后字幕区显示 ASR 文本
- [x] 刷新页面自动重连
---
## 9. 单工仲裁器
```python
class SessionState(Enum):
IDLE = "idle"
USER_SPEAKING = "user_speaking"
THINKING = "thinking"
AVATAR_SPEAKING = "avatar_speaking"
class Arbitrator:
def __init__(self):
self.state = SessionState.IDLE
self._lock = asyncio.Lock()
self._cancel = asyncio.Event()
async def on_speech_start(self):
async with self._lock:
if self.state == SessionState.AVATAR_SPEAKING:
self._cancel.set()
self.state = SessionState.USER_SPEAKING
async def on_speech_end(self, audio: bytes):
async with self._lock:
self.state = SessionState.THINKING
asyncio.create_task(self._run_pipeline(audio))
async def on_playback_done(self):
async with self._lock:
if not self._cancel.is_set():
self.state = SessionState.IDLE
```
### 验收
- [ ] 数字人说话中用户插嘴 → 200ms 内停止
- [ ] 连续抢话 20 次无死锁
---
## 10. 配置
### `.env`
```env
# LLM
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o-mini
LLM_MAX_TOKENS=150
LLM_TIMEOUT=8
# 模型路径(复用现有)
MUSETALK_DIR=/home/xsl/work/MuseTalk
MUSETALK_UNET=/home/xsl/work/MuseTalk/models/musetalkV15/unet.pth
MUSETALK_CONFIG=/home/xsl/work/MuseTalk/models/musetalkV15/musetalk.json
# 服务
WEBRTC_PORT=8080
AVATAR_FPS=25
```
---
## 11. 目录结构
```
/home/xsl/product/
├── main.py # 入口
├── config.py # 配置
├── .env
├── requirements.txt
├── core/
│ ├── state_machine.py # 单工仲裁器
│ ├── session.py # 会话管理
│ └── pipeline.py # ASR→LLM→TTS→Avatar 编排
├── services/
│ ├── vad.py # Silero VAD
│ ├── asr.py # SenseVoice-Small
│ ├── llm.py # LLM 客户端
│ ├── tts.py # Kokoro TTS
│ └── avatar.py # MuseTalk 实时推理
├── webrtc/
│ ├── gateway.py # aiortc 信令
│ ├── video_track.py # 数字人视频轨
│ └── audio_track.py # 数字人音频轨
├── web/
│ ├── index.html
│ ├── app.js
│ └── style.css
├── assets/
│ ├── avatar_loops/ # 预生成闭嘴循环视频
│ └── avatar_prepared/ # MuseTalk 预处理缓存
├── scripts/
│ ├── prepare_avatar.sh # 预处理底片
│ ├── start.sh # 一键启动
│ └── smoke_test.py # 冒烟测试
└── docs/
├── visual-chat-task-list.md
├── environments.md
└── research-notes.md
```
---
## 12. 里程碑
### M0:环境就绪(0.5 天)
- [x] 创建 `digital-human` conda 环境
- [x] 安装全部依赖(PyTorch nightly + MuseTalk + SenseVoice + Kokoro + aiortc
- [x] 验证四个 GPU 模型可分别加载并推理
- [ ] 验证四个模型可同时常驻 GPU(显存 < 10GB
### M1:离线管线串通(2 天)
- [x] `services/asr.py`SenseVoice 输入 WAV → 输出文本
- [x] `services/llm.py`:输入文本 → 流式输出文本
- [x] `services/tts.py`:Kokoro 输入文本 → 输出 WAV
- [x] `services/avatar.py`MuseTalk 输入 WAV → 输出帧序列(失败自动回退简易口型)
- [x] `scripts/smoke_test.py`:一段录音走完全链路输出视频文件(当前为文本输入链路,产出 `wav + mp4`
### M2WebRTC + 数字人常驻(2 天)
- [x] `webrtc/gateway.py`aiortc 信令接口
- [x] `webrtc/video_track.py`:发闭嘴循环帧
- [x] `webrtc/audio_track.py`:发静音
- [x] `web/`:前端页面
- [x] 浏览器可看到数字人循环视频
### M3:全链路联调 + 单工(2 天)
- [x] `services/vad.py`Silero VAD 处理上行音频
- [x] `core/state_machine.py`:仲裁器
- [x] `core/pipeline.py`:完整 VAD→ASR→LLM→TTS→MuseTalk→下行
- [x] barge-in 实现
- [x] WebSocket 字幕下发
### M4:优化与稳定(2 天)
- [ ] 端到端延迟调优(目标 < 3s)
- [ ] 1 小时长时间运行测试
- [x] 显存监控(nvidia-smi 周期采样)
- [x] LLM 降级测试
- [ ] 评估是否升级 Qwen3-TTS(中文质量对比)
- [x] 20 轮连续对话压测
补充自动化脚本:
- `scripts/qa_check.py`:文本链路压测(支持 20 轮)
- `scripts/llm_fallback_check.py`LLM 断网/超时兜底验证
- `scripts/gpu_monitor.sh`GPU 利用率与显存采样(CSV
- `scripts/model_probe.py`ASR/TTS/VAD 快速加载与推理探测
---
## 13. 被排除的方案及原因(决策日志)
| 方案 | 排除原因 |
|------|---------|
| faster-whisper (大模型中文) | 中文 WER 77%,完全不可接受 |
| CosyVoice2 (TTS) | 开源版流式首包 1-4.5s,与宣传 150ms 严重不符 |
| edge-tts | 依赖微软云服务器,非本地,离线不可用 |
| Fish-Speech S2 | 许可证限制商用,需单独授权 |
| LatentSync | 仅 4fps,无法实时 |
| Redis/ZeroMQ 消息总线 | 单机单进程无需中间件,asyncio Queue 延迟更低 |
| ChatTTS | 流式推理未完全实现,首包延迟不稳定 |