From 36e838caec624a3ce3fa1c0eb6b7b757f055318d Mon Sep 17 00:00:00 2001 From: xsl Date: Sun, 29 Mar 2026 20:50:53 +0800 Subject: [PATCH] save code --- AVATAR_3D_REFACTOR.md | 189 --------- DEPLOYMENT_GUIDE.md | 250 ----------- README.md | 119 ++++++ REMOTE_ACCESS.md | 72 ---- TECHNICAL_DOCUMENTATION.md | 278 ------------ core/pipeline.py | 10 +- core/state_machine.py | 3 +- docs/development-guide.md | 209 +++++++++ main.py | 33 +- research-notes.md | 517 ----------------------- scripts/gen-self-signed-cert.sh | 8 +- scripts/gpu_monitor.sh | 3 +- scripts/run_all_checks.sh | 4 +- scripts/service.sh | 2 +- scripts/start-public.sh | 4 +- scripts/start.sh | 4 +- visual-chat-task-list.md | 724 -------------------------------- 17 files changed, 381 insertions(+), 2048 deletions(-) delete mode 100644 AVATAR_3D_REFACTOR.md delete mode 100644 DEPLOYMENT_GUIDE.md create mode 100644 README.md delete mode 100644 REMOTE_ACCESS.md delete mode 100644 TECHNICAL_DOCUMENTATION.md create mode 100644 docs/development-guide.md delete mode 100644 research-notes.md delete mode 100644 visual-chat-task-list.md diff --git a/AVATAR_3D_REFACTOR.md b/AVATAR_3D_REFACTOR.md deleted file mode 100644 index 675f261..0000000 --- a/AVATAR_3D_REFACTOR.md +++ /dev/null @@ -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` 骨骼,找不到时回退调试头像 - -### 方案 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 服务,都可以在这个边界上继续演进。 diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md deleted file mode 100644 index 05f72dd..0000000 --- a/DEPLOYMENT_GUIDE.md +++ /dev/null @@ -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) - diff --git a/README.md b/README.md new file mode 100644 index 0000000..71b498f --- /dev/null +++ b/README.md @@ -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)。这个文档说明了: + +- 语音与文本两条链路的完整时序。 +- 前后端之间的协议约定。 +- 当前实现的边界和适合继续扩展的位置。 +- 已知的工程化约束。 diff --git a/REMOTE_ACCESS.md b/REMOTE_ACCESS.md deleted file mode 100644 index 9b37775..0000000 --- a/REMOTE_ACCESS.md +++ /dev/null @@ -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/` - -首次需要在浏览器信任自签证书。 - -### 方案 B:HTTP 测试 - -如果仅做 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(后续可加)。 diff --git a/TECHNICAL_DOCUMENTATION.md b/TECHNICAL_DOCUMENTATION.md deleted file mode 100644 index 89e15a6..0000000 --- a/TECHNICAL_DOCUMENTATION.md +++ /dev/null @@ -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. diff --git a/core/pipeline.py b/core/pipeline.py index bf699cf..48f4972 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -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 = { diff --git a/core/state_machine.py b/core/state_machine.py index adf5cec..7613698 100644 --- a/core/state_machine.py +++ b/core/state_machine.py @@ -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 diff --git a/docs/development-guide.md b/docs/development-guide.md new file mode 100644 index 0000000..f41c8b0 --- /dev/null +++ b/docs/development-guide.md @@ -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) diff --git a/main.py b/main.py index 90420b1..737995c 100644 --- a/main.py +++ b/main.py @@ -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} diff --git a/research-notes.md b/research-notes.md deleted file mode 100644 index d4ff3ac..0000000 --- a/research-notes.md +++ /dev/null @@ -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= -hf download --local-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 \ - -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 nightly(RTX 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= - -# 注意:此环境中命令是 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/.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 3:MuseTalk 嘴型同步 - -```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 -``` diff --git a/scripts/gen-self-signed-cert.sh b/scripts/gen-self-signed-cert.sh index 65f9ae4..073af58 100755 --- a/scripts/gen-self-signed-cert.sh +++ b/scripts/gen-self-signed-cert.sh @@ -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." diff --git a/scripts/gpu_monitor.sh b/scripts/gpu_monitor.sh index 08f034a..60cfd9e 100755 --- a/scripts/gpu_monitor.sh +++ b/scripts/gpu_monitor.sh @@ -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}" diff --git a/scripts/run_all_checks.sh b/scripts/run_all_checks.sh index 218c96c..b5dd7f0 100755 --- a/scripts/run_all_checks.sh +++ b/scripts/run_all_checks.sh @@ -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" diff --git a/scripts/service.sh b/scripts/service.sh index 66e1519..79f8216 100755 --- a/scripts/service.sh +++ b/scripts/service.sh @@ -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" diff --git a/scripts/start-public.sh b/scripts/start-public.sh index e52e15b..b3ed95f 100755 --- a/scripts/start-public.sh +++ b/scripts/start-public.sh @@ -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" diff --git a/scripts/start.sh b/scripts/start.sh index 3e29c67..58d334e 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -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 diff --git a/visual-chat-task-list.md b/visual-chat-task-list.md deleted file mode 100644 index ba77156..0000000 --- a/visual-chat-task-list.md +++ /dev/null @@ -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) | 流式 VADIterator,32ms 帧级延迟 | 无 | -| **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.0076(130x 实时) | -| 模型大小 | ~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 流 | 累积式 chunk(1A, 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.0076,3s × 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 nightly(RTX 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-TTS(M4 阶段再装) -# 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 -- 下行音频可保持 24kHz(WebRTC 支持) - -### 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 连接 → 请求麦克风权限 -- 下行视频+音频 → `