LiveTalking 源码解剖:一个开源实时数字人是怎么跑起来的
- 约 17 分钟读完 - 全文 6738 字LiveTalking 源码解剖:一个开源实时数字人是怎么跑起来的
上一篇《数字人链路从 0 到精通》画完了实时数字人的完整链路:耳朵(VAD+ASR)、眼睛(视觉感知)、大脑(LLM+RAG)、嘴巴(TTS)、脸(口型驱动)、神经(WebRTC)。概念图看完了,下一步自然的问题是:真实项目里这条链路长什么样?
这篇解剖 LiveTalking。选它的原因:它在实时数字人开源项目里 star 数排前列,架构足够典型——它把「输出侧」(TTS→口型驱动→WebRTC 推流)做到了工业级,而输入侧和智能侧保持着 demo 形态,正好当「链路完整但交互闭环不完整」的对照样本。读完你会知道一个能跑的数字人项目,哪些部分可以直接抄,哪些部分要自己做。
1. 项目定位与启动
一句话定位:纯 2D 真人克隆口型驱动 + 流式 TTS/LLM 的实时数字人服务端,前端是浏览器。
python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1- 入口
app.py,HTTP 服务用 aiohttp,默认端口 8010。 - 配置三级覆盖:命令行 >
config.yaml> 默认值(config.py),加上.env读密钥,配置管理做得比多数开源 demo 规范。 - 插件注册表
registry.py是扩展点:stt / llm / tts / avatar / output 五类插件用装饰器注册,想换实现不用改主流程。 - 输出模式五种:
--transport可选 webrtc(浏览器/offer信令)、whep、rtcpush(WHIP 主动推流到 SRS)、rtmp、virtualcam(虚拟摄像头)。
顶层目录即架构:
flowchart TB
WEB[web/ 浏览器前端] -->|HTTP /human /offer| SERVER[server/<br/>rtc_manager / session_manager / routes]
SERVER --> AVATAR[avatars/<br/>base_avatar 渲染管线]
SERVER --> LLM[llm.py<br/>qwen-plus 单轮对话]
LLM -->|标点切句| TTS[tts/<br/>11 种 TTS 插件]
TTS -->|20ms 音频帧| AVATAR
AVATAR --> OUT[streamout/<br/>webrtc / rtcpush / rtmp / virtualcam]
OUT --> USER([用户])
2. 输出侧精华一:三种形象驱动方案
概念篇讲过口型驱动有三条路线(3D Blendshape / 真人克隆 / Live2D),LiveTalking 全部押在真人克隆上,提供三个模型可切换,套路一致:离线预处理真人视频(抽帧 + s3fd 人脸检测 + 存坐标),运行时按音频特征只重绘嘴部区域,再贴回原始帧。
| 方案 | 音频特征 | 模型 | 参考性能 |
|---|---|---|---|
| wav2lip | Mel 频谱 | 256×256 | RTX3060 约 60fps |
| musetalk | Whisper encoder 特征(只提特征,不做识别) | VAE + UNet(fp16) | RTX3080Ti 约 42fps |
| ultralight | HuBERT 特征 | 160×160 轻量 UNet | 最轻量 |
三个读源码时要注意的点:
avatars/audio_features/目录下的 mel/whisper/hubert 三个提取器名字叫 ASR,但不做语音识别,只是把音频变成模型能吃的特征向量——新人最容易在这里迷路。- 帧率固定 25fps,音频 16kHz、每 20ms 一帧,这两个时钟是全管线的节拍器。
- README 里提到的 ernerf(NeRF 路线)在当前代码库已移除,别再找它。
3. 输出侧精华二:渲染管线怎么省 GPU
核心在 avatars/base_avatar.py 的 render 循环:四条线程 + 三组队列的流水线。
flowchart LR
TTS[TTS 线程<br/>合成 20ms 音频帧] --> AQ[音频队列]
AQ --> FEAT[主循环<br/>提取音频特征]
FEAT --> FQ[feat_queue]
FEAT --> OQ[output_queue<br/>音频透传]
FQ --> INF[inference 线程<br/>批量 GPU 推理]
INF --> RQ[res_frame_queue]
RQ --> PROC[process_frames 线程<br/>嘴部贴回原帧]
OQ --> PROC
PROC --> OUT[输出层推流]
两个值得抄的设计:
- 静音时不跑模型推理:检测到静音段直接循环播放原始视频帧,只有说话时才走 GPU。这一个开关把 GPU 占用降了一个量级,是低成本并发的关键。
- 背压降速:渲染循环检查输出缓冲区积压,积压多了就主动 sleep 降速,防止队列无限膨胀。
另外它还有 idle 待机动作编排:自定义 audiotype 对应预切好的图片序列 + 音频循环播放,沉默时数字人不会「死机」。说话到静音的过渡混合代码存在但默认关闭(enable_transition = False),追求自然度的可以自己打开调。
4. 输出侧精华三:TTS 插件与 RTC 推流
TTS 插件 11 种:edgetts、gpt-sovits、xtts、cosyvoice、fishtts、tencent、doubao、indextts2、azuretts、qwentts、omnitts。统一基类 tts/base_tts.py:队列收文本 → 合成 → 按 20ms chunk 推给渲染管线。注意两个坑:
- 流式程度不一:gpt-sovits 是真 HTTP 流式,edge-tts 是「流式下载、整句合完再切片下发」的伪流式——首音延迟差很多,选型时实测。
- 文本切分不在 TTS 层做,由上游 LLM 切好送进来。
RTC 输出用 aiortc 实现,自定义 PlayerStreamTrack 时钟(视频 40ms/90kHz、音频 20ms/16kHz),编解码偏好 H264 优先于 VP8,支持 STUN。五种输出模式覆盖浏览器播放、推 SRS、RTMP 直播、虚拟摄像头进会议软件,输出侧的场景覆盖度是项目最成熟的部分。
5. 耗时分析:从用户说完到数字人开口,时间花在哪
概念篇第 4 节说「每一环都要流式」。拿这个标准量 LiveTalking,结论先行:这是一条「半流式」管线——LLM、特征提取、推流是流式的,ASR 和口型推理是整段/攒批的,两个非流式环节贡献了大部分延迟。
逐环节拆解(耗时依据均来自代码常量与实现方式):
| 环节 | 实现 | 流式? | 耗时特征 |
|---|---|---|---|
| ASR 语音识别 | SenseVoice 整段识别(asr_server.py:87-99) |
❌ 非流式 | 等客户端发「说完了」才把整段 PCM 一次性识别,耗时与音频时长成正比(恒定 RTF);全局推理锁串行,并发用户互相排队 |
| LLM 首 token | qwen-plus stream=True(llm.py) |
✅ 流式 | 云 API 首 token 通常几百毫秒 |
| LLM 切句 | 攒标点且超 10 字才送 TTS(llm.py:44-49) |
⚠️ 半流式 | 短回复、无标点回复会憋到最后才出声 |
| TTS 首包 | 11 种插件差异大 | ⚠️ 看插件 | edge-tts 伪流式:整句合成完才切片下发,首音 ≈ 整句耗时;gpt-sovits/cosyvoice/doubao 真流式:首音 ≈ 首 chunk 到达 |
| 音频特征 | mel/whisper/hubert 滑窗(config.py:52-54) |
✅ 但有前瞻 | 右上下文 r=10 意味着必须多等 200ms「未来」音频;启动 warm_up 再加 200ms 相位偏移,合计约 400ms 结构性延迟 |
| 口型推理 | GPU 批推理(base_avatar.py:347) |
❌ 攒批 | 攒满 32 个 20ms chunk(640ms)才触发一次推理出 16 帧;一批推理约 267ms(wav2lip256 @ RTX3060 约 60fps 推算) |
| 画面合成 | process_frames 逐帧贴回 | ✅ 流式 | 毫秒级,可忽略 |
| WebRTC 发送 | aiortc 软件时钟 40ms/帧(webrtc.py:69-109) |
✅ 流式 | 发送节奏与渲染解耦,无攒批;节拍 40ms + 网络传输 |
flowchart LR
A[用户说完<br/>判停等待] --> B[ASR 整段识别<br/>非流式]
B --> C[LLM 首 token<br/>流式]
C --> D[攒句超 10 字<br/>半流式]
D --> E[TTS 首包<br/>edge 最差]
E --> F[特征右上下文<br/>+200ms 前瞻]
F --> G[攒批 640ms<br/>非流式]
G --> H[GPU 推理一批<br/>约 267ms]
H --> I[RTC 节拍 40ms<br/>+ 网络传输]
style B fill:#fee,stroke:#c00
style G fill:#fee,stroke:#c00
数量级估算(默认组合 wav2lip + edge-tts):
- 仅算「TTS 首段音频 → 口型首帧」:200ms 前瞻 + 最多 640ms 攒批 + 一批推理约 267ms + 40ms 发送节拍 ≈ 1.1 秒量级。这期间管线一直在播静音循环帧,用户看到的是「待机」而不是卡死。
- 再加上 ASR 整段识别(几秒的整段音频、全局锁排队)和 edge-tts 整句合成,端到端「用户说完 → 数字人开口」会到数秒级——这就是它演示效果还行、对话感弱的技术根源。
三个值得记住的点:
- 固定成本写死在常量里:640ms 攒批来自
batch_size=16(每批吃 32 个 chunk),200ms 前瞻来自-r 10。做延迟优化最先动这两个数——但batch_size调小会降 GPU 吞吐,这是延迟与并发的权衡,不是免费午餐。 - 假流式要会分辨:edge-tts 的
stream()是「流式下载、整句合成完才切片」;判断一个 TTS 插件是不是真流式,看首个音频 chunk 是否即时进队列——代码里 sovits/cosyvoice/doubao 都有Time to first chunk打点,edge 没有。 - 打点有但不成链:LLM 首 chunk、TTS 首包、推理 fps(每 100 帧打
actual avg infer fps)、推流 fps、ASR RTF 各有分散日志,但没有端到端统一 trace(无 request-id 串联)。想定位延迟瓶颈得自己加链路追踪,这也是第 8 节工程化缺口里的一项。
6. 输入侧与智能侧:demo 级,别照抄
对照概念篇的链路图,短板集中在上半场:
ASR(耳朵):server/asr_server.py 用 FunASR 的 SenseVoiceSmall + fsmn-vad,但不是真流式——客户端按 60ms chunk 上行,服务端攒缓冲,收到停止信号才整段识别一次,没有增量 partial 输出。还有一条更糙的路:直接用浏览器 Web Speech API 按住说话。
LLM(大脑):llm.py 单文件几十行,qwen-plus 流式调用,无多轮历史、无 RAG、无记忆、无工具调用——每次只发一条 user message。文本切句是最简版:扫标点攒够 10 字就 flush,没有概念篇 3.3.1 讲的首句激进策略、超时兜底、标点陷阱清洗。
打断(barge-in):有 /interrupt_talk 接口,清空 TTS 队列 + 置暂停,但这是前端主动 HTTP 打断,不是 VAD 检测到用户插话自动触发——算半个 barge-in。
视觉感知(眼睛):完全没有运行时摄像头输入。人脸检测只用于离线建 avatar 素材。
架构上的本质差异:它的 RTC 只发不收——用户音频根本不上行到服务端,ASR 走浏览器侧。这个设计 demo 友好,但意味着服务端做不了 AEC、做不了语义 VAD、打断链路不闭环,是「demo 架构」和「产品架构」的分水岭。
7. 跑通它的坑:安装、模型与素材
源码读得再明白,跑不起来都白搭。下面是从这个项目代码里逐条核实的坑,按严重程度分三级。
7.1 跑不起来级
依赖管理是半成品:
requirements.txt里没有 torch——必须按 README 手动装指定版本(torch==2.9.1+cu128),直接pip install -r requirements.txt后启动必报No module named 'torch'。- 关键依赖(aiortc、librosa、transformers、edge_tts)全不锁版本。aiortc 历年 API 变动大,新环境装到不兼容版本是常见翻车点。
- librosa 定时炸弹:
avatars/wav2lip/audio.py:18用了librosa.output.write_wav,这个 API 在 librosa 0.10 已删除,新版环境下走到该路径直接 AttributeError。 - Python 版本三个口径互不一致:README badge 写 3.10+,实测环境是 3.12 + CUDA 12.8,Dockerfile 里还是 3.10 + CUDA 11.3。
funasr/modelscope在 requirements.txt 里被注释掉了,不手动装的话/api/asr静默不注册(只打一行 info 日志),新人会以为 ASR 功能坏了。
模型权重全靠网盘手动摆位,路径还硬编码:
| 模型 | 坑 |
|---|---|
| wav2lip | 路径硬编码 ./models/wav2lip.pth(app.py:148),README 要求把下载的 wav2lip256.pth 手动改名,忘改直接报文件不存在 |
| musetalk | 四件套要全部就位:unet + sd-vae + HF transformers 格式的 whisper(不是 OpenAI 权重格式,放错 load 失败)+ face-parse 模型 |
| ultralight | 最深的坑:from_pretrained 传的是本地路径不会自动拉取;且每个 avatar 需要自己训练的 ultralight.pth——genavatar.py 末尾只是 cp 你在项目外训练好的 checkpoint,训练过程根本不在本仓库。新人选 ultralight 基本必卡 |
| s3fd 人脸检测 | 首次使用从外网(adrianbulat.com)下载权重,国内/离线环境 genavatar 直接挂 |
另外默认 TTS 是 edge-tts,依赖微软在线服务,离线或被墙环境无声且无重试(只打日志)。
7.2 avatar 素材制作的坑(genavatar)
- 输入视频必须 25fps,但代码不校验(
config.py:51注释写着 “must be 25”)。喂 30fps 视频会口型节奏错位,且无任何报错。 - 人脸检测失败静默兜底:检测不到人脸时直接用整帧坐标(
avatars/wav2lip/genavatar.py:107),产出垃圾素材也不报警,要等跑起来看到崩坏画面才知道。 - 全量帧读进内存:所有 PNG 帧一次性 load 进 RAM 且常驻(
wav2lip_avatar.py:81-86),长视频直接撑爆内存,也没有时长上限引导。 - 抽帧逐帧写 PNG,几分钟 1080p 视频就是几千个文件,磁盘消耗无提示。
7.3 体验受损级(能跑但别扭)
DASHSCOPE_API_KEY没配置时静默失败(llm.py只 catch 打日志),前端表现是「数字人就是不说话」,没有任何错误提示。- getUserMedia 需要安全上下文:用
http://<局域网IP>:8010访问时浏览器禁麦克风,语音功能无声失效,README 没提。 - WebRTC 端口要求夸张:README 要求开放 UDP 全端口,且只配 STUN 不配 TURN,严格 NAT 下连接失败无引导。
- LLM 切句粗糙:攒够标点且超 10 字才送 TTS,短回复、无标点回复会憋到最后才出声——概念篇 3.3.1 讲的超时兜底这里没有。
- wav2lip 嘴部硬贴回:直接
cv2.resize覆盖矩形区域,无边缘融合(musetalk 才有 mask blending);说话/静音过渡混合代码存在但默认关闭,帧切换有跳变。
一句话避坑总结:按 README 跑 wav2lip + webrtc + edgetts 默认组合成功率最高;深坑集中在模型权重手动摆位、ultralight 需项目外自训、25fps 与人脸检测失败均无校验这三处。
8. 从 demo 到商业级产品,还差什么
flowchart LR
subgraph 已有["LiveTalking 已覆盖(工业级)"]
A1[形象驱动三方案]
A2[渲染管线 + 省 GPU]
A3[TTS 插件 + 克隆]
A4[RTC 五种输出]
A5[idle 待机]
end
subgraph 缺失["到产品还差(要自己造)"]
B1[RTC 音频上行 + AEC]
B2[真流式 ASR + 语义 VAD]
B3[自动 barge-in]
B4[RAG + 多轮 + 记忆]
B5[视觉感知]
B6[测试/CI/鉴权/部署]
end
已有 -->|补全后| P[商业级<br/>数字人]
缺失 --> P
具体缺口清单,按补齐优先级排序:
- 交互闭环:用户语音走 RTC 上行 → 服务端 AEC + 流式 ASR + 语义 VAD → 自动 barge-in。这是从「能演示」到「能用」的第一刀。
- 智能:LLM 层接多轮历史 + RAG(概念篇 3.2 节的完整方案),
llm.py只有几十行,改造量小、收益最大。 - 视觉感知:按概念篇 3.1.3 的方案,端侧轻量模型起步。
- 工程化与安全:测试、CI、鉴权、部署——当前几乎是空白,见下面的上线隐患清单。
8.1 上线隐患级(想部署公网必须先补的课)
这些问题不影响跑 demo,但每一个都是生产事故的种子:
- 全无鉴权:
/human、/interrupt_talk、/record、/api/admin/*、/sse全部裸奔;admin 接口直接vars(opt)输出全量配置;CORS 是*全开。公网部署等于敞开大门。 - 录制功能用
os.system拼 ffmpeg 命令(base_avatar.py:285),sessionid 来自外部传入,存在路径注入风险;临时文件落在工作目录,同名会话重录互相覆盖。 - GPU 资源不释放:
session_manager.remove_session只 pop 字典,注释里自己留了一句「todo: 还可以主动调 avatar_session 释放」——avatar 素材常驻显存不回收,global_avatars缓存永不淘汰,频繁换 avatar 会显存泄漏。 - GPU 模型全局共享且无锁:每个 session 起独立 inference 线程并发调同一个模型,高并发同时说话时显存峰值不可控,没有任何排队或互斥防护。
- 线程泄漏风险:inference 线程里
output_queue.get()无超时阻塞(base_avatar.py:348),TTS 断供时线程永远卡住,shutdown 的join()会挂起;LLM 调用走默认 executor 无线程池上限,刷接口可起大量线程。 - 队列清空不持锁:打断时直接
queue.queue.clear()操作内部 deque(tts/base_tts.py:31),打断瞬间恰逢 put 有小概率竞态。 - Dockerfile 是坏的:CUDA 11.6/torch 1.12 与 README 要求的 torch 2.9.1/cu128 矛盾,且
COPY路径还是上个项目的名字nerfstream——这个 Dockerfile 根本 build 不出当前项目。
8.2 文档与代码不一致(宣称 vs 实际)
读这个项目要建立「README 说的是理想、代码才是现实」的警觉:
| README 宣称 | 代码实际 |
|---|---|
| 支持 ernerf(附演示视频链接) | ernerf 已移除,app.py 只注册 musetalk/wav2lip/ultralight |
| 支持说话被打断 | 仅 HTTP 主动打断,无语音 barge-in(服务端不收上行音轨) |
| 支持多并发 | 有限流,但模型共享无锁 + 资源不释放 |
| requirements.txt 列了 Flask | 实际用 aiohttp,app.py 里的 Flask 代码是死代码 |
| admin API 文档暗示 fps 可配 | config.py 写死 25,改了整条音视频时钟链都会错位 |
| 已在业内广泛商用 | 无鉴权/无测试/资源不释放,开源代码距离可商用部署有明显差距 |
9. 源码怎么读:推荐顺序
项目文件不少,按这个顺序读最省力:
app.py+config.py:看启动流程和配置体系,建立全局。registry.py:十几行的装饰器注册表,理解插件机制后所有模块的可替换性就清楚了。avatars/base_avatar.py:全文核心,渲染管线的四条线程、静音跳推理、idle 编排都在这。tts/base_tts.py+ 一个具体实现(如tts/sovits.py):理解真假流式的差别。server/webrtc.py:看自定义时钟轨道怎么把音视频帧喂给 WebRTC。- 最后回头看
llm.py和server/asr_server.py——看清 demo 级实现的边界在哪,正好是练手改造的切入点。
10. 总结
LiveTalking 的价值在于输出侧的真实工程参考:三种驱动方案的取舍、四线程渲染管线、静音跳推理省 GPU、插件化 TTS、五种 RTC 输出——这些在概念文章里只是一句话,在源码里是踩过坑的实现。它的边界也清晰:输入侧和智能侧保持 demo 形态,不是缺陷,是定位。
对学习者,正确的用法是概念篇当地图、这个项目当实地:先知道链路全貌,再在源码里逐个模块对号入座,最后从「补齐缺口清单」里挑一个动手改——比如给 llm.py 接上 RAG 和多轮历史,就是性价比最高的第一个实战任务。