dagloop5 commited on
Commit
34ea185
·
verified ·
1 Parent(s): 8b0f7dd

Upload h3_keyframe_control.py

Browse files
Files changed (1) hide show
  1. h3_keyframe_control.py +305 -0
h3_keyframe_control.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Per-keyframe injection frame and strength for the `fl2va` branch of the staged-denoising Space.
2
+
3
+ Two independent knobs per keyframe, both optional (defaults reproduce the stock pipeline exactly):
4
+
5
+ * **Injection frame.** A keyframe's conditioning rows carry one rotary time, `anchor_time`. Stock supports exactly
6
+ two values: "first" = `T` and "last" = `T + 5/3 * (N - 1)`, where `T` is the text-token count and `N` the number of
7
+ pixel frames the latent grid spans. The rotary clock runs at 5/3 units per *pixel* frame, so injecting at pixel
8
+ frame `p` is just `anchor_time = T + 5/3 * p`. Nothing else about the keyframe changes.
9
+ NOTE: the released model was only trained with the two stock positions. Other positions are untested.
10
+
11
+ * **Strength.** A keyframe is held at `t = 0.999` ("just short of clean": `x_t = t*x0 + (1-t)*noise`). Strength `s`
12
+ in `[0, 1]` maps to that `t`: `s = 1` is stock (0.999), `s = 0` is pure noise (`t = 0`, the keyframe carries no
13
+ information). The same `t` is what the rows are presented to the transformer at, so the label stays honest.
14
+
15
+ Blocks:
16
+ MiniMaxH3KeyframePlacementStep - rewrites the keyframe rows' rotary time after `prepare_layout`
17
+ MiniMaxH3KeyframeConditionStep - noises each keyframe to its own level instead of the shared 0.999
18
+ (build_row_timesteps patch) - presents each keyframe's rows at its own level, in every sampler
19
+
20
+ The patch of `MiniMaxH3SetTimestepsStep.build_row_timesteps` is deliberate: the dpmpp_2s / SDE / seeds_2 samplers
21
+ build their own second-evaluation row timesteps through that same static method, so patching it keeps all of them
22
+ consistent with the plan without touching their files. It is a no-op for anything with no conditioning rows
23
+ (`t2va`, the momentum pipeline) and whenever no per-keyframe levels were set for the request.
24
+ """
25
+
26
+ import torch
27
+
28
+ from diffusers.modular_pipelines.minimax_h3.before_denoise import (
29
+ _ROPE_FRAME_RESCALE,
30
+ _ROPE_FRAMES_PER_LATENT,
31
+ MiniMaxH3FL2VAPrepareLatentsStep,
32
+ MiniMaxH3PrepareConditionLatentsStep,
33
+ MiniMaxH3PrepareLayoutStep,
34
+ MiniMaxH3SetTimestepsStep,
35
+ patchify_video_latents,
36
+ )
37
+ from diffusers.modular_pipelines.minimax_h3.before_denoise import MiniMaxH3PrepareLatentsStep
38
+ from diffusers.modular_pipelines.minimax_h3.decoders import MiniMaxH3AfterDenoiseStep
39
+ from diffusers.modular_pipelines.minimax_h3.denoise import MiniMaxH3DenoiseStep
40
+ from diffusers.modular_pipelines.minimax_h3.modular_blocks_minimax_h3 import (
41
+ MiniMaxH3CoreDenoiseStep,
42
+ MiniMaxH3DecodeStep,
43
+ )
44
+ from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
45
+ from diffusers.modular_pipelines.modular_pipeline import (
46
+ ConditionalPipelineBlocks,
47
+ ModularPipelineBlocks,
48
+ PipelineState,
49
+ SequentialPipelineBlocks,
50
+ )
51
+ from diffusers.modular_pipelines.modular_pipeline_utils import InputParam, OutputParam
52
+ from diffusers.utils.torch_utils import randn_tensor
53
+
54
+ from h3_split_blocks import MiniMaxH3SplitBeforeEncodeStep, MiniMaxH3SplitVaeEncoderStep
55
+
56
+
57
+ # ---------------------------------------------------------------------------------------------------------------
58
+ # Per-request state shared with build_row_timesteps. Set by the condition step, read by the patched static method.
59
+ # ---------------------------------------------------------------------------------------------------------------
60
+ _LEVELS: tuple[float, ...] | None = None
61
+ _AUG: float = 0.999
62
+
63
+
64
+ def strength_to_level(strength: float, aug: float) -> float:
65
+ """Slider strength in [0, 1] -> the keyframe's `t`. 1 (or anything above `aug`) is stock."""
66
+ return max(0.0, min(float(strength), float(aug)))
67
+
68
+
69
+ _original_build_row_timesteps = MiniMaxH3SetTimestepsStep.build_row_timesteps
70
+
71
+
72
+ def _build_row_timesteps(
73
+ video_indices,
74
+ audio_indices,
75
+ num_condition_video_rows,
76
+ num_condition_audio_rows,
77
+ num_text_tokens,
78
+ video_timestep,
79
+ audio_timestep,
80
+ condition_video_timestep,
81
+ condition_audio_timestep,
82
+ ):
83
+ levels = _LEVELS
84
+ n = len(levels) if levels else 0
85
+ if n == 0 or num_condition_video_rows <= 0 or num_condition_video_rows % n != 0:
86
+ return _original_build_row_timesteps(
87
+ video_indices, audio_indices, num_condition_video_rows, num_condition_audio_rows, num_text_tokens,
88
+ video_timestep, audio_timestep, condition_video_timestep, condition_audio_timestep,
89
+ )
90
+
91
+ sequence_length = int(video_indices.numel() + audio_indices.numel() + num_text_tokens)
92
+ row_timesteps = torch.full((sequence_length,), video_timestep, dtype=torch.float32)
93
+ rows_per_keyframe = num_condition_video_rows // n
94
+ for i, level in enumerate(levels):
95
+ block = video_indices[i * rows_per_keyframe : (i + 1) * rows_per_keyframe]
96
+ # At or above the stock level the keyframe is presented exactly as stock does (max with the current step's
97
+ # timestep). Below it, the keyframe really is that noisy and stays labelled that way for every step.
98
+ row_timesteps[block] = condition_video_timestep if level >= _AUG else level
99
+ row_timesteps[audio_indices[num_condition_audio_rows:]] = audio_timestep
100
+ row_timesteps[audio_indices[:num_condition_audio_rows]] = condition_audio_timestep
101
+ return torch.unique(row_timesteps, sorted=True, return_inverse=True)
102
+
103
+
104
+ MiniMaxH3SetTimestepsStep.build_row_timesteps = staticmethod(_build_row_timesteps)
105
+
106
+
107
+ # ---------------------------------------------------------------------------------------------------------------
108
+ # Blocks
109
+ # ---------------------------------------------------------------------------------------------------------------
110
+ class MiniMaxH3KeyframePlacementStep(ModularPipelineBlocks):
111
+ model_name = "minimax-h3"
112
+
113
+ @property
114
+ def description(self) -> str:
115
+ return (
116
+ "Moves each keyframe's conditioning rows to a chosen pixel frame on the rotary time axis. The stock "
117
+ "layout only knows 'first' (frame 0) and 'last' (the final frame); `anchor_time = T + 5/3 * frame`."
118
+ )
119
+
120
+ @property
121
+ def inputs(self) -> list[InputParam]:
122
+ return [
123
+ InputParam(name="position_ids", type_hint=torch.Tensor, required=True),
124
+ InputParam(name="text_indices", type_hint=torch.Tensor, required=True),
125
+ InputParam(name="num_latent_frames", type_hint=int, required=True),
126
+ InputParam(name="latent_height", type_hint=int, required=True),
127
+ InputParam(name="latent_width", type_hint=int, required=True),
128
+ InputParam(name="keyframe_anchors", type_hint=tuple, default=()),
129
+ InputParam(
130
+ name="keyframe_frames",
131
+ type_hint=tuple,
132
+ default=None,
133
+ description="Requested pixel frame per keyframe, in packed order. None / missing entries keep stock.",
134
+ ),
135
+ ]
136
+
137
+ @property
138
+ def intermediate_outputs(self) -> list[OutputParam]:
139
+ return [OutputParam("position_ids", type_hint=torch.Tensor)]
140
+
141
+ @torch.no_grad()
142
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState):
143
+ block_state = self.get_block_state(state)
144
+ anchors = tuple(block_state.keyframe_anchors or ())
145
+ frames = block_state.keyframe_frames
146
+ if anchors and frames is not None:
147
+ _, patch_h, patch_w = components.patch_size
148
+ rows_per_frame = (block_state.latent_height // patch_h) * (block_state.latent_width // patch_w)
149
+ num_text = int(block_state.text_indices.numel())
150
+ pattern = _ROPE_FRAMES_PER_LATENT
151
+ total_pixel_frames = sum(pattern[i % len(pattern)] for i in range(block_state.num_latent_frames))
152
+ last_frame = total_pixel_frames - 1
153
+ position_ids = block_state.position_ids
154
+ for index, (anchor, requested) in enumerate(zip(anchors, frames)):
155
+ if requested is None:
156
+ continue
157
+ frame = max(0, min(int(requested), last_frame))
158
+ # Defaults reproduce the stock anchors bit for bit: leave those rows untouched.
159
+ if (anchor == "first" and frame <= 0) or (anchor == "last" and frame >= last_frame):
160
+ continue
161
+ rows = slice(num_text + index * rows_per_frame, num_text + (index + 1) * rows_per_frame)
162
+ position_ids[rows, 0] = float(num_text) + _ROPE_FRAME_RESCALE * frame
163
+ block_state.position_ids = position_ids
164
+ self.set_block_state(state, block_state)
165
+ return components, state
166
+
167
+
168
+ class MiniMaxH3KeyframeConditionStep(MiniMaxH3PrepareConditionLatentsStep):
169
+ model_name = "minimax-h3"
170
+
171
+ @property
172
+ def description(self) -> str:
173
+ return (
174
+ "`MiniMaxH3PrepareConditionLatentsStep` with a noise level per keyframe: `x_t = t*x0 + (1-t)*noise`, "
175
+ "`t = strength` capped at the stock 0.999. Draw order is unchanged (one draw per condition)."
176
+ )
177
+
178
+ @property
179
+ def inputs(self) -> list[InputParam]:
180
+ return super().inputs + [
181
+ InputParam(
182
+ name="keyframe_strengths",
183
+ type_hint=tuple,
184
+ default=None,
185
+ description="Strength in [0, 1] per keyframe, in packed order. None keeps the stock 0.999.",
186
+ )
187
+ ]
188
+
189
+ @property
190
+ def intermediate_outputs(self) -> list[OutputParam]:
191
+ return super().intermediate_outputs
192
+
193
+ @torch.no_grad()
194
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState):
195
+ global _LEVELS, _AUG
196
+
197
+ block_state = self.get_block_state(state)
198
+ device = components._execution_device
199
+ patch_size = components.patch_size
200
+ aug = float(components.keyframe_noise_aug)
201
+ strengths = block_state.keyframe_strengths
202
+
203
+ levels = []
204
+ packed = []
205
+ for index, condition in enumerate(block_state.condition_latents):
206
+ noise = randn_tensor(condition.shape, generator=block_state.generator, device=device, dtype=torch.float32)
207
+ strength = 1.0 if strengths is None or index >= len(strengths) or strengths[index] is None else strengths[index]
208
+ level = strength_to_level(strength, aug)
209
+ levels.append(level)
210
+ noised = components.scheduler.scale_noise(condition.to(device), level, noise)
211
+ packed.append(patchify_video_latents(noised, patch_size))
212
+ block_state.condition_rows = torch.cat(packed)
213
+
214
+ if block_state.condition_rows.shape[0] != block_state.num_condition_video_rows:
215
+ raise ValueError(
216
+ f"The layout reserved {block_state.num_condition_video_rows} conditioning rows but the encoded "
217
+ f"conditioning latents pack into {block_state.condition_rows.shape[0]}."
218
+ )
219
+
220
+ _AUG = aug
221
+ _LEVELS = tuple(levels) if any(level < aug for level in levels) else None
222
+
223
+ self.set_block_state(state, block_state)
224
+ return components, state
225
+
226
+
227
+ class MiniMaxH3KeyframeControlCoreDenoiseStep(SequentialPipelineBlocks):
228
+ """`MiniMaxH3FL2VACoreDenoiseStep` with the placement step after the layout and the per-keyframe condition step."""
229
+
230
+ model_name = "minimax-h3"
231
+ block_classes = [
232
+ MiniMaxH3PrepareLayoutStep,
233
+ MiniMaxH3KeyframePlacementStep,
234
+ MiniMaxH3KeyframeConditionStep,
235
+ MiniMaxH3PrepareLatentsStep,
236
+ MiniMaxH3FL2VAPrepareLatentsStep,
237
+ MiniMaxH3SetTimestepsStep,
238
+ MiniMaxH3DenoiseStep,
239
+ MiniMaxH3AfterDenoiseStep,
240
+ ]
241
+ block_names = [
242
+ "prepare_layout",
243
+ "keyframe_placement",
244
+ "prepare_condition_latents",
245
+ "prepare_latents",
246
+ "prepare_latents_fl2va",
247
+ "set_timesteps",
248
+ "denoise",
249
+ "after_denoise",
250
+ ]
251
+
252
+ @property
253
+ def description(self) -> str:
254
+ return "Core denoising workflow for `fl2va` with per-keyframe injection frame and strength."
255
+
256
+ @property
257
+ def outputs(self):
258
+ return [
259
+ OutputParam("latents", type_hint=torch.Tensor, description="The generated video latents."),
260
+ OutputParam("audio_latents", type_hint=torch.Tensor, description="The generated audio latents."),
261
+ ]
262
+
263
+
264
+ class MiniMaxH3KeyframeControlDenoiseStep(ConditionalPipelineBlocks):
265
+ """Split-deployment denoise selector with the keyframe-control `fl2va` core in place of the stock one."""
266
+
267
+ model_name = "minimax-h3"
268
+ block_classes = [MiniMaxH3KeyframeControlCoreDenoiseStep, MiniMaxH3CoreDenoiseStep]
269
+ block_names = ["fl2va", "t2va"]
270
+ block_trigger_inputs = ["image", "last_image"]
271
+ default_block_name = "t2va"
272
+
273
+ def select_block(self, **kwargs) -> str | None:
274
+ if kwargs.get("image") is not None or kwargs.get("last_image") is not None:
275
+ return "fl2va"
276
+ return None
277
+
278
+ @property
279
+ def description(self):
280
+ return "`fl2va` (keyframe control) when a keyframe is provided, `t2va` otherwise."
281
+
282
+
283
+ class MiniMaxH3KeyframeGeneratorBlocks(SequentialPipelineBlocks):
284
+ """Drop-in for `h3_split_blocks.MiniMaxH3GeneratorBlocks`, with keyframe injection frame / strength."""
285
+
286
+ model_name = "minimax-h3"
287
+ block_classes = [
288
+ MiniMaxH3SplitBeforeEncodeStep,
289
+ MiniMaxH3SplitVaeEncoderStep,
290
+ MiniMaxH3KeyframeControlDenoiseStep,
291
+ MiniMaxH3DecodeStep,
292
+ ]
293
+ block_names = ["resize", "vae_encoder", "denoise", "decode"]
294
+
295
+ @property
296
+ def description(self):
297
+ return "Denoising half of the split deployment with per-keyframe injection frame and strength."
298
+
299
+ @property
300
+ def outputs(self):
301
+ return [
302
+ OutputParam.template("videos", description="The generated video."),
303
+ OutputParam("audio", type_hint=torch.Tensor, description="The soundtrack, `(1, 2, num_samples)`."),
304
+ OutputParam("sampling_rate", type_hint=int, description="Sample rate of the soundtrack in Hz."),
305
+ ]