wan_animate_face ¶
Face-driving modules for Wan2.2-Animate.
These are the parts of the Animate checkpoint that plain Wan-I2V does not have: the LIA-style motion encoder that turns 512x512 face crops into identity-free motion vectors, the causal-conv face encoder that funnels those vectors down to the latent frame rate, and the face-adapter cross-attention blocks bolted onto the 40-block tower (one after every inject_face_latents_blocks-th block).
Ported against BOTH references -- the official Wan-Video/Wan2.2 (wan/modules/animate/{motion_encoder,face_blocks}.py) and diffusers' transformer_wan_animate.py. The official Diffusers-format checkpoint follows the diffusers naming, which this module reproduces exactly so every tensor loads verbatim.
The StyleGAN2 weight-scaling contract. The motion encoder is LIA's appearance/motion network, built from StyleGAN2 EqualConv2d/EqualLinear layers: the checkpoint stores weights unit-scale and the layer multiplies by 1 / sqrt(fan_in) at forward time (plus a fused leaky-ReLU that adds a channel bias and scales by sqrt(2)). Loading these tensors into vanilla nn.Conv2d/nn.Linear forwards succeeds and is silently wrong by a per-layer constant -- the classic "loads cleanly, generates garbage" failure. MotionConv2d/MotionLinear below reproduce the runtime scaling exactly.
Verified checkpoint shapes (Wan-AI/Wan2.2-Animate-14B-Diffusers): motion_encoder.conv_in.weight (32, 3, 1, 1) motion_encoder.res_blocks.0.conv1.weight (32, 32, 3, 3) motion_encoder.conv_out.weight (512, 512, 4, 4) motion_encoder.motion_network.4.weight (20, 512) motion_encoder.motion_synthesis_weight (512, 20) face_encoder.conv1_local.weight (4096, 512, 3) face_encoder.padding_tokens (1, 1, 1, 5120) face_adapter.0.to_q.weight (5120, 5120) face_adapter.0.norm_q.weight (128,) # per-head RMSNorm
Classes¶
fastvideo.models.dits.wan_animate_face.FusedLeakyReLU ¶
FusedLeakyReLU(bias_channels: int)
Bases: Module
StyleGAN2's activation: add a channel bias, leaky-ReLU(0.2), scale by sqrt(2).
The sqrt(2) keeps activation variance roughly constant through the leaky ReLU; the bias lives here (act_fn.bias in the checkpoint) rather than on the conv because StyleGAN2 fuses bias-add into the activation kernel.
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.MotionConv2d ¶
MotionConv2d(in_channels: int, out_channels: int, kernel_size: int, stride: int = 1, padding: int = 0, bias: bool = True, blur_kernel: tuple[int, ...] | None = None, use_activation: bool = True)
Bases: Module
StyleGAN2 EqualConv2d: unit-scale stored weight, 1/sqrt(fan_in) at forward.
blur_kernel implements the anti-aliasing FIR filter StyleGAN2 applies before strided (downsampling) convs. It is derived from a python tuple and registered non-persistent -- absent from the checkpoint, so it must be rebuilt after meta-device construction (see the model's materialize_non_persistent_buffers).
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.MotionEncoderResBlock ¶
MotionEncoderResBlock(in_channels: int, out_channels: int, kernel_size: int = 3, kernel_size_skip: int = 1, blur_kernel: tuple[int, ...] = (1, 3, 3, 1), downsample_factor: int = 2)
Bases: Module
One 2x-downsampling step of the LIA appearance encoder.
Main path: 3x3 conv + 3x3 strided conv; skip path: 1x1 strided conv (no bias); both blurred before the stride. The sum is divided by sqrt(2) to keep variance flat -- the same convention as the fused activation.
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.MotionLinear ¶
Bases: Module
StyleGAN2 EqualLinear: unit-scale stored weight, 1/sqrt(fan_in) at forward.
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.WanAnimateFaceCrossAttention ¶
Bases: Module
Face cross-attention: each latent frame's video tokens query that frame's 5 face tokens.
The per-frame confinement (reshape [B, T*S_f] -> [(B*T), S_f]) is the entire expression-sync mechanism -- frame i can only see frame i's face. Applied residually outside this module, after every inject_face_latents_blocks-th transformer block. The caller guarantees one face-token group per latent frame (checked loudly in the model).
qk-norm here is per-head RMSNorm over head_dim (checkpoint: norm_q.weight of shape (128,)) -- NOT the across-heads norm the main tower uses. Pre-norms are affine-free and contribute no tensors. The reference's optional multiplicative motion mask is not ported: the Animate pipeline never supplies one.
Source code in fastvideo/models/dits/wan_animate_face.py
Methods:¶
fastvideo.models.dits.wan_animate_face.WanAnimateFaceCrossAttention.forward ¶
hidden_states [B, S, C]; motion_vec [B, T, N, C]. Returns the residual (not added).
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.WanAnimateFaceEncoder ¶
WanAnimateFaceEncoder(in_dim: int, out_dim: int, hidden_dim: int = 1024, num_heads: int = 4, kernel_size: int = 3, eps: float = 1e-06, pad_mode: str = 'replicate')
Bases: Module
Per-frame motion vectors -> per-latent-frame face tokens.
The two stride-2 causal convs downsample time 4x -- exactly the VAE's temporal compression, so one token group lines up with one latent frame. Emits num_heads content tokens + 1 learned padding token per frame.
Convs are plain nn.Conv1d with the causal left-pad applied inline: the checkpoint keys are face_encoder.conv2.weight with no wrapper sublevel, and a test pins that naming.
Source code in fastvideo/models/dits/wan_animate_face.py
fastvideo.models.dits.wan_animate_face.WanAnimateMotionEncoder ¶
WanAnimateMotionEncoder(size: int = 512, style_dim: int = 512, motion_dim: int = 20, out_dim: int = 512, motion_blocks: int = 5, channels: dict[str, int] | None = None)
Bases: Module
Face crop -> identity-free 512-d motion vector (LIA linear motion decomposition).
A sizexsize RGB crop is squeezed to a style_dim appearance vector, bottlenecked to motion_dim (=20) coefficients -- too narrow to carry identity, wide enough to carry expression/pose -- and re-expanded as a linear combination of motion_dim learned direction vectors that are QR-orthonormalised at forward time (in fp32; upstream and diffusers both upcast, because the orthogonalisation is precision-sensitive).