InfiniteTalk 源码解析 #7:WanModel 改造:在视频扩散模型中加入音频条件控制 前一篇我们分析了InfiniteTalkPipeline的初始化过程。在那里T5、CLIP、VAE、Wav2Vec2、WanModel 被串成了一条完整的视频生成链路prompt → T5 → 文本条件 cond_video → CLIP → 图像条件 cond_video → VAE → latent 条件 audio → Wav2Vec2 → audio embedding WanModel → 扩散生成视频 latent VAE decode → 最终视频帧这一篇我们继续往模型内部走重点看wan/modules/multitalk_model.py这里定义了 InfiniteTalk 使用的WanModel。这篇要回答一个核心问题InfiniteTalk 是如何把一个原本偏 image-to-video 的视频扩散模型改造成可以被音频驱动的说话视频生成模型的简单说它主要做了三件事第一在 WanModel 中增加音频相关参数和 AudioProjModel 第二在每个 WanAttentionBlock 中增加 audio_cross_attn 第三在 forward 中把 Wav2Vec2 audio embedding 投影、对齐并注入到每一层 Transformer block。这就是 InfiniteTalk 从普通视频生成走向音频驱动视频生成的关键。一、先理解原始 WanModel 的基本角色在 InfiniteTalk 里WanModel是扩散生成的主干模型。它不是负责读音频的也不是负责把视频保存成 MP4 的。它真正负责的是在 latent 空间中根据多种条件一步步预测视频内容。这些条件包括文本条件T5 编码后的 context 图像条件CLIP 编码后的 clip_fea 参考帧条件VAE 编码后的 y 音频条件Wav2Vec2 编码并投影后的 audio embedding 人物区域条件ref_target_masks 时间步条件timestep embedding如果用一句话概括WanModel 是 InfiniteTalk 中融合文本、图像、视频 latent、音频和 mask 的扩散 Transformer 主模型。传统的 image-to-video 模型通常只需要文本、图像和参考 latent。但 InfiniteTalk 要让人物跟着声音说话所以必须让模型在生成每一帧时“听见”音频。这个“听见”的过程就是本文要分析的音频条件控制。二、WanModel 初始化新增 audio params在WanModel.__init__()中可以看到除了普通视频模型参数外还多了一组音频相关参数。大致包括audio_window5 intermediate_dim512 output_dim768 context_tokens32 vae_scale4 norm_output_audioTrue这些参数说明音频不是后面临时拼进去的而是在模型结构初始化时就被考虑进来了。它们分别可以这样理解audio_window每个时间位置取多宽的音频窗口 intermediate_dim音频投影中间层维度 output_dim音频条件输出维度 context_tokens每个时间位置生成多少个音频上下文 token vae_scaleVAE 时间下采样比例 norm_output_audio是否对音频投影输出做归一化其中最重要的是audio_window context_tokens output_dim vae_scale因为它们决定了原始 Wav2Vec2 embedding 如何被转换成 Transformer block 可以使用的 audio context。也就是说音频条件不是一个简单的一维向量而是会被整理成一组可被注意力机制读取的 token。三、为什么不能直接把 Wav2Vec2 embedding 塞进 WanModel第 5 篇我们讲过Wav2Vec2 输出的 audio embedding 可以理解为[时间位置, Wav2Vec2层级, 特征维度]但视频扩散模型内部的 Transformer block 期待的条件通常是另一种形式[batch, frame, context_tokens, dim]也就是说Wav2Vec2 的输出和 WanModel 内部需要的条件格式并不完全一致。两者之间需要一个适配层。这个适配层就是AudioProjModel它的作用可以概括为把 Wav2Vec2 多层语音特征转换成视频扩散模型可使用的音频上下文 token。这一步非常关键。如果没有音频投影层视频模型很难直接理解 Wav2Vec2 hidden states。所以 InfiniteTalk 并不是简单地把音频 embedding 拼到输入里而是专门设计了一个音频投影模块。四、AudioProjModel音频特征投影器源码中有一个类class AudioProjModel(ModelMixin, ConfigMixin): ...它的初始化参数包括seq_len5 seq_len_vf12 blocks12 channels768 intermediate_dim512 output_dim768 context_tokens32 norm_output_audioFalse从这些参数可以看出它假设输入音频特征来自多个 Wav2Vec2 block每个 block 有一定 channel 维度。它不是直接拿一个时间点的特征而是拿一个时间窗口的特征。这就是seq_len的意义。对于一个视频帧来说它不应该只看当前瞬间的音频。真实说话动作有连续性嘴巴的开合也会受到前后音素影响。比如一个人发出 “o” 或 “m” 这样的声音嘴型变化不是在一个点突然发生而是有一个过渡过程。所以模型需要看一个局部音频窗口而不是单个采样点。AudioProjModel的主要职责就是把局部窗口内的多层 Wav2Vec2 特征压平 ↓ 经过多层 Linear GELU ↓ 投影成 context_tokens 个音频条件 token ↓ 输出给 audio cross attention 使用它在结构上相当于一个音频适配器。五、AudioProjModel 的 forward从窗口特征到 context tokensAudioProjModel.forward()里有两个输入audio_embeds audio_embeds_vf可以简单理解为audio_embeds首帧相关音频窗口 audio_embeds_vf后续视频帧相关音频窗口为什么要区分这两类因为视频 latent 的时间维度经过 VAE 下采样后和原始视频帧不是一一对应的。在WanModel.forward()里源码会对音频 embedding 做一系列 reshape把音频按 VAE 时间尺度重新组织。这就是vae_scale参与音频对齐的原因。在AudioProjModel.forward()中音频特征会先被拉平窗口长度 × Wav2Vec2层数 × 特征维度然后通过proj1 / proj1_vf proj2 proj3 norm最终得到[batch, frame, context_tokens, output_dim]也就是说原始音频特征最后会被转换成每个时间位置的一组音频 context tokens。这些 token 后面会被audio_cross_attn读取。六、WanModel 初始化中如何挂上 AudioProjModel在WanModel.__init__()里可以看到音频适配器被初始化self.audio_proj AudioProjModel( seq_lenaudio_window, seq_len_vfaudio_window vae_scale - 1, intermediate_dimintermediate_dim, output_dimoutput_dim, context_tokenscontext_tokens, norm_output_audionorm_output_audio, )这里有一个细节seq_len_vf audio_window vae_scale - 1这说明后续帧使用的音频窗口长度和 VAE 时间下采样比例有关。也就是说音频和视频 latent 的对齐不是简单的“一帧对应一个音频点”。它要考虑视频通过 VAE 后的时间压缩。可以这样理解原始视频帧时间轴 ↓ VAE 时间下采样 ↓ latent 时间轴 ↓ 每个 latent 时间位置需要覆盖多个原始帧附近的音频信息这也是为什么音频窗口要和vae_scale绑定。InfiniteTalk 要解决的不是“当前帧听当前声音”这么简单而是要让音频条件在 latent 时间尺度上仍然保持合理对齐。七、WanAttentionBlock在每个 block 中加入 audio_cross_attn接下来进入最关键的结构改造。在WanAttentionBlock.__init__()中除了原本的 self attention、text/image cross attention、FFN 之外新增了self.audio_cross_attn SingleStreamMutiAttention( dimdim, encoder_hidden_states_dimoutput_dim, num_headsnum_heads, qk_normFalse, qkv_biasTrue, epseps, norm_layerWanRMSNorm, class_rangeclass_range, class_intervalclass_interval )这就是 InfiniteTalk 的音频注入点。普通视频 Transformer block 大致结构是Self-Attention ↓ Text/Image Cross-Attention ↓ Feed Forward Network而 InfiniteTalk 改造成Self-Attention ↓ Text/Image Cross-Attention ↓ Audio Cross-Attention ↓ Feed Forward Network这说明音频条件不是只在模型开头注入一次而是在每一层 Transformer block 中持续参与计算。这种设计比简单拼接更强。因为每一层 block 都可以根据当前 latent token 状态重新从音频 context 中取信息。换句话说音频可以在扩散生成的深层特征变换过程中持续影响视频。八、WanAttentionBlock.forward音频注入发生在哪里看WanAttentionBlock.forward()的主流程可以简化成1. 对 x 做 self-attention建模视频 token 内部关系 2. 对 x 做 text/image cross-attention注入文本和图像条件 3. 对 x 做 audio cross-attention注入音频条件 4. 对 x 做 FFN完成特征变换对应逻辑大致是y, x_ref_attn_map self.self_attn(...) x x y * e[2] x x self.cross_attn(self.norm3(x), context, context_lens) x_a self.audio_cross_attn( self.norm_x(x), encoder_hidden_statesaudio_embedding, shapegrid_sizes[0], x_ref_attn_mapx_ref_attn_map, human_numhuman_num ) x x x_a y self.ffn(...) x x y * e[5]这段逻辑非常重要。它说明音频注入的位置是在文本/图像 cross attention 之后、FFN 之前。也就是说模型先完成视频 token 自身交互再融合文本和图像条件然后再用音频条件调整当前 token 表示。为什么这样设计合理因为音频要控制的是已经具备一定视觉语义和人物结构的 token。如果音频太早注入模型可能还不知道哪些 token 是人物、哪些 token 是背景。而在 self-attention 和 image/text condition 之后token 已经包含更丰富的视觉上下文此时再注入音频有助于让声音作用到更合适的区域。九、self_attn 为什么返回 x_ref_attn_map在WanSelfAttention.forward()中除了返回 self-attention 的输出还返回x_ref_attn_map这个 attention map 来自get_attn_map_with_target(...)它会结合q k grid_sizes ref_target_masks得到和目标区域相关的注意力信息。这一步和音频控制强相关。因为在多人说话或局部人物驱动中模型不应该把音频作用到整个画面。比如一个画面里有两个人person1 在左边 person2 在右边 背景在后面当 person1 的音频出现时模型应该主要让 person1 的嘴型和表情变化而不是让 person2 或背景乱动。所以需要一种机制告诉 audio attention当前音频应该关注哪些 token 哪些区域是目标人物 哪些区域应该少受音频影响x_ref_attn_map就是在这个方向上服务的。它把 self-attention 中与目标区域相关的信息传给后面的audio_cross_attn。十、ref_target_masks人物区域如何参与音频控制在WanModel.forward()中传入了ref_target_masks然后源码会把 mask 转成 token 级别ref_target_masks ↓ interpolate 到 latent 空间大小 ↓ 转成 bool mask ↓ flatten 成 token mask ↓ 传给每个 block为什么要这样做因为输入的 mask 通常是在图像空间或视频帧空间而 Transformer block 处理的是 latent token。两者空间尺寸不同。所以必须把像素级或帧级 mask 映射到 token 级 mask。这一步让模型知道哪些 token 对应人物区域 哪些 token 对应非人物区域对于音频驱动来说mask 的意义非常大。没有 mask 时音频可能影响整个画面导致背景或非说话人物出现不必要变化。有 mask 后模型更容易把音频变化限制到目标人物区域。这也是 InfiniteTalk 支持多人或区域绑定的基础之一。十一、audio_cross_attn 和 human_numaudio_cross_attn调用时传入了human_numhuman_num而human_num来自human_num len(audio_embedding)这说明模型会根据音频 embedding 的数量判断当前有几个人物音频。单人场景下human_num 1双人场景下human_num 2这与前面cond_audio[person1]、cond_audio[person2]的设计对应。也就是说InfiniteTalk 在模型内部并不是只知道“有一段声音”而是知道“有几路人物音频条件”。这样后续SingleStreamMutiAttention就有机会根据人物数量、mask 和 attention map把不同音频条件绑定到不同人物区域。这也是它区别于简单音频驱动单人模型的重要点。十二、WanModel.forward音频条件如何进入主模型现在我们看WanModel.forward()中音频部分的主流程。函数签名里有def forward( self, x, t, context, seq_len, clip_feaNone, yNone, audioNone, ref_target_masksNone, ):这里的audio就是 Pipeline 传进来的 audio embedding。进入 forward 后源码先把它移动到和视频 token 相同的设备和 dtypeaudio_cond audio.to(devicex.device, dtypex.dtype)然后进行时间维度拆分first_frame_audio_emb_s audio_cond[:, :1, ...] latter_frame_audio_emb audio_cond[:, 1:, ...]可以理解为首帧音频条件 后续帧音频条件之后后续帧音频会按照vae_scale重新分组latter_frame_audio_emb ↓ 按 n_t 和 vae_scale 重新排列 ↓ 提取前段窗口、中间窗口、后段窗口 ↓ 拼接成 latter_frame_audio_emb_s这一步本质上是在做把原始音频时间序列整理成和 VAE latent 时间轴对应的局部音频窗口。然后调用audio_embedding self.audio_proj( first_frame_audio_emb_s, latter_frame_audio_emb_s )至此Wav2Vec2 的原始音频 embedding 被转换成适合 Transformer cross-attention 使用的 audio context tokens最后再处理多人音频根据人数量拆分 ↓ concat 到 audio context token 维度 ↓ 传入每个 block这就是音频条件进入 WanModel 主干的全过程。十三、为什么要区分首帧和后续帧源码中对首帧和后续帧分别处理这一点很值得注意。在 image-to-video 或 video-to-video 任务里首帧往往承担身份和初始状态约束。对于 InfiniteTalk 来说首帧不仅是视觉参考也对应视频开头的音频状态。后续帧则要考虑 VAE 时间下采样后的连续运动。所以首帧音频和后续帧音频采用不同窗口组织方式是为了更好地匹配视频 latent 的时间结构。可以这样理解首帧更关注起始状态和初始音频上下文 后续帧更关注连续时间段内的音频变化这类处理细节看起来复杂但正是长视频和音画同步的关键。如果简单地把音频 embedding 线性对齐到帧可能在短视频里还可以但在经过 VAE 下采样、patch embedding 和长视频分块之后就很容易出现时间错位。十四、音频不是控制像素而是控制 latent token很多人理解“音频驱动嘴型”时会想象模型直接根据声音去修改嘴巴像素。但在 InfiniteTalk 里实际发生的是音频条件影响 latent token latent token 经过扩散采样更新 最终由 VAE 解码成视频帧也就是说音频作用的对象不是像素而是 Transformer 里的视频 latent token。这样做有几个好处第一可以让音频影响更高层的视频语义 第二可以同时影响嘴型、表情、头部姿态和身体动作 第三可以和文本、图像、mask 等条件统一融合 第四比后处理嘴部区域更适合生成式视频模型。这也是为什么 InfiniteTalk 不只是对口型。它的音频条件在生成过程中参与了 latent 表示的更新而不是最后贴一个嘴巴动画。十五、音频注入和文本注入有什么区别在 block 中文本/图像条件通过self.cross_attn(...)注入。音频条件通过self.audio_cross_attn(...)注入。两者都是 cross attention 思路但控制目标不同。文本/图像 cross attention 主要回答画面应该符合什么描述 参考图像里的人物和视觉语义是什么音频 cross attention 主要回答当前时间应该如何说话 嘴型和语音节奏如何对应 谁在当前时间段说话 人物动作应该如何跟随音频变化文本条件更偏全局语义。音频条件更偏时间动态。这就是为什么 InfiniteTalk 不能只靠 prompt。Prompt 可以写“a person is talking”但 prompt 不知道每一帧应该发哪个音也不知道什么时候闭嘴、什么时候张嘴、什么时候停顿。这些必须来自音频。十六、音频条件为什么要在每个 block 注入如果只在模型输入处拼一次音频信息可能会在深层网络中被稀释。InfiniteTalk 在每个WanAttentionBlock中都加入audio_cross_attn意味着每一层都可以重新读取音频条件。这有几个好处低层可以学习局部动作和嘴型变化 中层可以学习表情和头部运动 高层可以学习更整体的说话状态 不同扩散步中可以持续利用音频条件纠偏。当然具体每层学到什么不是源码直接写死的而是模型训练后的结果。但从结构设计上看每层注入音频条件显然比单点注入更有表达能力。十七、audio_window 的意义局部时间上下文audio_window5说明每个时间位置不是只看一个音频点而是看一个局部窗口。这很符合说话视频生成的需求。因为嘴型变化具有连续性。比如发一个音节时嘴巴准备张开 ↓ 张到最大 ↓ 逐渐闭合这个过程可能跨越多个视频帧。如果模型只看当前帧的音频特征就容易出现动作抖动或延迟。使用局部窗口后模型可以看到前后音频变化更容易生成连续自然的嘴部动作。这也是 audio embedding 从“逐点特征”变成“窗口上下文”的原因。十八、context_tokens 的意义把音频变成可注意力读取的 tokenAudioProjModel最终会输出多个context_tokens。这说明每个时间位置的音频条件不是一个向量而是一组 token。为什么要一组 token因为语音信息很复杂。一个时间窗口里可能包含音素变化 能量变化 发音边界 停顿信息 上下文信息 多人角色信息如果压成一个向量表达能力可能不足。变成多个 context token 后audio cross attention 可以在这些 token 中选择性读取信息。这和文本 token 类似。文本 prompt 不是一个向量而是一串 token音频条件也被转换成一组 token方便 cross attention 使用。十九、vae_scale 的意义音频和 latent 时间轴对齐vae_scale是理解音频对齐的一个关键参数。视频不是直接以原始帧进入 WanModel而是经过 VAE 编码和 patch embedding。时间维度会被压缩。因此音频如果仍然按照原始帧逐点对齐就可能和 latent token 时间轴不一致。源码中通过vae_scale对后续帧音频进行分组和窗口重组本质上就是解决这个问题。可以这样理解原始音频时间轴 ↓ 按 25fps 对齐到视频帧 ↓ 根据 VAE 时间下采样重组 ↓ 得到 latent 时间位置对应的音频窗口 ↓ 通过 AudioProjModel 生成 audio context这一步保证音频条件在模型内部的时间尺度是合理的。二十、token_ref_target_masks从像素区域到 token 区域前面说到ref_target_masks会被转换成 token mask。源码逻辑大致是ref_target_masks.unsqueeze(0) ↓ interpolate 到 (N_h, N_w) ↓ 转成 bool ↓ flatten ↓ 传给 block其中N_h和N_w来自H // patch_size[1] W // patch_size[2]这说明 mask 被映射到了视频 token 的空间尺度。为什么要这样因为 audio attention 最终作用在 token 上不是像素上。如果 mask 仍然停留在原始图像尺寸就无法直接用于 Transformer token。所以必须做一次空间下采样和展平。这一步对多人说话尤其重要person1 的 mask → 约束 person1 对应 token person2 的 mask → 约束 person2 对应 token 背景区域 → 尽量避免被音频驱动影响二十一、从 forward 数据流看整个改造现在把 WanModel forward 中和音频相关的数据流串起来audioPipeline 传入的 Wav2Vec2 audio embedding ↓ audio.to(devicex.device, dtypex.dtype) ↓ 拆分 first_frame_audio_emb_s 和 latter_frame_audio_emb ↓ 根据 vae_scale 重组后续帧音频窗口 ↓ 拼接成 latter_frame_audio_emb_s ↓ AudioProjModel 投影 ↓ 得到 audio_embedding context tokens ↓ 根据 human_num 处理多人音频 ↓ 传入每个 WanAttentionBlock ↓ audio_cross_attn 根据 x、audio_embedding、mask、attention map 注入音频条件 ↓ 更新视频 latent token这条链路就是 InfiniteTalk 的音频条件控制主线。二十二、为什么这种设计比后处理 lip-sync 更强传统 lip-sync 方案通常是先有原视频 ↓ 检测脸部和嘴巴区域 ↓ 根据音频生成嘴部区域 ↓ 把嘴部贴回原视频InfiniteTalk 的路线是输入参考视频 / 图片 ↓ 输入音频 embedding ↓ 在扩散生成过程中注入音频条件 ↓ 生成整段人物视频两者最大区别是lip-sync音频作用在后处理嘴部区域 InfiniteTalk音频作用在生成过程中的视频 latent token所以 InfiniteTalk 有机会让音频影响嘴巴 脸颊 下巴 眼神 表情 头部 身体姿态 多人角色 局部区域当然这不意味着效果一定完美但从结构上看它的表达能力确实比单纯嘴部编辑更强。二十三、二次开发时哪些地方值得关注如果你要基于 InfiniteTalk 做二次开发multitalk_model.py里有几个重点位置值得关注。1. AudioProjModel这是音频 embedding 到 audio context tokens 的适配层。可以研究的问题包括是否可以修改 context_tokens 是否可以改变 audio_window 是否可以替换更强的音频投影网络 是否可以加入情绪特征、语速特征、音高特征不过这类改动通常需要重新训练或微调否则推理效果可能不可控。2. audio_cross_attn这是音频真正进入 Transformer block 的地方。可以研究的问题包括音频注入是否每层都需要 是否只在中后层注入 不同层注入音频对效果有什么影响 是否可以加入 gating 控制音频强度如果要做高级优化这里是核心。3. ref_target_masks多人说话、局部控制、背景稳定都和 mask 有关。如果你想让人物区域控制更准就需要关注mask 如何生成 mask 如何下采样 mask 是否覆盖嘴部、脸部、身体 多人 mask 是否互相重叠更好的 mask 可能直接改善多人对话效果。4. human_num 和多人音频绑定双人或多人场景下音频条件和人物区域的绑定非常重要。可以继续研究person1 音频如何对应 person1 mask person2 音频如何对应 person2 mask 多人同时说话时 attention 如何分配这也是做多人数字人或多人对话视频时必须关注的点。5. audio_window 和 vae_scale如果你发现嘴型有延迟或动作不连续可以关注这两个参数。它们直接影响音频窗口和 latent 时间轴的对齐。不过这类参数不能随便改因为它们和模型训练时的设置有关。二十四、常见问题排查如果你在改造或运行 InfiniteTalk 时遇到音频控制问题可以从下面几个方向排查。1. 嘴型完全不动可能原因audio embedding 没有正确传入 audio_proj 权重没有加载 audio_cross_attn 权重没有加载 cond_audio 路径错误 传入的是全零 audio2. 嘴型动了但和声音不同步可能原因音频长度和视频帧数不匹配 Wav2Vec2 embedding 时间维度异常 vae_scale 对齐出错 长视频分块时 audio clip 切分不准确 最终 video_audio 和 cond_audio 来源不一致3. 双人说话时角色错位可能原因person1 / person2 音频路径写反 mask 区域对应关系错误 human_num 和 audio embedding 数量不一致 audio_type 选择不合适 双人音频没有时间对齐4. 背景也跟着音频抖动可能原因ref_target_masks 不准确 mask 没有覆盖正确人物区域 audio attention 影响范围过大 参考视频本身运动太强5. 表情动作太夸张可能原因audio guide scale 过高 音频响度异常 LoRA 或量化影响稳定性 采样步数太少二十五、这一篇的核心结论InfiniteTalk 对 WanModel 的改造可以概括成一句话在原有 image-to-video 扩散 Transformer 中增加音频投影模块和 audio cross attention让音频条件在每一层生成过程中持续影响视频 latent token。具体来说WanModel新增了音频相关参数比如audio_window、context_tokens、output_dim、vae_scale。AudioProjModel负责把 Wav2Vec2 的多层音频 embedding 转换成视频模型可读取的 audio context tokens。WanAttentionBlock中新增了audio_cross_attn SingleStreamMutiAttention(...)。在 block forward 中模型先做 self-attention再做文本/图像 cross-attention然后通过 audio cross-attention 注入音频条件最后进入 FFN。ref_target_masks会被转换成 token 级 mask用于辅助音频条件绑定到目标人物区域。human_num则让模型知道当前是单人音频还是多人音频。所以InfiniteTalk 的音频控制不是后处理不是简单拼接也不是只改嘴巴区域。它是在视频扩散模型内部通过 audio projection 和 audio cross attention让语音参与 latent token 的生成过程。这就是它能够从“AI 对口型”升级为“音频驱动人物视频生成”的关键。下一篇我们会继续深入InfiniteTalk 源码解析 #8Audio Cross Attention语音如何控制嘴型、表情和身体动作那一篇会重点看SingleStreamMutiAttention分析 audio attention 内部到底如何把音频 token 和视频 token 联系起来。