摘要:内网跑了 20 多版才敢定稿的「数字人 5 秒待机动画」手册,这次原样公开:十条实测铁律、六步工序(全 CPU,5–6 分钟一版)、五个可调旋钮、八条坑速查,以及六项能拦住返工的验收闸门判据。每一条都带实测数值,可直接照抄执行。

一、适用范围与一句话原理
适用:实时数字人引擎里「老师」角色的「静音待机」动效。首个样板角色(密胡须 + 眼镜)。
目标观感:像真人老师准备交流、正在聆听 —— 轻微呼吸、头身自然微晃、5 秒 3 次自然眨眼、目光转动一次、带一点微笑。
来源:2026-10-09/10 的 r12 → r13 实测推导;旧 SOP 仅作参考,未复验的旧条目不作为强制要求。一句话原理:一个像素只能来自一张图(单一来源)。
所有”动”都是对原画本身的形变;任何”两张不同图羽化互叠”必然产生虚影/重影,任何”引入视频像素”都会把软糊带回来。
二、十条铁律(全部来自实测)
- 单一来源:成品每个像素只能来自原画、或原视频的某一帧;禁两源羽化互叠(虚影/脖子错位的真因)。
- 微动 = 形变原画:呼吸/头身晃动用解析形变场 +
cv2.remap重采样;不混视频、不做时间中值冻结。 - 眨眼只用真实两态:取源视频里真实的「半闭 M」「全闭 X」,排成
[M→X→M]三帧斜坡(≈0.21s),5 秒 3 处;禁止复制全闭帧充数——那是硬开关,看着像抽搐。 - 嘴型开合禁止;静态微笑/暖眼可以(局部形变 ≤5px)。
- 掩码不许”只留最大连通域”:会丢掉单侧眼的闭合区(本轮真凶)。改为保留所有 ≥30px 连通域。
- 禁用 dlib EAR 判眨眼:风格化原画上闭眼帧 EAR 反而升高(实测 0.259→0.332)。
- 禁用绝对阈值指标:会被”微动”本身污染;必须用局部对照(同一眨眼前后自比)或相对上升量。
- 禁用
cv2.seamlessClone:实测不可复现(同输入两次调用差 9118px)→ 改 确定性合成 = ECC 配准 + Reinhard 色彩迁移 + 羽化。 - 动效与素材包解耦:全装进
customvideo/<id>_idle+custom_config_<id>.json,说话包一像素不动。 - 写文件防截断:含中文的长内容写入会被静默截断(曾仅 243B,纯 ASCII 也中招)→ 分段写并核字节数。
三、六步工序(实测 ≈5–6 分钟,全 CPU;不烧 GPU、不动说话包)

步 0 素材
- 原画:绿幕
896×704(人物肖像,构图 1:1 零裁,alpha 已去污染)。 - 源视频:同一角色的短片,必须含自然眨眼(i2v 或录屏均可)。
- 全部脚本在项目根目录下跑;用项目自带 venv 的 python,并先清空 PYTHONPATH / PYTHONHOME 环境变量。
- ⚠ 含中文的路径不能直接喂
cv2.VideoCapture→ 先复制成 ASCII 路径。
步 1 挑眨眼两态(最关键的一步)
eye_scan.py:每帧对齐到原画后,按左眼/右眼分别量”相对闭合度”(局部对照 = 与额头区比),
找双眼同时上升的帧 → 最高者作 X(全闭)、次高者作 M(半闭)。
– ✗ 别用 EAR、也别用”对原画的绝对差”(基线 28 会淹掉信号,两处都是本轮踩过的坑)。
– eye_frames.py 出放大序列图目检(睁→闭→睁 + 左右是否同步)。
– 样板角色实测结论:X=15、M=18;源里另有多处单眼眨眼必须避开。
步 2 造底板
plates.py:① 原画 + 静态轻微笑/暖眼(高斯权重局部形变,嘴角/颊/下睑 ≤5px);
② 用选定的 X/M 帧做确定性合成叠进眼区 → plates/{art,M,X}.png。
闸门 plate_check.py:眨眼补丁像素范围必须同时覆盖左右眼(样板角色实测 x274–436)、
绿幕像素数几乎不变、锐度 ≥99%、无新增黑块。
步 3 渲染表演(render.py;微动档位试片用 motion.py)
- 呼吸:胸腔微缩放,1 个周期 / 5 秒(≈12 次/分)。
- 头身微晃:绕颈根微旋转 + 平移;档位 v1=4.7px / v2=9.3px / v3=15.8px(最大位移)。
- 目光:一次,虹膜位移 2.6px(≈虹膜直径 12%)+ 0.2° 头部随动;放在两处眨眼之间,眨眼帧上自动关闭。
- 眨眼:3 处,每处
[M,X,M]共 5 帧。 - 输出 125 帧 PNG +
perf_v2.mp4(crf12) + 预览(crf26、0.65 倍)。 - ⚠ 所有周期分量取整数周期/125 帧,保证首尾无缝(实测 |f124−f0| ≈ 单帧步进量)。
步 4 闸门(全过才安装)

