Skip to content

[new-model] Add Wan2.2-Animate-14B character animation/replacement inference - #1765

Open
SuhaanCoding wants to merge 10 commits into
hao-ai-lab:mainfrom
SuhaanCoding:wan-animate
Open

[new-model] Add Wan2.2-Animate-14B character animation/replacement inference#1765
SuhaanCoding wants to merge 10 commits into
hao-ai-lab:mainfrom
SuhaanCoding:wan-animate

Conversation

@SuhaanCoding

@SuhaanCoding SuhaanCoding commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Purpose

Add Wan2.2-Animate-14B inference: character animation driven by a reference image plus a driving video (skeleton + face crops), with an animation mode (character on the reference image's background) and a replacement mode (character composited into the driving video's scene).

The implementation follows FastVideo's component, pipeline-stage, preset, registry, and checkpoint-loading abstractions. It loads the official Diffusers-layout checkpoint (Wan-AI/Wan2.2-Animate-14B-Diffusers) directly — no weights conversion or upload is needed — and does not change the routing or behavior of existing pipelines. Driving inputs are the files the official preprocessing produces (wan/modules/animate/preprocess in the Wan2.2 repo); this PR does not re-implement that preprocessing, matching the choice the diffusers integration made.

Follow-up to #1683 (S2V); related community interest in #1126 (distilling this model).

Changes

  • Add WanAnimateTransformer3DModel as a subclass of the existing Wan transformer: the tower is unchanged Wan-I2V; the port adds a pose patch embedding (applied to every latent frame except the reference slot), a LIA-style motion encoder + face encoder, and 8 per-frame face cross-attention blocks (one after every 5th transformer block).
  • Reproduce the StyleGAN2 runtime weight scaling in the motion encoder (checkpoint stores unit-scale weights; the layer divides by sqrt(fan_in) at forward), and pin motion_synthesis_weight to fp32 via the loader's per-parameter dtype hook (an orthonormal basis is re-derived from it every forward).
  • Add five Animate pipeline stages: +1 latent frame for the reference slot, 20-channel conditional-latent assembly (4-channel folded I2V mask + VAE-encoded reference/target frames, mode-branched for animation vs replacement), pose-video VAE encoding, face video as raw pixels, and decode that drops the reference slot.
  • Add arch/pipeline configs transcribed from the official checkpoint manifest, the wan_animate_14b preset (77 frames, 30 fps, 20 steps, guidance 1.0 — the official runtime defaults), and registry resolution.
  • Carry five new request inputs through the API (pose_video_path, face_video_path, background_video_path, mask_video_path, animate_mode), with schema-parity inventory and parser-test updates.
  • Add a repack script for the replace-mode relighting LoRA (ships only in the non-diffusers repo, in PEFT key format the loader cannot read; the script renames keys and writes per-layer alpha values so the strength matches).
  • Add a runnable example and support-matrix entries.
  • Add 36 tests: config transcription and guard asserts, the StyleGAN2 scaling contract, per-frame face-attention confinement, pose reference-slot isolation, adapter wiring order, meta-device buffer survival, plus two weight-gated tests (strict checkpoint↔model tensor mapping in both directions; raw no-autocast bf16 forward).

Test Plan

# Unit + weight-gated tests
export WAN_ANIMATE_MODEL_PATH=/path/to/Wan2.2-Animate-14B-Diffusers
pytest fastvideo/tests/transformers/test_wan_animate.py -v

# End-to-end (driving inputs produced by the official preprocessing)
python examples/inference/basic/basic_wan_animate.py

Numerical parity against diffusers was checked with a standalone two-phase harness (each phase loads one implementation in fp32 from the same checkpoint, identical fixed-seed inputs, forward hooks at every stage); happy to attach the script or fold it into tests/local_tests/ if wanted.

Test Results

All runs on a DGX Spark (GB10) against the official checkpoint.

Test output
fastvideo/tests/transformers/test_wan_animate.py
================= 36 passed, 14 warnings in 172.69s (0:02:52) ==================

