很多人第一次给数字人做素材,思路是“录一段带声音的视频,播的时候对着放”。这条路走不通,而且是在上线之后才会发现走不通——因为它错在最开始的定义上。
我们把数字人素材这条链路从零做到稳定交付,中间踩的坑基本可以分成两类:一类是概念性的,比如素材到底是什么形态;一类是审美性的,比如轮廓那圈亮边到底要不要修。第二类比第一类更费钱,因为技术上门门都对,成品还是被判“看着不对”。这篇只写我们真实踩过的,每个结论后面都有当时的实测数字。
数字人前端的标准形态是三层同屏叠加:
| 层 | 元素 | 职责 |
|---|---|---|
| 画面·下 | 待机 <video> | 常显,循环播放,负责“人在那儿” |
| 画面·上 | 口播 <video> | 循环播放,靠透明度 0/1 显隐,负责“人在说话”。音轨在代码里被强制静音 |
| 声音 | 独立的音频元素 | 唯一声源。后端把语音分段推给音频队列,顺序播放 |
看清楚这张表,几个结论就自动出来了:
| 要求 | 判据 |
|---|---|
| 无声 | 用 ffprobe 检测,必须没有音频流 |
| 无缝循环 | 尾帧到首帧的像素差,约等于一帧的正常运动量 |
| 锚点闭环 | 口播首帧 = 待机首帧 = 同一个临界点(详见第三节,只考核一半) |
| 姿态可维持 | 源图里的姿态必须是一个人能长时间自然保持的(详见第四节) |
规格不用自己发明,直接抄成品:我们线上实际引用的两支素材是——待机 204 帧 / 8.16 秒,口播 262 帧 / 10.48 秒,都是 1080×1920 @25fps,都没有音轨。口播那 262 帧是 131 帧往返折返出来的,所以单程 131 帧 = 5.24 秒。
这个 5.24 秒容易被误解。它不是“每句话要说 5.24 秒”,而是它定义了嘴动的节奏——循环素材的语速感由单程长度决定,跟某一句话实际多长没关系。
前端代码的注释写的是“首尾帧已统一(锚点闭环),无需等待机循环到首尾帧,立即切换”。但用线上标尺实测反推,真实约束比注释更精确:
也就是说,锚点闭环这件事只需要保证一半。知道这一点,能省掉大量“为什么首帧对不上”的无效返工。
顺带说切换的实现细节,它是“为什么不闪”的关键:切的时候先建新元素并 load(),等到 loadeddata(或 readyState ≥ 2)才开始 0.16 秒交叉淡化,200 毫秒后把另一个元素载入同源并同步 currentTime 续播,另有 2 秒兜底强制交换。切回待机时还带 3 秒(放歌场景 5 秒)的语音活动检测冷却,防止它自己的声音触发新一轮。
“一个人正常情况下,不可能一直把手举起来。口播视频也不可能一直举着手说话。”
这句话把问题说透了。一次性动作不能进循环素材——举手的拇指手势、比划、叉腰、指方向,全都是“拍照那一瞬”的姿态。待机要循环 8 秒、口播要循环 10 秒,而用户会一直看着它,任何摆拍动作在三秒内就会露馅。
允许的姿态只有一种:静态站姿 + 双手自然垂放体侧(可以含轻微的自然眨眼和眼神)。
更麻烦的是,举手多半是源图自带的,渲染只会沿用,剪帧也剪不掉,所以没有省事的路,必须在源图上改、然后重新渲染。我们一次到位的做法是:
input_fidelity = 0.6——实测既真的改了姿态,又保住了人脸、服装和整体风格。cv2.warpAffine 平移校正即可,无损、无痕迹、不必重新生成。重绘姿态之后,人物轮廓外缘(耳后、下颌线、肩线,还有袖口)会出现一道宽 5~9 像素的浅灰白细边。在纯黑底上放大看非常像瑕疵。
我们前后试了四轮处理方案,全部被否。这不是技术问题,是审美判断:
| 方案 | 做法 | 副作用 |
|---|---|---|
| 沿法线取色替换 | 沿边缘向内 10px 取色覆盖 | 出现硬化白块 |
| 开运算削峰 | 亮度 120 → 65 | 纯白细线仍在 |
| 挖掉后修补 | 去掉亮像素再修补 | 边缘发糊,人工感明显 |
| 只降亮度 | 不碰形状,亮度削 27%(α=0.88) | 形状零变化、边缘像素相似度 0.9999,技术无瑕——仍被判“看起来不正常” |
最后一行的结论值得记住:反光恰恰是人物立体感的来源,把它压掉,整个人就假了。所以最终定版是——
源图原样使用,不做任何处理。
更普适的元教训:症状看着像缺陷时,先确认它是不是特征。这类问题必须问出图的人,不要按“像素异常 → 该修”的直觉自己动手。每一次重渲的代价是 85~140 分钟。
裁或补成 9:16(1080×1920)。补边要用纵向均匀色带——取边缘若干行列的均值,做横向高斯平滑,再加约 45 像素的过渡融合。不要用“拉伸行复制”,那会留下明显条纹。
用 edge-tts 合成,拼成“静音 + 语音 + 尾静音”,严格铺满输出时长。一个实操坑:edge-tts 命令行不支持调节语速,必须走 Python 接口:edge_tts.Communicate(text, voice, rate="-12%", pitch="-2Hz")。沉稳男声可以用 zh-CN-YunjianNeural。
走 ComfyUI + Wan2.1 图生视频 + MultiTalk 的口型驱动。几条实测经验:
如果你的集群里跑了不止一个数字人,请注意:不同实例的素材文件名可能一字不差,而它们对应的形象、音色、部署位置完全不同。我们曾因此整轮改错对象。
可靠的判别方式是认音色,不认文件名:去目标机读它的语音配置,看用的是哪个音色标识,而不是看素材叫什么。动手前的 30 秒核查,能省掉一整轮白工。
这套东西做完,素材就从“看起来能动”变成“能长期挂着不出事”。数字人这个行业真正的门槛其实不在模型,而在这些每一条都能被一句话验证、但事前完全想不到的细节上。
看看我们的方言词库产品 →不是。正确形态是两段无声、可无缝循环的视频素材,声音由主程序另外配。口播素材是循环素材,语音讲多久就循环多久,所以不需要和某句话等长,也不需要带音轨。判断标准很硬:检测不到音频流才算合格。
因为切换时要对得上帧。前端切回待机是等口播播到时长一半才切,切回后待机从 0 帧起播;两段素材首尾帧不重合,切回去就会跳帧。往返折返让尾帧与首帧的运动量差约等于一帧,切换才看不出来。
因为那是一次性动作。待机循环 8 秒、口播循环 10 秒,用户会一直看着它,任何拍照瞬间的摆拍动作都会立刻违和。允许的姿态只有静态站姿加双手自然垂放体侧。而且举手通常是源图自带的,渲染只会沿用、剪帧剪不掉,只能在源图上改并重新渲染。
先量再动。原图常靠单侧举手形成视觉平衡,手臂放下来之后人物会整体偏移,我们实测偏了 +48 像素。纯黑底素材上用平移校正即可,无损无痕,不需要重新生成。同时记得把提示词的正负向一起翻过来,否则模型会按旧方向把手臂又抬起来。
默认不处理。那通常是灯光反光(轮廓光),本身就是立体感的来源。我们试过四种消除方案,其中一种形状零变化、边缘像素相似度 0.9999,技术上无瑕,成品仍被判“看起来不正常”。结论是源图原样使用。这类问题属于审美判断不是技术缺陷,必须问出图的人。