darask0 commited on
Commit
e545366
·
verified ·
1 Parent(s): 67a4969

Anima In-Context Character LoRA v1 + ComfyUI nodes + workflow

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ samples/example_output.png filter=lfs diff=lfs merge=lfs -text
37
+ samples/example_output2.png filter=lfs diff=lfs merge=lfs -text
38
+ samples/example_reference.jpg filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: circlestone-labs-non-commercial
4
+ license_link: https://huggingface.co/circlestone-labs/Anima
5
+ base_model: circlestone-labs/Anima
6
+ tags:
7
+ - anima
8
+ - lora
9
+ - in-context
10
+ - character-reference
11
+ - ip-adapter-alternative
12
+ - comfyui
13
+ - anime
14
+ pipeline_tag: text-to-image
15
+ library_name: diffusers
16
+ ---
17
+
18
+ # Anima In-Context Character
19
+
20
+ **Reference-driven character generation for [Anima](https://huggingface.co/circlestone-labs/Anima)** — attach a few images of a character and generate that character in new poses, scenes and expressions. No per-character training. Works on characters the base model has never seen.
21
+
22
+ This is a LoRA + a small ComfyUI node pack. Unlike CLIP-embedding IP-Adapters, the reference enters the model as its **own VAE latent inside self-attention**, so fine details (hair ornaments, clothing patterns, eye color) are preserved in principle rather than summarized into a single embedding.
23
+
24
+ | | |
25
+ |---|---|
26
+ | Base model | Anima 2B (Cosmos-Predict2 DiT + Qwen3-0.6B text encoder, WanVAE) |
27
+ | Type | in-context reference LoRA (DiT, rank 64) + ComfyUI nodes |
28
+ | Trained on | ~994k anime images / ~62k (reference≠target) character pairs |
29
+ | Use | attach 1–3 reference images → generate in any pose/scene |
30
+
31
+ ---
32
+
33
+ ## How it works
34
+
35
+ Anima's DiT is a **video** architecture: latents flow as `(B, C, T, H, W)` and self-attention runs over the flattened `(t h w)` sequence with 3D RoPE (`max_frames=128`, `patch_temporal=1`).
36
+
37
+ This adapter exploits that:
38
+
39
+ 1. The reference image's VAE latent is **concatenated as an extra frame on the T axis** — `[generated frame, reference frame(s)]`. It gets a distinct temporal RoPE coordinate, so it never collides spatially with the generated frame.
40
+ 2. Per-frame timesteps: the reference frame is conditioned at **timestep 0 (a clean image)** while the generated frame follows the sampler. This is the OminiControl-style "clean condition token" recipe.
41
+ 3. The generated frame attends to the reference tokens via **shared self-attention** — the reference's appearance flows in directly, not through a lossy embedding.
42
+ 4. Reference frames are sliced off the output before the sampler sees it.
43
+
44
+ The LoRA teaches the base T2I model to *use* those reference tokens. It was trained on **same-character / different-artist** pairs with character names removed from the captions, so identity must flow through the reference frames (not the text) — the network learns a transferable "copy this character into a new context" skill rather than memorizing specific characters.
45
+
46
+ ## How it was made
47
+
48
+ - **Data**: ~994k tagged anime images → grouped by character → **same character, different artist** pairs (so the character stream carries identity, not style) → filtered by anime-character identity similarity (deepghs *ccip*) to drop costume/wrong-tag noise → references composited onto white via anime segmentation.
49
+ - **Captions**: general tags only ([OppaiOracle](https://huggingface.co/Grio43/OppaiOracle)), with character/artist/copyright tags stripped — so identity can only flow through the reference.
50
+ - **Training**: DiT LoRA (rank 64, α 32), reference frames appended at timestep 0, loss on the generated frame only, 10% reference-dropout for CFG. Multi-view pairs (2 references from distinct artists) so "attach a few images" is trained, not just inference-time.
51
+ - Full pipeline & node source: [github.com/daraskme/anima-duet](https://github.com/daraskme) *(see project repo)*.
52
+
53
+ ---
54
+
55
+ ## ComfyUI usage
56
+
57
+ ### 1. Install the custom nodes
58
+ Clone the node pack into `ComfyUI/custom_nodes/`:
59
+ ```
60
+ comfyui-anima-incontext/ (from this repo's `comfyui-anima-incontext/` folder)
61
+ ```
62
+ Restart ComfyUI. You should see nodes under the **anima/incontext** category.
63
+
64
+ ### 2. Get the models
65
+ | File | Put in |
66
+ |---|---|
67
+ | `anima-incontext-character.safetensors` (this repo) | `ComfyUI/models/loras/` |
68
+ | `anima-base-v1.0.safetensors` | `ComfyUI/models/diffusion_models/` |
69
+ | `qwen_3_06b_base.safetensors` | `ComfyUI/models/text_encoders/` |
70
+ | `qwen_image_vae.safetensors` | `ComfyUI/models/vae/` |
71
+
72
+ (The three base files come from the [Anima](https://huggingface.co/circlestone-labs/Anima) release.)
73
+
74
+ ### 3. Load the workflow
75
+ Drag `workflow_anima_incontext_character.json` onto the ComfyUI canvas. It's wired as:
76
+
77
+ ```
78
+ UNETLoader ─► LoraLoaderModelOnly (this LoRA) ─┐
79
+ LoadImage ×2 ─► AnimaRefEncode ×2 ─► AnimaRefLatentBatch ─► AnimaInContextApply ─► KSampler ─► VAEDecode ─► SaveImage
80
+ ```
81
+
82
+ ### 4. Nodes
83
+ - **Anima Reference Encode** — IMAGE (+ optional MASK) → LATENT. A mask composites the subject on white (recommended). `target_width/height` resizes onto a white canvas so refs match the generation resolution.
84
+ - **Anima Reference Latent Batch** — combine 2+ references (full-body + face works best).
85
+ - **Anima In-Context Reference Apply** — attach references to the model.
86
+ - `strength` 1.0 = neutral, >1 stronger reference pull, 0 = off
87
+ - `cond_only` (default on) — reference masked on the CFG-uncond half; matches training
88
+ - `fit_mode` `pad` (aspect-preserving, default) / `stretch` / `crop`
89
+ - `start_percent`/`end_percent` — sampling window
90
+
91
+ ## Tips for best results
92
+
93
+ - **Attach a full-body shot + a face close-up.** Two references (batched) noticeably improve hair-length and face fidelity over a single one.
94
+ - **Also describe the appearance in the prompt** (hair color, outfit, ears, etc.). Reference + matching tags is the strongest combination — the prompt describes the *pose/scene*, the reference carries the *identity*.
95
+ - Composite the subject on a **white background** (use the mask input) — reduces background bleed.
96
+ - If identity drifts, raise `strength` to 1.2–1.5 or add a third reference.
97
+ - Recommended base sampler: `er_sde` / `simple`, 30 steps, CFG 4, `discrete_flow_shift` 3.0.
98
+
99
+ ## Limitations
100
+
101
+ - Fine ornament/pattern detail can drift; multi-reference + appearance tags mitigate it.
102
+ - Strong reference pull can slightly wash out backgrounds — trade off with `strength` and the sampling window.
103
+ - Anime domain (the training data is anime illustration).
104
+
105
+ ## License
106
+
107
+ Base model **Anima** is under the **CircleStone Labs Non-Commercial License** (derives from Cosmos-Predict2 → NVIDIA Open Model License also applies). **This LoRA is a derivative and is released for non-commercial use.** Generated images may be usable commercially per the base license, but **verify the current Anima LICENSE before any commercial use or redistribution.** Training data is derived from public booru sources.
108
+
109
+ ---
110
+
111
+ ## 日本語
112
+
113
+ **Anima 向けの参照画像キャラ生成 LoRA。** キャラ画像を数枚添付するだけで、そのキャラを別ポーズ・別シーンで生成できます(キャラ毎の学習不要、未知キャラも可)。
114
+
115
+ CLIP 埋め込み型 IP-Adapter と違い、参照画像を**モデル自身の VAE latent のまま self-attention に入れる**ため、髪飾り・服の柄・瞳の色などの細部が原理的に落ちません。参照を DiT の時間軸に「クリーンフレーム(timestep=0)」として連結する OminiControl 系の方式です。
116
+
117
+ **使い方**: `comfyui-anima-incontext` ノードを導入 → このLoRAを `models/loras/` へ → `workflow_anima_incontext_character.json` を読み込み → 参照は**全身1枚+顔アップ1枚**を推奨、プロンプトにも外見タグを併記すると最も安定します。`strength` は 1.0 中立、効きが弱ければ 1.2〜1.5。
118
+
119
+ **ライセンス**: ベースの Anima は CircleStone Labs 非商用ライセンス。本LoRAは派生物として**非商用**での配布です。商用利用・再配布前に最新 LICENSE を確認してください。
anima-incontext-character.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:77eeb2c4263551f4760cc7af46196c6811ff623b7706f97a8f97c31ce5f0fe56
3
+ size 554343064
comfyui-anima-incontext/README.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # comfyui-anima-incontext
2
+
3
+ Anima (circlestone-labs, Cosmos-Predict2 2B DiT) 向けの
4
+ **参照画像条件付け**カスタムノード集。2 つの独立したストリームを持つ:
5
+
6
+ - **キャラストリーム (in-context)** — 参照画像のVAE latentをDiTの
7
+ 時間軸(T軸)に「クリーンフレーム」として連結し、shared
8
+ self-attentionでキャラの見た目を参照させる。CLIP埋め込み型
9
+ IP-Adapterと違い、参照はモデル自身のVAE latentのまま自己注意に
10
+ 入るため、髪飾り・服の柄・瞳などの細部が原理的に落ちない
11
+ (OminiControl / IC-LoRA / UNO 系のアプローチ)。
12
+ - **絵柄ストリーム (decoupled cross-attn)** — SigLIP パッチトークンを
13
+ 各ブロックのテキスト cross-attn に decoupled attention で注入する
14
+ 統計的転写(AnimeAdapter 系)。細部をコピーしないことが目的なので、
15
+ あえて埋め込み注入型。**要学習済みアダプタ**。
16
+
17
+ 2 つは別モジュール(self-attn / cross-attn)をパッチするので併用可能。
18
+ `char` と `style` の強度は独立に制御できる。
19
+
20
+ ## なぜT軸連結か
21
+
22
+ ComfyUI本体の実装(`comfy/ldm/cosmos/predict2.py`, `comfy/ldm/anima/model.py`)
23
+ を読んだ結果:
24
+
25
+ - AnimaのDiTはvideoアーキテクチャで、latentは `(B, C, T, H, W)`、
26
+ 自己注意は `(t h w)` を平坦化した列に **3D RoPE** 付きで計算される。
27
+ Animaのconfigは `max_frames=128`, `patch_temporal=1` なので、
28
+ T=2(生成1 + 参照1)はRoPE的に完全に正当。参照トークンは時間座標が
29
+ ずれるため、生成フレームと空間位置が衝突しない。
30
+ - `timesteps` は `(B, T)` のフレーム毎形式を受け付ける
31
+ (`ndim==1` のときだけ unsqueeze)。よって **参照フレームだけ
32
+ timestep=0(クリーン画像)としてAdaLN変調** できる。
33
+ - `forward` は `WrappersMP.DIFFUSION_MODEL` のWrapperExecutor経由
34
+ なので、ModelPatcherのwrapper登録だけで非侵襲にフックできる。
35
+ - 各Blockの `self_attn.attn_op` は属性(`torch_attention_op`)なので、
36
+ `add_object_patch` で差し替えて参照トークンへの
37
+ attention bias(log(strength))による強度制御ができる。
38
+
39
+ ## ノード
40
+
41
+ ### Anima Reference Encode (in-context)
42
+ IMAGE (+ 任意で MASK) + VAE → LATENT。
43
+ マスクを与えるとキャラを白背景に合成してからエンコードする
44
+ (背景バイアス除去。アニメ被写体では効果が確認されている定石)。
45
+ `target_width` / `target_height` を生成解像度に合わせると、
46
+ アスペクト保持の白パディングでリサイズしてからエンコードする
47
+ (サンプリング時のlatent空間リサンプルを回避できるので推奨)。
48
+ 複数枚バッチ入力可 — 各画像が独立した参照フレームになる。
49
+
50
+ ### Anima Reference Latent Batch
51
+ 解像度の異なる参照LATENT同士を1つの複数フレーム参照に結合する
52
+ (2つ目を1つ目のサイズにfit)。チェーン可能。
53
+
54
+ ### Anima In-Context Reference Apply
55
+ MODEL + 参照LATENT → MODEL。
56
+
57
+ - `strength` — 参照トークンへのattention bias。1.0=中立、0=無効
58
+ - `start_percent` / `end_percent` — 参照を有効にするステップ範囲
59
+ - `cond_only`(既定 ON)— CFGのuncond側では参照キーをマスクする。
60
+ 学習契約(uncond=参照なし分布)と一致するので推奨
61
+ - `fit_mode` — 参照latentが生成解像度と違う場合のfit方法
62
+ (`pad`=アスペクト保持+エッジ複製パディング(既定)/ `stretch` / `crop`)
63
+ - `ref_timestep` — 参照フレームのtimestep(既定0=クリーン。
64
+ noise augmentation学習をした場合のみ変更)
65
+
66
+ ### Anima Style Adapter Loader / Anima Style Encode (SigLIP) / Anima Style Apply
67
+ 絵柄ストリーム。`models/anima_style_adapters/` に学習済みアダプタを置く。
68
+ SigLIP系の CLIP_VISION モデルでエンコードし、`style_weight` で強度制御。
69
+ `cond_only`(既定 ON)。未学習アダプタは gate=0 で完全no-op。
70
+
71
+ ## ワークフロー例
72
+
73
+ ```
74
+ LoadImage ─┐
75
+ ├→ AnimaRefEncode ─┐
76
+ VAELoader ─┘ │
77
+
78
+ UNETLoader → LoraLoader → AnimaInContextApply → (AnimaStyleApply) → KSampler → ...
79
+ (in-context LoRA) ▲
80
+ │ (絵柄も使う場合)
81
+ CLIPVisionLoader → AnimaStyleEncode ┘ + AnimaStyleAdapterLoader
82
+ ```
83
+
84
+ ## 重要: 学習との契約 (training contract)
85
+
86
+ Anima本体は単フレームT2Iとして学習されているため、**このノード単体
87
+ (zero-shot)での参照効果は弱い/不安定**。専用のin-context LoRAを
88
+ 学習して初めて本来の精度が出る。学習コード
89
+ (リポジトリの `training/anima_incontext_train_network.py`)は
90
+ 以下の推論時仕様をバイト単位��再現する:
91
+
92
+ 1. 参照latentは生成フレームの **後ろ** にT軸連結(生成frames→参照frames)
93
+ 2. 参照フレームのtimestepは **0**(生成フレームはサンプラーのt)
94
+ 3. 参照latentは `latent_format` 正規化済み(`process_latent_in` 相当、
95
+ Wan21のper-channel mean/std)
96
+ 4. 損失は生成フレームのみに適用(参照フレームの出力は破棄)
97
+ 5. テキスト条件(LLMAdapter経由)は変更なし
98
+ 6. 学習ペア: 同一キャラ・**異なる**出典の2枚(参照≠ターゲット)。
99
+ 同一画像ペアはコピー機化するため禁止
100
+ 7. CFG dropout: 参照を確率~10%でドロップ(フレーム連結自体を外す)
101
+ → 推論側 `cond_only=True` のuncondがこの分布に対応
102
+
103
+ ## 実機検証結果(2026-07-04, RTX PRO 6000 / anima-base-v1.0)
104
+
105
+ - **strength=0 ⇔ 通常生成: bit単位で完全一致**(strength≤0 は連結自体を
106
+ スキップする実装。連結+全マスクでも数学的には同値だが、bf16 では
107
+ attention kernel差がステップ毎に複利で増幅する — fp32 では1ステップ差
108
+ mean 0.05/255 と確認済み)
109
+ - 全バリアント完走: strength=1.0(cond_only 両方)/ 解像度違い参照
110
+ (512x768→1024x1024, fit=pad) / 複数参照 (RefLatentBatch) / sigma窓
111
+ - **zero-shot(LoRA なし, strength=1.0)の挙動**: 参照キャラを
112
+ ポーズ・衣装込みで**ほぼ完全にコピー**し、背景が崩壊する。
113
+ video プライアが参照を「隣接フレーム」として扱うため。
114
+ 経路が機能している証拠だが、実用には in-context LoRA が必須
115
+ (参照≠ターゲットのペア学習がこのコピー機挙動を
116
+ 「同一キャラ・別ポーズ/シーン」に矯正する)。
117
+ strength を下げても中間にはならず画質が劣化するだけ
118
+
119
+ ## 既知の制約
120
+
121
+ - 参照フレームのK/Vは全ステップ不変だがキャッシュしていない
122
+ (2Bモデルなので実害は小さい。最適化はTODO)
123
+ - `tests/` にモックComfyUIでの契約テストあり
comfyui-anima-incontext/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2
+
3
+ try:
4
+ from .style_nodes import (
5
+ NODE_CLASS_MAPPINGS as STYLE_CLASS_MAPPINGS,
6
+ NODE_DISPLAY_NAME_MAPPINGS as STYLE_DISPLAY_NAME_MAPPINGS,
7
+ )
8
+
9
+ NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **STYLE_CLASS_MAPPINGS}
10
+ NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **STYLE_DISPLAY_NAME_MAPPINGS}
11
+ except ImportError: # style stream needs full ComfyUI (folder_paths)
12
+ pass
13
+
14
+ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
comfyui-anima-incontext/incontext.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Anima In-Context Reference — core logic.
3
+
4
+ Strategy
5
+ --------
6
+ Anima's DiT (Cosmos-Predict2 MiniTrainDIT) is a *video* architecture:
7
+ latents flow through the blocks as (B, T, H, W, D) and self-attention is
8
+ computed over the flattened (t h w) sequence with 3D RoPE
9
+ (max_frames=128, patch_temporal=1).
10
+
11
+ We exploit this: the reference image latent is concatenated as an extra
12
+ *frame* along the T axis. This gives us, for free:
13
+
14
+ * a distinct temporal RoPE coordinate for reference tokens
15
+ (no spatial position collision with the generated frame),
16
+ * per-frame timestep conditioning — MiniTrainDIT accepts
17
+ timesteps of shape (B, T), so the reference frame can be
18
+ conditioned at t=0 (clean image) while the generated frame
19
+ follows the sampler's sigma. This matches the
20
+ OminiControl-style "clean condition token" recipe.
21
+
22
+ The generated frame's self-attention can then attend to reference
23
+ tokens (shared attention / in-context conditioning). Reference frames
24
+ are sliced off the output before returning to the sampler.
25
+
26
+ Strength control is implemented by patching each block's
27
+ `self_attn.attn_op` (a plain attribute, cleanly replaceable via
28
+ ModelPatcher.add_object_patch) with a version that adds a per-sample
29
+ additive bias on reference-token key columns:
30
+
31
+ * log(strength) amplifies/attenuates reference attention,
32
+ * cond_only masks reference keys for the uncond half of the CFG
33
+ batch (equivalent to not concatenating the reference at all for
34
+ the uncond forward — this matches the training contract, where
35
+ the reference is dropped ~10% of the time to form the ref-free
36
+ distribution).
37
+
38
+ NOTE: the base model was finetuned as a T2I model (single frame), so
39
+ zero-shot behaviour without a trained in-context LoRA is expected to be
40
+ weak. This module defines the exact inference-time contract that the
41
+ LoRA training code must replicate:
42
+ - reference frames appended after generated frames on the T axis
43
+ - reference frames receive timestep 0
44
+ - reference latents are latent_format-normalized (process_latent_in)
45
+ - text conditioning unchanged
46
+ """
47
+
48
+ import math
49
+
50
+ import torch
51
+ import torch.nn.functional as F
52
+
53
+ import comfy.patcher_extension
54
+ from comfy.patcher_extension import WrappersMP
55
+
56
+ WRAPPER_KEY = "anima_incontext_ref"
57
+
58
+ NEG_BIAS = -1e9 # finite mask value; softmax subtracts the row max so this is NaN-safe
59
+
60
+
61
+ class RefState:
62
+ """Mutable state shared between the diffusion-model wrapper and the
63
+ patched attention ops. The wrapper fills in per-forward token counts
64
+ and the per-sample reference bias (they depend on resolution and on
65
+ the CFG batch layout), the attention ops read them."""
66
+
67
+ def __init__(self):
68
+ self.active = False
69
+ self.total_tokens = -1
70
+ self.gen_tokens = -1
71
+ # per-sample additive bias on reference key columns, shape (B,).
72
+ # None means "all zero" (neutral -> attn ops fall back).
73
+ self.bias_B = None
74
+ # lazily-built full bias tensor (B, 1, 1, S), cached across the
75
+ # 28 blocks of one forward pass
76
+ self._bias_cache = None
77
+
78
+ def bias_for(self, device, dtype):
79
+ if self._bias_cache is None or self._bias_cache.device != device or self._bias_cache.dtype != dtype:
80
+ b = torch.zeros((self.bias_B.shape[0], 1, 1, self.total_tokens), device=device, dtype=dtype)
81
+ b[:, 0, 0, self.gen_tokens:] = self.bias_B.to(device=device, dtype=dtype).unsqueeze(1)
82
+ self._bias_cache = b
83
+ return self._bias_cache
84
+
85
+
86
+ def _tokens_per_frame(h, w, patch_spatial):
87
+ # pad_to_patch_size pads H and W up to a multiple of patch_spatial
88
+ hp = math.ceil(h / patch_spatial)
89
+ wp = math.ceil(w / patch_spatial)
90
+ return hp * wp
91
+
92
+
93
+ def _fit_latent(r, H, W, mode):
94
+ """Fit reference latent frames (N, C, h, w) to the generation
95
+ latent size (H, W).
96
+
97
+ stretch: plain bilinear resize (aspect distortion)
98
+ pad: aspect-preserving resize + edge-replicate center pad.
99
+ Replicate keeps a white-background reference white at the
100
+ borders instead of introducing a mean-gray frame.
101
+ crop: aspect-filling resize + center crop
102
+ """
103
+ h, w = r.shape[-2:]
104
+ if (h, w) == (H, W):
105
+ return r
106
+ if mode == "stretch":
107
+ return F.interpolate(r, size=(H, W), mode="bilinear", align_corners=False)
108
+ if mode == "pad":
109
+ scale = min(H / h, W / w)
110
+ nh = max(1, min(H, round(h * scale)))
111
+ nw = max(1, min(W, round(w * scale)))
112
+ r = F.interpolate(r, size=(nh, nw), mode="bilinear", align_corners=False)
113
+ pt = (H - nh) // 2
114
+ pl = (W - nw) // 2
115
+ return F.pad(r, (pl, W - nw - pl, pt, H - nh - pt), mode="replicate")
116
+ if mode == "crop":
117
+ scale = max(H / h, W / w)
118
+ nh = max(H, round(h * scale))
119
+ nw = max(W, round(w * scale))
120
+ r = F.interpolate(r, size=(nh, nw), mode="bilinear", align_corners=False)
121
+ ot = (nh - H) // 2
122
+ ol = (nw - W) // 2
123
+ return r[:, :, ot:ot + H, ol:ol + W]
124
+ raise ValueError(f"unknown fit mode: {mode}")
125
+
126
+
127
+ def make_ref_attn_op(state, fallback_op):
128
+ """Replacement for Attention.attn_op on self-attention modules.
129
+
130
+ Adds the per-sample reference bias to attention logits for
131
+ reference-token key columns. Falls back to the original op whenever
132
+ the reference is not active, the bias is neutral, or the sequence
133
+ length does not match the full (gen + ref) self-attention sequence
134
+ (which excludes cross-attention and the LLMAdapter, whose K length
135
+ differs).
136
+ """
137
+
138
+ def ref_attn_op(q_B_S_H_D, k_B_S_H_D, v_B_S_H_D, transformer_options={}):
139
+ if (
140
+ not state.active
141
+ or state.bias_B is None
142
+ or k_B_S_H_D.shape[1] != state.total_tokens
143
+ or q_B_S_H_D.shape[1] != state.total_tokens
144
+ ):
145
+ return fallback_op(q_B_S_H_D, k_B_S_H_D, v_B_S_H_D, transformer_options=transformer_options)
146
+
147
+ # (B, S, H, D) -> (B, H, S, D)
148
+ q = q_B_S_H_D.transpose(1, 2)
149
+ k = k_B_S_H_D.transpose(1, 2)
150
+ v = v_B_S_H_D.transpose(1, 2)
151
+
152
+ bias = state.bias_for(q.device, q.dtype)
153
+
154
+ out = F.scaled_dot_product_attention(q, k, v, attn_mask=bias)
155
+ # (B, H, S, D) -> (B, S, H*D)
156
+ out = out.transpose(1, 2).reshape(q_B_S_H_D.shape[0], q_B_S_H_D.shape[1], -1)
157
+ return out
158
+
159
+ return ref_attn_op
160
+
161
+
162
+ def _per_sample_bias(B, strength, cond_only, cond_or_uncond, device):
163
+ """Build the per-sample reference-key bias, shape (B,).
164
+
165
+ cond samples get log(strength) (0 at strength=1); uncond samples get
166
+ the same unless cond_only, in which case their reference keys are
167
+ masked out entirely. strength <= 0 masks the reference everywhere.
168
+
169
+ Returns None when every entry is zero (neutral -> no attn patch).
170
+ """
171
+ if strength <= 0.0:
172
+ base = NEG_BIAS
173
+ else:
174
+ base = math.log(strength)
175
+
176
+ bias = torch.full((B,), base, device=device, dtype=torch.float32)
177
+
178
+ if cond_only and cond_or_uncond is not None and len(cond_or_uncond) > 0 and B % len(cond_or_uncond) == 0:
179
+ # calc_cond_batch concatenates equal-sized chunks along B, one
180
+ # per entry of cond_or_uncond (0 = cond, 1 = uncond).
181
+ chunk = B // len(cond_or_uncond)
182
+ for i, kind in enumerate(cond_or_uncond):
183
+ if kind == 1:
184
+ bias[i * chunk:(i + 1) * chunk] = NEG_BIAS
185
+
186
+ if torch.count_nonzero(bias) == 0:
187
+ return None
188
+ return bias
189
+
190
+
191
+ def make_diffusion_wrapper(opts):
192
+ """DIFFUSION_MODEL wrapper around MiniTrainDIT._forward.
193
+
194
+ opts is a dict with:
195
+ ref_latent: (N, C, 1, H, W) latent tensor, already
196
+ latent_format-normalized (process_latent_in)
197
+ state: RefState instance shared with the attn ops
198
+ strength: attention bias multiplier for reference tokens
199
+ cond_only: mask reference keys for the uncond CFG half
200
+ fit_mode: stretch | pad | crop (reference latent resize)
201
+ sigma_start: apply when current sigma <= sigma_start
202
+ sigma_end: ... and sigma >= sigma_end
203
+ patch_spatial: DiT spatial patch size (2 for Anima)
204
+ ref_timestep: timestep value for reference frames (default 0.0)
205
+ """
206
+
207
+ def wrapper(executor, x, timesteps, context, fps=None, padding_mask=None, **kwargs):
208
+ state = opts["state"]
209
+ ref = opts["ref_latent"]
210
+ to = kwargs.get("transformer_options", {})
211
+
212
+ # strength <= 0 fully masks the reference for every sample, which
213
+ # is mathematically identical to not attaching it — skip the
214
+ # concat so the output is bit-exact with the reference-free
215
+ # forward (and faster). Verified on-device: with the concat, the
216
+ # masked-attention kernel差 compounds over sampling steps.
217
+ if opts["strength"] <= 0.0:
218
+ return executor(x, timesteps, context, fps, padding_mask, **kwargs)
219
+
220
+ # ---- sigma window gating ----
221
+ sigmas = to.get("sigmas", None)
222
+ if sigmas is not None:
223
+ s = float(sigmas.max())
224
+ if s > opts["sigma_start"] or s < opts["sigma_end"]:
225
+ return executor(x, timesteps, context, fps, padding_mask, **kwargs)
226
+
227
+ squeeze_t = False
228
+ if x.ndim == 4: # (B, C, H, W) -> (B, C, 1, H, W)
229
+ x = x.unsqueeze(2)
230
+ squeeze_t = True
231
+
232
+ B, C, T, H, W = x.shape
233
+ n_ref = ref.shape[0]
234
+
235
+ # ---- prepare reference frames ----
236
+ r = ref.to(device=x.device, dtype=x.dtype) # (N, C, 1, H', W')
237
+ r = r.squeeze(2) # (N, C, H', W')
238
+ r = _fit_latent(r, H, W, opts.get("fit_mode", "pad"))
239
+ # (N, C, H, W) -> (1, C, N, H, W) -> (B, C, N, H, W)
240
+ r = r.permute(1, 0, 2, 3).unsqueeze(0).expand(B, -1, -1, -1, -1)
241
+
242
+ x_cat = torch.cat([x, r], dim=2) # (B, C, T + N, H, W)
243
+
244
+ # ---- per-frame timesteps: generated frames keep the sampler's t,
245
+ # reference frames get ref_timestep (0 = clean) ----
246
+ t = timesteps
247
+ if t.ndim == 1:
248
+ t = t.unsqueeze(1) # (B, 1)
249
+ t = t.expand(B, T)
250
+ t_ref = torch.full((B, n_ref), opts.get("ref_timestep", 0.0), device=t.device, dtype=t.dtype)
251
+ t_cat = torch.cat([t, t_ref], dim=1) # (B, T + N)
252
+
253
+ # ---- arm the attention-op state ----
254
+ tpf = _tokens_per_frame(H, W, opts["patch_spatial"])
255
+ state.gen_tokens = T * tpf
256
+ state.total_tokens = (T + n_ref) * tpf
257
+ state.bias_B = _per_sample_bias(
258
+ B, opts["strength"], opts.get("cond_only", False), to.get("cond_or_uncond", None), x.device
259
+ )
260
+ state._bias_cache = None
261
+ state.active = True
262
+ try:
263
+ out = executor(x_cat, t_cat, context, fps, padding_mask, **kwargs)
264
+ finally:
265
+ state.active = False
266
+ state.bias_B = None
267
+ state._bias_cache = None
268
+
269
+ out = out[:, :, :T] # drop reference frames
270
+ if squeeze_t:
271
+ out = out.squeeze(2)
272
+ return out
273
+
274
+ return wrapper
275
+
276
+
277
+ def apply_incontext_ref(
278
+ model_patcher,
279
+ ref_latent,
280
+ strength,
281
+ start_percent,
282
+ end_percent,
283
+ cond_only=True,
284
+ fit_mode="pad",
285
+ ref_timestep=0.0,
286
+ ):
287
+ """Clone the ModelPatcher and install the in-context reference patches.
288
+
289
+ ref_latent: raw LATENT samples tensor from a VAE encode,
290
+ (N, C, H, W) or (N, C, 1, H, W).
291
+ """
292
+ m = model_patcher.clone()
293
+
294
+ lat = ref_latent
295
+ if lat.ndim == 4:
296
+ lat = lat.unsqueeze(2) # (N, C, 1, H, W)
297
+ # Normalize into the model's latent space (Wan21 per-channel
298
+ # mean/std). The sampler does this for the generated latent via
299
+ # process_latent_in; we must match it for reference frames.
300
+ lat = m.model.process_latent_in(lat.clone())
301
+
302
+ ms = m.get_model_object("model_sampling")
303
+ sigma_start = ms.percent_to_sigma(start_percent)
304
+ sigma_end = ms.percent_to_sigma(end_percent)
305
+
306
+ dm = m.get_model_object("diffusion_model")
307
+ patch_spatial = getattr(dm, "patch_spatial", 2)
308
+
309
+ state = RefState()
310
+ opts = {
311
+ "ref_latent": lat,
312
+ "state": state,
313
+ "strength": strength,
314
+ "cond_only": cond_only,
315
+ "fit_mode": fit_mode,
316
+ "sigma_start": sigma_start,
317
+ "sigma_end": sigma_end,
318
+ "patch_spatial": patch_spatial,
319
+ "ref_timestep": ref_timestep,
320
+ }
321
+
322
+ wrapper = make_diffusion_wrapper(opts)
323
+ if hasattr(m, "add_wrapper_with_key"):
324
+ m.add_wrapper_with_key(WrappersMP.DIFFUSION_MODEL, WRAPPER_KEY, wrapper)
325
+ else:
326
+ comfy.patcher_extension.add_wrapper_with_key(
327
+ WrappersMP.DIFFUSION_MODEL, WRAPPER_KEY, wrapper, m.model_options, is_model_options=True
328
+ )
329
+
330
+ # Patch every block's self-attention op for strength control.
331
+ # TODO: reference-side K/V is constant across steps and could be
332
+ # cached; skipped for now (2B model, minor win).
333
+ for i, block in enumerate(dm.blocks):
334
+ orig_op = block.self_attn.attn_op
335
+ m.add_object_patch(
336
+ "diffusion_model.blocks.{}.self_attn.attn_op".format(i),
337
+ make_ref_attn_op(state, orig_op),
338
+ )
339
+
340
+ return m
comfyui-anima-incontext/nodes.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ComfyUI nodes for Anima in-context character reference."""
2
+
3
+ import torch
4
+ import torch.nn.functional as F
5
+
6
+ from .incontext import apply_incontext_ref, _fit_latent
7
+
8
+
9
+ def _fit_pixels_white_pad(pixels, target_h, target_w):
10
+ """Aspect-preserving resize of (B, H, W, C) pixels to the target
11
+ size, centered on a white canvas. White matches the background that
12
+ AnimaRefEncode composites masked subjects onto."""
13
+ b, h, w, c = pixels.shape
14
+ if (h, w) == (target_h, target_w):
15
+ return pixels
16
+ scale = min(target_h / h, target_w / w)
17
+ nh = max(1, min(target_h, round(h * scale)))
18
+ nw = max(1, min(target_w, round(w * scale)))
19
+ resized = F.interpolate(
20
+ pixels.permute(0, 3, 1, 2), size=(nh, nw), mode="bilinear", align_corners=False
21
+ ).permute(0, 2, 3, 1)
22
+ canvas = torch.ones((b, target_h, target_w, c), device=pixels.device, dtype=pixels.dtype)
23
+ top = (target_h - nh) // 2
24
+ left = (target_w - nw) // 2
25
+ canvas[:, top:top + nh, left:left + nw] = resized
26
+ return canvas
27
+
28
+
29
+ class AnimaRefEncode:
30
+ """Prepare a reference image for in-context conditioning.
31
+
32
+ Optionally composites the subject onto a white background using a
33
+ mask (recommended: character segmentation mask). Background removal
34
+ measurably improves appearance fidelity for anime subjects.
35
+
36
+ target_width / target_height (0 = keep): aspect-preserving resize
37
+ onto a white canvas before encoding. Set these to the generation
38
+ resolution to avoid any latent-space resampling at sampling time.
39
+
40
+ Multiple reference images can be batched; each becomes its own
41
+ reference frame.
42
+ """
43
+
44
+ @classmethod
45
+ def INPUT_TYPES(cls):
46
+ return {
47
+ "required": {
48
+ "vae": ("VAE",),
49
+ "image": ("IMAGE",),
50
+ },
51
+ "optional": {
52
+ "mask": ("MASK",),
53
+ "target_width": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
54
+ "target_height": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
55
+ },
56
+ }
57
+
58
+ RETURN_TYPES = ("LATENT",)
59
+ FUNCTION = "encode"
60
+ CATEGORY = "anima/incontext"
61
+
62
+ def encode(self, vae, image, mask=None, target_width=0, target_height=0):
63
+ pixels = image
64
+ if mask is not None:
65
+ m = mask
66
+ if m.ndim == 2:
67
+ m = m.unsqueeze(0)
68
+ m = m.unsqueeze(-1) # (B, H, W, 1)
69
+ if m.shape[1:3] != pixels.shape[1:3]:
70
+ m = F.interpolate(
71
+ m.permute(0, 3, 1, 2), size=pixels.shape[1:3], mode="bilinear", align_corners=False
72
+ ).permute(0, 2, 3, 1)
73
+ pixels = pixels * m + (1.0 - m) # subject on white
74
+
75
+ if target_width > 0 and target_height > 0:
76
+ pixels = _fit_pixels_white_pad(pixels, target_height, target_width)
77
+
78
+ latent = vae.encode(pixels[:, :, :, :3])
79
+ return ({"samples": latent},)
80
+
81
+
82
+ class AnimaRefLatentBatch:
83
+ """Batch two reference latents into one multi-frame reference.
84
+
85
+ Reference latents of different resolutions are combined by fitting
86
+ the second latent to the first one's size (aspect-preserving pad by
87
+ default). Chain several of these to stack many references."""
88
+
89
+ @classmethod
90
+ def INPUT_TYPES(cls):
91
+ return {
92
+ "required": {
93
+ "ref_latent_1": ("LATENT",),
94
+ "ref_latent_2": ("LATENT",),
95
+ "fit_mode": (["pad", "stretch", "crop"], {"default": "pad"}),
96
+ },
97
+ }
98
+
99
+ RETURN_TYPES = ("LATENT",)
100
+ FUNCTION = "batch"
101
+ CATEGORY = "anima/incontext"
102
+
103
+ def batch(self, ref_latent_1, ref_latent_2, fit_mode):
104
+ s1 = ref_latent_1["samples"]
105
+ s2 = ref_latent_2["samples"]
106
+ if s1.ndim == 5:
107
+ s1 = s1.squeeze(2)
108
+ if s2.ndim == 5:
109
+ s2 = s2.squeeze(2)
110
+ if s2.shape[-2:] != s1.shape[-2:]:
111
+ s2 = _fit_latent(s2, s1.shape[-2], s1.shape[-1], fit_mode)
112
+ return ({"samples": torch.cat([s1, s2], dim=0)},)
113
+
114
+
115
+ class AnimaInContextApply:
116
+ """Attach in-context reference frames to an Anima model.
117
+
118
+ The reference latent is concatenated on the DiT's temporal axis as
119
+ clean (t=0) frames; the generated frame attends to it via shared
120
+ self-attention. Combine with an in-context reference LoRA trained
121
+ with the same contract for full effect.
122
+
123
+ strength: attention bias toward reference tokens.
124
+ 1.0 = neutral, >1 stronger, 0 = off.
125
+ start_percent / end_percent: sampling step window where the
126
+ reference is attached.
127
+ cond_only: mask the reference for the uncond half of CFG. Matches
128
+ the training contract (reference dropped for the
129
+ unconditional distribution) — recommended on.
130
+ fit_mode: how a reference latent that does not match the generation
131
+ resolution is fitted (pad = aspect-preserving, default).
132
+ ref_timestep: timestep for reference frames. 0 = clean image
133
+ (the training contract). Small values (~0.05–0.1 of the
134
+ schedule) act as noise augmentation if a future LoRA is
135
+ trained that way.
136
+ """
137
+
138
+ @classmethod
139
+ def INPUT_TYPES(cls):
140
+ return {
141
+ "required": {
142
+ "model": ("MODEL",),
143
+ "ref_latent": ("LATENT",),
144
+ "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.05}),
145
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
146
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
147
+ },
148
+ "optional": {
149
+ "cond_only": ("BOOLEAN", {"default": True}),
150
+ "fit_mode": (["pad", "stretch", "crop"], {"default": "pad"}),
151
+ "ref_timestep": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.5}),
152
+ },
153
+ }
154
+
155
+ RETURN_TYPES = ("MODEL",)
156
+ FUNCTION = "apply"
157
+ CATEGORY = "anima/incontext"
158
+
159
+ def apply(self, model, ref_latent, strength, start_percent, end_percent,
160
+ cond_only=True, fit_mode="pad", ref_timestep=0.0):
161
+ samples = ref_latent["samples"]
162
+ m = apply_incontext_ref(
163
+ model, samples, strength, start_percent, end_percent,
164
+ cond_only=cond_only, fit_mode=fit_mode, ref_timestep=ref_timestep,
165
+ )
166
+ return (m,)
167
+
168
+
169
+ NODE_CLASS_MAPPINGS = {
170
+ "AnimaRefEncode": AnimaRefEncode,
171
+ "AnimaRefLatentBatch": AnimaRefLatentBatch,
172
+ "AnimaInContextApply": AnimaInContextApply,
173
+ }
174
+
175
+ NODE_DISPLAY_NAME_MAPPINGS = {
176
+ "AnimaRefEncode": "Anima Reference Encode (in-context)",
177
+ "AnimaRefLatentBatch": "Anima Reference Latent Batch",
178
+ "AnimaInContextApply": "Anima In-Context Reference Apply",
179
+ }
comfyui-anima-incontext/style_adapter.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Anima style stream — decoupled cross-attention adapter (stage 2).
2
+
3
+ Design (spec §6): the character stream is in-context latent
4
+ concatenation (detail copying is the point); the *style* stream must
5
+ NOT copy details, so it uses statistical transfer via embedding
6
+ injection instead:
7
+
8
+ encoder SigLIP 2 patch tokens from the last K hidden layers,
9
+ aggregated with learnable softmax layer weights and
10
+ projected to style tokens (AnimeAdapter-style).
11
+ injection each DiT block's text cross-attention output gains a
12
+ decoupled attention term:
13
+ out = out_text + style_weight * gamma_i * Attn(Q, K_s, V_s)
14
+ where Q is the block's frozen query, K_s/V_s are new
15
+ trainable projections of the style tokens, and gamma_i is a
16
+ per-block learnable gate initialized to 0 — the adapter is
17
+ an exact no-op at init, so training starts from the base
18
+ model's behaviour.
19
+
20
+ This module is pure torch (no ComfyUI imports) so the training code can
21
+ import the same definition. ComfyUI integration lives in style_nodes.py.
22
+ """
23
+
24
+ import torch
25
+ import torch.nn.functional as F
26
+ from torch import nn
27
+
28
+ # Anima 2B DiT geometry
29
+ ANIMA_X_DIM = 2048
30
+ ANIMA_N_HEADS = 16
31
+ ANIMA_N_BLOCKS = 28
32
+
33
+
34
+ class RMSNorm(nn.Module):
35
+ def __init__(self, dim, eps=1e-6):
36
+ super().__init__()
37
+ self.eps = eps
38
+ self.weight = nn.Parameter(torch.ones(dim))
39
+
40
+ def forward(self, x):
41
+ norm = x.float() * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + self.eps)
42
+ return (norm * self.weight.float()).type_as(x)
43
+
44
+
45
+ class StyleTokenAggregator(nn.Module):
46
+ """Aggregate SigLIP patch tokens from the last K hidden layers into
47
+ style tokens: softmax-weighted layer mix -> LayerNorm -> projection."""
48
+
49
+ def __init__(self, siglip_dim=1152, style_dim=1024, n_layers=6):
50
+ super().__init__()
51
+ self.n_layers = n_layers
52
+ self.layer_weights = nn.Parameter(torch.zeros(n_layers))
53
+ self.norm = nn.LayerNorm(siglip_dim)
54
+ self.proj = nn.Linear(siglip_dim, style_dim)
55
+
56
+ def forward(self, hidden_states_B_K_N_D):
57
+ """hidden_states: (B, K, N, D) — the last K hidden layers of the
58
+ vision tower (K == n_layers), N patch tokens of dim D."""
59
+ assert hidden_states_B_K_N_D.shape[1] == self.n_layers, (
60
+ f"expected {self.n_layers} layers, got {hidden_states_B_K_N_D.shape[1]}"
61
+ )
62
+ w = torch.softmax(self.layer_weights, dim=0)
63
+ mixed = (hidden_states_B_K_N_D * w[None, :, None, None].to(hidden_states_B_K_N_D)).sum(dim=1)
64
+ return self.proj(self.norm(mixed)) # (B, N, style_dim)
65
+
66
+
67
+ class StyleBlockKV(nn.Module):
68
+ """Per-DiT-block decoupled K/V projections + gate."""
69
+
70
+ def __init__(self, x_dim=ANIMA_X_DIM, style_dim=1024, n_heads=ANIMA_N_HEADS):
71
+ super().__init__()
72
+ self.n_heads = n_heads
73
+ self.head_dim = x_dim // n_heads
74
+ self.k_proj = nn.Linear(style_dim, x_dim, bias=False)
75
+ self.v_proj = nn.Linear(style_dim, x_dim, bias=False)
76
+ # match the base attention's K normalization (RMSNorm per head)
77
+ self.k_norm = RMSNorm(self.head_dim)
78
+ # gate init 0 -> adapter is a no-op until trained
79
+ self.gate = nn.Parameter(torch.zeros(1))
80
+
81
+ def kv(self, style_tokens_B_N_D):
82
+ B, N, _ = style_tokens_B_N_D.shape
83
+ k = self.k_proj(style_tokens_B_N_D).view(B, N, self.n_heads, self.head_dim)
84
+ v = self.v_proj(style_tokens_B_N_D).view(B, N, self.n_heads, self.head_dim)
85
+ k = self.k_norm(k)
86
+ return k, v
87
+
88
+
89
+ class AnimaStyleAdapter(nn.Module):
90
+ """Aggregator + one StyleBlockKV per DiT block."""
91
+
92
+ def __init__(self, siglip_dim=1152, style_dim=1024, n_layers=6,
93
+ x_dim=ANIMA_X_DIM, n_heads=ANIMA_N_HEADS, n_blocks=ANIMA_N_BLOCKS):
94
+ super().__init__()
95
+ self.config = {
96
+ "siglip_dim": siglip_dim, "style_dim": style_dim, "n_layers": n_layers,
97
+ "x_dim": x_dim, "n_heads": n_heads, "n_blocks": n_blocks,
98
+ }
99
+ self.aggregator = StyleTokenAggregator(siglip_dim, style_dim, n_layers)
100
+ self.blocks = nn.ModuleList(
101
+ [StyleBlockKV(x_dim, style_dim, n_heads) for _ in range(n_blocks)]
102
+ )
103
+
104
+ @classmethod
105
+ def from_state_dict(cls, sd):
106
+ """Instantiate with dimensions inferred from a checkpoint."""
107
+ siglip_dim = sd["aggregator.proj.weight"].shape[1]
108
+ style_dim = sd["aggregator.proj.weight"].shape[0]
109
+ n_layers = sd["aggregator.layer_weights"].shape[0]
110
+ x_dim = sd["blocks.0.k_proj.weight"].shape[0]
111
+ n_blocks = 0
112
+ while f"blocks.{n_blocks}.k_proj.weight" in sd:
113
+ n_blocks += 1
114
+ head_dim = sd["blocks.0.k_norm.weight"].shape[0]
115
+ adapter = cls(siglip_dim, style_dim, n_layers, x_dim, x_dim // head_dim, n_blocks)
116
+ adapter.load_state_dict(sd)
117
+ return adapter
118
+
119
+
120
+ class StyleState:
121
+ """Runtime state shared by all patched cross-attention wrappers.
122
+ Armed by the style diffusion wrapper before each forward."""
123
+
124
+ def __init__(self):
125
+ self.active = False
126
+ # style K/V per block, computed once per forward: list of (k, v)
127
+ self.kv_per_block = None
128
+ # per-sample multiplier (0 masks a sample, e.g. the uncond chunk)
129
+ self.sample_scale_B = None
130
+ self.weight = 1.0
131
+
132
+
133
+ def style_attention(q_B_S_H_D, k_B_N_H_D, v_B_N_H_D):
134
+ """Decoupled attention with the block's frozen query. Returns
135
+ (B, S, H*D) to match the text branch pre-output_proj layout."""
136
+ q = q_B_S_H_D.transpose(1, 2)
137
+ k = k_B_N_H_D.transpose(1, 2)
138
+ v = v_B_N_H_D.transpose(1, 2)
139
+ out = F.scaled_dot_product_attention(q, k, v)
140
+ return out.transpose(1, 2).reshape(q_B_S_H_D.shape[0], q_B_S_H_D.shape[1], -1)
141
+
142
+
143
+ class StyleCrossAttention(nn.Module):
144
+ """Replacement for a DiT block's cross_attn module.
145
+
146
+ Reimplements the original attention using the original (frozen)
147
+ projections, then adds the decoupled style term *before*
148
+ output_proj — the IP-Adapter formulation:
149
+
150
+ out = output_proj( Attn(Q, K_text, V_text)
151
+ + weight * gamma * Attn(Q, K_style, V_style) )
152
+
153
+ When the state is inactive it computes exactly the original
154
+ attention (the reimplementation is numerically identical: same
155
+ modules, same attn_op).
156
+ """
157
+
158
+ def __init__(self, orig_attn, block_kv, state, block_index):
159
+ super().__init__()
160
+ self.orig = orig_attn
161
+ self.block_kv = block_kv
162
+ self.state = state
163
+ self.block_index = block_index
164
+
165
+ def forward(self, x, context=None, rope_emb=None, transformer_options={}):
166
+ state = self.state
167
+ if not state.active or state.kv_per_block is None:
168
+ return self.orig(x, context, rope_emb=rope_emb, transformer_options=transformer_options)
169
+
170
+ q, k, v = self.orig.compute_qkv(x, context, rope_emb=rope_emb)
171
+ text_out = self.orig.attn_op(q, k, v, transformer_options=transformer_options)
172
+
173
+ ks, vs = state.kv_per_block[self.block_index]
174
+ B = q.shape[0]
175
+ if ks.shape[0] != B: # single style batch broadcast over CFG batch
176
+ ks = ks.expand(B, -1, -1, -1)
177
+ vs = vs.expand(B, -1, -1, -1)
178
+ style_out = style_attention(q, ks.to(q.dtype), vs.to(q.dtype))
179
+
180
+ gate = self.block_kv.gate.to(q.dtype)
181
+ scale = state.weight * gate
182
+ if state.sample_scale_B is not None:
183
+ scale = scale * state.sample_scale_B.to(q.dtype).view(B, 1, 1)
184
+ text_out = text_out + scale * style_out
185
+
186
+ return self.orig.output_dropout(self.orig.output_proj(text_out))
comfyui-anima-incontext/style_nodes.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ComfyUI nodes for the Anima style stream (decoupled cross-attention).
2
+
3
+ Requires a trained AnimaStyleAdapter checkpoint (see style_adapter.py
4
+ and the project docs); with an untrained adapter the gates are zero and
5
+ the nodes are an exact no-op.
6
+
7
+ The style stream composes freely with the in-context character stream:
8
+ AnimaInContextApply patches self-attention (T-axis reference frames),
9
+ AnimaStyleApply patches cross-attention — chain both Apply nodes and
10
+ control char/style strength independently.
11
+ """
12
+
13
+ import os
14
+
15
+ import torch
16
+
17
+ import comfy.utils
18
+ import folder_paths
19
+ from comfy.patcher_extension import WrappersMP
20
+
21
+ from .style_adapter import AnimaStyleAdapter, StyleState
22
+
23
+ STYLE_WRAPPER_KEY = "anima_style_ref"
24
+
25
+ _ADAPTER_DIR = "anima_style_adapters"
26
+ if _ADAPTER_DIR not in folder_paths.folder_names_and_paths:
27
+ folder_paths.add_model_folder_path(
28
+ _ADAPTER_DIR, os.path.join(folder_paths.models_dir, _ADAPTER_DIR)
29
+ )
30
+
31
+
32
+ class AnimaStyleAdapterLoader:
33
+ @classmethod
34
+ def INPUT_TYPES(cls):
35
+ return {
36
+ "required": {
37
+ "adapter_name": (folder_paths.get_filename_list(_ADAPTER_DIR),),
38
+ },
39
+ }
40
+
41
+ RETURN_TYPES = ("ANIMA_STYLE_ADAPTER",)
42
+ FUNCTION = "load"
43
+ CATEGORY = "anima/style"
44
+
45
+ def load(self, adapter_name):
46
+ path = folder_paths.get_full_path_or_raise(_ADAPTER_DIR, adapter_name)
47
+ sd = comfy.utils.load_torch_file(path, safe_load=True)
48
+ adapter = AnimaStyleAdapter.from_state_dict(sd)
49
+ adapter.eval()
50
+ return (adapter,)
51
+
52
+
53
+ class AnimaStyleEncode:
54
+ """Encode style reference image(s) with a SigLIP CLIP_VISION model,
55
+ keeping the hidden-state stack the adapter aggregates over.
56
+
57
+ Multiple images are encoded independently and their patch tokens are
58
+ concatenated at apply time (attention pools over all of them)."""
59
+
60
+ @classmethod
61
+ def INPUT_TYPES(cls):
62
+ return {
63
+ "required": {
64
+ "clip_vision": ("CLIP_VISION",),
65
+ "image": ("IMAGE",),
66
+ },
67
+ }
68
+
69
+ RETURN_TYPES = ("ANIMA_STYLE_EMBEDS",)
70
+ FUNCTION = "encode"
71
+ CATEGORY = "anima/style"
72
+
73
+ def encode(self, clip_vision, image):
74
+ out = clip_vision.encode_image(image)
75
+ hs = out["all_hidden_states"] # (B, L, N, D)
76
+ if hs is None:
77
+ raise RuntimeError(
78
+ "this CLIP_VISION model does not expose all hidden states; "
79
+ "use a SigLIP vision model"
80
+ )
81
+ return ({"hidden_states": hs},)
82
+
83
+
84
+ class AnimaStyleApply:
85
+ """Attach the style stream to an Anima model.
86
+
87
+ style_weight: global multiplier on the (learned, per-block) gates.
88
+ cond_only: apply style only to the cond half of the CFG batch.
89
+ start/end_percent: sampling window, same semantics as the
90
+ in-context Apply node.
91
+ """
92
+
93
+ @classmethod
94
+ def INPUT_TYPES(cls):
95
+ return {
96
+ "required": {
97
+ "model": ("MODEL",),
98
+ "style_adapter": ("ANIMA_STYLE_ADAPTER",),
99
+ "style_embeds": ("ANIMA_STYLE_EMBEDS",),
100
+ "style_weight": ("FLOAT", {"default": 1.0, "min": -2.0, "max": 5.0, "step": 0.05}),
101
+ "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
102
+ "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
103
+ },
104
+ "optional": {
105
+ "cond_only": ("BOOLEAN", {"default": True}),
106
+ },
107
+ }
108
+
109
+ RETURN_TYPES = ("MODEL",)
110
+ FUNCTION = "apply"
111
+ CATEGORY = "anima/style"
112
+
113
+ def apply(self, model, style_adapter, style_embeds, style_weight,
114
+ start_percent, end_percent, cond_only=True):
115
+ m = model.clone()
116
+
117
+ ms = m.get_model_object("model_sampling")
118
+ sigma_start = ms.percent_to_sigma(start_percent)
119
+ sigma_end = ms.percent_to_sigma(end_percent)
120
+
121
+ dm = m.get_model_object("diffusion_model")
122
+ n_layers = style_adapter.config["n_layers"]
123
+ hidden_states = style_embeds["hidden_states"][:, -n_layers:] # (B, K, N, D)
124
+
125
+ state = StyleState()
126
+ # per-(device, dtype) cache of the per-block K/V — the style
127
+ # tokens are constant across sampling steps
128
+ kv_cache = {}
129
+
130
+ def compute_kv(device, dtype):
131
+ key = (device, dtype)
132
+ if key not in kv_cache:
133
+ adapter = style_adapter.to(device)
134
+ hs = hidden_states.to(device=device, dtype=next(adapter.parameters()).dtype)
135
+ with torch.no_grad():
136
+ tokens = adapter.aggregator(hs) # (B, N, style_dim)
137
+ # multiple style images -> one long token sequence
138
+ tokens = tokens.reshape(1, -1, tokens.shape[-1])
139
+ kv_cache[key] = [
140
+ tuple(t.to(dtype) for t in blk.kv(tokens)) for blk in adapter.blocks
141
+ ]
142
+ return kv_cache[key]
143
+
144
+ def wrapper(executor, x, timesteps, context, fps=None, padding_mask=None, **kwargs):
145
+ to = kwargs.get("transformer_options", {})
146
+ sigmas = to.get("sigmas", None)
147
+ if sigmas is not None:
148
+ s = float(sigmas.max())
149
+ if s > sigma_start or s < sigma_end:
150
+ return executor(x, timesteps, context, fps, padding_mask, **kwargs)
151
+
152
+ state.kv_per_block = compute_kv(x.device, x.dtype)
153
+ state.weight = style_weight
154
+
155
+ state.sample_scale_B = None
156
+ cou = to.get("cond_or_uncond", None)
157
+ if cond_only and cou and x.shape[0] % len(cou) == 0:
158
+ chunk = x.shape[0] // len(cou)
159
+ scale = torch.ones(x.shape[0], device=x.device)
160
+ for i, kind in enumerate(cou):
161
+ if kind == 1:
162
+ scale[i * chunk:(i + 1) * chunk] = 0.0
163
+ state.sample_scale_B = scale
164
+
165
+ state.active = True
166
+ try:
167
+ return executor(x, timesteps, context, fps, padding_mask, **kwargs)
168
+ finally:
169
+ state.active = False
170
+
171
+ m.add_wrapper_with_key(WrappersMP.DIFFUSION_MODEL, STYLE_WRAPPER_KEY, wrapper)
172
+
173
+ from .style_adapter import StyleCrossAttention
174
+
175
+ for i, block in enumerate(dm.blocks):
176
+ m.add_object_patch(
177
+ "diffusion_model.blocks.{}.cross_attn".format(i),
178
+ StyleCrossAttention(block.cross_attn, style_adapter.blocks[i], state, i),
179
+ )
180
+
181
+ return (m,)
182
+
183
+
184
+ NODE_CLASS_MAPPINGS = {
185
+ "AnimaStyleAdapterLoader": AnimaStyleAdapterLoader,
186
+ "AnimaStyleEncode": AnimaStyleEncode,
187
+ "AnimaStyleApply": AnimaStyleApply,
188
+ }
189
+
190
+ NODE_DISPLAY_NAME_MAPPINGS = {
191
+ "AnimaStyleAdapterLoader": "Anima Style Adapter Loader",
192
+ "AnimaStyleEncode": "Anima Style Encode (SigLIP)",
193
+ "AnimaStyleApply": "Anima Style Apply",
194
+ }
samples/example_output.png ADDED

Git LFS Details

  • SHA256: 2e8ee9560d3b7439a51d606f60774580cf458e8e2824cc1ce05d3d96eb8443d4
  • Pointer size: 131 Bytes
  • Size of remote file: 994 kB
samples/example_output2.png ADDED

Git LFS Details

  • SHA256: 5d59ad46814831e3d2d33aa2c1e835a0d5107058affe1e1a5106380d34c21ee9
  • Pointer size: 132 Bytes
  • Size of remote file: 1.52 MB
samples/example_reference.jpg ADDED

Git LFS Details

  • SHA256: a650522a306a4091679afdb37499909ad81c6ceb696a6e98f31e45bedce7d073
  • Pointer size: 131 Bytes
  • Size of remote file: 205 kB
workflow_anima_incontext_character.json ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "last_node_id": 25,
3
+ "last_link_id": 18,
4
+ "nodes": [
5
+ {
6
+ "id": 1,
7
+ "type": "UNETLoader",
8
+ "pos": [
9
+ 40,
10
+ 40
11
+ ],
12
+ "size": [
13
+ 320,
14
+ 100
15
+ ],
16
+ "flags": {},
17
+ "order": 1,
18
+ "mode": 0,
19
+ "inputs": [],
20
+ "outputs": [
21
+ {
22
+ "name": "MODEL",
23
+ "type": "MODEL",
24
+ "links": [
25
+ 1
26
+ ],
27
+ "slot_index": 0
28
+ }
29
+ ],
30
+ "properties": {
31
+ "Node name for S&R": "UNETLoader"
32
+ },
33
+ "widgets_values": [
34
+ "anima-base-v1.0.safetensors",
35
+ "default"
36
+ ]
37
+ },
38
+ {
39
+ "id": 2,
40
+ "type": "CLIPLoader",
41
+ "pos": [
42
+ 40,
43
+ 190
44
+ ],
45
+ "size": [
46
+ 320,
47
+ 110
48
+ ],
49
+ "flags": {},
50
+ "order": 2,
51
+ "mode": 0,
52
+ "inputs": [],
53
+ "outputs": [
54
+ {
55
+ "name": "CLIP",
56
+ "type": "CLIP",
57
+ "links": [
58
+ 11,
59
+ 12
60
+ ],
61
+ "slot_index": 0
62
+ }
63
+ ],
64
+ "properties": {
65
+ "Node name for S&R": "CLIPLoader"
66
+ },
67
+ "widgets_values": [
68
+ "qwen_3_06b_base.safetensors",
69
+ "stable_diffusion",
70
+ "default"
71
+ ]
72
+ },
73
+ {
74
+ "id": 3,
75
+ "type": "VAELoader",
76
+ "pos": [
77
+ 40,
78
+ 340
79
+ ],
80
+ "size": [
81
+ 320,
82
+ 80
83
+ ],
84
+ "flags": {},
85
+ "order": 3,
86
+ "mode": 0,
87
+ "inputs": [],
88
+ "outputs": [
89
+ {
90
+ "name": "VAE",
91
+ "type": "VAE",
92
+ "links": [
93
+ 3,
94
+ 4,
95
+ 5
96
+ ],
97
+ "slot_index": 0
98
+ }
99
+ ],
100
+ "properties": {
101
+ "Node name for S&R": "VAELoader"
102
+ },
103
+ "widgets_values": [
104
+ "qwen_image_vae.safetensors"
105
+ ]
106
+ },
107
+ {
108
+ "id": 4,
109
+ "type": "LoraLoaderModelOnly",
110
+ "pos": [
111
+ 400,
112
+ 40
113
+ ],
114
+ "size": [
115
+ 330,
116
+ 110
117
+ ],
118
+ "flags": {},
119
+ "order": 4,
120
+ "mode": 0,
121
+ "inputs": [
122
+ {
123
+ "name": "model",
124
+ "type": "MODEL",
125
+ "link": 1
126
+ }
127
+ ],
128
+ "outputs": [
129
+ {
130
+ "name": "MODEL",
131
+ "type": "MODEL",
132
+ "links": [
133
+ 2
134
+ ],
135
+ "slot_index": 0
136
+ }
137
+ ],
138
+ "properties": {
139
+ "Node name for S&R": "LoraLoaderModelOnly"
140
+ },
141
+ "widgets_values": [
142
+ "anima-incontext-character.safetensors",
143
+ 1.0
144
+ ],
145
+ "title": "Character LoRA"
146
+ },
147
+ {
148
+ "id": 10,
149
+ "type": "LoadImage",
150
+ "pos": [
151
+ 40,
152
+ 470
153
+ ],
154
+ "size": [
155
+ 320,
156
+ 300
157
+ ],
158
+ "flags": {},
159
+ "order": 10,
160
+ "mode": 0,
161
+ "inputs": [],
162
+ "outputs": [
163
+ {
164
+ "name": "IMAGE",
165
+ "type": "IMAGE",
166
+ "links": [
167
+ 6
168
+ ],
169
+ "slot_index": 0
170
+ },
171
+ {
172
+ "name": "MASK",
173
+ "type": "MASK",
174
+ "links": [],
175
+ "slot_index": 0
176
+ }
177
+ ],
178
+ "properties": {
179
+ "Node name for S&R": "LoadImage"
180
+ },
181
+ "widgets_values": [
182
+ "ref_fullbody.png",
183
+ "image"
184
+ ],
185
+ "title": "Reference 1 (full body)"
186
+ },
187
+ {
188
+ "id": 11,
189
+ "type": "LoadImage",
190
+ "pos": [
191
+ 40,
192
+ 800
193
+ ],
194
+ "size": [
195
+ 320,
196
+ 300
197
+ ],
198
+ "flags": {},
199
+ "order": 11,
200
+ "mode": 0,
201
+ "inputs": [],
202
+ "outputs": [
203
+ {
204
+ "name": "IMAGE",
205
+ "type": "IMAGE",
206
+ "links": [
207
+ 7
208
+ ],
209
+ "slot_index": 0
210
+ },
211
+ {
212
+ "name": "MASK",
213
+ "type": "MASK",
214
+ "links": [],
215
+ "slot_index": 0
216
+ }
217
+ ],
218
+ "properties": {
219
+ "Node name for S&R": "LoadImage"
220
+ },
221
+ "widgets_values": [
222
+ "ref_face.png",
223
+ "image"
224
+ ],
225
+ "title": "Reference 2 (face, optional)"
226
+ },
227
+ {
228
+ "id": 12,
229
+ "type": "AnimaRefEncode",
230
+ "pos": [
231
+ 400,
232
+ 470
233
+ ],
234
+ "size": [
235
+ 300,
236
+ 130
237
+ ],
238
+ "flags": {},
239
+ "order": 12,
240
+ "mode": 0,
241
+ "inputs": [
242
+ {
243
+ "name": "vae",
244
+ "type": "VAE",
245
+ "link": 3
246
+ },
247
+ {
248
+ "name": "image",
249
+ "type": "IMAGE",
250
+ "link": 6
251
+ },
252
+ {
253
+ "name": "mask",
254
+ "type": "MASK",
255
+ "link": null
256
+ }
257
+ ],
258
+ "outputs": [
259
+ {
260
+ "name": "LATENT",
261
+ "type": "LATENT",
262
+ "links": [
263
+ 8
264
+ ],
265
+ "slot_index": 0
266
+ }
267
+ ],
268
+ "properties": {
269
+ "Node name for S&R": "AnimaRefEncode"
270
+ },
271
+ "widgets_values": [
272
+ 1024,
273
+ 1024
274
+ ]
275
+ },
276
+ {
277
+ "id": 13,
278
+ "type": "AnimaRefEncode",
279
+ "pos": [
280
+ 400,
281
+ 800
282
+ ],
283
+ "size": [
284
+ 300,
285
+ 130
286
+ ],
287
+ "flags": {},
288
+ "order": 13,
289
+ "mode": 0,
290
+ "inputs": [
291
+ {
292
+ "name": "vae",
293
+ "type": "VAE",
294
+ "link": 4
295
+ },
296
+ {
297
+ "name": "image",
298
+ "type": "IMAGE",
299
+ "link": 7
300
+ },
301
+ {
302
+ "name": "mask",
303
+ "type": "MASK",
304
+ "link": null
305
+ }
306
+ ],
307
+ "outputs": [
308
+ {
309
+ "name": "LATENT",
310
+ "type": "LATENT",
311
+ "links": [
312
+ 9
313
+ ],
314
+ "slot_index": 0
315
+ }
316
+ ],
317
+ "properties": {
318
+ "Node name for S&R": "AnimaRefEncode"
319
+ },
320
+ "widgets_values": [
321
+ 1024,
322
+ 1024
323
+ ]
324
+ },
325
+ {
326
+ "id": 14,
327
+ "type": "AnimaRefLatentBatch",
328
+ "pos": [
329
+ 740,
330
+ 470
331
+ ],
332
+ "size": [
333
+ 300,
334
+ 110
335
+ ],
336
+ "flags": {},
337
+ "order": 14,
338
+ "mode": 0,
339
+ "inputs": [
340
+ {
341
+ "name": "ref_latent_1",
342
+ "type": "LATENT",
343
+ "link": 8
344
+ },
345
+ {
346
+ "name": "ref_latent_2",
347
+ "type": "LATENT",
348
+ "link": 9
349
+ }
350
+ ],
351
+ "outputs": [
352
+ {
353
+ "name": "LATENT",
354
+ "type": "LATENT",
355
+ "links": [
356
+ 10
357
+ ],
358
+ "slot_index": 0
359
+ }
360
+ ],
361
+ "properties": {
362
+ "Node name for S&R": "AnimaRefLatentBatch"
363
+ },
364
+ "widgets_values": [
365
+ "pad"
366
+ ]
367
+ },
368
+ {
369
+ "id": 15,
370
+ "type": "AnimaInContextApply",
371
+ "pos": [
372
+ 760,
373
+ 40
374
+ ],
375
+ "size": [
376
+ 330,
377
+ 220
378
+ ],
379
+ "flags": {},
380
+ "order": 15,
381
+ "mode": 0,
382
+ "inputs": [
383
+ {
384
+ "name": "model",
385
+ "type": "MODEL",
386
+ "link": 2
387
+ },
388
+ {
389
+ "name": "ref_latent",
390
+ "type": "LATENT",
391
+ "link": 10
392
+ }
393
+ ],
394
+ "outputs": [
395
+ {
396
+ "name": "MODEL",
397
+ "type": "MODEL",
398
+ "links": [
399
+ 13
400
+ ],
401
+ "slot_index": 0
402
+ }
403
+ ],
404
+ "properties": {
405
+ "Node name for S&R": "AnimaInContextApply"
406
+ },
407
+ "widgets_values": [
408
+ 1.0,
409
+ 0.0,
410
+ 1.0,
411
+ true,
412
+ "pad",
413
+ 0.0
414
+ ],
415
+ "title": "Anima In-Context Apply"
416
+ },
417
+ {
418
+ "id": 20,
419
+ "type": "CLIPTextEncode",
420
+ "pos": [
421
+ 400,
422
+ 640
423
+ ],
424
+ "size": [
425
+ 330,
426
+ 140
427
+ ],
428
+ "flags": {},
429
+ "order": 20,
430
+ "mode": 0,
431
+ "inputs": [
432
+ {
433
+ "name": "clip",
434
+ "type": "CLIP",
435
+ "link": 11
436
+ }
437
+ ],
438
+ "outputs": [
439
+ {
440
+ "name": "CONDITIONING",
441
+ "type": "CONDITIONING",
442
+ "links": [
443
+ 14
444
+ ],
445
+ "slot_index": 0
446
+ }
447
+ ],
448
+ "properties": {
449
+ "Node name for S&R": "CLIPTextEncode"
450
+ },
451
+ "widgets_values": [
452
+ "1girl, silver hair, twin tails, blue eyes, black sailor uniform with red ribbon, standing in a park, cherry blossoms, detailed illustration"
453
+ ],
454
+ "title": "Positive (describe pose/scene + appearance)"
455
+ },
456
+ {
457
+ "id": 21,
458
+ "type": "CLIPTextEncode",
459
+ "pos": [
460
+ 400,
461
+ 300
462
+ ],
463
+ "size": [
464
+ 330,
465
+ 100
466
+ ],
467
+ "flags": {},
468
+ "order": 21,
469
+ "mode": 0,
470
+ "inputs": [
471
+ {
472
+ "name": "clip",
473
+ "type": "CLIP",
474
+ "link": 12
475
+ }
476
+ ],
477
+ "outputs": [
478
+ {
479
+ "name": "CONDITIONING",
480
+ "type": "CONDITIONING",
481
+ "links": [
482
+ 15
483
+ ],
484
+ "slot_index": 0
485
+ }
486
+ ],
487
+ "properties": {
488
+ "Node name for S&R": "CLIPTextEncode"
489
+ },
490
+ "widgets_values": [
491
+ "worst quality, low quality, blurry, jpeg artifacts"
492
+ ],
493
+ "title": "Negative"
494
+ },
495
+ {
496
+ "id": 22,
497
+ "type": "EmptyLatentImage",
498
+ "pos": [
499
+ 760,
500
+ 300
501
+ ],
502
+ "size": [
503
+ 300,
504
+ 110
505
+ ],
506
+ "flags": {},
507
+ "order": 22,
508
+ "mode": 0,
509
+ "inputs": [],
510
+ "outputs": [
511
+ {
512
+ "name": "LATENT",
513
+ "type": "LATENT",
514
+ "links": [
515
+ 16
516
+ ],
517
+ "slot_index": 0
518
+ }
519
+ ],
520
+ "properties": {
521
+ "Node name for S&R": "EmptyLatentImage"
522
+ },
523
+ "widgets_values": [
524
+ 1024,
525
+ 1024,
526
+ 1
527
+ ]
528
+ },
529
+ {
530
+ "id": 23,
531
+ "type": "KSampler",
532
+ "pos": [
533
+ 1140,
534
+ 40
535
+ ],
536
+ "size": [
537
+ 300,
538
+ 270
539
+ ],
540
+ "flags": {},
541
+ "order": 23,
542
+ "mode": 0,
543
+ "inputs": [
544
+ {
545
+ "name": "model",
546
+ "type": "MODEL",
547
+ "link": 13
548
+ },
549
+ {
550
+ "name": "positive",
551
+ "type": "CONDITIONING",
552
+ "link": 14
553
+ },
554
+ {
555
+ "name": "negative",
556
+ "type": "CONDITIONING",
557
+ "link": 15
558
+ },
559
+ {
560
+ "name": "latent_image",
561
+ "type": "LATENT",
562
+ "link": 16
563
+ }
564
+ ],
565
+ "outputs": [
566
+ {
567
+ "name": "LATENT",
568
+ "type": "LATENT",
569
+ "links": [
570
+ 17
571
+ ],
572
+ "slot_index": 0
573
+ }
574
+ ],
575
+ "properties": {
576
+ "Node name for S&R": "KSampler"
577
+ },
578
+ "widgets_values": [
579
+ 42,
580
+ "fixed",
581
+ 30,
582
+ 4.0,
583
+ "er_sde",
584
+ "simple",
585
+ 1.0
586
+ ]
587
+ },
588
+ {
589
+ "id": 24,
590
+ "type": "VAEDecode",
591
+ "pos": [
592
+ 1480,
593
+ 40
594
+ ],
595
+ "size": [
596
+ 260,
597
+ 80
598
+ ],
599
+ "flags": {},
600
+ "order": 24,
601
+ "mode": 0,
602
+ "inputs": [
603
+ {
604
+ "name": "samples",
605
+ "type": "LATENT",
606
+ "link": 17
607
+ },
608
+ {
609
+ "name": "vae",
610
+ "type": "VAE",
611
+ "link": 5
612
+ }
613
+ ],
614
+ "outputs": [
615
+ {
616
+ "name": "IMAGE",
617
+ "type": "IMAGE",
618
+ "links": [
619
+ 18
620
+ ],
621
+ "slot_index": 0
622
+ }
623
+ ],
624
+ "properties": {
625
+ "Node name for S&R": "VAEDecode"
626
+ },
627
+ "widgets_values": []
628
+ },
629
+ {
630
+ "id": 25,
631
+ "type": "SaveImage",
632
+ "pos": [
633
+ 1760,
634
+ 40
635
+ ],
636
+ "size": [
637
+ 320,
638
+ 300
639
+ ],
640
+ "flags": {},
641
+ "order": 25,
642
+ "mode": 0,
643
+ "inputs": [
644
+ {
645
+ "name": "images",
646
+ "type": "IMAGE",
647
+ "link": 18
648
+ }
649
+ ],
650
+ "outputs": [],
651
+ "properties": {
652
+ "Node name for S&R": "SaveImage"
653
+ },
654
+ "widgets_values": [
655
+ "Anima_InContext"
656
+ ]
657
+ }
658
+ ],
659
+ "links": [
660
+ [
661
+ 1,
662
+ 1,
663
+ 0,
664
+ 4,
665
+ 0,
666
+ "MODEL"
667
+ ],
668
+ [
669
+ 2,
670
+ 4,
671
+ 0,
672
+ 15,
673
+ 0,
674
+ "MODEL"
675
+ ],
676
+ [
677
+ 3,
678
+ 3,
679
+ 0,
680
+ 12,
681
+ 0,
682
+ "VAE"
683
+ ],
684
+ [
685
+ 4,
686
+ 3,
687
+ 0,
688
+ 13,
689
+ 0,
690
+ "VAE"
691
+ ],
692
+ [
693
+ 5,
694
+ 3,
695
+ 0,
696
+ 24,
697
+ 1,
698
+ "VAE"
699
+ ],
700
+ [
701
+ 6,
702
+ 10,
703
+ 0,
704
+ 12,
705
+ 1,
706
+ "IMAGE"
707
+ ],
708
+ [
709
+ 7,
710
+ 11,
711
+ 0,
712
+ 13,
713
+ 1,
714
+ "IMAGE"
715
+ ],
716
+ [
717
+ 8,
718
+ 12,
719
+ 0,
720
+ 14,
721
+ 0,
722
+ "LATENT"
723
+ ],
724
+ [
725
+ 9,
726
+ 13,
727
+ 0,
728
+ 14,
729
+ 1,
730
+ "LATENT"
731
+ ],
732
+ [
733
+ 10,
734
+ 14,
735
+ 0,
736
+ 15,
737
+ 1,
738
+ "LATENT"
739
+ ],
740
+ [
741
+ 11,
742
+ 2,
743
+ 0,
744
+ 20,
745
+ 0,
746
+ "CLIP"
747
+ ],
748
+ [
749
+ 12,
750
+ 2,
751
+ 0,
752
+ 21,
753
+ 0,
754
+ "CLIP"
755
+ ],
756
+ [
757
+ 13,
758
+ 15,
759
+ 0,
760
+ 23,
761
+ 0,
762
+ "MODEL"
763
+ ],
764
+ [
765
+ 14,
766
+ 20,
767
+ 0,
768
+ 23,
769
+ 1,
770
+ "CONDITIONING"
771
+ ],
772
+ [
773
+ 15,
774
+ 21,
775
+ 0,
776
+ 23,
777
+ 2,
778
+ "CONDITIONING"
779
+ ],
780
+ [
781
+ 16,
782
+ 22,
783
+ 0,
784
+ 23,
785
+ 3,
786
+ "LATENT"
787
+ ],
788
+ [
789
+ 17,
790
+ 23,
791
+ 0,
792
+ 24,
793
+ 0,
794
+ "LATENT"
795
+ ],
796
+ [
797
+ 18,
798
+ 24,
799
+ 0,
800
+ 25,
801
+ 0,
802
+ "IMAGE"
803
+ ]
804
+ ],
805
+ "groups": [],
806
+ "config": {},
807
+ "extra": {},
808
+ "version": 0.4
809
+ }