blink_check.py(局部对照法)+ plate_check.py:
| 项 | 判据 | 样板角色实测 |
|---|---|---|
| 三处眨眼 | 对照 ≪ M < X,且闭环回落 | 5.8/19.5/13.9 → 36/41/36 → 42.6/46.4/41.7 → 36.9/42.2/35.8 |
| 左右眼同步 | 补丁范围覆盖左右眼 | x274–436 ✔ |
| 绿幕保留 | 像素数几乎不变 | 262867 → 262822 |
| 锐度 | ≥99% 原画 | 546 → 542(99.4%) |
| 循环接缝 | ≈ 单帧步进量 | 5.1 ✔ |
| 目检 | 无重影/糊块/黑块/绿边 | ✔ |
步 5 安装与生效(install.py)
拷 125 帧到 data/customvideo/<id>_idle/(0000.png 起)+ 写
data/custom_config_<id>.json = [{"audiotype":1,"imgpath":"data/customvideo/<id>_idle"}]。
– 引擎主程序 每次会话读该配置 → 不用重启;下次选中该老师即生效(静音态播放模块播该序列,乒乓回放)。
– 端到端验收:WebRTC 拉流落帧(e2e_video.py)比清晰度 + 目检。
四、参数旋钮(一次只改一个,出预览再看)

| 旋钮 | 现取值 | 位置 |
|---|---|---|
| 微动幅度档 | v2(9.3px) | render.py 的 P=dict(rot,x,y,br) |
| 目光位移/方向 | 2.6px(可改”向下看一眼再回来”) | 同上 GAZE_SHIFT / GAZE |
| 微笑强度 | ≤5px | plates.py 里的 add(...) 各项 |
| 眨眼时长/处数 | 5 帧/处、3 处 | render.py 的 BLINK=[20,62,100] |
| 呼吸周期 | 1 周期/5s | 同上(br 与 w_breath 权重) |
五、坑速查(现象 → 根因 → 对策)
| 现象 | 根因 | 对策 |
|---|---|---|
| 虚影 / 脸含脖子错位 | 原画 × 视频羽化互叠 | 单一来源;形变原画 |
| 眨眼像抽搐 | 复制全闭帧充”慢眨眼” | [M,X,M] 真实三态斜坡 |
| 眨眼只闭一只眼 | 掩码”只留最大连通域” | 保留所有 ≥30px 连通域 |
| 眨眼测不出来 | dlib EAR 在风格化原画上反向 | 局部像素差 + 局部对照 |
| 合成结果每次不同 | seamlessClone 非确定性 |
ECC + 色彩迁移 + 羽化 |
| 说话变糊/嘴卡 | 逐帧人脸框抖动 76px | 全睁眼静态源建包(抖动→8px);眨眼帧只写回眼睛像素 |
| 脚本内容写不进去 | 含中文的长写入被截断 | 分段写 + 核字节数 |
| 形变后边缘露出绿 | 形变取到画外 | BORDER_REPLICATE + 位移 ≤16px |
六、时间预算(实测)
底板 ~1min → 渲染 ~2min → 闸门 ~1min → 安装 <1min → 端到端 ~1min ≈ 5–6 分钟(全 CPU)。
七、与旧版 SOP 的关系
旧 SOP 里未经本轮复验的条目(RGBA 序列交付、25fps、乒乓轴 census、复用母本 idle_pipeline_v9.py 等)
本工作流不作为强制要求;以本 SOP 的实测闸门为准。需要时再逐条评估后并入。
八、把这份 SOP 用起来的建议
手册的价值不在条目多,而在每一条都有实测数值兜底。落到执行上建议三件事:
- 先跑闸门,再谈观感。 六项判据全过再安装——尤其「左右眼同步覆盖」与「循环接缝 ≈ 单帧步进」,这两项能拦住八成的返工。
- 一次只改一个旋钮。 微动幅度、目光位移、微笑强度、眨眼处数与呼吸周期彼此耦合;一次改两个,出了问题无法归因。
- 保留失败记录。 第五节「坑速查」里的每一条都曾是真实翻车;新角色上手先对照扫一遍,能省掉一整轮试错。
最后一句经验:当「像不像活的」卡住时,先怀疑「是不是有两个来源」,而不是急着加效果。



