AI 生成短片里,最容易被观众一眼看穿的往往不是画质,而是"人开口了,嘴没跟上"。背景可以糊,手可以藏,但只要有对白、有人物特写,嘴型和声音对不对得上,基本决定了这条片子是"AI 做的"还是"能看的"。本文把 AI 视频的口型对齐拆成音频侧和画面侧两条线,讲清端到端生成与两阶段后处理的取舍,并给出一套能接进短片流水线的工程做法。技术点对照公开方法(Wav2Lip、SadTalker、EMO/Hallo/EchoMimic 这类音频驱动肖像生成,以及 SyncNet 同步度量),不堆名词。
为什么口型是最难糊弄的破绽
文本生成视频(T2V)和图生视频(I2V)的条件只有提示词和参考图,画面里没有"声音"这个输入。模型生成的嘴部动作是为了"看起来在说话"而采样的,和后来配上去的音频没有因果联系——两张嘴各自随机,撞上的概率极低。
偏偏人对嘴部运动的敏感度极高。研究里常说人类识别面部身份、情绪、语音都高度依赖嘴部和下巴的形变,几帧的错位就能被察觉。所以口型问题不能靠"生成得更真"解决,只能靠把音频作为条件喂进去,或者事后按音频重绘嘴部。
两条路线 端到端音频驱动 vs 两阶段后处理
路线 A:端到端音频驱动。 把音频特征作为条件,直接生成整段说话视频。Wav2Lip 走的是"给一段音频 + 一段视频,重建嘴部区域";SadTalker 用 3DMM 系数(表情/姿态基)把音频映射成头姿与口型;更新的 EMO、Hallo、EchoMimic 这类扩散方法,则把音频当作 condition 联合生成整帧肖像视频。
优点是口型准、表情丰富;代价是整帧被重新生成——你已经定稿的人物长相、背景、打光、镜头运动全部会漂。短片里角色一致性本来就得靠首尾帧、参考图硬锁,再来一次整帧重生成,等于把前面锁好的东西又打散。
路线 B:两阶段后处理。 先用 T2V/I2V 把镜头画面定稿,再单独对嘴部区域做口型重绘,用 mask 把新嘴融合回原画面。
这条路是短片产线的更优解:画面是已经审过的、角色一致的,动刀范围被限制在嘴和下巴附近,背景和其余五官一个像素都不变。代价是口型精度不如端到端,但只要音频对齐做好,肉眼足够过关。
本文后续都按路线 B 展开——它更符合"先定画面、后修细节"的剪辑逻辑。
音频侧的统一表示 mel 频谱
几乎所有口型模型吃的都不是原始波形,而是 mel 频谱:16kHz 采样、按短时傅里叶变换切成帧、按 mel 刻度压缩频带。它把"声音"变成一张二维图(时间 × 频段),方便和视频帧对齐。
关键在于时间分辨率要能对上。口型模型通常以 25fps 左右输出,而音频帧的 hop 更细(例如 10ms 一帧 = 100fps),需要把音频帧按视频帧率重采样、并按滑动窗口取上下文。
import numpy as np, librosa
def mel_features(wav, sr=16000, n_mels=80, hop=160, win=800):
# hop=160 @16k => 10ms 一帧; win=800 => 50ms 窗
m = librosa.feature.melspectrogram(
y=wav, sr=sr, n_fft=win, hop_length=hop,
win_length=win, n_mels=n_mels, fmin=55, fmax=7600)
return librosa.power_to_db(m, ref=np.max) # (n_mels, T_audio)
def align_to_video(mel, fps=25, ctx=5):
"""把音频 mel 按视频帧率取上下文窗口,供逐帧口型生成。"""
T_audio = mel.shape[1]
step = 100 / fps # 每视频帧跨多少音频帧(100fps 基准)
frames = []
for i in range(int(T_audio / step)):
c = int(i * step)
lo, hi = max(0, c - ctx), min(T_audio, c + ctx + 1)
win = mel[:, lo:hi]
win = np.pad(win, ((0, 0), (0, 2 * ctx + 1 - win.shape[1])), mode="edge")
frames.append(win)
return np.stack(frames, axis=1) # (n_mels, T_video, 2ctx+1)
两个容易翻车的点:采样率必须统一——TTS 输出常常是 22.05k 或 24k,混进 16k 的模型会整体变速、口型全飘;上下文窗口要够——只看当前帧,模型不知道这一口是开是合,取前后各若干帧才稳。
只改嘴不动脸 口型重绘的 mask 融合
重绘完嘴部区域,绝不能整帧覆盖回去。正确做法是构造一张下半脸 soft mask,只在嘴周围做强融合,边缘羽化过渡。
import cv2, numpy as np
def mouth_blend(frame, redrawn, landmarks, feather=15):
"""landmarks: dlib/mediapipe 68 点或 468 点,取嘴+下巴区域做 mask。"""
h, w = frame.shape[:2]
m = np.zeros((h, w), np.uint8)
pts = np.array(landmarks, np.int32) # 嘴部 + 下巴轮廓点
cv2.fillPoly(m, [pts], 255)
m = cv2.GaussianBlur(m, (feather | 1, feather | 1), 0).astype(np.float32) / 255.0
m = m[..., None]
return (redrawn * m + frame * (1 - m)).astype(np.uint8)
mask 的范围就是精度与自然度的旋钮:只框嘴唇会留下唇线与皮肤接缝;扩到下巴能让形变更自然,但重绘区一大,模型就要重新"想"脸颊光影,容易再次引入不一致。实践中取"嘴 + 下巴上缘"、羽化 10~20 像素是常见档位。
另外,重绘区应该始终从原始定稿帧出发,而不是从上一帧重绘结果出发——否则误差会逐帧累积,几十帧后嘴就飘出脸了。
音画同步怎么量 怎么校
做完口型,得有个客观判据,别只靠肉眼看。业界常用 SyncNet 系列指标:
- LSE-D(Lip Sync Error - Distance):音画特征距离,越小越同步;
- LSE-C(Confidence):相关性置信度,越大越可信。
更实用的是先量音画偏移 offset:配音轨往往比画面早或晚若干毫秒,这个偏移不修掉,后面做多少口型都是错的。可以在一段已知开口的片段上,用互相关扫一遍偏移量。
import numpy as np
def estimate_offset(audio_env, mouth_open, fps=25, max_ms=300):
"""audio_env: 音频包络; mouth_open: 逐帧嘴部开合度(0~1)。
在 ±max_ms 内找相关性最大的偏移(帧), 正值=画面滞后。"""
a = (audio_env - audio_env.mean()) / (audio_env.std() + 1e-6)
m = (mouth_open - mouth_open.mean()) / (mouth_open.std() + 1e-6)
max_shift = int(max_ms / 1000 * fps)
best, best_lag = -2, 0
for lag in range(-max_shift, max_shift + 1):
if lag >= 0:
a2, m2 = a[lag:], m[:len(m) - lag]
else:
a2, m2 = a[:len(a) + lag], m[-lag:]
n = min(len(a2), len(m2))
if n < 10:
continue
c = float(np.corrcoef(a2[:n], m2[:n])[0, 1])
if c > best:
best, best_lag = c, lag
return best_lag, best
拿到 offset 后,是把音频往前挪、还是把画面往后挪,取决于主时钟是谁——短片流水线里应当以配音为绝对主时钟,画面对齐到声音,而不是反过来。
接进短片产线 顺序比算法更重要
真正决定成败的往往不是用了哪个模型,而是流水线的先后顺序。正确顺序是:
- 剧本定稿 → 分镜拆解;
- 先出配音(TTS 或真人),得到最终音频与逐句时间码;
- 再按分镜生成画面(T2V/I2V),镜头时长按音频段落裁剪;
- 对含对白的镜头做口型对齐(路线 B 的 mask 重绘);
- 回到剪辑线,做转场、音效、字幕。
反面做法是先出画面再配声音,然后指望口型模型"圆回来"——音频和画面本来就不是一回事,后面每一步都在还债。把音频提到最前面当主时钟,口型对齐就只是"对着音轨修嘴"这一步,可控得多。
批量跑时还要留回退位:某个镜头口型失败(脸被遮挡、侧脸过深、mask 抓到头发),应当直接跳过、保留原画面,绝不让它污染成片。宁可一张嘴不对,也不要整段花掉。
小结与坑清单
口型对齐的本质,是把声音变成条件:要么在生成时喂进去(端到端),要么在生成后按声音修嘴(两阶段)。短片产线优先选后者,因为它保护了已经定稿的画面一致性。
落地前把这几条对齐:
- 采样率统一到 16k,TTS 输出先重采样,别让模型整体变速;
- 音频当主时钟,先配音再生成画面,最后修口型;
- mask 只框嘴 + 下巴,羽化过渡,重绘区永远从原始帧出发防累积;
- 上线前先量音画 offset,再用 LSE-D/LSE-C 兜底;
- 失败镜头直接跳过回退,不污染成片。
把这些顺序固定成流水线的一环,口型就不再是每条片子都要手动救火的地方,而是像字幕一样,写完脚本顺手就过了。