linoyts HF Staff commited on
Commit
dbc0953
·
verified ·
1 Parent(s): 84783f7

Ship a modular workflow that carries keyframes and references in one run

Browse files
Files changed (1) hide show
  1. combined_blocks.py +296 -0
combined_blocks.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A MiniMax-H3 modular workflow that accepts keyframes **and** references in the same run.
2
+
3
+ Why this exists. MiniMax-H3 denoises one packed sequence, and that sequence can hold keyframe conditioning rows and
4
+ reference conditioning rows at the same time. `diffusers`' shipped blocks cannot express it: their conditional steps
5
+ dispatch either/or (`select_block` checks `references` first), so a request carrying both is accepted and the
6
+ keyframes are **silently dropped** — no error, no warning, just a reference-only generation.
7
+
8
+ `MiniMaxH3CombinedBlocks` is `MiniMaxH3Blocks` with three of its conditional steps replaced by ones that know a
9
+ fourth shape. Nothing else changes: `t2va`, `fl2va` and `ref2va` requests take exactly the same path they always did,
10
+ and the denoising loop is untouched, because the conditioning rows are simply the leading rows of the sequence and it
11
+ only ever steps what comes after them.
12
+
13
+ from diffusers import ModularPipeline
14
+
15
+ pipe = ModularPipeline.from_pretrained(REPO, trust_remote_code=True, workflow="combined")
16
+ pipe.load_components(dtype=torch.bfloat16, trust_remote_code=True)
17
+ out = pipe(prompt=..., references=[...], image=first, last_image=last, num_frames=124, ...)
18
+
19
+ The layout itself lives in `combined_layout.py`, which is pinned by reproducing both shipped builders bit for bit in
20
+ their degenerate cases.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import numpy as np
26
+ import torch
27
+ from diffusers.modular_pipelines import ConditionalPipelineBlocks, SequentialPipelineBlocks
28
+ from diffusers.modular_pipelines.minimax_h3.before_denoise import (
29
+ MiniMaxH3PrepareConditionLatentsStep,
30
+ MiniMaxH3PrepareLatentsStep,
31
+ MiniMaxH3Ref2VAPrepareLatentsStep,
32
+ MiniMaxH3Ref2VAPrepareLayoutStep,
33
+ MiniMaxH3SetTimestepsStep,
34
+ )
35
+ from diffusers.modular_pipelines.minimax_h3.before_encoder import MiniMaxH3Ref2VASetupStep
36
+ from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
37
+ from diffusers.modular_pipelines.minimax_h3.denoise import MiniMaxH3Ref2VADenoiseStep
38
+ from diffusers.modular_pipelines.minimax_h3.encoders import (
39
+ MiniMaxH3Ref2VAReferenceEncoderStep,
40
+ encode_vae_condition,
41
+ )
42
+ from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
43
+ MiniMaxH3AutoDenoiseStep,
44
+ MiniMaxH3AutoTextEncoderStep,
45
+ MiniMaxH3AutoVaeEncoderStep,
46
+ MiniMaxH3AutoBeforeEncodeStep,
47
+ MiniMaxH3Blocks,
48
+ MiniMaxH3DecodeStep,
49
+ )
50
+ from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
51
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
52
+
53
+ from .combined_layout import build_combined_packed_sequence
54
+
55
+
56
+ def _anchors_of(image, last_image) -> tuple[str, ...]:
57
+ """Which end of the clip each keyframe is anchored to, in packed order."""
58
+ return tuple(name for name, value in (("first", image), ("last", last_image)) if value is not None)
59
+
60
+
61
+ class MiniMaxH3KeyframesOnCanvasStep(ModularPipelineBlocks):
62
+ """Put the keyframes on the canvas the reference setup already resolved.
63
+
64
+ The `fl2va` resize step *derives* the canvas from the keyframe's aspect ratio. Here the references have already
65
+ settled it, so the keyframes are stretched onto it — which is what `fl2va` does to a keyframe whose aspect does
66
+ not match the target anyway.
67
+ """
68
+
69
+ model_name = "minimax-h3"
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ return "Stretches the keyframes of a combined request onto the canvas the reference setup resolved."
74
+
75
+ @property
76
+ def inputs(self) -> list[InputParam]:
77
+ return [
78
+ InputParam(name="image", description="Keyframe the video starts from."),
79
+ InputParam(name="last_image", description="Keyframe the video ends on."),
80
+ InputParam(name="height", type_hint=int, required=True, description="Resolved height in pixels."),
81
+ InputParam(name="width", type_hint=int, required=True, description="Resolved width in pixels."),
82
+ ]
83
+
84
+ @property
85
+ def intermediate_outputs(self) -> list[OutputParam]:
86
+ return [
87
+ OutputParam("keyframes", type_hint=list, description="The keyframes on the target canvas, packed order."),
88
+ OutputParam("keyframe_anchors", type_hint=tuple, description="Which end each keyframe is anchored to."),
89
+ ]
90
+
91
+ @torch.no_grad()
92
+ def __call__(self, components, state: PipelineState) -> PipelineState:
93
+ block_state = self.get_block_state(state)
94
+ size = (block_state.width, block_state.height)
95
+ block_state.keyframe_anchors = _anchors_of(block_state.image, block_state.last_image)
96
+ block_state.keyframes = [
97
+ frame.convert("RGB").resize(size)
98
+ for frame in (block_state.image, block_state.last_image)
99
+ if frame is not None
100
+ ]
101
+ self.set_block_state(state, block_state)
102
+ return components, state
103
+
104
+
105
+ class MiniMaxH3CombinedKeyframeEncoderStep(ModularPipelineBlocks):
106
+ """Encode the keyframes and put them **in front of** the reference latents.
107
+
108
+ Order is the contract: the combined layout reserves `[text | keyframe cond | reference blocks | targets]`, and the
109
+ stock prepare-latents step concatenates this list in order — then asserts the rows it produced equal the rows the
110
+ layout reserved, so a mismatch raises instead of degrading quietly.
111
+ """
112
+
113
+ model_name = "minimax-h3"
114
+
115
+ @property
116
+ def description(self) -> str:
117
+ return "Encodes a combined request's keyframes and prepends them to the reference conditioning latents."
118
+
119
+ @property
120
+ def expected_components(self) -> list[ComponentSpec]:
121
+ return [ComponentSpec("vae")]
122
+
123
+ @property
124
+ def inputs(self) -> list[InputParam]:
125
+ return [
126
+ InputParam(name="keyframes", type_hint=list, required=True, description="Keyframes on the canvas."),
127
+ InputParam(name="condition_latents", type_hint=list, required=True,
128
+ description="Reference conditioning latents, packed order."),
129
+ ]
130
+
131
+ @property
132
+ def intermediate_outputs(self) -> list[OutputParam]:
133
+ return [
134
+ OutputParam("condition_latents", type_hint=list,
135
+ description="Keyframe latents first, then the reference latents."),
136
+ ]
137
+
138
+ @torch.no_grad()
139
+ def __call__(self, components, state: PipelineState) -> PipelineState:
140
+ block_state = self.get_block_state(state)
141
+ device = components._execution_device
142
+ keyframe_latents = [
143
+ encode_vae_condition(
144
+ components.vae,
145
+ torch.from_numpy(np.array(image)).to(device).permute(2, 0, 1)[None, :, None],
146
+ components.pixel_mean,
147
+ components.pixel_std,
148
+ components.keyframe_encode_seed,
149
+ )
150
+ for image in block_state.keyframes
151
+ ]
152
+ block_state.condition_latents = keyframe_latents + list(block_state.condition_latents)
153
+ self.set_block_state(state, block_state)
154
+ return components, state
155
+
156
+
157
+ class MiniMaxH3CombinedPrepareLayoutStep(MiniMaxH3Ref2VAPrepareLayoutStep):
158
+ """The `ref2va` layout step, with keyframe rows packed ahead of the reference blocks."""
159
+
160
+ @property
161
+ def description(self) -> str:
162
+ return (
163
+ "Resolves the latent shapes of a combined request and builds its packed layout — "
164
+ "`[text | keyframe conditions | reference blocks | target audio | target video]`. The references push the "
165
+ "target timeline out, so the keyframe anchors ride on the timeline their spans leave behind."
166
+ )
167
+
168
+ @property
169
+ def inputs(self) -> list[InputParam]:
170
+ return super().inputs + [
171
+ InputParam(name="keyframe_anchors", type_hint=tuple, default=(),
172
+ description="Which end of the video each keyframe is anchored to, in packed order."),
173
+ ]
174
+
175
+ # Overrides the parent's `@staticmethod` as a bound method, which is how the anchors reach the builder: the parent
176
+ # calls `self.build_ref2va_packed_sequence(...)` positionally and knows nothing about keyframes.
177
+ def build_ref2va_packed_sequence(self, *args, **kwargs):
178
+ return build_combined_packed_sequence(*args, keyframe_anchors=self._keyframe_anchors, **kwargs)
179
+
180
+ @torch.no_grad()
181
+ def __call__(self, components, state: PipelineState) -> PipelineState:
182
+ block_state = self.get_block_state(state)
183
+ self._keyframe_anchors = tuple(getattr(block_state, "keyframe_anchors", ()) or ())
184
+ try:
185
+ return super().__call__(components, state)
186
+ finally:
187
+ self._keyframe_anchors = ()
188
+
189
+
190
+ class MiniMaxH3CombinedSetupStep(SequentialPipelineBlocks):
191
+ model_name = "minimax-h3"
192
+ block_classes = [MiniMaxH3Ref2VASetupStep, MiniMaxH3KeyframesOnCanvasStep]
193
+ block_names = ["references", "keyframes"]
194
+
195
+ @property
196
+ def description(self) -> str:
197
+ return "Resolves the request plan from the references, then puts the keyframes on the resolved canvas."
198
+
199
+
200
+ class MiniMaxH3CombinedVaeEncoderStep(SequentialPipelineBlocks):
201
+ model_name = "minimax-h3"
202
+ block_classes = [MiniMaxH3Ref2VAReferenceEncoderStep, MiniMaxH3CombinedKeyframeEncoderStep]
203
+ block_names = ["references", "keyframes"]
204
+
205
+ @property
206
+ def description(self) -> str:
207
+ return "Encodes the references, then the keyframes, leaving the keyframe latents first in packed order."
208
+
209
+
210
+ class MiniMaxH3CombinedCoreDenoiseStep(SequentialPipelineBlocks):
211
+ model_name = "minimax-h3"
212
+ block_classes = [
213
+ MiniMaxH3CombinedPrepareLayoutStep,
214
+ MiniMaxH3PrepareConditionLatentsStep,
215
+ MiniMaxH3PrepareLatentsStep,
216
+ MiniMaxH3Ref2VAPrepareLatentsStep,
217
+ MiniMaxH3SetTimestepsStep,
218
+ MiniMaxH3Ref2VADenoiseStep,
219
+ MiniMaxH3AfterDenoiseStep,
220
+ ]
221
+ block_names = [
222
+ "prepare_layout",
223
+ "prepare_condition_latents",
224
+ "prepare_latents",
225
+ "prepare_ref_latents",
226
+ "set_timesteps",
227
+ "denoise",
228
+ "after_denoise",
229
+ ]
230
+
231
+ @property
232
+ def description(self) -> str:
233
+ return "Core denoising for a combined request: the `ref2va` chain over the combined packed layout."
234
+
235
+
236
+ def _is_combined(kwargs) -> bool:
237
+ return kwargs.get("references") is not None and (
238
+ kwargs.get("image") is not None or kwargs.get("last_image") is not None
239
+ )
240
+
241
+
242
+ class MiniMaxH3CombinedAutoBeforeEncodeStep(MiniMaxH3AutoBeforeEncodeStep):
243
+ block_classes = [MiniMaxH3CombinedSetupStep] + MiniMaxH3AutoBeforeEncodeStep.block_classes
244
+ block_names = ["combined"] + MiniMaxH3AutoBeforeEncodeStep.block_names
245
+
246
+ def select_block(self, **kwargs) -> str | None:
247
+ return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)
248
+
249
+
250
+ class MiniMaxH3CombinedAutoVaeEncoderStep(MiniMaxH3AutoVaeEncoderStep):
251
+ block_classes = [MiniMaxH3CombinedVaeEncoderStep] + MiniMaxH3AutoVaeEncoderStep.block_classes
252
+ block_names = ["combined"] + MiniMaxH3AutoVaeEncoderStep.block_names
253
+
254
+ def select_block(self, **kwargs) -> str | None:
255
+ return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)
256
+
257
+
258
+ class MiniMaxH3CombinedAutoDenoiseStep(MiniMaxH3AutoDenoiseStep):
259
+ block_classes = [MiniMaxH3CombinedCoreDenoiseStep] + MiniMaxH3AutoDenoiseStep.block_classes
260
+ block_names = ["combined"] + MiniMaxH3AutoDenoiseStep.block_names
261
+
262
+ def select_block(self, **kwargs) -> str | None:
263
+ return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)
264
+
265
+
266
+ class MiniMaxH3CombinedBlocks(MiniMaxH3Blocks):
267
+ """`MiniMaxH3Blocks` plus a fourth shape: keyframes and references in the same generation.
268
+
269
+ Supported workflows: `t2va`, `fl2va`, `ref2va` — unchanged — and `combined`, which needs `prompt`, `references`
270
+ and at least one of `image` / `last_image`. A combined request runs against the `transformer_ref` partition, the
271
+ same one `ref2va` uses.
272
+ """
273
+
274
+ block_classes = [
275
+ MiniMaxH3CombinedAutoBeforeEncodeStep,
276
+ MiniMaxH3AutoTextEncoderStep,
277
+ MiniMaxH3CombinedAutoVaeEncoderStep,
278
+ MiniMaxH3CombinedAutoDenoiseStep,
279
+ MiniMaxH3DecodeStep,
280
+ ]
281
+ block_names = ["before_encode", "text_encoder", "vae_encoder", "denoise", "decode"]
282
+ _workflow_map = dict(
283
+ MiniMaxH3Blocks._workflow_map,
284
+ combined=(
285
+ {"prompt": True, "references": True, "image": True},
286
+ {"prompt": True, "references": True, "last_image": True},
287
+ ),
288
+ )
289
+
290
+ @property
291
+ def description(self) -> str:
292
+ return (
293
+ "MiniMax-H3 blocks for joint video + audio generation, with the `t2va`, `fl2va` and `ref2va` workflows "
294
+ "unchanged and a fourth, `combined`, that carries keyframe *and* reference conditioning in one packed "
295
+ "sequence instead of dropping one of them."
296
+ )