07 · 数字人
架构剖析第七篇:数字人(Digital Human)渲染管线、语音合成(GPT-SoVITS)、后端抽象、VRAM 门控。 对应
ARCHITECTURE.md§8。
§8.1 系统概览
数字人 = 语音克隆(GPT-SoVITS) + 动画驱动(anime backend) + 流式多路复用
text gen ──► tts_worker ──► audio_q ──┬─► backend_worker ──► frame_q ──┐
│ │
└─► mux_worker ◄─────────────────┘
│
▼
StreamChunk三个 asyncio 任务通过有界 asyncio.Queue 通信。取消:关闭消费者迭代器 → finally 块取消所有 worker。
§8.2 DigitalHumanSession(src/digital_human/session.py)
8.2.1 构造
python
class DigitalHumanSession:
def __init__(
self,
config: Optional[DigitalHumanConfig] = None,
library: Optional[CharacterLibrary] = None,
):
self.config = config or DigitalHumanConfig()
self.tts = StreamingTTS(self.config)
self.library = library or CharacterLibrary()
self._backend: Optional[Backend] = None
self._source: Optional[Source] = None8.2.2 生命周期
python
async def prepare(
self,
*,
voice_ref: str, # voice.wav 路径
voice_ref_text: str = "", # 参考文本
character_id: Optional[str] = None,
backend: Optional[str] = None,
) -> None:
"""加载 TTS + 选择后端。
backend 未指定时由 select_backend() 根据输入自动选择。
"""
async def stream(
self,
text_source: AsyncIterator[str],
) -> AsyncIterator[StreamChunk]:
"""流式生成。text_source 是 token 异步迭代器。
产出 StreamChunk = (pcm_bytes, video_frame)。
"""8.2.3 TTS 存活检查
python
@property
def _tts_loaded(self) -> bool:
"""True when TTS subprocess is admitted AND actually alive.
旧版只检查 self.tts._runner is not None → 死后仍标记 "loaded" →
用户首次 _synth 命中 5-15s 冷启动(正是 eager load 要防止的死寂)。
现在轮询子进程;死了 → 当作 not-loaded → 下次 load() 重新 spawn。
"""§8.3 DigitalHumanConfig(src/digital_human/config.py)
python
@dataclass
class DigitalHumanConfig:
# 路径
gpt_sovits_dir: str = _default_gpt_sovits_dir() # GPT-SoVITS 集成包根
gpt_sovits_version: str = "v2" # v1/v2/v3
gpt_sovits_checkpoint: Optional[str] = None # .ckpt/.pth 权重
tha3_dir: str = _default_tha3_dir() # THA3 demo checkout
tha3_variant: str = "separable_float" # THA3 变体
# 流式参数
audio_chunk_ms: int = 200 # PCM chunk 目标大小
# 图像处理
anime_auto_bust_up: bool = True # rembg + 半身重构环境变量:
GPT_SOVITS_DIR—— 覆盖 GPT-SoVITS 路径THA3_DIR—— 覆盖 THA3 路径
§8.4 后端抽象
8.4.1 后端清单
| 后端 | 目录 | 说明 |
|---|---|---|
anime_morph | backends/anime_morph/ | 主后端。配对口型 atlas + 显式 mask blend |
anime_dynamic | backends/anime_dynamic/ | THA3 单图驱动(fallback) |
anime_pretrained | backends/anime_pretrained/ | Live2D 预训练角色 |
anime_seethrough | backends/anime_seethrough/ | 透视模式 |
realphoto_dynamic | backends/realphoto_dynamic/ | 真实照片驱动(已弃用) |
8.4.2 Backend 基类
python
# src/digital_human/backends/base.py
class Backend:
"""抽象基类。每个后端实现:
- prepare(spec: PrepareSpec) → Source
- drive(audio_chunk) → list[MotionFrame]
- render(frame) → np.ndarray
"""
@dataclass
class PrepareSpec:
voice_ref: str
voice_ref_text: str
character_id: Optional[str]
source_image: Optional[str]
@dataclass
class Source:
"""后端资源句柄。"""8.4.3 后端选择
python
# src/digital_human/backends/registry.py
def select_backend(hint: Optional[str] = None, **kwargs) -> Backend:
"""根据 hint 和可用资源选择后端。
hint=None → 默认 anime_morph。
"""8.4.4 项目意图
The project is intentionally anime-first. Older LivePortrait / JoyVASA real-photo experiments are deprecated because they did not produce reliable Chinese lip-sync and required far more weights/runtime complexity than the current mouth-atlas pipeline.
§8.5 流式参数
8.5.1 audio_chunk_ms = 200
为什么 200ms(旧值 500ms):
mux worker 将每个 audio chunk 配对 audio_chunk_ms × fps / 1000 渲染帧后
产出 StreamChunk。
500ms × 25fps = 12 帧/chunk
→ seethrough GPU render ~30-50ms/帧
→ 每 chunk 等待 360-600ms 墙钟时间
→ 首 chunk 到达比 TTS 完成晚 ~500ms
200ms × 25fps = 5 帧/chunk
→ ~150-250ms 渲染预算
→ 首 chunk 几乎在 TTS 完成时到达 renderer
代价:更多 WS 消息/秒。收益:首 chunk 延迟大幅降低。8.5.2 StreamChunk 结构
python
# src/digital_human/types.py
@dataclass
class StreamChunk:
audio_pcm: bytes # PCM 音频数据
video_frame: np.ndarray # 视频帧
sample_rate: int # 采样率
pts_ms: float # 呈现时间戳§8.6 管线子模块
src/digital_human/pipeline/
├── tts.py # StreamingTTS(GPT-SoVITS 子进程通信)
├── motion.py # 动作生成(音频 → 驱动信号)
├── render.py # 渲染(驱动信号 → 视频帧)
├── emotion.py # 情绪分类器
├── theme.py # 主题/风格
└── voice_ref_gen.py # 声音参考生成§8.7 VRAM 门控
8.7.1 GPU 分级与功能矩阵
| 分级 | VRAM | 数字人支持 | 说明 |
|---|---|---|---|
mode0 | 无 GPU | ❌ | 全部 API-only |
tier4 | ~4GB | ⚠️ 受限 | 音频 only,无视频/情绪分类 |
tier6 | ~6GB | ⚠️ 音频 | 音频 + 基础视频 |
tier8 | ≥8GB | ✅ 完整 | 全功能 |
8.7.2 VRAM 优先级
| 优先级 | 数字人组件 | 说明 |
|---|---|---|
| 1 — CRITICAL | TTS(说话中)、whisper(听中) | 用户活跃请求 |
| 2 — IMPORTANT | bge embedding | 稳态交互 |
| 3 — NICE | emotion tagger, avatar poser | 有用但可等 |
| 4 — BACKGROUND | 训练 | 永不用户可见 |
8.7.3 预热策略
Bridge 启动时预热"最近 Agent 的 DH":
python
# src/bridge/app.py 模块级
# 预热 embedder / rembg / rewriter / 最近 Agent 的数字人启动后瞬时可交互。
§8.8 与记忆协同
- DH 会话用同一套
DialogueManager/ 记忆 - 数字人只是另一个"发言外壳"
- 记忆不因 DH 隔离
Assistant.send_message()是入口:
python
async def send_message(self, text_stream, *, ref_text="",
character_id=None, backend=None):
"""Stream reply as text+audio+video.
text_stream: 异步 token 迭代器
voice_ref: {agent_path}/voice.wav
yield: (text_chunk, StreamChunk)
"""
session = DigitalHumanSession(DigitalHumanConfig())
await asyncio.to_thread(session.prepare,
voice_ref=voice_ref, voice_ref_text=ref_text,
character_id=character_id, backend=backend)
async for chunk in session.stream(_text_gen()):
yield chunk§8.9 CharacterLibrary
python
# src/digital_human/library.py
class CharacterLibrary:
"""角色库管理。
get_default() → 默认角色条目
list() → 所有可用角色
"""角色数据存储在 workspace/agents/agent_<id>/:
voice.wav—— 声音参考client_portraits/{key}.png—— 客户端肖像(白色背景 →strip_white_file透明化)
下一篇:08-安全与监控 →