fp32 parity vs diffusers (same checkpoint, identical inputs, per-stage probes):
- input assembly (patchify + pose add): bitwise identical
- condition embeddings (timestep/text/CLIP): <= 4.8e-07 max abs
- block 0 self-attention:  max_rel 5.4e-07
- block 0 cross-attention: max_rel 5.2e-07
- after all 40 blocks:     max_rel 8.4e-07
- face adapters:           max_rel <= 8.9e-07
- final output:            max_rel 1.7e-06

End to end: official example clip -> official preprocessing (skeleton video +
face crops) -> 77 frames @ 832x464, 20 steps, guidance 1.0 -> saved video.

source video:
https://github.com/user-attachments/assets/339706fa-0e4c-477a-85fe-c154b565510e

source ref:
src_ref

source face:
https://github.com/user-attachments/assets/d9cfa32d-1133-4206-ba9a-6c983e6eaac1

source pose:
https://github.com/user-attachments/assets/e172b655-d5f3-4219-b02d-c437bfd6c82c

output:
https://github.com/user-attachments/assets/023960c3-f864-49c1-aeb2-da61bf098c91

Note: shares a few files with #1683 (S2V) so whichever merges second needs a small rebase.

@mergify mergify Bot added scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build scope: docs Documentation scope: model Model architecture (DiTs, encoders, VAEs) labels Aug 27, 2026
@mergify

mergify Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI

Protection Waiting on
🔴 PR merge requirements 👀 reviews and 🤖 CI

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=fastcheck-passed
  • check-success=full-suite-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@SuhaanCoding SuhaanCoding changed the title Wan animate [new-model] Add Wan2.2-Animate-14B character animation/replacement Aug 27, 2026
@SuhaanCoding
SuhaanCoding marked this pull request as ready for review August 27, 2026 11:44
@mergify mergify Bot added the type: new-model New model support label Aug 27, 2026
@SuhaanCoding SuhaanCoding changed the title [new-model] Add Wan2.2-Animate-14B character animation/replacement [new-model] Add Wan2.2-Animate-14B character animation/replacement inference Aug 27, 2026
SuhaanCoding pushed a commit to SuhaanCoding/FastVideo that referenced this pull request Aug 27, 2026
Control build for the CI stall seen on hao-ai-lab#1765 (pr-fastcheck hao-ai-lab#931/hao-ai-lab#932): a
different branch from the same author/fork, to separate a wedged runner from
anything branch-specific.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
suhaan-commits and others added 9 commits August 28, 2026 07:49
Batch 1 of the Animate port: WanAnimateArchConfig transcribed verbatim
from Wan-AI/Wan2.2-Animate-14B-Diffusers transformer/config.json (all 26
keys verified field-for-field against the manifest), and a
WanAnimate14BConfig pipeline config subclassing the I2V line -- Animate
is a dense 14B on the Wan2.1-I2V skeleton (in_dim 36 channel-concat,
CLIP ViT-H image branch, Wan2.1 VAE), not the Wan2.2 MoE.

Field names match the checkpoint config keys exactly so
update_model_arch overlays them at load time; __post_init__ asserts the
in_channels = 2*latent_channels + 4 decomposition and the dense
face-adapter indexing rule (adapter i serves block i*5), which the
checkpoint does not record. Model, stages, and registry land in
follow-up batches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests

Batch 2 of the Animate port. The tower needed no new block: Animate is
the stock Wan-I2V transformer (36ch channel-concat input, CLIP image
branch, single timestep, standard RoPE), so WanAnimateTransformer3DModel
subclasses WanTransformer3DModel and adds only the three Animate pieces:
a pose patchifier added to every frame but the reference slot, the LIA
motion encoder + causal face funnel (wan_animate_face.py), and 8 dense
face-adapter cross-attention hatches applied after every 5th block.

