prithivMLmods commited on
Commit
790c72e
·
verified ·
1 Parent(s): b2d7ce2

h3_lora [processing/.]

Browse files
Files changed (2) hide show
  1. h3_lora.py +292 -0
  2. h3_split_blocks.py +147 -0
h3_lora.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Turbo and style LoRA support for the diffusers MiniMax-H3 transformer: several checkpoint layouts, one fold
2
+ mechanism.
3
+
4
+ Every LoRA is applied by folding `scale * (lora_B @ lora_A)` into the bf16 weights rather than as runtime
5
+ wrappers, because the AoTI block package (`h3_aoti`) reads each block's live weights and a wrapper module would
6
+ be invisible to it. Deltas are computed in float32 and round once on the way back into bf16. The low-rank factors
7
+ of every loaded LoRA stay resident, so the active one can be switched per request (`set_active`) — unfold the
8
+ old, fold the new, in place, through one bf16 rounding.
9
+
10
+ The supported checkpoints ship in different layouts:
11
+
12
+ * `larry` (`larryvrh/MiniMax-H3-Turbo-Lora`) targets the reference (ComfyUI) module tree —
13
+ `blocks.N.attn.qkv_proj`, `blocks.N.mlp.fc1`, `token_refiner.blocks.N`, `final_layer.adaln_proj.linear` — with
14
+ `alpha == rank` (scale 1). Each delta gets the same transform the base weights got in the diffusers conversion
15
+ (scripts/convert_minimax_h3_to_diffusers.py, huggingface/diffusers#14371): fused-QKV row thirds onto
16
+ `attn.to_q/k/v`, the `SwiGLU` gate/value swap onto `ff.net.0.proj`, `fc2` -> `ff.net.2`, `blocks.` ->
17
+ `transformer_blocks.`, `token_refiner.blocks.` -> `token_refiner.refiner_blocks.`,
18
+ `final_layer.adaln_proj.linear` -> `norm_out.linear`. The row transforms are applied to `lora_B` directly
19
+ (rows of `B @ A` are rows of `B`), so no full delta is ever materialized at load.
20
+
21
+ * `lightx` / `lightx8` (`lightx2v/Minimax-h3-Turbo`) are PEFT checkpoints against the diffusers tree itself —
22
+ `transformer_blocks.N.attn.to_q.lora_A.default.weight` and friends — rank 128, `alpha == 8`, so the fold scale
23
+ is `8 / 128 = 0.0625` (matching `set_adapters(weights=1.0)` in their inference script). Keys map name-for-name.
24
+ `lightx` is the 4-step file, `lightx8` the 8-step v1.0 file.
25
+
26
+ * `realism` (`fal/MiniMax-H3-Realism-People-LoRA`) is a style LoRA, not a turbo one — realistic people, trigger
27
+ word `r34l1sm`. Same reference tree as larry under a `diffusion_model.` prefix, attention only (`qkv_proj` /
28
+ `out_proj` on the 52 blocks), rank 16, `alpha == rank` (the card's scale 1.0), so it goes through the same
29
+ `_larry_targets` mapping. It wants the full step count, not 4–6.
30
+
31
+ * `joyfox` (`joyfox/MiniMax-H3-Turbo`) is another 4-step turbo LoRA, ComfyUI-native like realism but covering
32
+ more of the tree: attention, MLP, the block and final AdaLN projections, and both output heads
33
+ (`final_layer.video_out` -> `proj_out`, `final_layer.audio_out` -> `audio_proj_out`). Ranks are mixed
34
+ (32 attention/MLP, 8 modulation/heads) with a scalar `alpha` per key, so each `lora_B` is prescaled by
35
+ `alpha / rank` at load (here always 1.0). Its `adaln_proj` entries are skipped: the Comfy-Org checkpoint it was
36
+ trained against gives the modulation projection an 8-dimensional input (`[96768, 8]`), while the diffusers
37
+ transformer projects the full 2688-dim time embedding (`[96768, 2688]`), so those deltas have no counterpart
38
+ to fold into.
39
+
40
+ * `H3-Facial-Realism-CloseUp` (`prithivMLmods/MiniMax-H3-Facial-Realism-CloseUp`) is a close-up facial realism
41
+ style LoRA, not a turbo one — it wants the full step count, not 4–6. Its layout is detected at load time:
42
+ PEFT keys against the diffusers tree map name-for-name, everything else is treated as the reference tree
43
+ (with or without the `diffusion_model.` prefix) and goes through `_larry_targets`. Per-key scalar `alpha`
44
+ values are folded into `lora_B` as `alpha / rank` when present, otherwise `alpha == rank` is assumed;
45
+ reference-tree `adaln_proj` entries whose input dim is not the 2688-dim time embedding of the diffusers tree
46
+ are skipped, like joyfox's.
47
+
48
+ `H3_LORA` selects the larry file (`off` skips loading it), `H3_LIGHTX=off` skips lightx, `H3_REALISM=off` skips
49
+ realism, `H3_JOYFOX=off` skips joyfox, `H3_LIGHTX8=off` skips the lightx 8-step file, `H3_FACIAL=off` skips the
50
+ facial realism LoRA, `H3_LORA_DEFAULT` picks which set starts folded, and `H3_LORA_STRENGTH` is the larry card's
51
+ sharpness/artifact dial.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ import os
57
+
58
+ import torch
59
+
60
+ LARRY_REPO = os.environ.get("H3_LORA_REPO", "larryvrh/MiniMax-H3-Turbo-Lora")
61
+ LARRY_FILE = os.environ.get("H3_LORA", "minimax_h3_turbo_v4_step600_ema.safetensors")
62
+ LIGHTX_REPO = os.environ.get("H3_LIGHTX_REPO", "lightx2v/Minimax-h3-Turbo")
63
+ LIGHTX_FILE = os.environ.get("H3_LIGHTX_FILE", "minimax_h3_fl2v_turbo_4step_v0.1.safetensors")
64
+ LIGHTX8_FILE = os.environ.get("H3_LIGHTX8_FILE", "minimax_h3_fl2v_turbo_8step_v1.0_bf16.safetensors")
65
+ LIGHTX_ALPHA = 8
66
+ REALISM_REPO = os.environ.get("H3_REALISM_REPO", "fal/MiniMax-H3-Realism-People-LoRA")
67
+ REALISM_FILE = os.environ.get("H3_REALISM_FILE", "h3-realism-people-t2v-i2v-r2v.safetensors")
68
+ JOYFOX_REPO = os.environ.get("H3_JOYFOX_REPO", "joyfox/MiniMax-H3-Turbo")
69
+ JOYFOX_FILE = os.environ.get("H3_JOYFOX_FILE", "minimax_h3_fl2va_4step_lora.safetensors")
70
+ FACIAL_REPO = os.environ.get("H3_FACIAL_REPO", "prithivMLmods/MiniMax-H3-Facial-Realism-CloseUp")
71
+ FACIAL_FILE = os.environ.get("H3_FACIAL_FILE", "minimax-h3-facial-realism-closeup-cp2000.safetensors")
72
+ FACIAL_NAME = "H3-Facial-Realism-CloseUp"
73
+ LARRY_STRENGTH = float(os.environ.get("H3_LORA_STRENGTH", "1.0"))
74
+ DEFAULT_LORA = os.environ.get("H3_LORA_DEFAULT", "larry")
75
+
76
+
77
+ def _larry_targets(name: str, b: torch.Tensor, inner_dim: int) -> list[tuple[str, torch.Tensor]]:
78
+ """Map one reference-tree base name and its `lora_B` onto diffusers parameter key + row-transformed B."""
79
+ if name.startswith("token_refiner.blocks."):
80
+ target = name.replace("token_refiner.blocks.", "token_refiner.refiner_blocks.", 1)
81
+ elif name.startswith("blocks."):
82
+ target = name.replace("blocks.", "transformer_blocks.", 1)
83
+ else:
84
+ target = name
85
+ target = target.replace("final_layer.adaln_proj.linear", "norm_out.linear")
86
+ target = target.replace("final_layer.video_out", "proj_out").replace("final_layer.audio_out", "audio_proj_out")
87
+
88
+ if target.endswith(".attn.qkv_proj"):
89
+ prefix = target.removesuffix("qkv_proj")
90
+ return [
91
+ (f"{prefix}to_{kind}.weight", part.contiguous())
92
+ for kind, part in zip(("q", "k", "v"), b.split(inner_dim, dim=0))
93
+ ]
94
+ if target.endswith(".mlp.fc1"):
95
+ gate, value = b.chunk(2, dim=0)
96
+ return [(target.replace(".mlp.fc1", ".ff.net.0.proj") + ".weight", torch.cat([value, gate]).contiguous())]
97
+ if target.endswith(".mlp.fc2"):
98
+ return [(target.replace(".mlp.fc2", ".ff.net.2") + ".weight", b)]
99
+ if target.endswith(".attn.out_proj"):
100
+ return [(target.replace(".attn.out_proj", ".attn.to_out.0") + ".weight", b)]
101
+ return [(target + ".weight", b)]
102
+
103
+
104
+ def _load_larry(inner_dim: int) -> dict:
105
+ from huggingface_hub import hf_hub_download
106
+ from safetensors.torch import load_file
107
+
108
+ lora = load_file(hf_hub_download(LARRY_REPO, LARRY_FILE))
109
+ bases = sorted({key.rsplit(".lora_", 1)[0] for key in lora})
110
+ entries = []
111
+ for name in bases:
112
+ a = lora[f"{name}.lora_A.weight"]
113
+ b = lora[f"{name}.lora_B.weight"]
114
+ entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
115
+ return {
116
+ "label": f"{LARRY_REPO}/{LARRY_FILE}",
117
+ "scale": LARRY_STRENGTH,
118
+ "entries": entries,
119
+ }
120
+
121
+
122
+ def _load_lightx(file: str = LIGHTX_FILE) -> dict:
123
+ """A diffusers-native PEFT checkpoint from `lightx2v/Minimax-h3-Turbo` — keys map name-for-name."""
124
+ from huggingface_hub import hf_hub_download
125
+ from safetensors.torch import load_file
126
+
127
+ lora = load_file(hf_hub_download(LIGHTX_REPO, file))
128
+ suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
129
+ bases = sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)})
130
+ ranks = {lora[f"{name}{suffix_a}"].shape[0] for name in bases}
131
+ if len(ranks) != 1:
132
+ raise ValueError(f"Mixed LoRA ranks in {file}: {sorted(ranks)}")
133
+ entries = [(f"{name}.weight", lora[f"{name}{suffix_a}"], lora[f"{name}{suffix_b}"]) for name in bases]
134
+ return {
135
+ "label": f"{LIGHTX_REPO}/{file}",
136
+ "scale": LIGHTX_ALPHA / ranks.pop(),
137
+ "entries": entries,
138
+ }
139
+
140
+
141
+ def _load_realism(inner_dim: int) -> dict:
142
+ from huggingface_hub import hf_hub_download
143
+ from safetensors.torch import load_file
144
+
145
+ lora = load_file(hf_hub_download(REALISM_REPO, REALISM_FILE))
146
+ bases = sorted({key.rsplit(".lora_", 1)[0].removeprefix("diffusion_model.") for key in lora})
147
+ entries = []
148
+ for name in bases:
149
+ a = lora[f"diffusion_model.{name}.lora_A.weight"]
150
+ b = lora[f"diffusion_model.{name}.lora_B.weight"]
151
+ entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
152
+ return {
153
+ "label": f"{REALISM_REPO}/{REALISM_FILE}",
154
+ "scale": 1.0,
155
+ "entries": entries,
156
+ }
157
+
158
+
159
+ def _load_joyfox(inner_dim: int) -> dict:
160
+ from huggingface_hub import hf_hub_download
161
+ from safetensors.torch import load_file
162
+
163
+ lora = load_file(hf_hub_download(JOYFOX_REPO, JOYFOX_FILE))
164
+ bases = sorted({key.rsplit(".lora_", 1)[0].removeprefix("diffusion_model.") for key in lora if ".lora_" in key})
165
+ entries = []
166
+ for name in bases:
167
+ if "adaln_proj" in name:
168
+ continue
169
+ prefixed = f"diffusion_model.{name}"
170
+ a = lora[f"{prefixed}.lora_A.weight"]
171
+ b = lora[f"{prefixed}.lora_B.weight"] * (lora[f"{prefixed}.alpha"].item() / a.shape[0])
172
+ entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
173
+ return {
174
+ "label": f"{JOYFOX_REPO}/{JOYFOX_FILE}",
175
+ "scale": 1.0,
176
+ "entries": entries,
177
+ }
178
+
179
+
180
+ def _load_facial(inner_dim: int) -> dict:
181
+ """Close-up facial realism style LoRA. The checkpoint layout is detected at load time: PEFT keys against the
182
+ diffusers tree map name-for-name, everything else is treated as the reference tree (with or without a
183
+ `diffusion_model.` prefix) and goes through `_larry_targets`. Per-key scalar `alpha` is folded into `lora_B`
184
+ as `alpha / rank`; reference-tree `adaln_proj` entries whose input dim is not the 2688-dim time embedding of
185
+ the diffusers tree are skipped, like joyfox's."""
186
+ from huggingface_hub import hf_hub_download
187
+ from safetensors.torch import load_file
188
+
189
+ lora = load_file(hf_hub_download(FACIAL_REPO, FACIAL_FILE))
190
+ label = f"{FACIAL_REPO}/{FACIAL_FILE}"
191
+
192
+ suffix_a, suffix_b = ".lora_A.default.weight", ".lora_B.default.weight"
193
+ if any(key.endswith(suffix_a) for key in lora):
194
+ entries = []
195
+ for base in sorted({key[: -len(suffix_a)] for key in lora if key.endswith(suffix_a)}):
196
+ a = lora[f"{base}{suffix_a}"]
197
+ b = lora[f"{base}{suffix_b}"]
198
+ alpha = lora.get(f"{base}.alpha")
199
+ if alpha is not None:
200
+ b = b * (alpha.item() / a.shape[0])
201
+ name = base.removeprefix("transformer.")
202
+ entries.append((f"{name}.weight", a, b))
203
+ return {"label": label, "scale": 1.0, "entries": entries}
204
+
205
+ prefix = "diffusion_model." if any(key.startswith("diffusion_model.") for key in lora) else ""
206
+ entries = []
207
+ for name in sorted({key.rsplit(".lora_", 1)[0].removeprefix(prefix) for key in lora if ".lora_" in key}):
208
+ full = f"{prefix}{name}"
209
+ a = lora[f"{full}.lora_A.weight"]
210
+ if "adaln_proj" in name and a.shape[1] != 2688:
211
+ continue
212
+ b = lora[f"{full}.lora_B.weight"]
213
+ alpha = lora.get(f"{full}.alpha")
214
+ if alpha is not None:
215
+ b = b * (alpha.item() / a.shape[0])
216
+ entries.extend((key, a, b_part) for key, b_part in _larry_targets(name, b, inner_dim))
217
+ return {"label": label, "scale": 1.0, "entries": entries}
218
+
219
+
220
+ def _apply(entries, params, sign: float) -> None:
221
+ for key, a, b in entries:
222
+ param = params.get(key)
223
+ if param is None:
224
+ raise KeyError(f"LoRA target `{key}` not found in the transformer")
225
+ delta = sign * (b.to(torch.float32) @ a.to(torch.float32))
226
+ param.data = (param.data.float() + delta.to(param.device)).to(param.dtype)
227
+
228
+
229
+ def available() -> list[str]:
230
+ """The LoRA sets that were loaded at startup, plus `off`."""
231
+ state = getattr(_PIPE_TRANSFORMER, "_lora_state", None) if _PIPE_TRANSFORMER is not None else None
232
+ return sorted(state["sets"]) + ["off"] if state else ["off"]
233
+
234
+
235
+ _PIPE_TRANSFORMER = None
236
+
237
+
238
+ def apply_lora(transformer) -> str | None:
239
+ """Load every enabled LoRA set, fold the default one into `transformer`, and stash the factors for per-request
240
+ switching. Returns a status line, or `None` when everything is disabled."""
241
+ global _PIPE_TRANSFORMER
242
+ _PIPE_TRANSFORMER = transformer
243
+
244
+ inner_dim = transformer.config.num_attention_heads * transformer.config.attention_head_dim
245
+ sets = {}
246
+ if LARRY_FILE.lower() not in ("", "off", "none"):
247
+ sets["larry"] = _load_larry(inner_dim)
248
+ if os.environ.get("H3_LIGHTX", "on").lower() not in ("", "off", "none"):
249
+ sets["lightx"] = _load_lightx()
250
+ if os.environ.get("H3_LIGHTX8", "on").lower() not in ("", "off", "none"):
251
+ sets["lightx8"] = _load_lightx(LIGHTX8_FILE)
252
+ if os.environ.get("H3_REALISM", "on").lower() not in ("", "off", "none"):
253
+ sets["realism"] = _load_realism(inner_dim)
254
+ if os.environ.get("H3_JOYFOX", "on").lower() not in ("", "off", "none"):
255
+ sets["joyfox"] = _load_joyfox(inner_dim)
256
+ if os.environ.get("H3_FACIAL", "on").lower() not in ("", "off", "none"):
257
+ sets[FACIAL_NAME] = _load_facial(inner_dim)
258
+ if not sets:
259
+ return None
260
+
261
+ active = DEFAULT_LORA if DEFAULT_LORA in sets else sorted(sets)[0]
262
+ params = dict(transformer.named_parameters())
263
+ _apply(sets[active]["entries"], params, sets[active]["scale"])
264
+ transformer._lora_state = {"active": active, "sets": sets}
265
+ return (
266
+ f"LoRAs loaded: "
267
+ + ", ".join(f"`{name}` ({spec['label']}, {len(spec['entries'])} weights)" for name, spec in sets.items())
268
+ + f" · active `{active}`"
269
+ )
270
+
271
+
272
+ def set_active(transformer, name: str) -> str:
273
+ """Switch the folded LoRA in place. No-op when the state already matches. Returns the active set."""
274
+ state = getattr(transformer, "_lora_state", None)
275
+ if state is None:
276
+ return "off"
277
+ name = name if name in state["sets"] else "off"
278
+ if state["active"] == name:
279
+ return name
280
+ params = dict(transformer.named_parameters())
281
+ if state["active"] != "off":
282
+ old = state["sets"][state["active"]]
283
+ _apply(old["entries"], params, -old["scale"])
284
+ if name != "off":
285
+ _apply(state["sets"][name]["entries"], params, state["sets"][name]["scale"])
286
+ state["active"] = name
287
+ return name
288
+
289
+
290
+ def set_enabled(transformer, enabled: bool) -> bool:
291
+ """Backwards-compatible boolean toggle over the default set."""
292
+ return set_active(transformer, DEFAULT_LORA if enabled else "off") != "off"
h3_split_blocks.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The halves of a **split** MiniMax-H3 deployment, for both of its checkpoint partitions.
2
+
3
+ MiniMax-H3 is 195.9 GiB in bfloat16 and a ZeroGPU Space is evicted at 150 GB of storage, so `MiniMaxH3Blocks` is cut
4
+ at its `text_encoder` step: the 62.14 GiB Qwen3-VL runs in the conditioner Space, everything else in a generator
5
+ Space, and `prompt_embeds` + `text_token_tags` is the whole wire format between them.
6
+
7
+ `resize` / `setup` run on **both** sides: they own no pretrained component, and each half needs the canvas and the
8
+ prepared keyframes or normalized references. Both conditioner halves also return the resolved `height` / `width` /
9
+ `num_frames`, which the generating half pins rather than re-deriving.
10
+
11
+ Two things the blocks leave to the caller: a keyframe reaches them EXIF-transposed and in RGB, and the `t2va` / `fl2va`
12
+ frame count is aligned to `17 * n + 5` before the call, since that arithmetic lives on the denoising side of the cut.
13
+ """
14
+
15
+ from diffusers.modular_pipelines.minimax_h3.before_encoder import MiniMaxH3Ref2VASetupStep
16
+ from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
17
+ from diffusers.modular_pipelines.minimax_h3.encoders import (
18
+ MiniMaxH3Ref2VAReferenceEncoderStep,
19
+ MiniMaxH3Ref2VATextEncoderStep,
20
+ MiniMaxH3TextEncoderStep,
21
+ )
22
+ from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
23
+ MiniMaxH3AutoKeyframeVaeEncoderStep,
24
+ MiniMaxH3AutoResizeStep,
25
+ MiniMaxH3CoreDenoiseStep,
26
+ MiniMaxH3DecodeStep,
27
+ MiniMaxH3Ref2VACoreDenoiseStep,
28
+ _generation_outputs,
29
+ )
30
+ from diffusers.modular_pipelines.modular_pipeline import SequentialPipelineBlocks
31
+ from diffusers.modular_pipelines.modular_pipeline_utils import OutputParam
32
+
33
+
34
+ def _wire_outputs(num_frames: bool = True) -> list[OutputParam]:
35
+ """The wire format of the split. `num_frames` is declared by the `ref2va` half alone, whose setup resolves one."""
36
+ return [
37
+ OutputParam.template("prompt_embeds"),
38
+ OutputParam("text_token_tags", description="The per-row modality tag of every row of `prompt_embeds`."),
39
+ OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
40
+ OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
41
+ *(
42
+ [OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5.")]
43
+ if num_frames
44
+ else []
45
+ ),
46
+ ]
47
+
48
+
49
+ class MiniMaxH3ConditionerBlocks(SequentialPipelineBlocks):
50
+ """The conditioner half of a split MiniMax-H3: the keyframes on the canvas plus the Qwen3-VL read at layer 50."""
51
+
52
+ model_name = "minimax-h3"
53
+ block_classes = [MiniMaxH3AutoResizeStep, MiniMaxH3TextEncoderStep]
54
+ block_names = ["resize", "text_encoder"]
55
+
56
+ @property
57
+ def description(self):
58
+ return (
59
+ "The conditioner half of a split MiniMax-H3 deployment: puts the keyframes onto the target canvas and "
60
+ "encodes MiniMax-H3's presentation of the request into the `prompt_embeds` / `text_token_tags` pair the "
61
+ "denoising half consumes. The frame count is the caller's to align."
62
+ )
63
+
64
+ @property
65
+ def outputs(self):
66
+ return _wire_outputs(num_frames=False)
67
+
68
+
69
+ class MiniMaxH3GeneratorBlocks(SequentialPipelineBlocks):
70
+ """The denoising half of a split MiniMax-H3: `MiniMaxH3Blocks` with its `text_encoder` step removed."""
71
+
72
+ model_name = "minimax-h3"
73
+ block_classes = [
74
+ MiniMaxH3AutoResizeStep,
75
+ MiniMaxH3AutoKeyframeVaeEncoderStep,
76
+ MiniMaxH3CoreDenoiseStep,
77
+ MiniMaxH3AfterDenoiseStep,
78
+ MiniMaxH3DecodeStep,
79
+ ]
80
+ block_names = ["resize", "vae_encoder", "denoise", "after_denoise", "decode"]
81
+
82
+ @property
83
+ def description(self):
84
+ return (
85
+ "The denoising half of a split MiniMax-H3 deployment: the `t2va` / `fl2va` branch of `MiniMaxH3Blocks` "
86
+ "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
87
+ "62.14 GiB Qwen3-VL conditioner is never loaded here."
88
+ )
89
+
90
+ @property
91
+ def outputs(self):
92
+ return _generation_outputs()
93
+
94
+
95
+ class MiniMaxH3Ref2VAConditionerBlocks(SequentialPipelineBlocks):
96
+ """The conditioner half of a split `ref2va`: the resolved plan plus the Qwen3-VL read at its 50th layer.
97
+
98
+ Component for component this is `MiniMaxH3ConditionerBlocks`, so one conditioner Space serves both partitions.
99
+ What differs is the presentation: `ref2va` prepends a label per reference and a vision block per image and per
100
+ merged video frame pair, so the references themselves have to reach this half.
101
+ """
102
+
103
+ model_name = "minimax-h3"
104
+ block_classes = [MiniMaxH3Ref2VASetupStep, MiniMaxH3Ref2VATextEncoderStep]
105
+ block_names = ["setup", "text_encoder"]
106
+
107
+ @property
108
+ def description(self):
109
+ return (
110
+ "The conditioner half of a split MiniMax-H3 `ref2va` deployment: resolves the request plan (canvas, frame "
111
+ "count, references normalized onto MiniMax-H3's own rates and resolutions) and encodes MiniMax-H3's "
112
+ "presentation of it into the `prompt_embeds` / `text_token_tags` pair the denoising half consumes."
113
+ )
114
+
115
+ @property
116
+ def outputs(self):
117
+ return _wire_outputs()
118
+
119
+
120
+ class MiniMaxH3Ref2VAGeneratorBlocks(SequentialPipelineBlocks):
121
+ """The denoising half of a split `ref2va`: the `ref2va` branch with its `text_encoder` step removed.
122
+
123
+ `reference_encoder` stays here, next to the two autoencoders it runs: its output shapes are where every reference
124
+ block's geometry in the packed layout comes from.
125
+ """
126
+
127
+ model_name = "minimax-h3"
128
+ block_classes = [
129
+ MiniMaxH3Ref2VASetupStep,
130
+ MiniMaxH3Ref2VAReferenceEncoderStep,
131
+ MiniMaxH3Ref2VACoreDenoiseStep,
132
+ MiniMaxH3AfterDenoiseStep,
133
+ MiniMaxH3DecodeStep,
134
+ ]
135
+ block_names = ["setup", "reference_encoder", "denoise", "after_denoise", "decode"]
136
+
137
+ @property
138
+ def description(self):
139
+ return (
140
+ "The denoising half of a split MiniMax-H3 `ref2va` deployment: the `ref2va` branch of `MiniMaxH3Blocks` "
141
+ "without its text-encoder step, so `prompt_embeds` and `text_token_tags` come in as inputs and the "
142
+ "62.14 GiB Qwen3-VL conditioner is never loaded here. The transformer is the `transformer_ref` partition."
143
+ )
144
+
145
+ @property
146
+ def outputs(self):
147
+ return _generation_outputs()