










一张照片加一个摄像头,实时驱动照片里的人跟着你做表情、转头,输出能直接推流。这件事的难点不在效果,在帧率——扩散模型生成一帧要跑好几次 UNet,天生和实时是反的。
PersonaLive 是澳门大学、Dzine.ai 与大湾区大学 GVC Lab 的工作,CVPR2026 收录,代码和权重都放出来了。它给这个矛盾的答案很漂亮:不减少去噪步数,而是让不同新鲜度的帧共享同一次前向。
这篇把它的流式去噪机制、三条条件通路、在线服务链路和 TensorRT 融合拆开记一遍。
肖像动画这条线上,质量和速度一直在打架。
GAN 系(LivePortrait 那一路)快,单帧几毫秒,但表情迁移的自然度和身份保真有上限
扩散系(AnimateAnyone、X-NeMo 那一路)质量高,但一帧要跑 N 次 UNet,N 通常 20 以上
把 N 压到 4 是常规操作(蒸馏),但 4 次前向摊到每帧还是太贵
更麻烦的是长度:视频扩散一般按固定长度的片段生成,直播要的是无限长且不能有拼接痕迹
PersonaLive 的定位是 real-time + streamable + infinite-length 三个词同时成立。核心手法下面单独说,先给它的规格:512×512、fp16、DDIM 4 步、ε 预测,timestep_spacing 用 trailing。
这是整个项目最值得看的一处,也是「4 步却只跑 1 次前向」的来源。
常规 4 步 DDIM 是这样:拿一批帧,加噪到 t=999,跑一次 UNet 降到 666,再跑一次到 333,再跑一次到 0。同一批帧过了 4 次网络。
PersonaLive 把这个过程侧过来。它维护一个窗口队列 latents_pile(一个 deque),里面常驻 4 个窗口,每个窗口 4 帧,但每个窗口停在不同的噪声档上:
latents_pile 窗口A t=0 窗口B t=333 窗口C t=666 窗口D t=999
最老 ↑ ↑ 最新(刚入队)每轮做三件事:新来的 4 帧驱动条件加噪到 t=999 追加成新窗口;把 4 个窗口拼成一个 16 帧的批,跑一次 UNet3D,DDIM 让每个窗口按自己的 timestep 各前进一档;最老的那个窗口这一步正好走到 t=0,popleft 出来送 VAE 解码。
配置里就是这三个数:
temporal_adaptive_step: 4 # 4 个窗口
temporal_window_size: 4 # 每窗口 4 帧
num_inference_steps: 4 # 4 步timestep 表和步长写死在 pipeline 里:
timesteps = torch.tensor([999, 666, 333, 0], device=device).long()
self.scheduler.set_step_length(333)于是 4 步的计算量被摊平:一次前向 16 帧、吐出 4 帧成品,等效每帧 1/4 次 UNet 前向。而且队列长度恒定,输入 4 帧输出 4 帧,不随时间增长,这就是「无限长」的来路——没有片段边界,也就没有拼接痕迹。
出队时用的是 pred_original_sample 而不是最后一步的采样结果,所以送进 VAE 的 latent 里没有残余噪声。
队列刚开始是空的,直接跑会有 3 轮没有成品可吐,而且第一批画面会从参考姿态猛跳到驱动姿态。
它的处理是融合参考图时预填 12 帧阶梯噪声把队列一次填满,同时姿态和表情各做 12 步线性插值:
mot_bbox_param, kps_ref, kps_frame1, kps_dri = self.pose_encoder.interpolate_kps_online(
self.ref_cond_tensor, tgt_cond_tensor, num_interp=12+1)
init_motion_hidden_states = self.interpolate_tensors(ref_motion, dri_motion[:,:1], num=12+1)[:,:-1]从参考图的姿态平滑过渡到第一帧驱动姿态,所以开播瞬间不会抽一下。
流式生成跑久了,身份会往驱动者那边飘。它的对策是监控表情嵌入与历史的距离:
add_flag, motion_bank = self.calculate_dis(motion_bank, motion_hidden_state, threshold=17.)最小 L2 距离超过 17 就把当前这帧干净图再过一次 Reference UNet,把写出的 16 层特征追加进 kv_bank,等于给注意力多一个「这个人长这样」的锚点。全程最多追加 3 次,否则 KV 会无限膨胀——这个上限还和后面 TensorRT 的动态维度档位对上了。
身份、姿态、表情走三条完全分开的路,注入 UNet 的位置也各不相同。
身份走注意力。参考图过 CLIP ViT 得到 1×768 嵌入走标准 cross-attention 提供全局语义;同时过 VAE 编码(乘 0.18215)喂给一个和去噪 UNet 同构的 Reference UNet2D,前向时把每层的 norm_hidden_states 存进 bank。去噪侧把这 16 层特征拼在自己的 KV 前面做空间自注意力——效果上等价于「一边看着参考图一边作画」。参考图只编码一次,整段直播都复用。
姿态走残差。MotionExtractor(convnextv2 主干)预测 21 个隐式 3D 关键点,以及 pitch / yaw / roll / 平移 / 尺度。重定向时保留参考图的规范关键点和尺度,只取驱动帧的旋转,平移按 0.5 缩放——所以你大幅晃头时照片里的人跟着转,但不会跟着你把脸怼到镜头上。然后从 21 点里挑 7 点画成彩色圆点图,PoseGuider(输出层零初始化)卷成 320 通道,加到 conv_in 的输出上。
表情走交叉注意力。按关键点算出的框裁一张 224² 人脸,和全局姿态解耦,过 MotEncoder(FAN 沙漏网络)出 512 维,reshape 成 32 个 16 维 token 再加正弦位置编码。注意 motion_module 的配置:
cross_attention_dim: 16
attention_block_types: [Spatial_Cross, Spatial_Cross]
num_attention_heads: 8交叉注意力的维度只有 16,注意力开销几乎可以忽略。表情单独走一路而不是和姿态揉在一起,是这套方案表情能做到「expressive」的关键——挑眉、眨眼、嘴型这些细节不会被头部大幅运动冲掉。
工程上分得很干净:FastAPI 主进程只做 WebSocket 协议和队列搬运,绝不碰 GPU;推理跑在 mp.set_start_method("spawn") 起的守护子进程里,两边用 mp.Queue 通信。
浏览器端是 SvelteKit,摄像头帧编码成 JPEG 走 WS 二进制推上来,驱动帧率 1–30 FPS 可调。服务端转张量归一化后入队。子进程侧凑够 4 帧才触发一次前向:
read_size = min(queue.qsize(), num_frames_needed * 3)队列里超过需要的量时按等距索引抽样,宁可丢帧也不排队——这条决定了延迟不会随时间累积。README 里提到调延迟可以把这个乘数从 3 调大。
回传侧也限速。解码帧转 JPEG 进 asyncio 队列,推送协程用 EMA 估计帧间隔:
MIN_FPS = 10
MAX_FPS = 30
SMOOTHING = 0.8
...
ema_frame_interval = SMOOTHING * ema_frame_interval + (1 - SMOOTHING) * raw_interval
sleep_time = min(max(ema_frame_interval, 1 / MAX_FPS), 1 / MIN_FPS)钳在 1/30 到 1/10 秒之间,队列满了丢最旧帧。所以延迟的下界很清楚:4 帧采集时间 + 1 次前向时间。
控制面上 pause 只清输入队列,reset 才会清 bank 并要求重新上传参考图——因为参考图的编码结果是整段直播的身份基础,换人必须重来。
官方说转 TensorRT 大约快 2 倍,做法比「把 UNet 单独转一下」激进得多:PoseGuider、MotEncoder、UNet3D、DDIM 一步、VAE 解码,五个模块被包进同一个 nn.Module 前向,导出成一张图。
融合后的签名是 23 输入 6 输出:
inputs: sample, encoder_hidden_states, motion_hidden_states, motion,
pose_cond_fea, pose, new_noise,
d00 d01 d10 d11 d20 d21 m u10 u11 u12 u20 u21 u22 u30 u31 u32
outputs: pred_video, latents, pose_cond_fea_out,
motion_hidden_states_out, motion_out, latent_first7 个常规输入加 16 个参考特征输入。注意这 16 个:PyTorch 版靠 Python 侧的 hook 把 Reference UNet 的特征塞进注意力,图里没法这么干,所以换成 16 个显式输入 d00…u32,对应 UNet 各层。这也是为什么仓库里有一个 unet_3d_explicit_reference.py。
导出走 ONNX opset 17,再用 polygraphy 建 fp16 引擎。动态维度只留给那 16 个参考特征的序列长度,按 1x / 2x / 4x 设 min / opt / max——正好覆盖「历史关键帧最多追加 3 次」,前面那个上限在这里闭环了。其余输入固定形状。
运行期最漂亮的一手是 bind():把输出的显存地址直接设成下一轮对应输入的地址。滚动状态(latents、pose、motion)在显存里自环,零拷贝。整轮只有一次 host 调用,省掉逐算子的 Python 调度开销。
代价也写清楚了:建引擎约 20 分钟;仓库自带的引擎是 H100 上产的,所有人都建议本机重建;导出前要把 FAN_temporal 的 pos_embed.requires_grad 置 False;TensorRT 优化会带来轻微画质波动。
训练代码 2026-05-15 才放出来,8×H100 上大约 13h / 15h / 20h。
Stage 1 图像级预热。随机采一个 timestep 加噪,网络预测 ε,换算回 x0 和真值算 latent MSE。额外对人脸、眼、嘴的掩码区域再算一遍 MSE 强化五官——数据预处理那步 tools/get_boxes.py 抽的就是这三种框。10% 概率走无条件前向,给分类器自由引导留空间。
Stage 2 四步蒸馏加对抗精修。时间表固定成 [999, 666, 333, 0],随机取 1~4 步自举,只对最后一步回传梯度。损失搬到像素域:MSE + 掩码 MSE + LPIPS×2 + StyleGAN2 对抗×0.05,判别器从 FFHQ512 预训练权重起步,和生成器交替更新。这一阶段才是让 4 步能出好图的地方。
Stage 3 时序模块微调。冻结 VAE、CLIP、两个 UNet、MotEncoder、PoseGuider,只训 temporal_modules。关键是训练里按 4×4 滑窗复现推理时的调度:窗口错位、逐步加噪、pred_x0 覆盖,让时序模块在训练时就见过推理时那种「一批里各帧噪声档不同」的分布。同时参考特征 bank 以 0.3 的比例随机丢 token,逼时序模块不要只依赖参考图。
时序模块不是从零开始,是从 X-NeMo 的权重初始化的。三个阶段之间的衔接要手改 yaml 指向上一阶段 exp_output 里的 checkpoint,没有自动串联。
另外注意推理时 stage1&2 和 stage3 用不同的 inference yaml,前者 use_temporal_module=false。
环境是 torch 2.1.0 + diffusers 0.27.0 + transformers 4.36.2 + xformers 0.0.22 这一套,python 3.10。
conda create -n personalive python=3.10 && conda activate personalive
pip install -r requirements_base.txt
python tools/download_weights.py权重六个 .pth:denoising_unet / reference_unet / motion_encoder / motion_extractor / pose_guider / temporal_module,加上 sd-vae-ft-mse 和 sd-image-variations-diffusers 两个基座。
离线推理一条命令,流式策略默认开着,官方说 12GB 显存也能生成长视频:
python inference_offline.py在线要先起 WebUI,然后:
python inference_online.py --acceleration none | xformers | tensorrt浏览器开 http://localhost:7860,流程是上传图片 → 融合参考 → 开始动画。
RTX 50 系(Blackwell)注意:xformers 还没完全适配,要 --use_xformers False 或者 --acceleration none,否则会崩。
抛开肖像动画这个具体领域,这套东西里有几个可迁移的想法:
conv_in 残差、交叉注意力三个不同位置。三者互不挤占容量。bind() 让滚动状态在显存自环,这一手很省。仓库在 GVCLab/PersonaLive,论文 arXiv:2512.11253,权重在 HuggingFace 和 ModelScope 都有。ComfyUI 也有社区实现。
读完最大的感受是,把扩散模型做到实时这件事,真正的杠杆不在模型也不在算子,在调度。同样 4 步、同样的网络,换个排法就从离线变成了直播。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。