Ported against both references (official wan/modules/animate/ and the
merged diffusers WanAnimateTransformer3DModel); tensor names match the
official Diffusers-format checkpoint verbatim, so only the two
patchifiers need mapping entries. The StyleGAN2 runtime weight scaling
(unit-scale stored weights x 1/sqrt(fan_in) at forward) is reproduced
exactly and pinned by tests -- loading those tensors into vanilla layers
succeeds and is silently wrong. Blur kernels are non-persistent buffers
rebuilt by materialize_non_persistent_buffers after meta-device loading.

29 tests: config transcription + guard asserts, the weight-scaling
contract, per-frame face-attention confinement, pose skip-ref isolation,
adapter firing order, meta-device survival, and two weight-gated tests
(strict meta mapping of every checkpoint tensor; raw no-autocast bf16
CUDA forward) gated on WAN_ANIMATE_MODEL_PATH.

Sequence parallelism is explicitly out of scope for v1 (the face
adapter's per-frame reshape needs the whole sequence on one rank);
forward raises on sp_world_size > 1. Registry needs no alias: EntryClass
auto-discovery matches the checkpoint's _class_name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 3 of the Animate port. Five stages transcribed against diffusers'
WanAnimatePipeline and the official runner:

- AnimateLatentPreparationStage: +1 latent frame (the reference slot).
- AnimateConditioningLatentsStage: assembles the 20-channel y =
  [4ch folded I2V mask | 16ch cond latent] x [ref frame | target frames]
  into batch.image_latent; animation mode encodes a black video through
  the VAE (VAE(zeros) != zero latents), replace mode encodes the
  background video and inverts + nearest-downsamples the character mask.
- AnimatePoseVideoEncodingStage: skeleton video -> VAE latents (argmax,
  standard normalisation), T_lat frames (no pose on the ref slot).
- AnimateFaceVideoStage: face crops as raw pixels at motion_encoder_size,
  [-1, 1].
- AnimateDecodingStage: decode latents[:, :, 1:] (drop the ref slot).

DenoisingStage passes pose_latents/face_pixel_values by name (dropped
for every other model); the CFG negative pass keeps pose and blanks the
face to -1, matching diffusers -- though Animate defaults to guidance
1.0, so it rarely fires. The existing image_latent channel-concat path
provides the final [noise 16 | mask 4 | cond 16] = 36ch DiT input.

v1 scope: single 77-frame segment; multi-segment refert chaining is a
follow-up on top of these stages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 4 of the Animate port. Threads the five new inputs
(pose_video_path, face_video_path, background_video_path,
mask_video_path, animate_mode) through every public surface:
SamplingParam (+CLI args), the typed schema's InputConfig, the parser
roundtrip test, and the schema-parity inventory (which also declares the
WanAnimate14BConfig-owned refert_num).

Registers the pipeline: Wan-AI/Wan2.2-Animate-14B-Diffusers resolves via
path + an "animate" detector to WanAnimate14BConfig with the new
wan_animate_14b preset (720p-area, 77 frames @ 30 fps, 20 UniPC steps,
guidance 1.0 -- the official runtime defaults; CFG off because the
prompt is non-core for this model).

animate_mode arriving as None from the API layer normalises to
"animation" in the conditioning stage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 5 of the Animate port. The rank-128 relighting LoRA (replace mode
only) ships PEFT-format in the native repo and is absent from the
Diffusers-format one. Two pieces make it loadable through the existing
LoRAPipeline --lora-path mechanism:

- wan_animate_relight_lora.py repacks it (key renames only, no tensors
  modified): strips PEFT's base_model.model. prefix / .default. infix,
  emits per-layer lora_alpha from adapter_config.json so alpha/rank
  scaling matches PEFT, and can meta-validate that every target layer
  resolves onto a FastVideo model parameter.
- The Animate LoRA name mapping extends the base Wan mapping with the
  I2V image-KV heads (cross_attn.k_img/v_img -> attn2.add_k_proj/
  add_v_proj), which the base mapping never needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 6 of the Animate port: a runnable example (animation mode, the
official runtime defaults, documents the preprocessed-input contract and
where replace mode's bg/mask/LoRA fit) and the two support-matrix rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four parallel reviews (model core / stages / configs+scripts / tests),
findings verified against the diffusers reference and applied.

Bugs fixed:
- motion_encoder.motion_synthesis_weight now pins to fp32 via the
  loader's _get_parameter_dtype hook: diffusers keeps it in
  _keep_in_fp32_modules, and the QR basis is re-derived from it every
  forward, so bf16 rounding changed the basis itself.
- FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN now raises at
  construction: the base __init__ would build VSA blocks whose
  to_gate_compress weights the checkpoint lacks, and the loader's
  ALLOWED_NEW_PARAM_PATTERNS whitelist zero-fills exactly that name.
- The relight-LoRA repack now emits every key (including the per-layer
  lora_alpha entries) in FastVideo naming: the loader strips
  .lora_alpha before mapping and no mapping regex can then match, so
  native-named alphas were silently dropped and the LoRA merged at
  scale 1.0. --validate also initialises a size-1 distributed group,
  without which model construction asserts.
- vae_tiling was ignored by both new encode stages; unknown
  animate_mode values (including diffusers' spelling "animate") were
  silently treated as animation mode and now raise; height/width must
  be multiples of 16 (the reference raises, we silently truncated a
  latent row); _pad_frames IndexError'd on 0/1-frame driving videos.

Vacuous tests fixed: the confinement test's channel-constant
perturbation was exactly cancelled by the affine-free pre-norm
LayerNorm; the adapter-order test used stride 1 (idx%1 and idx//1 are
degenerate); the padding-token test compared zeros to a zero-init
parameter; the orthonormality test asserted a property of LAPACK
without calling the module (now a scale-invariance test). New tests:
motion-layer dtype cast, batched-clip face non-interleaving, the full
LIA channel table on meta, _pad_frames and _fold_i2v_mask.

Dead code removed: refert_num (no reader), 31 lines of CLI args behind
a method with zero callers, an unreachable duplicate guard in the face
attention, a redundant dtype cast, a no-op device hop, never-varied
constructor knobs, a no-op mask_len guard, an unreachable None-filter.
Mapping helpers inlined as dict unions; backends tuple now lives on
the arch config; FSDP shard predicate no longer matches the motion
encoder's res_blocks; int(math.log2) -> bit_length. Comments: invented
metaphors and references to out-of-repo code replaced with plain
statements; inverted assert message fixed; divergences from the
reference (letterbox resize, motion mask) now stated where they occur.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ale)

First run on real weights (DGX Spark, 33/36 passed) settled two things:

- The Animate checkpoint ships no blocks.N.attn2.norm_added_q -- the
  dead parameter FastVideo's WanI2VCrossAttention allocates only because
  base Wan-I2V checkpoints carry it (never used in forward). The strict
  load failed on 40 missing keys; the model now replaces it with
  nn.Identity at construction so both load directions are clean.

- The tiny-model init helper used a flat std=0.02, which double-shrinks
  the StyleGAN2-style motion-encoder layers (they apply 1/sqrt(fan_in)
  at runtime and expect unit-scale weights, as the real checkpoint has):
  the face signal attenuated over its ~15-layer path until it was
  bitwise invisible at the output, failing
  test_forward_responds_to_pose_and_face. Weights now get unit gain per
  layer, unit scale in the motion encoder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yapf reformatting (two files), one mypy annotation (_pad_frames's
accumulator), and one codespell reword ("refert" in a comment; unlike
S2V's 'padd' nothing needs the literal spelling, so no ignore entry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build hao-ai-lab#975 ran during a runner outage (every PR built 14:30-17:10Z failed all
six lanes). Rebuilding on a healthy runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Documentation scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build scope: model Model architecture (DiTs, encoders, VAEs) type: new-model New model support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants