上一篇我们分析了 InfiniteTalk 对 WanModel 的改造。
核心结论是:InfiniteTalk 并不是在视频生成完成后再做嘴部替换,而是在视频扩散模型内部加入了音频条件控制。
具体来说,它在 WanModel 中增加了:
AudioProjModel
audio_cross_attn
ref_target_masks
human_num
音频窗口对齐逻辑
其中真正让音频参与 Transformer 计算的模块,就是:
SingleStreamMutiAttention
它定义在:
wan/modules/attention.py
这一篇我们重点分析这个模块。
这篇要回答的问题是:
音频 embedding 进入 WanModel 后,究竟是如何影响视频 token 的?
更通俗一点说:
语音是怎么控制嘴型、表情、头部和身体动作的?
答案不是"音频直接改嘴巴像素",而是:
视频 token 作为 Query
音频 token 作为 Key / Value
通过 cross attention 让视频 token 从音频条件中读取信息
再结合人物区域 attention map,把不同音频条件尽量绑定到不同人物区域
这就是 Audio Cross Attention 的核心。
一、先理解 Cross Attention:谁看谁?
在 Transformer 里,attention 最常见的形式是 self-attention。
Self-attention 的意思是:一组 token 内部互相看。
比如视频 token 做 self-attention,就是视频里不同空间位置、不同时间帧之间互相建立关系。
而 cross-attention 的意思是:一组 token 去看另一组 token。
在 InfiniteTalk 的音频场景里,大致可以理解成:
视频 token:当前正在生成的视频 latent 表示
音频 token:由 Wav2Vec2 + AudioProjModel 得到的语音条件
Audio Cross Attention 做的事情就是:
视频 token 去查询音频 token,
判断当前视频位置应该从当前语音里读取什么信息。
所以它不是让音频单独生成动作,而是让视频生成过程在每一层都可以参考音频。
这很关键。
因为最终生成的仍然是视频 latent,音频只是条件。
二、音频控制的对象不是像素,而是视频 latent token
很多人理解"语音控制嘴型"时,会想象模型直接根据音频去修改嘴巴区域像素。
但在 InfiniteTalk 里不是这样。
它的实际路径是:
语音文件
↓
Wav2Vec2
↓
audio embedding
↓
AudioProjModel
↓
audio context tokens
↓
Audio Cross Attention
↓
影响视频 latent token
↓
VAE decode
↓
最终视频帧
也就是说,音频真正作用的是 Transformer 中的视频 latent token。
这些 token 不一定只对应嘴巴,它们可能对应:
嘴部区域
脸部区域
头部区域
上半身区域
背景区域
不同时间帧
不同人物区域
如果音频条件通过 attention 影响到嘴部 token,就会体现为嘴型变化。
如果影响到面部 token,就可能体现为表情变化。
如果影响到头部或身体 token,就可能体现为点头、转头、姿态变化。
这就是 InfiniteTalk 比传统嘴部后处理更有表达力的地方。
三、SingleStreamAttention:基础音频 cross attention
在看 SingleStreamMutiAttention 之前,要先看它的父类:
class SingleStreamAttention(nn.Module):
...
这个类实现的是基础 cross attention。
它的核心输入有两个:
x
encoder_hidden_states
可以这样理解:
x:视频 token
encoder_hidden_states:外部条件 token,在这里主要是音频 token
在 forward 里,它先从视频 token 生成 Query:
x → q_linear → q
再从音频条件生成 Key 和 Value:
encoder_hidden_states → kv_linear → encoder_k / encoder_v
然后执行 attention:
Attention(q, encoder_k, encoder_v)
最后再通过线性层投影回视频 token 维度。
这就是最标准的 cross attention 思路:
Query 来自视频 token
Key / Value 来自音频 token
输出仍然是视频 token 的更新量
所以 Audio Cross Attention 的本质是:
让视频 token 根据自身状态,从音频条件中取出当前需要的信息。
四、为什么 Query 来自视频,而 Key / Value 来自音频?
这个设计非常重要。
如果 Query 来自视频,说明是"视频 token 主动去问音频":
我这个视频位置,现在应该根据音频怎么变化?
Key / Value 来自音频,说明音频提供可被查询的信息:
当前音节是什么?
当前是否在说话?
声音节奏如何?
是否有停顿?
应该对应怎样的发音状态?
最终 attention 会决定:
某个视频 token 应该读取哪些音频 token 的信息。
如果一个视频 token 对应嘴部附近,它可能会强烈关注与当前时间相关的音频 token。
如果一个 token 对应背景,它理想情况下应该较少受到音频影响。
这也是为什么后面还要引入 x_ref_attn_map 和人物区域信息。
单纯 cross attention 可以让视频读音频,但还需要额外机制来告诉模型:
哪些视频 token 是人?
哪个人对应哪路音频?
哪些区域是背景?
五、单人场景:直接使用基础 cross attention
SingleStreamMutiAttention.forward() 一开始有一个判断:
if human_num == 1:
return super().forward(x, encoder_hidden_states, shape)
这说明单人场景比较简单。
如果画面中只有一个说话人,或者只有一路音频条件,那么没有必要区分 person1、person2。
此时直接走父类 SingleStreamAttention 即可:
视频 token → Query
单人音频 token → Key / Value
cross attention → 更新视频 token
这种情况下,模型默认整个人物都由同一路音频驱动。
当然,这并不意味着音频一定会影响整个画面。
具体影响范围还取决于模型训练、参考图像、mask、latent 表示和采样参数。
但从代码路径上看,单人场景不需要额外的人物绑定逻辑。
六、多人场景:为什么不能直接共用基础 attention?
多人场景就复杂了。
假设画面里有两个人:
person1 在左边
person2 在右边
同时输入两路音频:
person1_audio
person2_audio
如果只是把两路音频 token 混在一起,然后让所有视频 token 自由 cross attention,就可能出现问题:
person1 的嘴跟着 person2 的声音动
person2 的表情跟着 person1 的声音变
两个人同时乱动
背景区域也被音频影响
所以多人场景必须解决一个绑定问题:
哪一路音频应该控制哪一个人物区域?
SingleStreamMutiAttention 的主要价值就在这里。
它不是简单地多输入几路音频,而是结合 x_ref_attn_map 给不同人物和背景分配不同的位置编码区间,让 query 和 key 在 attention 中更容易区分人物归属。
七、x_ref_attn_map:从 self-attention 中拿到人物区域线索
在上一篇里我们提到,WanSelfAttention 会返回:
x_ref_attn_map
然后传给:
self.audio_cross_attn(
self.norm_x(x),
encoder_hidden_states=audio_embedding,
shape=grid_sizes[0],
x_ref_attn_map=x_ref_attn_map,
human_num=human_num
)
x_ref_attn_map 可以理解成当前视频 token 与参考人物区域之间的关联图。
它不是普通图像 mask,而是从 attention 计算中得到的区域相关性信息。
在多人场景中,x_ref_attn_map 通常包含不同人物的参考关联。
比如:
x_ref_attn_map[0]:person1 相关区域的注意力线索
x_ref_attn_map[1]:person2 相关区域的注意力线索
这个 map 会被 SingleStreamMutiAttention 用来判断:
某个视频 token 更像属于 person1?
更像属于 person2?
还是更像背景?
这一步是音频绑定人物区域的基础。
八、class_range、class_interval:给人物分配位置区间
SingleStreamMutiAttention.__init__() 里有几个特殊参数:
class_range = 24
class_interval = 4
然后初始化:
self.rope_h1 = (0, self.class_interval)
self.rope_h2 = (self.class_range - self.class_interval, self.class_range)
self.rope_bak = int(self.class_range // 2)
self.rope_1d = RotaryPositionalEmbedding1D(self.head_dim)
这段代码很有意思。
它给不同区域分配了不同的位置编码范围:
person1:靠近 0 到 class_interval 的区间
person2:靠近 class_range - class_interval 到 class_range 的区间
background:class_range 中间位置
如果默认值是:
class_range = 24
class_interval = 4
那么可以理解成:
person1 区间:0 ~ 4
background 位置:12
person2 区间:20 ~ 24
这不是普通空间坐标,而是一种用于 RoPE 的"类别位置"。
它的目标是让 person1、person2 和背景在 attention 中具有不同的位置编码特征。
换句话说,模型不是只靠 mask 硬切区域,而是通过位置编码增强不同人物区域的可区分性。
九、normalize_and_scale:把 attention map 映射到人物区间
在多人 forward 中,源码会先计算 x_ref_attn_map 的最大值和最小值。
然后分别对两个人物区域做归一化和缩放:
human1 = normalize_and_scale(
x_ref_attn_map[0],
human1_min_max,
rope_h1_range
)
human2 = normalize_and_scale(
x_ref_attn_map[1],
human2_min_max,
rope_h2_range
)
这一步的意义是:
把 person1 的 attention 强弱映射到 person1 的位置编码区间;
把 person2 的 attention 强弱映射到 person2 的位置编码区间。
然后背景区域会被设置为固定中间值:
back = rope_bak
最终通过:
max_indices = x_ref_attn_map.argmax(dim=0)
判断每个 token 更偏向哪个区域。
如果某个 token 对 person1 的关联更强,就使用 person1 的编码位置。
如果对 person2 更强,就使用 person2 的编码位置。
如果都不明显,就更接近背景位置。
最终得到:
normalized_pos
这个 normalized_pos 后面会用于视频 Query 的 RoPE 位置编码。
十、为什么要给 Query 加 RoPE?
源码里对 q 做了这样的处理:
q = rearrange(...)
q = self.rope_1d(q, normalized_pos)
q = rearrange(...)
也就是说,视频 token 生成的 Query 会根据 normalized_pos 加上一维旋转位置编码。
这里的 normalized_pos 不是普通时间位置,而是带有人物区域意义的位置。
所以它表达的是:
这个视频 token 更像 person1 区域?
更像 person2 区域?
还是背景区域?
给 Query 加上这种位置编码后,后续它和音频 Key 做 attention 时,就会受到人物区域信息的影响。
这有点像给视频 token 加上身份标签。
不是显式写:
这是 person1
这是 person2
这是 background
而是通过 RoPE 位置编码,让不同区域在注意力空间中自然区分开。
十一、音频 Key 也要加 RoPE:让音频带上人物归属
不仅 Query 要加 RoPE,音频 Key 也会加。
源码会先构造:
per_frame
然后把前半段音频 token 分配到 person1 的位置区间中间,把后半段音频 token 分配到 person2 的位置区间中间:
前半段音频 token → person1 编码位置
后半段音频 token → person2 编码位置
接着构造:
encoder_pos = torch.concat([per_frame] * N_t, dim=0)
再对 encoder_k 加 RoPE:
encoder_k = self.rope_1d(encoder_k, encoder_pos)
这一步非常关键。
它相当于让音频 Key 也带上人物归属信息。
于是 attention 中就出现了这种关系:
person1 区域的视频 Query
更容易匹配 person1 音频 Key
person2 区域的视频 Query
更容易匹配 person2 音频 Key
背景 Query
不会强烈匹配某个人物音频 Key
这就是多人音频绑定的核心机制之一。
十二、为什么音频 token 前半段对应 person1,后半段对应 person2?
在前面的音频处理流程中,双人音频会分别生成 1.pt 和 2.pt。
进入 Pipeline 和 WanModel 后,音频条件会按人物组织并拼接。
到 SingleStreamMutiAttention 时,encoder_hidden_states 里已经包含了多个人物的音频 context。
源码中用:
per_frame[:per_frame.size(0)//2]
per_frame[per_frame.size(0)//2:]
把音频 token 分成前半和后半。
可以理解成:
前半:person1 的 audio context
后半:person2 的 audio context
所以它给前半 token 加 person1 的位置编码,给后半 token 加 person2 的位置编码。
这和前面的 cond_audio['person1']、cond_audio['person2'] 是对应的。
这也说明一个重要点:
多人音频条件的顺序不能乱。
如果 person1 和 person2 的音频路径写反,后续 attention 绑定也会错。
十三、真正的 attention 计算:memory_efficient_attention
经过 Query、Key、Value 准备后,源码最终调用:
xformers.ops.memory_efficient_attention(q, encoder_k, encoder_v)
这里执行的就是实际 cross attention。
输入是:
q:视频 token 的 Query,已经带有人物区域 RoPE
encoder_k:音频 token 的 Key,已经带有人物归属 RoPE
encoder_v:音频 token 的 Value
输出是音频条件对视频 token 的更新量。
可以概括为:
视频 token 根据自身区域和当前状态,
从对应人物的音频 token 中读取信息,
得到一个音频驱动的特征更新。
然后经过:
proj
proj_drop
rearrange 回原形状
最后返回给 WanAttentionBlock。
在 block 里,它会被加回原视频 token:
x = x + x_a
这就是音频条件真正作用到视频 token 的地方。
十四、从数据流角度看 Audio Cross Attention
现在把整个 SingleStreamMutiAttention 的多人流程串起来:
输入 x:视频 token
输入 encoder_hidden_states:音频 context token
输入 x_ref_attn_map:人物区域 attention map
输入 human_num:人物数量
↓
如果 human_num == 1:直接走基础 SingleStreamAttention
↓
如果 human_num > 1:
↓
把视频 token 按时间帧重排
↓
视频 token 生成 q
↓
根据 x_ref_attn_map 判断 token 更属于 person1 / person2 / background
↓
把人物归属映射成 normalized_pos
↓
给 q 加人物区域 RoPE
↓
音频 token 生成 encoder_k / encoder_v
↓
给 person1 音频 Key 和 person2 音频 Key 加不同 RoPE
↓
执行 memory_efficient_attention(q, encoder_k, encoder_v)
↓
得到音频驱动的视频 token 更新量
↓
返回给 WanAttentionBlock
↓
x = x + x_a
这条链路就是 Audio Cross Attention 的核心。
十五、它如何影响嘴型?
嘴型变化本质上是视频 token 的局部变化。
如果某些 token 对应嘴部区域,那么在扩散采样过程中,这些 token 会通过 audio cross attention 读取音频特征。
音频特征包含:
发音节奏
音素变化
开口闭口信息
停顿信息
语速变化
视频 token 读取这些信息后,就会在生成过程中向相应的嘴部形态演化。
比如:
发元音时,嘴部 token 可能更倾向于张开;
发闭口音时,嘴部 token 可能更倾向于闭合;
静音时,嘴部 token 可能更倾向于保持稳定;
连续语音时,嘴部 token 会持续变化。
当然,源码里不会显式写"发 a 嘴巴张开"。这是模型训练后学到的映射。
源码提供的是机制:
让嘴部相关视频 token 能够在每层 Transformer 中读取音频条件。
十六、它如何影响表情?
表情不是独立于语音存在的。
人在说话时,面部肌肉会随着语气、节奏、发音和情绪发生变化。
虽然 Wav2Vec2 主要是语音表征模型,不是情绪识别模型,但音频特征中仍然会包含很多和说话状态有关的信息:
音量变化
语速
停顿
重音
音调走势
发音强弱
当脸部 token 通过 audio cross attention 读取这些信息时,就有机会生成更自然的面部变化。
例如:
语速快时,表情变化可能更频繁;
语气强时,脸部动作幅度可能更大;
停顿时,表情可能趋于稳定;
轻声说话时,面部动作可能更收敛。
这也是 InfiniteTalk 不只是改嘴巴的原因。
音频条件注入的是视频 token,而不是单独嘴部 mask,因此它有机会影响整个面部区域。
十七、它如何影响头部和身体动作?
头部和身体动作比嘴型更复杂。
嘴型和语音的关系比较直接,而身体动作和语音的关系更偏统计相关。
比如:
说话时轻微点头
强调某句话时身体前倾
停顿时动作变缓
语速较快时头部小幅变化更多
这些不是每个音素都一一对应,但在真实视频数据中,它们和语音节奏存在一定关联。
由于 Audio Cross Attention 作用在视频 latent token 上,而不是只作用在嘴部区域,所以头部、肩膀、上半身 token 也可能读取音频条件。
这让模型有机会生成:
轻微点头
面部朝向变化
肩颈微动
身体姿态随说话节奏变化
这也是 InfiniteTalk 强调不只是 lip-sync 的关键。
它的结构允许音频影响更大范围的人物动态。
十八、为什么不会让背景也乱动?
理论上,如果音频条件可以影响所有 token,背景也可能跟着动。
所以 InfiniteTalk 需要额外机制抑制这种问题。
这里有几个约束来源:
参考视频或图片条件
VAE latent 条件
CLIP 视觉条件
ref_target_masks
x_ref_attn_map
背景位置编码 rope_bak
模型训练中的数据分布
在 SingleStreamMutiAttention 中,背景 token 会被分配到中间位置:
rope_bak = class_range // 2
而 person1 和 person2 会被分配到两端区间。
这让背景 token 在人物音频匹配上不那么突出。
不过,这不代表背景绝对不会变化。
生成式视频模型本身就可能引入背景漂移,尤其在长视频、低步数、低显存或强音频引导下更明显。
所以源码机制只是降低风险,不是绝对保证。
如果背景抖动严重,还需要结合:
更准确的 mask
更合理的 audio guide scale
更稳定的参考视频
更多采样步数
更合适的分块策略
十九、为什么多人场景需要 RoPE,而不是简单 mask?
简单 mask 的做法可能是:
person1 区域只看 person1 音频
person2 区域只看 person2 音频
背景区域不看音频
这听起来直接,但在生成式 Transformer 里未必最优。
因为视频 token 不是固定像素。
它们经过 VAE、patch embedding、Transformer 层层变换后,语义和空间位置会更复杂。
而且人物区域边界也不是绝对清晰的。
比如:
嘴部和脸颊相连
脸部和头发相连
身体和背景接触
两个人物可能有遮挡
如果用硬 mask,可能导致边界不自然。
RoPE 编码则是一种更柔性的方式。
它不是强制切断 attention,而是通过位置编码让不同人物和背景在 attention 空间中更容易区分。
这种方法更适合生成式模型。
可以理解为:
mask / attention map 提供区域线索;
RoPE 把区域线索编码进 attention;
attention 自己学习如何使用这些线索。
二十、Audio Cross Attention 和 audio guide scale 的关系
在 Pipeline 采样阶段,InfiniteTalk 还区分了:
text_guide_scale
audio_guide_scale
audio_cross_attn 是模型结构层面的音频注入方式。
audio_guide_scale 是采样策略层面的音频引导强度。
两者关系可以这样理解:
audio_cross_attn:模型有没有能力读取音频
audio_guide_scale:采样时多大程度强调音频条件
如果没有 audio cross attention,audio guide scale 再大也没有意义。
如果有 audio cross attention,但 audio guide scale 过低,嘴型同步可能不够明显。
如果 audio guide scale 过高,可能导致动作过度、画面不稳定或人物区域变化太强。
所以调参时要把两者区分开。
结构层面由 audio_cross_attn 提供能力,采样层面由 audio_guide_scale 控制强度。
二十一、SingleStreamMutiAttention 的设计亮点
总结一下,SingleStreamMutiAttention 有几个值得注意的设计点。
1. 复用基础 cross attention
单人场景直接走 SingleStreamAttention,逻辑简洁。
不需要为了单人任务引入额外复杂度。
2. 多人场景加入人物区域编码
多人场景通过 x_ref_attn_map、normalize_and_scale 和 RoPE,把视频 token 的人物归属编码进 Query。
3. 音频 Key 也带人物归属
音频 token 的 Key 也会被加上对应人物的 RoPE,使 person1 视频 Query 更容易匹配 person1 音频 Key。
4. 使用 memory efficient attention
源码使用 xFormers 的 memory-efficient attention,说明它考虑了视频 token 序列长、显存压力大的问题。
5. 输出仍然是视频 token 更新量
Audio attention 不直接输出图像,而是输出对视频 latent token 的修正。
这符合扩散 Transformer 的整体设计。
二十二、这套机制有什么局限?
虽然这个设计很巧妙,但也有一些局限。
1. 依赖 mask 和 attention map 质量
如果 x_ref_attn_map 或人物区域 mask 不准确,音频绑定就可能出错。
比如 person1 和 person2 的区域混在一起,模型就可能让两个人都跟着同一路音频动。
2. 当前逻辑更明显针对双人
源码中对前半音频 token 和后半音频 token 的处理,更像是针对双人场景设计。
如果要扩展到三人、四人,就需要重新设计音频 token 分组和人物位置编码。
3. 背景稳定不是绝对保证
背景 token 有单独位置编码,但生成式模型仍然可能出现背景漂移。
尤其是长视频生成时,背景稳定还要依赖分块策略、参考帧控制、采样参数和后处理。
4. 音频不等于情绪理解
Wav2Vec2 embedding 包含语音特征,但不等于完整情绪语义理解。
如果想让表情更准确表达情绪,可能还需要额外的情绪特征、文本语义或专门训练。
5. 强音频引导可能带来画面副作用
如果过度强调音频条件,模型可能为了嘴型同步牺牲画面稳定性。
这需要在 audio_guide_scale、采样步数、LoRA、APG 等参数之间平衡。
二十三、二次开发时可以怎么改?
如果你要基于 InfiniteTalk 做二次开发,Audio Cross Attention 是一个高级改造点。
1. 改进多人绑定
可以尝试把当前双人逻辑扩展成更通用的多人逻辑:
person1 → 区间 1
person2 → 区间 2
person3 → 区间 3
person4 → 区间 4
不过这需要重新设计 class_range 和音频 token 分组。
2. 加入更明确的人脸 / 嘴部 mask
当前区域线索主要来自 ref_target_masks 和 attention map。
如果想强化嘴型同步,可以额外引入嘴部区域 mask,让嘴部 token 更强地读取音频。
但这可能会降低整体自然度,因为人说话不只是嘴巴动。
3. 加入情绪或语速特征
可以在 audio context 中加入额外条件:
音高 pitch
能量 energy
语速 speed
情绪 embedding
停顿标记
这些特征可以辅助表情和身体动作控制。
4. 设计 gating 控制音频影响范围
可以给 audio attention 增加门控:
嘴部区域:强音频影响
面部区域:中等音频影响
身体区域:弱音频影响
背景区域:极弱音频影响
这样可能有助于减少背景抖动。
5. 做 attention 可视化
如果要调试多人错位问题,可以尝试可视化:
x_ref_attn_map
normalized_pos
person1 / person2 token 分布
audio attention 权重
这能帮助判断音频到底作用到了哪里。
二十四、常见问题排查
如果你运行 InfiniteTalk 时遇到音频控制问题,可以从 Audio Cross Attention 角度排查。
1. 单人嘴型不动
可能原因:
audio embedding 没传入
AudioProjModel 输出异常
audio_cross_attn 权重没加载
audio guide scale 太低
采样步数太少
2. 双人嘴型错位
可能原因:
person1 / person2 音频顺序反了
mask 区域反了
x_ref_attn_map 质量差
输入视频中人物区域太接近
audio_type 选择错误
3. 两个人同时动
可能原因:
两路音频时间上重叠
mask 区域重叠
attention map 无法区分人物
audio guide scale 太高
4. 背景抖动
可能原因:
背景 token 受到音频 attention 影响
参考视频本身不稳定
分辨率或采样步数设置不合适
mask 覆盖范围过大
5. 表情僵硬但嘴型同步
可能原因:
音频条件主要影响嘴部,面部其他 token 影响不足
参考视频表情太少
audio guide scale 偏低
模型对该语音风格泛化不足
二十五、这一篇的核心结论
Audio Cross Attention 是 InfiniteTalk 实现音频驱动视频生成的核心机制之一。
它的基本逻辑是:
视频 token 生成 Query
音频 context token 生成 Key / Value
视频 token 通过 cross attention 从音频中读取信息
输出音频驱动的视频 token 更新量
在单人场景下,SingleStreamMutiAttention 直接复用基础 SingleStreamAttention。
在多人场景下,它会结合 x_ref_attn_map,判断不同视频 token 更接近 person1、person2 还是背景。
然后通过 normalize_and_scale 和 RotaryPositionalEmbedding1D,把人物区域信息编码进视频 Query。
同时,它也会给不同人物的音频 Key 加上对应位置编码,让 person1 视频区域更容易关注 person1 音频,让 person2 视频区域更容易关注 person2 音频。
最后通过 xFormers 的 memory-efficient attention,得到音频条件对视频 token 的更新。
所以,语音不是直接控制像素,也不是只控制嘴巴。
它是通过 audio cross attention 影响视频 latent token,从而间接影响嘴型、表情、头部和身体动作。
这正是 InfiniteTalk 能够从传统 lip-sync 升级到音频驱动人物视频生成的关键。
下一篇我们会继续分析:
InfiniteTalk 源码解析 #9:长视频生成机制:streaming、motion_frame 与分块续接策略
前面我们已经理解了单个片段如何被音频驱动,下一篇要解决的问题是:如果视频很长,InfiniteTalk 如何让多个片段连续起来,不至于人物身份、动作和背景突然断掉?