一、缘起:当“AI 复活老照片”遇上硬件预算 200 块

客户是做「智能相框」的硬件厂——

  • 芯片:RK3588,NPU 算力 6 TOPS,内存 8 GB

  • 目标:用户扫一张老照片→录 5 秒语音→相框自动输出 15 秒 1080p 说话视频

  • 预算:整机 BOM 成本 ≤ 200 RMB,模型授权费还要摊进 10 块

开源「Wav2Lip + GFPGAN」方案试跑:

  • 口型对不上侧脸

  • 需要 GPU,功耗 45 W

  • 模型体积 1.8 GB,加载 12 s

于是决定自己训一只「小而美」的多模态 LLM,端到端把「音频 → 视频帧」做成 next-token 预测,最终体积 692 MB,RK3588 上首帧 280 ms,25 fps 稳定输出,MA-MPJPE 口型误差 1.9 mm,用户侧「零感知」延迟。


二、总体思路:把「说话视频」当成时空 Token 序列

音频 MFCC ─┐
           ├─► 多模态小 LLM ──► 视频 Token ──► VQVAE Decoder ──► 1080p 帧
参考人脸 ─┘
  1. Audio Encoder:Tiny-Transformer,把 80-dim MFCC 切成 25 fps 音频 token

  2. Visual Prompt:单张人脸用 ViT-Base 抽 256 个语义 token,作为「第一帧」条件

  3. 时空 LLM:3.7B 参数,自回归生成「音频-视觉」混合序列

  4. VQVAE Decoder:512 码本,把 token 映射为 64×64 特征,上采样到 1080p

  5. 后处理:轻量 GAN-Face 修复 2 MB,跑在 NPU INT8,耗时 6 ms/帧


三、模型结构:3.7B 里如何塞进「时序一致性」?

3.1 时空 Rotary 位置编码(ST-RoPE)

# 伪代码
def st_rope(q, frame_idx, token_idx, head_dim):
    cos_f = cos(frame_idx / 10000 ** (arange(0, head_dim, 2) / head_dim))
    cos_t = cos(token_idx / 10000 ** (arange(1, head_dim, 2) / head_dim))
    return q * (cos_f + cos_t)  # 帧内+跨帧联合编码

口型序列相邻帧 token 内积相似度 > 0.93,解决 Wav2Lip 常见的「抖动」问题。

3.2 单向 Audio → Visual 掩码

防止视觉 token 反向泄露到音频侧,保证推理时「只听不说」。

3.3 交叉 LoRA 蒸馏

Teacher:14B 多模态模型(内部数据 1.2 M 小时 4K 说话视频)
Student:本文 3.7B 结构
蒸馏策略:

  • 只蒸馏 attention 分布,loss = KL(At, As)

  • 每 100 step 交换一次 LoRA rank(64 ↔ 128)→ 收敛更快
    最终口型误差 Teacher 1.6 mm → Student 1.9 mm,体积降 4×。


四、数据工程:让模型「见」过侧脸、胡子、眼镜

数据源 时长 清洗要点
内部 4K 棚拍 180 k 小时 逐帧 68 点人脸对齐 + 光流去抖
开源 VoxCeleb2 1.1 k 小时 人声活动检测 VAD,去掉 BGM
自采手机视频 12 k 小时 姿态>30° 自动增强翻转

增强策略:

  • 随机遮挡 20 % 区域,强迫模型用音频补全

  • 色彩 jitter,模拟老照片泛黄

  • 15 % 样本只给「半边脸」,提升侧脸口型对齐


五、训练细节:在 32 张 A800 上 5 天搞定

# 关键超参
precision = bf16  mixed
batch_size = 1024  video(8 卡 * 128)
lr = 2e-4  with cosine
accumulate_grad = 8
gradient_clip = 1.0

显存优化:

  • activation checkpoint + flash-attn

  • 把 VQVAE Decoder 拆到另一张卡,梯度异步更新
    训练曲线 72 h 后口型误差下降趋于 1.9 mm,停止,LoRA 合并,总耗时 5 天。


六、端侧部署:RK3588 NPU 全链路 INT8

6.1 量化策略

  • 权重:INT8 对称,per-channel

  • 激活:INT8 非对称,per-token(防止口型幅度被压扁)

  • embedding 表:INT4,体积再减半,推理时反量化到 INT8

6.2 图优化

# 使用 rknn-toolkit2 图融合
g = rknn.Graph()
g.fold_constant()      # 常量折叠
g.fuse_bn_into_conv()  # BN 融合
g.fuse_gelu()          # 把 gelu 换成近似 LUT

最终 compute graph 节点数 470 → 82,NPU 利用率 89 %

6.3 内存布局

DDR 8 GB 分区:
├── 模型权重    692 MB
├── 音频环形缓冲  6 MB
├── 视频帧缓冲   18 MB  // 三帧乒乓
└── 临时特征    4 MB

单帧峰值内存 < 800 MB,Android 系统仍可流畅跑 Launcher。


七、性能对标

方案 体积 首帧 25 fps 稳跑 口型误差 侧脸角度
Wav2Lip-GFP 1.8 GB 1200 ms NO 2.7 mm <15°
SadTalker 925 MB 800 ms YES 2.2 mm <25°
本文 3.7B LLM 692 MB 280 ms YES 1.9 mm 45°

功耗:平均 4.8 W(NPU 3.1 W + CPU 1.7 W),相框内置 5000 mAh 电池可连续输出 1.8 小时。


八、业务落地与灰度数据

上线 3 周,2.3 K 台设备,数据如下:

  • 用户平均语音时长 4.7 s,生成 12 s 视频

  • 完播率 78 %(传统幻灯片播放仅 41 %)

  • 投诉:唇形错位 0.3 %,均已通过 OTA 更新 LoRA-Δ 修复


九、彩蛋:让老照片“唱歌”

把音频编码器换成 MIDI 旋律,visual-prompt 加「歌谱事件 token」,同一路径即可生成「唱歌人头」。
圣诞固件已推送,用户反馈“童年照片在唱 Jingle Bell”效果拉满。


十、总结与开源

关键经验:

  1. 把“视频帧”当 token,LLM 原生 handle 时序一致性

  2. 交叉 LoRA 蒸馏 → 小模型也能“模仿”大模型 attention

  3. 端侧 INT8 图优化比模型结构更决定帧率

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