File size: 12,880 Bytes
dbc0953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""A MiniMax-H3 modular workflow that accepts keyframes **and** references in the same run.

Why this exists. MiniMax-H3 denoises one packed sequence, and that sequence can hold keyframe conditioning rows and
reference conditioning rows at the same time. `diffusers`' shipped blocks cannot express it: their conditional steps
dispatch either/or (`select_block` checks `references` first), so a request carrying both is accepted and the
keyframes are **silently dropped** — no error, no warning, just a reference-only generation.

`MiniMaxH3CombinedBlocks` is `MiniMaxH3Blocks` with three of its conditional steps replaced by ones that know a
fourth shape. Nothing else changes: `t2va`, `fl2va` and `ref2va` requests take exactly the same path they always did,
and the denoising loop is untouched, because the conditioning rows are simply the leading rows of the sequence and it
only ever steps what comes after them.

    from diffusers import ModularPipeline

    pipe = ModularPipeline.from_pretrained(REPO, trust_remote_code=True, workflow="combined")
    pipe.load_components(dtype=torch.bfloat16, trust_remote_code=True)
    out = pipe(prompt=..., references=[...], image=first, last_image=last, num_frames=124, ...)

The layout itself lives in `combined_layout.py`, which is pinned by reproducing both shipped builders bit for bit in
their degenerate cases.
"""

from __future__ import annotations

import numpy as np
import torch
from diffusers.modular_pipelines import ConditionalPipelineBlocks, SequentialPipelineBlocks
from diffusers.modular_pipelines.minimax_h3.before_denoise import (
    MiniMaxH3PrepareConditionLatentsStep,
    MiniMaxH3PrepareLatentsStep,
    MiniMaxH3Ref2VAPrepareLatentsStep,
    MiniMaxH3Ref2VAPrepareLayoutStep,
    MiniMaxH3SetTimestepsStep,
)
from diffusers.modular_pipelines.minimax_h3.before_encoder import MiniMaxH3Ref2VASetupStep
from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
from diffusers.modular_pipelines.minimax_h3.denoise import MiniMaxH3Ref2VADenoiseStep
from diffusers.modular_pipelines.minimax_h3.encoders import (
    MiniMaxH3Ref2VAReferenceEncoderStep,
    encode_vae_condition,
)
from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
    MiniMaxH3AutoDenoiseStep,
    MiniMaxH3AutoTextEncoderStep,
    MiniMaxH3AutoVaeEncoderStep,
    MiniMaxH3AutoBeforeEncodeStep,
    MiniMaxH3Blocks,
    MiniMaxH3DecodeStep,
)
from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam

from .combined_layout import build_combined_packed_sequence


def _anchors_of(image, last_image) -> tuple[str, ...]:
    """Which end of the clip each keyframe is anchored to, in packed order."""
    return tuple(name for name, value in (("first", image), ("last", last_image)) if value is not None)


class MiniMaxH3KeyframesOnCanvasStep(ModularPipelineBlocks):
    """Put the keyframes on the canvas the reference setup already resolved.

    The `fl2va` resize step *derives* the canvas from the keyframe's aspect ratio. Here the references have already
    settled it, so the keyframes are stretched onto it — which is what `fl2va` does to a keyframe whose aspect does
    not match the target anyway.
    """

    model_name = "minimax-h3"

    @property
    def description(self) -> str:
        return "Stretches the keyframes of a combined request onto the canvas the reference setup resolved."

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam(name="image", description="Keyframe the video starts from."),
            InputParam(name="last_image", description="Keyframe the video ends on."),
            InputParam(name="height", type_hint=int, required=True, description="Resolved height in pixels."),
            InputParam(name="width", type_hint=int, required=True, description="Resolved width in pixels."),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam("keyframes", type_hint=list, description="The keyframes on the target canvas, packed order."),
            OutputParam("keyframe_anchors", type_hint=tuple, description="Which end each keyframe is anchored to."),
        ]

    @torch.no_grad()
    def __call__(self, components, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)
        size = (block_state.width, block_state.height)
        block_state.keyframe_anchors = _anchors_of(block_state.image, block_state.last_image)
        block_state.keyframes = [
            frame.convert("RGB").resize(size)
            for frame in (block_state.image, block_state.last_image)
            if frame is not None
        ]
        self.set_block_state(state, block_state)
        return components, state


class MiniMaxH3CombinedKeyframeEncoderStep(ModularPipelineBlocks):
    """Encode the keyframes and put them **in front of** the reference latents.

    Order is the contract: the combined layout reserves `[text | keyframe cond | reference blocks | targets]`, and the
    stock prepare-latents step concatenates this list in order — then asserts the rows it produced equal the rows the
    layout reserved, so a mismatch raises instead of degrading quietly.
    """

    model_name = "minimax-h3"

    @property
    def description(self) -> str:
        return "Encodes a combined request's keyframes and prepends them to the reference conditioning latents."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [ComponentSpec("vae")]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam(name="keyframes", type_hint=list, required=True, description="Keyframes on the canvas."),
            InputParam(name="condition_latents", type_hint=list, required=True,
                       description="Reference conditioning latents, packed order."),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam("condition_latents", type_hint=list,
                        description="Keyframe latents first, then the reference latents."),
        ]

    @torch.no_grad()
    def __call__(self, components, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)
        device = components._execution_device
        keyframe_latents = [
            encode_vae_condition(
                components.vae,
                torch.from_numpy(np.array(image)).to(device).permute(2, 0, 1)[None, :, None],
                components.pixel_mean,
                components.pixel_std,
                components.keyframe_encode_seed,
            )
            for image in block_state.keyframes
        ]
        block_state.condition_latents = keyframe_latents + list(block_state.condition_latents)
        self.set_block_state(state, block_state)
        return components, state


class MiniMaxH3CombinedPrepareLayoutStep(MiniMaxH3Ref2VAPrepareLayoutStep):
    """The `ref2va` layout step, with keyframe rows packed ahead of the reference blocks."""

    @property
    def description(self) -> str:
        return (
            "Resolves the latent shapes of a combined request and builds its packed layout — "
            "`[text | keyframe conditions | reference blocks | target audio | target video]`. The references push the "
            "target timeline out, so the keyframe anchors ride on the timeline their spans leave behind."
        )

    @property
    def inputs(self) -> list[InputParam]:
        return super().inputs + [
            InputParam(name="keyframe_anchors", type_hint=tuple, default=(),
                       description="Which end of the video each keyframe is anchored to, in packed order."),
        ]

    # Overrides the parent's `@staticmethod` as a bound method, which is how the anchors reach the builder: the parent
    # calls `self.build_ref2va_packed_sequence(...)` positionally and knows nothing about keyframes.
    def build_ref2va_packed_sequence(self, *args, **kwargs):
        return build_combined_packed_sequence(*args, keyframe_anchors=self._keyframe_anchors, **kwargs)

    @torch.no_grad()
    def __call__(self, components, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)
        self._keyframe_anchors = tuple(getattr(block_state, "keyframe_anchors", ()) or ())
        try:
            return super().__call__(components, state)
        finally:
            self._keyframe_anchors = ()


class MiniMaxH3CombinedSetupStep(SequentialPipelineBlocks):
    model_name = "minimax-h3"
    block_classes = [MiniMaxH3Ref2VASetupStep, MiniMaxH3KeyframesOnCanvasStep]
    block_names = ["references", "keyframes"]

    @property
    def description(self) -> str:
        return "Resolves the request plan from the references, then puts the keyframes on the resolved canvas."


class MiniMaxH3CombinedVaeEncoderStep(SequentialPipelineBlocks):
    model_name = "minimax-h3"
    block_classes = [MiniMaxH3Ref2VAReferenceEncoderStep, MiniMaxH3CombinedKeyframeEncoderStep]
    block_names = ["references", "keyframes"]

    @property
    def description(self) -> str:
        return "Encodes the references, then the keyframes, leaving the keyframe latents first in packed order."


class MiniMaxH3CombinedCoreDenoiseStep(SequentialPipelineBlocks):
    model_name = "minimax-h3"
    block_classes = [
        MiniMaxH3CombinedPrepareLayoutStep,
        MiniMaxH3PrepareConditionLatentsStep,
        MiniMaxH3PrepareLatentsStep,
        MiniMaxH3Ref2VAPrepareLatentsStep,
        MiniMaxH3SetTimestepsStep,
        MiniMaxH3Ref2VADenoiseStep,
        MiniMaxH3AfterDenoiseStep,
    ]
    block_names = [
        "prepare_layout",
        "prepare_condition_latents",
        "prepare_latents",
        "prepare_ref_latents",
        "set_timesteps",
        "denoise",
        "after_denoise",
    ]

    @property
    def description(self) -> str:
        return "Core denoising for a combined request: the `ref2va` chain over the combined packed layout."


def _is_combined(kwargs) -> bool:
    return kwargs.get("references") is not None and (
        kwargs.get("image") is not None or kwargs.get("last_image") is not None
    )


class MiniMaxH3CombinedAutoBeforeEncodeStep(MiniMaxH3AutoBeforeEncodeStep):
    block_classes = [MiniMaxH3CombinedSetupStep] + MiniMaxH3AutoBeforeEncodeStep.block_classes
    block_names = ["combined"] + MiniMaxH3AutoBeforeEncodeStep.block_names

    def select_block(self, **kwargs) -> str | None:
        return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)


class MiniMaxH3CombinedAutoVaeEncoderStep(MiniMaxH3AutoVaeEncoderStep):
    block_classes = [MiniMaxH3CombinedVaeEncoderStep] + MiniMaxH3AutoVaeEncoderStep.block_classes
    block_names = ["combined"] + MiniMaxH3AutoVaeEncoderStep.block_names

    def select_block(self, **kwargs) -> str | None:
        return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)


class MiniMaxH3CombinedAutoDenoiseStep(MiniMaxH3AutoDenoiseStep):
    block_classes = [MiniMaxH3CombinedCoreDenoiseStep] + MiniMaxH3AutoDenoiseStep.block_classes
    block_names = ["combined"] + MiniMaxH3AutoDenoiseStep.block_names

    def select_block(self, **kwargs) -> str | None:
        return "combined" if _is_combined(kwargs) else super().select_block(**kwargs)


class MiniMaxH3CombinedBlocks(MiniMaxH3Blocks):
    """`MiniMaxH3Blocks` plus a fourth shape: keyframes and references in the same generation.

    Supported workflows: `t2va`, `fl2va`, `ref2va` — unchanged — and `combined`, which needs `prompt`, `references`
    and at least one of `image` / `last_image`. A combined request runs against the `transformer_ref` partition, the
    same one `ref2va` uses.
    """

    block_classes = [
        MiniMaxH3CombinedAutoBeforeEncodeStep,
        MiniMaxH3AutoTextEncoderStep,
        MiniMaxH3CombinedAutoVaeEncoderStep,
        MiniMaxH3CombinedAutoDenoiseStep,
        MiniMaxH3DecodeStep,
    ]
    block_names = ["before_encode", "text_encoder", "vae_encoder", "denoise", "decode"]
    _workflow_map = dict(
        MiniMaxH3Blocks._workflow_map,
        combined=(
            {"prompt": True, "references": True, "image": True},
            {"prompt": True, "references": True, "last_image": True},
        ),
    )

    @property
    def description(self) -> str:
        return (
            "MiniMax-H3 blocks for joint video + audio generation, with the `t2va`, `fl2va` and `ref2va` workflows "
            "unchanged and a fourth, `combined`, that carries keyframe *and* reference conditioning in one packed "
            "sequence instead of dropping one of them."
        )