multimodalart HF Staff commited on
Commit
cd458ae
·
verified ·
1 Parent(s): 186aa49

Sync the split MiniMax-H3 Spaces (part 2)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. diffusers/modular_pipelines/flux2/decoders.py +185 -0
  2. diffusers/modular_pipelines/flux2/denoise.py +501 -0
  3. diffusers/modular_pipelines/flux2/encoders.py +608 -0
  4. diffusers/modular_pipelines/flux2/inputs.py +242 -0
  5. diffusers/modular_pipelines/flux2/modular_blocks_flux2.py +356 -0
  6. diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein.py +399 -0
  7. diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein_base.py +413 -0
  8. diffusers/modular_pipelines/flux2/modular_pipeline.py +99 -0
  9. diffusers/modular_pipelines/helios/__init__.py +59 -0
  10. diffusers/modular_pipelines/helios/before_denoise.py +836 -0
  11. diffusers/modular_pipelines/helios/decoders.py +112 -0
  12. diffusers/modular_pipelines/helios/denoise.py +1069 -0
  13. diffusers/modular_pipelines/helios/encoders.py +392 -0
  14. diffusers/modular_pipelines/helios/modular_blocks_helios.py +542 -0
  15. diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid.py +520 -0
  16. diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid_distilled.py +530 -0
  17. diffusers/modular_pipelines/helios/modular_pipeline.py +87 -0
  18. diffusers/modular_pipelines/hunyuan_video1_5/__init__.py +49 -0
  19. diffusers/modular_pipelines/hunyuan_video1_5/before_denoise.py +324 -0
  20. diffusers/modular_pipelines/hunyuan_video1_5/decoders.py +70 -0
  21. diffusers/modular_pipelines/hunyuan_video1_5/denoise.py +401 -0
  22. diffusers/modular_pipelines/hunyuan_video1_5/encoders.py +441 -0
  23. diffusers/modular_pipelines/hunyuan_video1_5/modular_blocks_hunyuan_video1_5.py +535 -0
  24. diffusers/modular_pipelines/hunyuan_video1_5/modular_pipeline.py +90 -0
  25. diffusers/modular_pipelines/ideogram4/__init__.py +47 -0
  26. diffusers/modular_pipelines/ideogram4/before_denoise.py +558 -0
  27. diffusers/modular_pipelines/ideogram4/decoders.py +112 -0
  28. diffusers/modular_pipelines/ideogram4/denoise.py +363 -0
  29. diffusers/modular_pipelines/ideogram4/encoders.py +327 -0
  30. diffusers/modular_pipelines/ideogram4/modular_blocks_ideogram4.py +185 -0
  31. diffusers/modular_pipelines/ideogram4/modular_pipeline.py +46 -0
  32. diffusers/modular_pipelines/krea2/__init__.py +49 -0
  33. diffusers/modular_pipelines/krea2/before_denoise.py +590 -0
  34. diffusers/modular_pipelines/krea2/decoders.py +121 -0
  35. diffusers/modular_pipelines/krea2/denoise.py +369 -0
  36. diffusers/modular_pipelines/krea2/encoders.py +276 -0
  37. diffusers/modular_pipelines/krea2/modular_blocks_krea2.py +170 -0
  38. diffusers/modular_pipelines/krea2/modular_blocks_krea2_turbo.py +164 -0
  39. diffusers/modular_pipelines/krea2/modular_pipeline.py +67 -0
  40. diffusers/modular_pipelines/ltx/__init__.py +47 -0
  41. diffusers/modular_pipelines/ltx/before_denoise.py +392 -0
  42. diffusers/modular_pipelines/ltx/decoders.py +132 -0
  43. diffusers/modular_pipelines/ltx/denoise.py +458 -0
  44. diffusers/modular_pipelines/ltx/encoders.py +273 -0
  45. diffusers/modular_pipelines/ltx/modular_blocks_ltx.py +487 -0
  46. diffusers/modular_pipelines/ltx/modular_pipeline.py +95 -0
  47. diffusers/modular_pipelines/mellon_node_utils.py +1101 -0
  48. diffusers/modular_pipelines/minimax_h3/__init__.py +49 -0
  49. diffusers/modular_pipelines/minimax_h3/before_denoise.py +425 -0
  50. diffusers/modular_pipelines/minimax_h3/before_encoder.py +408 -0
diffusers/modular_pipelines/flux2/decoders.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Union
18
+
19
+ import numpy as np
20
+ import PIL
21
+ import torch
22
+
23
+ from ...configuration_utils import FrozenDict
24
+ from ...models import AutoencoderKLFlux2
25
+ from ...pipelines.flux2.image_processor import Flux2ImageProcessor
26
+ from ...utils import logging
27
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
28
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
29
+
30
+
31
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
32
+
33
+
34
+ class Flux2UnpackLatentsStep(ModularPipelineBlocks):
35
+ model_name = "flux2"
36
+
37
+ @property
38
+ def description(self) -> str:
39
+ return "Step that unpacks the latents from the denoising step"
40
+
41
+ @property
42
+ def inputs(self) -> list[tuple[str, Any]]:
43
+ return [
44
+ InputParam(
45
+ "latents",
46
+ required=True,
47
+ type_hint=torch.Tensor,
48
+ description="The denoised latents from the denoising step",
49
+ ),
50
+ InputParam(
51
+ "latent_ids",
52
+ required=True,
53
+ type_hint=torch.Tensor,
54
+ description="Position IDs for the latents, used for unpacking",
55
+ ),
56
+ ]
57
+
58
+ @property
59
+ def intermediate_outputs(self) -> list[str]:
60
+ return [
61
+ OutputParam(
62
+ "latents",
63
+ type_hint=torch.Tensor,
64
+ description="The denoise latents from denoising step, unpacked with position IDs.",
65
+ )
66
+ ]
67
+
68
+ @staticmethod
69
+ def _unpack_latents_with_ids(x: torch.Tensor, x_ids: torch.Tensor) -> torch.Tensor:
70
+ """
71
+ Unpack latents using position IDs to scatter tokens into place.
72
+
73
+ Args:
74
+ x: Packed latents tensor of shape (B, seq_len, C)
75
+ x_ids: Position IDs tensor of shape (B, seq_len, 4) with (T, H, W, L) coordinates
76
+
77
+ Returns:
78
+ Unpacked latents tensor of shape (B, C, H, W)
79
+ """
80
+ x_list = []
81
+ for data, pos in zip(x, x_ids):
82
+ _, ch = data.shape # noqa: F841
83
+ h_ids = pos[:, 1].to(torch.int64)
84
+ w_ids = pos[:, 2].to(torch.int64)
85
+
86
+ h = torch.max(h_ids) + 1
87
+ w = torch.max(w_ids) + 1
88
+
89
+ flat_ids = h_ids * w + w_ids
90
+
91
+ out = torch.zeros((h * w, ch), device=data.device, dtype=data.dtype)
92
+ out.scatter_(0, flat_ids.unsqueeze(1).expand(-1, ch), data)
93
+
94
+ out = out.view(h, w, ch).permute(2, 0, 1)
95
+ x_list.append(out)
96
+
97
+ return torch.stack(x_list, dim=0)
98
+
99
+ @torch.no_grad()
100
+ def __call__(self, components, state: PipelineState) -> PipelineState:
101
+ block_state = self.get_block_state(state)
102
+
103
+ latents = block_state.latents
104
+ latent_ids = block_state.latent_ids
105
+
106
+ latents = self._unpack_latents_with_ids(latents, latent_ids)
107
+
108
+ block_state.latents = latents
109
+
110
+ self.set_block_state(state, block_state)
111
+ return components, state
112
+
113
+
114
+ class Flux2DecodeStep(ModularPipelineBlocks):
115
+ model_name = "flux2"
116
+
117
+ @property
118
+ def expected_components(self) -> list[ComponentSpec]:
119
+ return [
120
+ ComponentSpec("vae", AutoencoderKLFlux2),
121
+ ComponentSpec(
122
+ "image_processor",
123
+ Flux2ImageProcessor,
124
+ config=FrozenDict({"vae_scale_factor": 16, "vae_latent_channels": 32}),
125
+ default_creation_method="from_config",
126
+ ),
127
+ ]
128
+
129
+ @property
130
+ def description(self) -> str:
131
+ return "Step that decodes the denoised latents into images using Flux2 VAE with batch norm denormalization"
132
+
133
+ @property
134
+ def inputs(self) -> list[tuple[str, Any]]:
135
+ return [
136
+ InputParam("output_type", default="pil"),
137
+ InputParam(
138
+ "latents",
139
+ required=True,
140
+ type_hint=torch.Tensor,
141
+ description="The denoised latents from the denoising step",
142
+ ),
143
+ ]
144
+
145
+ @property
146
+ def intermediate_outputs(self) -> list[str]:
147
+ return [
148
+ OutputParam(
149
+ "images",
150
+ type_hint=Union[list[PIL.Image.Image], torch.Tensor, np.ndarray],
151
+ description="The generated images, can be a list of PIL.Image.Image, torch.Tensor or a numpy array",
152
+ )
153
+ ]
154
+
155
+ @staticmethod
156
+ def _unpatchify_latents(latents):
157
+ """Convert patchified latents back to regular format."""
158
+ batch_size, num_channels_latents, height, width = latents.shape
159
+ latents = latents.reshape(batch_size, num_channels_latents // (2 * 2), 2, 2, height, width)
160
+ latents = latents.permute(0, 1, 4, 2, 5, 3)
161
+ latents = latents.reshape(batch_size, num_channels_latents // (2 * 2), height * 2, width * 2)
162
+ return latents
163
+
164
+ @torch.no_grad()
165
+ def __call__(self, components, state: PipelineState) -> PipelineState:
166
+ block_state = self.get_block_state(state)
167
+ vae = components.vae
168
+
169
+ latents = block_state.latents
170
+
171
+ latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(latents.device, latents.dtype)
172
+ latents_bn_std = torch.sqrt(vae.bn.running_var.view(1, -1, 1, 1) + vae.config.batch_norm_eps).to(
173
+ latents.device, latents.dtype
174
+ )
175
+ latents = latents * latents_bn_std + latents_bn_mean
176
+
177
+ latents = self._unpatchify_latents(latents)
178
+
179
+ block_state.images = vae.decode(latents, return_dict=False)[0]
180
+ block_state.images = components.image_processor.postprocess(
181
+ block_state.images, output_type=block_state.output_type
182
+ )
183
+
184
+ self.set_block_state(state, block_state)
185
+ return components, state
diffusers/modular_pipelines/flux2/denoise.py ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Any
16
+
17
+ import torch
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...guiders import ClassifierFreeGuidance
21
+ from ...models import Flux2Transformer2DModel
22
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
23
+ from ...utils import is_torch_xla_available, logging
24
+ from ..modular_pipeline import (
25
+ BlockState,
26
+ LoopSequentialPipelineBlocks,
27
+ ModularPipelineBlocks,
28
+ PipelineState,
29
+ )
30
+ from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
31
+ from .modular_pipeline import Flux2KleinModularPipeline, Flux2ModularPipeline
32
+
33
+
34
+ if is_torch_xla_available():
35
+ import torch_xla.core.xla_model as xm
36
+
37
+ XLA_AVAILABLE = True
38
+ else:
39
+ XLA_AVAILABLE = False
40
+
41
+
42
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
43
+
44
+
45
+ class Flux2LoopDenoiser(ModularPipelineBlocks):
46
+ model_name = "flux2"
47
+
48
+ @property
49
+ def expected_components(self) -> list[ComponentSpec]:
50
+ return [ComponentSpec("transformer", Flux2Transformer2DModel)]
51
+
52
+ @property
53
+ def description(self) -> str:
54
+ return (
55
+ "Step within the denoising loop that denoises the latents for Flux2. "
56
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
57
+ "object (e.g. `Flux2DenoiseLoopWrapper`)"
58
+ )
59
+
60
+ @property
61
+ def inputs(self) -> list[tuple[str, Any]]:
62
+ return [
63
+ InputParam("joint_attention_kwargs"),
64
+ InputParam(
65
+ "latents",
66
+ required=True,
67
+ type_hint=torch.Tensor,
68
+ description="The latents to denoise. Shape: (B, seq_len, C)",
69
+ ),
70
+ InputParam(
71
+ "image_latents",
72
+ type_hint=torch.Tensor,
73
+ description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
74
+ ),
75
+ InputParam(
76
+ "image_latent_ids",
77
+ type_hint=torch.Tensor,
78
+ description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
79
+ ),
80
+ InputParam(
81
+ "guidance",
82
+ required=True,
83
+ type_hint=torch.Tensor,
84
+ description="Guidance scale as a tensor",
85
+ ),
86
+ InputParam(
87
+ "prompt_embeds",
88
+ required=True,
89
+ type_hint=torch.Tensor,
90
+ description="Text embeddings from Mistral3",
91
+ ),
92
+ InputParam(
93
+ "txt_ids",
94
+ required=True,
95
+ type_hint=torch.Tensor,
96
+ description="4D position IDs for text tokens (T, H, W, L)",
97
+ ),
98
+ InputParam(
99
+ "latent_ids",
100
+ required=True,
101
+ type_hint=torch.Tensor,
102
+ description="4D position IDs for latent tokens (T, H, W, L)",
103
+ ),
104
+ ]
105
+
106
+ @torch.no_grad()
107
+ def __call__(
108
+ self, components: Flux2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
109
+ ) -> PipelineState:
110
+ latents = block_state.latents
111
+ latent_model_input = latents.to(components.transformer.dtype)
112
+ img_ids = block_state.latent_ids
113
+
114
+ image_latents = getattr(block_state, "image_latents", None)
115
+ if image_latents is not None:
116
+ latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
117
+ image_latent_ids = block_state.image_latent_ids
118
+ img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
119
+
120
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
121
+
122
+ noise_pred = components.transformer(
123
+ hidden_states=latent_model_input,
124
+ timestep=timestep / 1000,
125
+ guidance=block_state.guidance,
126
+ encoder_hidden_states=block_state.prompt_embeds,
127
+ txt_ids=block_state.txt_ids,
128
+ img_ids=img_ids,
129
+ joint_attention_kwargs=block_state.joint_attention_kwargs,
130
+ return_dict=False,
131
+ )[0]
132
+
133
+ noise_pred = noise_pred[:, : latents.size(1)]
134
+ block_state.noise_pred = noise_pred
135
+
136
+ return components, block_state
137
+
138
+
139
+ # same as Flux2LoopDenoiser but guidance=None
140
+ class Flux2KleinLoopDenoiser(ModularPipelineBlocks):
141
+ model_name = "flux2-klein"
142
+
143
+ @property
144
+ def expected_components(self) -> list[ComponentSpec]:
145
+ return [ComponentSpec("transformer", Flux2Transformer2DModel)]
146
+
147
+ @property
148
+ def description(self) -> str:
149
+ return (
150
+ "Step within the denoising loop that denoises the latents for Flux2. "
151
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
152
+ "object (e.g. `Flux2DenoiseLoopWrapper`)"
153
+ )
154
+
155
+ @property
156
+ def inputs(self) -> list[tuple[str, Any]]:
157
+ return [
158
+ InputParam("joint_attention_kwargs"),
159
+ InputParam(
160
+ "latents",
161
+ required=True,
162
+ type_hint=torch.Tensor,
163
+ description="The latents to denoise. Shape: (B, seq_len, C)",
164
+ ),
165
+ InputParam(
166
+ "image_latents",
167
+ type_hint=torch.Tensor,
168
+ description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
169
+ ),
170
+ InputParam(
171
+ "image_latent_ids",
172
+ type_hint=torch.Tensor,
173
+ description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
174
+ ),
175
+ InputParam(
176
+ "prompt_embeds",
177
+ required=True,
178
+ type_hint=torch.Tensor,
179
+ description="Text embeddings from Qwen3",
180
+ ),
181
+ InputParam(
182
+ "txt_ids",
183
+ required=True,
184
+ type_hint=torch.Tensor,
185
+ description="4D position IDs for text tokens (T, H, W, L)",
186
+ ),
187
+ InputParam(
188
+ "latent_ids",
189
+ required=True,
190
+ type_hint=torch.Tensor,
191
+ description="4D position IDs for latent tokens (T, H, W, L)",
192
+ ),
193
+ ]
194
+
195
+ @torch.no_grad()
196
+ def __call__(
197
+ self, components: Flux2KleinModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
198
+ ) -> PipelineState:
199
+ latents = block_state.latents
200
+ latent_model_input = latents.to(components.transformer.dtype)
201
+ img_ids = block_state.latent_ids
202
+
203
+ image_latents = getattr(block_state, "image_latents", None)
204
+ if image_latents is not None:
205
+ latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
206
+ image_latent_ids = block_state.image_latent_ids
207
+ img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
208
+
209
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
210
+
211
+ noise_pred = components.transformer(
212
+ hidden_states=latent_model_input,
213
+ timestep=timestep / 1000,
214
+ guidance=None,
215
+ encoder_hidden_states=block_state.prompt_embeds,
216
+ txt_ids=block_state.txt_ids,
217
+ img_ids=img_ids,
218
+ joint_attention_kwargs=block_state.joint_attention_kwargs,
219
+ return_dict=False,
220
+ )[0]
221
+
222
+ noise_pred = noise_pred[:, : latents.size(1)]
223
+ block_state.noise_pred = noise_pred
224
+
225
+ return components, block_state
226
+
227
+
228
+ # support CFG for Flux2-Klein base model
229
+ class Flux2KleinBaseLoopDenoiser(ModularPipelineBlocks):
230
+ model_name = "flux2-klein"
231
+
232
+ @property
233
+ def expected_components(self) -> list[ComponentSpec]:
234
+ return [
235
+ ComponentSpec("transformer", Flux2Transformer2DModel),
236
+ ComponentSpec(
237
+ "guider",
238
+ ClassifierFreeGuidance,
239
+ config=FrozenDict({"guidance_scale": 4.0}),
240
+ default_creation_method="from_config",
241
+ ),
242
+ ]
243
+
244
+ @property
245
+ def expected_configs(self) -> list[ConfigSpec]:
246
+ return [
247
+ ConfigSpec(name="is_distilled", default=False),
248
+ ]
249
+
250
+ @property
251
+ def description(self) -> str:
252
+ return (
253
+ "Step within the denoising loop that denoises the latents for Flux2. "
254
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
255
+ "object (e.g. `Flux2DenoiseLoopWrapper`)"
256
+ )
257
+
258
+ @property
259
+ def inputs(self) -> list[tuple[str, Any]]:
260
+ return [
261
+ InputParam("joint_attention_kwargs"),
262
+ InputParam(
263
+ "latents",
264
+ required=True,
265
+ type_hint=torch.Tensor,
266
+ description="The latents to denoise. Shape: (B, seq_len, C)",
267
+ ),
268
+ InputParam(
269
+ "image_latents",
270
+ type_hint=torch.Tensor,
271
+ description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
272
+ ),
273
+ InputParam(
274
+ "image_latent_ids",
275
+ type_hint=torch.Tensor,
276
+ description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
277
+ ),
278
+ InputParam(
279
+ "prompt_embeds",
280
+ required=True,
281
+ type_hint=torch.Tensor,
282
+ description="Text embeddings from Qwen3",
283
+ ),
284
+ InputParam(
285
+ "negative_prompt_embeds",
286
+ required=False,
287
+ type_hint=torch.Tensor,
288
+ description="Negative text embeddings from Qwen3",
289
+ ),
290
+ InputParam(
291
+ "txt_ids",
292
+ required=True,
293
+ type_hint=torch.Tensor,
294
+ description="4D position IDs for text tokens (T, H, W, L)",
295
+ ),
296
+ InputParam(
297
+ "negative_txt_ids",
298
+ required=False,
299
+ type_hint=torch.Tensor,
300
+ description="4D position IDs for negative text tokens (T, H, W, L)",
301
+ ),
302
+ InputParam(
303
+ "latent_ids",
304
+ required=True,
305
+ type_hint=torch.Tensor,
306
+ description="4D position IDs for latent tokens (T, H, W, L)",
307
+ ),
308
+ ]
309
+
310
+ @torch.no_grad()
311
+ def __call__(
312
+ self, components: Flux2KleinModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
313
+ ) -> PipelineState:
314
+ latents = block_state.latents
315
+ latent_model_input = latents.to(components.transformer.dtype)
316
+ img_ids = block_state.latent_ids
317
+
318
+ image_latents = getattr(block_state, "image_latents", None)
319
+ if image_latents is not None:
320
+ latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
321
+ image_latent_ids = block_state.image_latent_ids
322
+ img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
323
+
324
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
325
+
326
+ guider_inputs = {
327
+ "encoder_hidden_states": (
328
+ getattr(block_state, "prompt_embeds", None),
329
+ getattr(block_state, "negative_prompt_embeds", None),
330
+ ),
331
+ "txt_ids": (
332
+ getattr(block_state, "txt_ids", None),
333
+ getattr(block_state, "negative_txt_ids", None),
334
+ ),
335
+ }
336
+
337
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
338
+ guider_state = components.guider.prepare_inputs(guider_inputs)
339
+
340
+ for guider_state_batch in guider_state:
341
+ components.guider.prepare_models(components.transformer)
342
+ cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
343
+
344
+ noise_pred = components.transformer(
345
+ hidden_states=latent_model_input,
346
+ timestep=timestep / 1000,
347
+ guidance=None,
348
+ img_ids=img_ids,
349
+ joint_attention_kwargs=block_state.joint_attention_kwargs,
350
+ return_dict=False,
351
+ **cond_kwargs,
352
+ )[0]
353
+ guider_state_batch.noise_pred = noise_pred[:, : latents.size(1)]
354
+ components.guider.cleanup_models(components.transformer)
355
+
356
+ # perform guidance
357
+ block_state.noise_pred = components.guider(guider_state)[0]
358
+
359
+ return components, block_state
360
+
361
+
362
+ class Flux2LoopAfterDenoiser(ModularPipelineBlocks):
363
+ model_name = "flux2"
364
+
365
+ @property
366
+ def expected_components(self) -> list[ComponentSpec]:
367
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
368
+
369
+ @property
370
+ def description(self) -> str:
371
+ return (
372
+ "Step within the denoising loop that updates the latents after denoising. "
373
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
374
+ "object (e.g. `Flux2DenoiseLoopWrapper`)"
375
+ )
376
+
377
+ @property
378
+ def intermediate_outputs(self) -> list[OutputParam]:
379
+ return [OutputParam("latents", type_hint=torch.Tensor, description="The denoised latents")]
380
+
381
+ @torch.no_grad()
382
+ def __call__(self, components: Flux2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
383
+ latents_dtype = block_state.latents.dtype
384
+ block_state.latents = components.scheduler.step(
385
+ block_state.noise_pred,
386
+ t,
387
+ block_state.latents,
388
+ return_dict=False,
389
+ )[0]
390
+
391
+ if block_state.latents.dtype != latents_dtype:
392
+ if torch.backends.mps.is_available():
393
+ block_state.latents = block_state.latents.to(latents_dtype)
394
+
395
+ return components, block_state
396
+
397
+
398
+ class Flux2DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
399
+ model_name = "flux2"
400
+
401
+ @property
402
+ def description(self) -> str:
403
+ return (
404
+ "Pipeline block that iteratively denoises the latents over `timesteps`. "
405
+ "The specific steps within each iteration can be customized with `sub_blocks` attribute"
406
+ )
407
+
408
+ @property
409
+ def loop_expected_components(self) -> list[ComponentSpec]:
410
+ return [
411
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
412
+ ComponentSpec("transformer", Flux2Transformer2DModel),
413
+ ]
414
+
415
+ @property
416
+ def loop_inputs(self) -> list[InputParam]:
417
+ return [
418
+ InputParam(
419
+ "timesteps",
420
+ required=True,
421
+ type_hint=torch.Tensor,
422
+ description="The timesteps to use for the denoising process.",
423
+ ),
424
+ InputParam(
425
+ "num_inference_steps",
426
+ required=True,
427
+ type_hint=int,
428
+ description="The number of inference steps to use for the denoising process.",
429
+ ),
430
+ ]
431
+
432
+ @torch.no_grad()
433
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
434
+ block_state = self.get_block_state(state)
435
+
436
+ block_state.num_warmup_steps = max(
437
+ len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
438
+ )
439
+
440
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
441
+ for i, t in enumerate(block_state.timesteps):
442
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
443
+
444
+ if i == len(block_state.timesteps) - 1 or (
445
+ (i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
446
+ ):
447
+ progress_bar.update()
448
+
449
+ if XLA_AVAILABLE:
450
+ xm.mark_step()
451
+
452
+ self.set_block_state(state, block_state)
453
+ return components, state
454
+
455
+
456
+ class Flux2DenoiseStep(Flux2DenoiseLoopWrapper):
457
+ block_classes = [Flux2LoopDenoiser, Flux2LoopAfterDenoiser]
458
+ block_names = ["denoiser", "after_denoiser"]
459
+
460
+ @property
461
+ def description(self) -> str:
462
+ return (
463
+ "Denoise step that iteratively denoises the latents for Flux2. \n"
464
+ "Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
465
+ "At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
466
+ " - `Flux2LoopDenoiser`\n"
467
+ " - `Flux2LoopAfterDenoiser`\n"
468
+ "This block supports both text-to-image and image-conditioned generation."
469
+ )
470
+
471
+
472
+ class Flux2KleinDenoiseStep(Flux2DenoiseLoopWrapper):
473
+ block_classes = [Flux2KleinLoopDenoiser, Flux2LoopAfterDenoiser]
474
+ block_names = ["denoiser", "after_denoiser"]
475
+
476
+ @property
477
+ def description(self) -> str:
478
+ return (
479
+ "Denoise step that iteratively denoises the latents for Flux2. \n"
480
+ "Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
481
+ "At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
482
+ " - `Flux2KleinLoopDenoiser`\n"
483
+ " - `Flux2LoopAfterDenoiser`\n"
484
+ "This block supports both text-to-image and image-conditioned generation."
485
+ )
486
+
487
+
488
+ class Flux2KleinBaseDenoiseStep(Flux2DenoiseLoopWrapper):
489
+ block_classes = [Flux2KleinBaseLoopDenoiser, Flux2LoopAfterDenoiser]
490
+ block_names = ["denoiser", "after_denoiser"]
491
+
492
+ @property
493
+ def description(self) -> str:
494
+ return (
495
+ "Denoise step that iteratively denoises the latents for Flux2. \n"
496
+ "Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
497
+ "At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
498
+ " - `Flux2KleinBaseLoopDenoiser`\n"
499
+ " - `Flux2LoopAfterDenoiser`\n"
500
+ "This block supports both text-to-image and image-conditioned generation."
501
+ )
diffusers/modular_pipelines/flux2/encoders.py ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+ from transformers import AutoProcessor, Mistral3ForConditionalGeneration, Qwen2TokenizerFast, Qwen3ForCausalLM
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...guiders import ClassifierFreeGuidance
21
+ from ...models import AutoencoderKLFlux2
22
+ from ...utils import logging
23
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
24
+ from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
25
+ from .modular_pipeline import Flux2KleinModularPipeline, Flux2ModularPipeline
26
+
27
+
28
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
29
+
30
+
31
+ def format_text_input(prompts: list[str], system_message: str = None):
32
+ """Format prompts for Mistral3 chat template."""
33
+ cleaned_txt = [prompt.replace("[IMG]", "") for prompt in prompts]
34
+
35
+ return [
36
+ [
37
+ {
38
+ "role": "system",
39
+ "content": [{"type": "text", "text": system_message}],
40
+ },
41
+ {"role": "user", "content": [{"type": "text", "text": prompt}]},
42
+ ]
43
+ for prompt in cleaned_txt
44
+ ]
45
+
46
+
47
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
48
+ def retrieve_latents(
49
+ encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
50
+ ):
51
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
52
+ return encoder_output.latent_dist.sample(generator)
53
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
54
+ return encoder_output.latent_dist.mode()
55
+ elif hasattr(encoder_output, "latents"):
56
+ return encoder_output.latents
57
+ else:
58
+ raise AttributeError("Could not access latents of provided encoder_output")
59
+
60
+
61
+ class Flux2TextEncoderStep(ModularPipelineBlocks):
62
+ model_name = "flux2"
63
+
64
+ # fmt: off
65
+ DEFAULT_SYSTEM_MESSAGE = "You are an AI that reasons about image descriptions. You give structured responses focusing on object relationships, object attribution and actions without speculation."
66
+ # fmt: on
67
+
68
+ @property
69
+ def description(self) -> str:
70
+ return "Text Encoder step that generates text embeddings using Mistral3 to guide the image generation"
71
+
72
+ @property
73
+ def expected_components(self) -> list[ComponentSpec]:
74
+ return [
75
+ ComponentSpec("text_encoder", Mistral3ForConditionalGeneration),
76
+ ComponentSpec("tokenizer", AutoProcessor),
77
+ ]
78
+
79
+ @property
80
+ def inputs(self) -> list[InputParam]:
81
+ return [
82
+ InputParam("prompt"),
83
+ InputParam("max_sequence_length", type_hint=int, default=512, required=False),
84
+ InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(10, 20, 30), required=False),
85
+ ]
86
+
87
+ @property
88
+ def intermediate_outputs(self) -> list[OutputParam]:
89
+ return [
90
+ OutputParam(
91
+ "prompt_embeds",
92
+ kwargs_type="denoiser_input_fields",
93
+ type_hint=torch.Tensor,
94
+ description="Text embeddings from Mistral3 used to guide the image generation",
95
+ ),
96
+ ]
97
+
98
+ @staticmethod
99
+ def check_inputs(block_state):
100
+ prompt = block_state.prompt
101
+ if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
102
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
103
+
104
+ @staticmethod
105
+ def _get_mistral_3_prompt_embeds(
106
+ text_encoder: Mistral3ForConditionalGeneration,
107
+ tokenizer: AutoProcessor,
108
+ prompt: str | list[str],
109
+ dtype: torch.dtype | None = None,
110
+ device: torch.device | None = None,
111
+ max_sequence_length: int = 512,
112
+ # fmt: off
113
+ system_message: str = "You are an AI that reasons about image descriptions. You give structured responses focusing on object relationships, object attribution and actions without speculation.",
114
+ # fmt: on
115
+ hidden_states_layers: tuple[int] = (10, 20, 30),
116
+ ):
117
+ dtype = text_encoder.dtype if dtype is None else dtype
118
+ device = text_encoder.device if device is None else device
119
+
120
+ prompt = [prompt] if isinstance(prompt, str) else prompt
121
+
122
+ messages_batch = format_text_input(prompts=prompt, system_message=system_message)
123
+
124
+ inputs = tokenizer.apply_chat_template(
125
+ messages_batch,
126
+ add_generation_prompt=False,
127
+ tokenize=True,
128
+ return_dict=True,
129
+ return_tensors="pt",
130
+ padding="max_length",
131
+ truncation=True,
132
+ max_length=max_sequence_length,
133
+ )
134
+
135
+ input_ids = inputs["input_ids"].to(device)
136
+ attention_mask = inputs["attention_mask"].to(device)
137
+
138
+ output = text_encoder(
139
+ input_ids=input_ids,
140
+ attention_mask=attention_mask,
141
+ output_hidden_states=True,
142
+ use_cache=False,
143
+ )
144
+
145
+ out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
146
+ out = out.to(dtype=dtype, device=device)
147
+
148
+ batch_size, num_channels, seq_len, hidden_dim = out.shape
149
+ prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
150
+
151
+ return prompt_embeds
152
+
153
+ @torch.no_grad()
154
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
155
+ block_state = self.get_block_state(state)
156
+ self.check_inputs(block_state)
157
+
158
+ block_state.device = components._execution_device
159
+
160
+ prompt = block_state.prompt
161
+ if prompt is None:
162
+ prompt = ""
163
+ prompt = [prompt] if isinstance(prompt, str) else prompt
164
+
165
+ block_state.prompt_embeds = self._get_mistral_3_prompt_embeds(
166
+ text_encoder=components.text_encoder,
167
+ tokenizer=components.tokenizer,
168
+ prompt=prompt,
169
+ device=block_state.device,
170
+ max_sequence_length=block_state.max_sequence_length,
171
+ system_message=self.DEFAULT_SYSTEM_MESSAGE,
172
+ hidden_states_layers=block_state.text_encoder_out_layers,
173
+ )
174
+
175
+ self.set_block_state(state, block_state)
176
+ return components, state
177
+
178
+
179
+ class Flux2RemoteTextEncoderStep(ModularPipelineBlocks):
180
+ model_name = "flux2"
181
+
182
+ REMOTE_URL = "https://remote-text-encoder-flux-2.huggingface.co/predict"
183
+
184
+ @property
185
+ def description(self) -> str:
186
+ return "Text Encoder step that generates text embeddings using a remote API endpoint"
187
+
188
+ @property
189
+ def expected_components(self) -> list[ComponentSpec]:
190
+ return []
191
+
192
+ @property
193
+ def inputs(self) -> list[InputParam]:
194
+ return [
195
+ InputParam("prompt"),
196
+ ]
197
+
198
+ @property
199
+ def intermediate_outputs(self) -> list[OutputParam]:
200
+ return [
201
+ OutputParam(
202
+ "prompt_embeds",
203
+ kwargs_type="denoiser_input_fields",
204
+ type_hint=torch.Tensor,
205
+ description="Text embeddings from remote API used to guide the image generation",
206
+ ),
207
+ ]
208
+
209
+ @staticmethod
210
+ def check_inputs(block_state):
211
+ prompt = block_state.prompt
212
+ if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
213
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(block_state.prompt)}")
214
+
215
+ @torch.no_grad()
216
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
217
+ import io
218
+
219
+ import requests
220
+ from huggingface_hub import get_token
221
+
222
+ block_state = self.get_block_state(state)
223
+ self.check_inputs(block_state)
224
+
225
+ block_state.device = components._execution_device
226
+
227
+ prompt = block_state.prompt
228
+ if prompt is None:
229
+ prompt = ""
230
+ prompt = [prompt] if isinstance(prompt, str) else prompt
231
+
232
+ response = requests.post(
233
+ self.REMOTE_URL,
234
+ json={"prompt": prompt},
235
+ headers={
236
+ "Authorization": f"Bearer {get_token()}",
237
+ "Content-Type": "application/json",
238
+ },
239
+ )
240
+ response.raise_for_status()
241
+
242
+ block_state.prompt_embeds = torch.load(io.BytesIO(response.content), weights_only=True)
243
+ block_state.prompt_embeds = block_state.prompt_embeds.to(block_state.device)
244
+
245
+ self.set_block_state(state, block_state)
246
+ return components, state
247
+
248
+
249
+ class Flux2KleinTextEncoderStep(ModularPipelineBlocks):
250
+ model_name = "flux2-klein"
251
+
252
+ @property
253
+ def description(self) -> str:
254
+ return "Text Encoder step that generates text embeddings using Qwen3 to guide the image generation"
255
+
256
+ @property
257
+ def expected_components(self) -> list[ComponentSpec]:
258
+ return [
259
+ ComponentSpec("text_encoder", Qwen3ForCausalLM),
260
+ ComponentSpec("tokenizer", Qwen2TokenizerFast),
261
+ ]
262
+
263
+ @property
264
+ def expected_configs(self) -> list[ConfigSpec]:
265
+ return [
266
+ ConfigSpec(name="is_distilled", default=True),
267
+ ]
268
+
269
+ @property
270
+ def inputs(self) -> list[InputParam]:
271
+ return [
272
+ InputParam("prompt"),
273
+ InputParam("max_sequence_length", type_hint=int, default=512, required=False),
274
+ InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(9, 18, 27), required=False),
275
+ ]
276
+
277
+ @property
278
+ def intermediate_outputs(self) -> list[OutputParam]:
279
+ return [
280
+ OutputParam(
281
+ "prompt_embeds",
282
+ kwargs_type="denoiser_input_fields",
283
+ type_hint=torch.Tensor,
284
+ description="Text embeddings from qwen3 used to guide the image generation",
285
+ ),
286
+ ]
287
+
288
+ @staticmethod
289
+ def check_inputs(block_state):
290
+ prompt = block_state.prompt
291
+
292
+ if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
293
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
294
+
295
+ @staticmethod
296
+ # Copied from diffusers.pipelines.flux2.pipeline_flux2_klein.Flux2KleinPipeline._get_qwen3_prompt_embeds
297
+ def _get_qwen3_prompt_embeds(
298
+ text_encoder: Qwen3ForCausalLM,
299
+ tokenizer: Qwen2TokenizerFast,
300
+ prompt: str | list[str],
301
+ dtype: torch.dtype | None = None,
302
+ device: torch.device | None = None,
303
+ max_sequence_length: int = 512,
304
+ hidden_states_layers: list[int] = (9, 18, 27),
305
+ ):
306
+ dtype = text_encoder.dtype if dtype is None else dtype
307
+ device = text_encoder.device if device is None else device
308
+
309
+ prompt = [prompt] if isinstance(prompt, str) else prompt
310
+
311
+ all_input_ids = []
312
+ all_attention_masks = []
313
+
314
+ for single_prompt in prompt:
315
+ messages = [{"role": "user", "content": single_prompt}]
316
+ text = tokenizer.apply_chat_template(
317
+ messages,
318
+ tokenize=False,
319
+ add_generation_prompt=True,
320
+ enable_thinking=False,
321
+ )
322
+ inputs = tokenizer(
323
+ text,
324
+ return_tensors="pt",
325
+ padding="max_length",
326
+ truncation=True,
327
+ max_length=max_sequence_length,
328
+ )
329
+
330
+ all_input_ids.append(inputs["input_ids"])
331
+ all_attention_masks.append(inputs["attention_mask"])
332
+
333
+ input_ids = torch.cat(all_input_ids, dim=0).to(device)
334
+ attention_mask = torch.cat(all_attention_masks, dim=0).to(device)
335
+
336
+ # Forward pass through the model
337
+ output = text_encoder(
338
+ input_ids=input_ids,
339
+ attention_mask=attention_mask,
340
+ output_hidden_states=True,
341
+ use_cache=False,
342
+ )
343
+
344
+ # Only use outputs from intermediate layers and stack them
345
+ out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
346
+ out = out.to(dtype=dtype, device=device)
347
+
348
+ batch_size, num_channels, seq_len, hidden_dim = out.shape
349
+ prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
350
+
351
+ return prompt_embeds
352
+
353
+ @torch.no_grad()
354
+ def __call__(self, components: Flux2KleinModularPipeline, state: PipelineState) -> PipelineState:
355
+ block_state = self.get_block_state(state)
356
+ self.check_inputs(block_state)
357
+
358
+ device = components._execution_device
359
+
360
+ prompt = block_state.prompt
361
+ if prompt is None:
362
+ prompt = ""
363
+ prompt = [prompt] if isinstance(prompt, str) else prompt
364
+
365
+ block_state.prompt_embeds = self._get_qwen3_prompt_embeds(
366
+ text_encoder=components.text_encoder,
367
+ tokenizer=components.tokenizer,
368
+ prompt=prompt,
369
+ device=device,
370
+ max_sequence_length=block_state.max_sequence_length,
371
+ hidden_states_layers=block_state.text_encoder_out_layers,
372
+ )
373
+
374
+ self.set_block_state(state, block_state)
375
+ return components, state
376
+
377
+
378
+ class Flux2KleinBaseTextEncoderStep(ModularPipelineBlocks):
379
+ model_name = "flux2-klein"
380
+
381
+ @property
382
+ def description(self) -> str:
383
+ return "Text Encoder step that generates text embeddings using Qwen3 to guide the image generation"
384
+
385
+ @property
386
+ def expected_components(self) -> list[ComponentSpec]:
387
+ return [
388
+ ComponentSpec("text_encoder", Qwen3ForCausalLM),
389
+ ComponentSpec("tokenizer", Qwen2TokenizerFast),
390
+ ComponentSpec(
391
+ "guider",
392
+ ClassifierFreeGuidance,
393
+ config=FrozenDict({"guidance_scale": 4.0}),
394
+ default_creation_method="from_config",
395
+ ),
396
+ ]
397
+
398
+ @property
399
+ def expected_configs(self) -> list[ConfigSpec]:
400
+ return [
401
+ ConfigSpec(name="is_distilled", default=False),
402
+ ]
403
+
404
+ @property
405
+ def inputs(self) -> list[InputParam]:
406
+ return [
407
+ InputParam("prompt"),
408
+ InputParam("max_sequence_length", type_hint=int, default=512, required=False),
409
+ InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(9, 18, 27), required=False),
410
+ ]
411
+
412
+ @property
413
+ def intermediate_outputs(self) -> list[OutputParam]:
414
+ return [
415
+ OutputParam(
416
+ "prompt_embeds",
417
+ kwargs_type="denoiser_input_fields",
418
+ type_hint=torch.Tensor,
419
+ description="Text embeddings from qwen3 used to guide the image generation",
420
+ ),
421
+ OutputParam(
422
+ "negative_prompt_embeds",
423
+ kwargs_type="denoiser_input_fields",
424
+ type_hint=torch.Tensor,
425
+ description="Negative text embeddings from qwen3 used to guide the image generation",
426
+ ),
427
+ ]
428
+
429
+ @staticmethod
430
+ def check_inputs(block_state):
431
+ prompt = block_state.prompt
432
+
433
+ if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
434
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
435
+
436
+ @staticmethod
437
+ # Copied from diffusers.pipelines.flux2.pipeline_flux2_klein.Flux2KleinPipeline._get_qwen3_prompt_embeds
438
+ def _get_qwen3_prompt_embeds(
439
+ text_encoder: Qwen3ForCausalLM,
440
+ tokenizer: Qwen2TokenizerFast,
441
+ prompt: str | list[str],
442
+ dtype: torch.dtype | None = None,
443
+ device: torch.device | None = None,
444
+ max_sequence_length: int = 512,
445
+ hidden_states_layers: list[int] = (9, 18, 27),
446
+ ):
447
+ dtype = text_encoder.dtype if dtype is None else dtype
448
+ device = text_encoder.device if device is None else device
449
+
450
+ prompt = [prompt] if isinstance(prompt, str) else prompt
451
+
452
+ all_input_ids = []
453
+ all_attention_masks = []
454
+
455
+ for single_prompt in prompt:
456
+ messages = [{"role": "user", "content": single_prompt}]
457
+ text = tokenizer.apply_chat_template(
458
+ messages,
459
+ tokenize=False,
460
+ add_generation_prompt=True,
461
+ enable_thinking=False,
462
+ )
463
+ inputs = tokenizer(
464
+ text,
465
+ return_tensors="pt",
466
+ padding="max_length",
467
+ truncation=True,
468
+ max_length=max_sequence_length,
469
+ )
470
+
471
+ all_input_ids.append(inputs["input_ids"])
472
+ all_attention_masks.append(inputs["attention_mask"])
473
+
474
+ input_ids = torch.cat(all_input_ids, dim=0).to(device)
475
+ attention_mask = torch.cat(all_attention_masks, dim=0).to(device)
476
+
477
+ # Forward pass through the model
478
+ output = text_encoder(
479
+ input_ids=input_ids,
480
+ attention_mask=attention_mask,
481
+ output_hidden_states=True,
482
+ use_cache=False,
483
+ )
484
+
485
+ # Only use outputs from intermediate layers and stack them
486
+ out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
487
+ out = out.to(dtype=dtype, device=device)
488
+
489
+ batch_size, num_channels, seq_len, hidden_dim = out.shape
490
+ prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
491
+
492
+ return prompt_embeds
493
+
494
+ @torch.no_grad()
495
+ def __call__(self, components: Flux2KleinModularPipeline, state: PipelineState) -> PipelineState:
496
+ block_state = self.get_block_state(state)
497
+ self.check_inputs(block_state)
498
+
499
+ device = components._execution_device
500
+
501
+ prompt = block_state.prompt
502
+ if prompt is None:
503
+ prompt = ""
504
+ prompt = [prompt] if isinstance(prompt, str) else prompt
505
+
506
+ block_state.prompt_embeds = self._get_qwen3_prompt_embeds(
507
+ text_encoder=components.text_encoder,
508
+ tokenizer=components.tokenizer,
509
+ prompt=prompt,
510
+ device=device,
511
+ max_sequence_length=block_state.max_sequence_length,
512
+ hidden_states_layers=block_state.text_encoder_out_layers,
513
+ )
514
+
515
+ if components.requires_unconditional_embeds:
516
+ negative_prompt = [""] * len(prompt)
517
+ block_state.negative_prompt_embeds = self._get_qwen3_prompt_embeds(
518
+ text_encoder=components.text_encoder,
519
+ tokenizer=components.tokenizer,
520
+ prompt=negative_prompt,
521
+ device=device,
522
+ max_sequence_length=block_state.max_sequence_length,
523
+ hidden_states_layers=block_state.text_encoder_out_layers,
524
+ )
525
+ else:
526
+ block_state.negative_prompt_embeds = None
527
+
528
+ self.set_block_state(state, block_state)
529
+ return components, state
530
+
531
+
532
+ class Flux2VaeEncoderStep(ModularPipelineBlocks):
533
+ model_name = "flux2"
534
+
535
+ @property
536
+ def description(self) -> str:
537
+ return "VAE Encoder step that encodes preprocessed images into latent representations for Flux2."
538
+
539
+ @property
540
+ def expected_components(self) -> list[ComponentSpec]:
541
+ return [ComponentSpec("vae", AutoencoderKLFlux2)]
542
+
543
+ @property
544
+ def inputs(self) -> list[InputParam]:
545
+ return [
546
+ InputParam("condition_images", type_hint=list[torch.Tensor]),
547
+ InputParam("generator"),
548
+ ]
549
+
550
+ @property
551
+ def intermediate_outputs(self) -> list[OutputParam]:
552
+ return [
553
+ OutputParam(
554
+ "image_latents",
555
+ type_hint=list[torch.Tensor],
556
+ description="List of latent representations for each reference image",
557
+ ),
558
+ ]
559
+
560
+ @staticmethod
561
+ def _patchify_latents(latents):
562
+ """Convert latents to patchified format for Flux2."""
563
+ batch_size, num_channels_latents, height, width = latents.shape
564
+ latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
565
+ latents = latents.permute(0, 1, 3, 5, 2, 4)
566
+ latents = latents.reshape(batch_size, num_channels_latents * 4, height // 2, width // 2)
567
+ return latents
568
+
569
+ def _encode_vae_image(self, vae: AutoencoderKLFlux2, image: torch.Tensor, generator: torch.Generator):
570
+ """Encode a single image using Flux2 VAE with batch norm normalization."""
571
+ if image.ndim != 4:
572
+ raise ValueError(f"Expected image dims 4, got {image.ndim}.")
573
+
574
+ image_latents = retrieve_latents(vae.encode(image), generator=generator, sample_mode="argmax")
575
+ image_latents = self._patchify_latents(image_latents)
576
+
577
+ latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(image_latents.device, image_latents.dtype)
578
+ latents_bn_std = torch.sqrt(vae.bn.running_var.view(1, -1, 1, 1) + vae.config.batch_norm_eps)
579
+ latents_bn_std = latents_bn_std.to(image_latents.device, image_latents.dtype)
580
+ image_latents = (image_latents - latents_bn_mean) / latents_bn_std
581
+
582
+ return image_latents
583
+
584
+ @torch.no_grad()
585
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
586
+ block_state = self.get_block_state(state)
587
+ condition_images = block_state.condition_images
588
+
589
+ if condition_images is None:
590
+ return components, state
591
+
592
+ device = components._execution_device
593
+ dtype = components.vae.dtype
594
+
595
+ image_latents = []
596
+ for image in condition_images:
597
+ image = image.to(device=device, dtype=dtype)
598
+ latent = self._encode_vae_image(
599
+ vae=components.vae,
600
+ image=image,
601
+ generator=block_state.generator,
602
+ )
603
+ image_latents.append(latent)
604
+
605
+ block_state.image_latents = image_latents
606
+
607
+ self.set_block_state(state, block_state)
608
+ return components, state
diffusers/modular_pipelines/flux2/inputs.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ...configuration_utils import FrozenDict
18
+ from ...pipelines.flux2.image_processor import Flux2ImageProcessor
19
+ from ...utils import logging
20
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
21
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
22
+ from .modular_pipeline import Flux2ModularPipeline
23
+
24
+
25
+ logger = logging.get_logger(__name__)
26
+
27
+
28
+ class Flux2TextInputStep(ModularPipelineBlocks):
29
+ model_name = "flux2"
30
+
31
+ @property
32
+ def description(self) -> str:
33
+ return (
34
+ "This step:\n"
35
+ " 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
36
+ " 2. Ensures all text embeddings have consistent batch sizes (batch_size * num_images_per_prompt)"
37
+ )
38
+
39
+ @property
40
+ def inputs(self) -> list[InputParam]:
41
+ return [
42
+ InputParam("num_images_per_prompt", default=1),
43
+ InputParam(
44
+ "prompt_embeds",
45
+ required=True,
46
+ kwargs_type="denoiser_input_fields",
47
+ type_hint=torch.Tensor,
48
+ description="Pre-generated text embeddings. Can be generated from text_encoder step.",
49
+ ),
50
+ ]
51
+
52
+ @property
53
+ def intermediate_outputs(self) -> list[str]:
54
+ return [
55
+ OutputParam(
56
+ "batch_size",
57
+ type_hint=int,
58
+ description="Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt",
59
+ ),
60
+ OutputParam(
61
+ "dtype",
62
+ type_hint=torch.dtype,
63
+ description="Data type of model tensor inputs (determined by `prompt_embeds`)",
64
+ ),
65
+ OutputParam(
66
+ "prompt_embeds",
67
+ type_hint=torch.Tensor,
68
+ kwargs_type="denoiser_input_fields",
69
+ description="Text embeddings used to guide the image generation",
70
+ ),
71
+ ]
72
+
73
+ @torch.no_grad()
74
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
75
+ block_state = self.get_block_state(state)
76
+
77
+ block_state.batch_size = block_state.prompt_embeds.shape[0]
78
+ block_state.dtype = block_state.prompt_embeds.dtype
79
+
80
+ _, seq_len, _ = block_state.prompt_embeds.shape
81
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_images_per_prompt, 1)
82
+ block_state.prompt_embeds = block_state.prompt_embeds.view(
83
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
84
+ )
85
+
86
+ self.set_block_state(state, block_state)
87
+ return components, state
88
+
89
+
90
+ class Flux2KleinBaseTextInputStep(ModularPipelineBlocks):
91
+ model_name = "flux2-klein"
92
+
93
+ @property
94
+ def description(self) -> str:
95
+ return (
96
+ "This step:\n"
97
+ " 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
98
+ " 2. Ensures all text embeddings have consistent batch sizes (batch_size * num_images_per_prompt)"
99
+ )
100
+
101
+ @property
102
+ def inputs(self) -> list[InputParam]:
103
+ return [
104
+ InputParam("num_images_per_prompt", default=1),
105
+ InputParam(
106
+ "prompt_embeds",
107
+ required=True,
108
+ kwargs_type="denoiser_input_fields",
109
+ type_hint=torch.Tensor,
110
+ description="Pre-generated text embeddings. Can be generated from text_encoder step.",
111
+ ),
112
+ InputParam(
113
+ "negative_prompt_embeds",
114
+ required=False,
115
+ kwargs_type="denoiser_input_fields",
116
+ type_hint=torch.Tensor,
117
+ description="Pre-generated negative text embeddings. Can be generated from text_encoder step.",
118
+ ),
119
+ ]
120
+
121
+ @property
122
+ def intermediate_outputs(self) -> list[str]:
123
+ return [
124
+ OutputParam(
125
+ "batch_size",
126
+ type_hint=int,
127
+ description="Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt",
128
+ ),
129
+ OutputParam(
130
+ "dtype",
131
+ type_hint=torch.dtype,
132
+ description="Data type of model tensor inputs (determined by `prompt_embeds`)",
133
+ ),
134
+ OutputParam(
135
+ "prompt_embeds",
136
+ type_hint=torch.Tensor,
137
+ kwargs_type="denoiser_input_fields",
138
+ description="Text embeddings used to guide the image generation",
139
+ ),
140
+ OutputParam(
141
+ "negative_prompt_embeds",
142
+ type_hint=torch.Tensor,
143
+ kwargs_type="denoiser_input_fields",
144
+ description="Negative text embeddings used to guide the image generation",
145
+ ),
146
+ ]
147
+
148
+ @torch.no_grad()
149
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
150
+ block_state = self.get_block_state(state)
151
+
152
+ block_state.batch_size = block_state.prompt_embeds.shape[0]
153
+ block_state.dtype = block_state.prompt_embeds.dtype
154
+
155
+ _, seq_len, _ = block_state.prompt_embeds.shape
156
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_images_per_prompt, 1)
157
+ block_state.prompt_embeds = block_state.prompt_embeds.view(
158
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
159
+ )
160
+
161
+ if block_state.negative_prompt_embeds is not None:
162
+ _, seq_len, _ = block_state.negative_prompt_embeds.shape
163
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(
164
+ 1, block_state.num_images_per_prompt, 1
165
+ )
166
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
167
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
168
+ )
169
+
170
+ self.set_block_state(state, block_state)
171
+ return components, state
172
+
173
+
174
+ class Flux2ProcessImagesInputStep(ModularPipelineBlocks):
175
+ model_name = "flux2"
176
+
177
+ @property
178
+ def description(self) -> str:
179
+ return "Image preprocess step for Flux2. Validates and preprocesses reference images."
180
+
181
+ @property
182
+ def expected_components(self) -> list[ComponentSpec]:
183
+ return [
184
+ ComponentSpec(
185
+ "image_processor",
186
+ Flux2ImageProcessor,
187
+ config=FrozenDict({"vae_scale_factor": 16, "vae_latent_channels": 32}),
188
+ default_creation_method="from_config",
189
+ ),
190
+ ]
191
+
192
+ @property
193
+ def inputs(self) -> list[InputParam]:
194
+ return [
195
+ InputParam("image"),
196
+ InputParam("height"),
197
+ InputParam("width"),
198
+ ]
199
+
200
+ @property
201
+ def intermediate_outputs(self) -> list[OutputParam]:
202
+ return [OutputParam(name="condition_images", type_hint=list[torch.Tensor])]
203
+
204
+ @torch.no_grad()
205
+ def __call__(self, components: Flux2ModularPipeline, state: PipelineState):
206
+ block_state = self.get_block_state(state)
207
+ images = block_state.image
208
+
209
+ if images is None:
210
+ block_state.condition_images = None
211
+ self.set_block_state(state, block_state)
212
+ return components, state
213
+
214
+ if not isinstance(images, list):
215
+ images = [images]
216
+
217
+ condition_images = []
218
+ for img in images:
219
+ components.image_processor.check_image_input(img)
220
+
221
+ image_width, image_height = img.size
222
+ if image_width * image_height > 1024 * 1024:
223
+ img = components.image_processor._resize_to_target_area(img, 1024 * 1024)
224
+ image_width, image_height = img.size
225
+
226
+ multiple_of = components.vae_scale_factor * 2
227
+ image_width = (image_width // multiple_of) * multiple_of
228
+ image_height = (image_height // multiple_of) * multiple_of
229
+ condition_img = components.image_processor.preprocess(
230
+ img, height=image_height, width=image_width, resize_mode="crop"
231
+ )
232
+ condition_images.append(condition_img)
233
+
234
+ if block_state.height is None:
235
+ block_state.height = image_height
236
+ if block_state.width is None:
237
+ block_state.width = image_width
238
+
239
+ block_state.condition_images = condition_images
240
+
241
+ self.set_block_state(state, block_state)
242
+ return components, state
diffusers/modular_pipelines/flux2/modular_blocks_flux2.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Flux2PrepareGuidanceStep,
21
+ Flux2PrepareImageLatentsStep,
22
+ Flux2PrepareLatentsStep,
23
+ Flux2RoPEInputsStep,
24
+ Flux2SetTimestepsStep,
25
+ )
26
+ from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
27
+ from .denoise import Flux2DenoiseStep
28
+ from .encoders import (
29
+ Flux2TextEncoderStep,
30
+ Flux2VaeEncoderStep,
31
+ )
32
+ from .inputs import (
33
+ Flux2ProcessImagesInputStep,
34
+ Flux2TextInputStep,
35
+ )
36
+
37
+
38
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
39
+
40
+
41
+ # auto_docstring
42
+ class Flux2VaeEncoderSequentialStep(SequentialPipelineBlocks):
43
+ """
44
+ VAE encoder step that preprocesses, encodes, and prepares image latents for Flux2 conditioning.
45
+
46
+ Components:
47
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
48
+
49
+ Inputs:
50
+ image (`None`, *optional*):
51
+ TODO: Add description.
52
+ height (`None`, *optional*):
53
+ TODO: Add description.
54
+ width (`None`, *optional*):
55
+ TODO: Add description.
56
+ generator (`None`, *optional*):
57
+ TODO: Add description.
58
+
59
+ Outputs:
60
+ condition_images (`list`):
61
+ TODO: Add description.
62
+ image_latents (`list`):
63
+ List of latent representations for each reference image
64
+ """
65
+
66
+ model_name = "flux2"
67
+
68
+ block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
69
+ block_names = ["preprocess", "encode"]
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ return "VAE encoder step that preprocesses, encodes, and prepares image latents for Flux2 conditioning."
74
+
75
+
76
+ # auto_docstring
77
+ class Flux2AutoVaeEncoderStep(AutoPipelineBlocks):
78
+ """
79
+ VAE encoder step that encodes the image inputs into their latent representations.
80
+ This is an auto pipeline block that works for image conditioning tasks.
81
+ - `Flux2VaeEncoderSequentialStep` is used when `image` is provided.
82
+ - If `image` is not provided, step will be skipped.
83
+
84
+ Components:
85
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
86
+
87
+ Inputs:
88
+ image (`None`, *optional*):
89
+ TODO: Add description.
90
+ height (`None`, *optional*):
91
+ TODO: Add description.
92
+ width (`None`, *optional*):
93
+ TODO: Add description.
94
+ generator (`None`, *optional*):
95
+ TODO: Add description.
96
+
97
+ Outputs:
98
+ condition_images (`list`):
99
+ TODO: Add description.
100
+ image_latents (`list`):
101
+ List of latent representations for each reference image
102
+ """
103
+
104
+ block_classes = [Flux2VaeEncoderSequentialStep]
105
+ block_names = ["img_conditioning"]
106
+ block_trigger_inputs = ["image"]
107
+
108
+ @property
109
+ def description(self):
110
+ return (
111
+ "VAE encoder step that encodes the image inputs into their latent representations.\n"
112
+ "This is an auto pipeline block that works for image conditioning tasks.\n"
113
+ " - `Flux2VaeEncoderSequentialStep` is used when `image` is provided.\n"
114
+ " - If `image` is not provided, step will be skipped."
115
+ )
116
+
117
+
118
+ Flux2CoreDenoiseBlocks = InsertableDict(
119
+ [
120
+ ("input", Flux2TextInputStep()),
121
+ ("prepare_latents", Flux2PrepareLatentsStep()),
122
+ ("set_timesteps", Flux2SetTimestepsStep()),
123
+ ("prepare_guidance", Flux2PrepareGuidanceStep()),
124
+ ("prepare_rope_inputs", Flux2RoPEInputsStep()),
125
+ ("denoise", Flux2DenoiseStep()),
126
+ ("after_denoise", Flux2UnpackLatentsStep()),
127
+ ]
128
+ )
129
+
130
+
131
+ # auto_docstring
132
+ class Flux2CoreDenoiseStep(SequentialPipelineBlocks):
133
+ """
134
+ Core denoise step that performs the denoising process for Flux2-dev.
135
+
136
+ Components:
137
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
138
+
139
+ Inputs:
140
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
141
+ TODO: Add description.
142
+ prompt_embeds (`Tensor`):
143
+ Pre-generated text embeddings. Can be generated from text_encoder step.
144
+ height (`int`, *optional*):
145
+ TODO: Add description.
146
+ width (`int`, *optional*):
147
+ TODO: Add description.
148
+ latents (`Tensor | NoneType`, *optional*):
149
+ TODO: Add description.
150
+ generator (`None`, *optional*):
151
+ TODO: Add description.
152
+ num_inference_steps (`None`, *optional*, defaults to 50):
153
+ TODO: Add description.
154
+ timesteps (`None`, *optional*):
155
+ TODO: Add description.
156
+ sigmas (`None`, *optional*):
157
+ TODO: Add description.
158
+ guidance_scale (`None`, *optional*, defaults to 4.0):
159
+ TODO: Add description.
160
+ joint_attention_kwargs (`None`, *optional*):
161
+ TODO: Add description.
162
+ image_latents (`Tensor`, *optional*):
163
+ Packed image latents for conditioning. Shape: (B, img_seq_len, C)
164
+ image_latent_ids (`Tensor`, *optional*):
165
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
166
+
167
+ Outputs:
168
+ latents (`Tensor`):
169
+ Denoised latents.
170
+ """
171
+
172
+ model_name = "flux2"
173
+
174
+ block_classes = Flux2CoreDenoiseBlocks.values()
175
+ block_names = Flux2CoreDenoiseBlocks.keys()
176
+
177
+ @property
178
+ def description(self):
179
+ return "Core denoise step that performs the denoising process for Flux2-dev."
180
+
181
+ @property
182
+ def outputs(self):
183
+ return [
184
+ OutputParam.template("latents"),
185
+ ]
186
+
187
+
188
+ Flux2ImageConditionedCoreDenoiseBlocks = InsertableDict(
189
+ [
190
+ ("input", Flux2TextInputStep()),
191
+ ("prepare_image_latents", Flux2PrepareImageLatentsStep()),
192
+ ("prepare_latents", Flux2PrepareLatentsStep()),
193
+ ("set_timesteps", Flux2SetTimestepsStep()),
194
+ ("prepare_guidance", Flux2PrepareGuidanceStep()),
195
+ ("prepare_rope_inputs", Flux2RoPEInputsStep()),
196
+ ("denoise", Flux2DenoiseStep()),
197
+ ("after_denoise", Flux2UnpackLatentsStep()),
198
+ ]
199
+ )
200
+
201
+
202
+ # auto_docstring
203
+ class Flux2ImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
204
+ """
205
+ Core denoise step that performs the denoising process for Flux2-dev with image conditioning.
206
+
207
+ Components:
208
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
209
+
210
+ Inputs:
211
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
212
+ TODO: Add description.
213
+ prompt_embeds (`Tensor`):
214
+ Pre-generated text embeddings. Can be generated from text_encoder step.
215
+ image_latents (`list`, *optional*):
216
+ TODO: Add description.
217
+ height (`int`, *optional*):
218
+ TODO: Add description.
219
+ width (`int`, *optional*):
220
+ TODO: Add description.
221
+ latents (`Tensor | NoneType`, *optional*):
222
+ TODO: Add description.
223
+ generator (`None`, *optional*):
224
+ TODO: Add description.
225
+ num_inference_steps (`None`, *optional*, defaults to 50):
226
+ TODO: Add description.
227
+ timesteps (`None`, *optional*):
228
+ TODO: Add description.
229
+ sigmas (`None`, *optional*):
230
+ TODO: Add description.
231
+ guidance_scale (`None`, *optional*, defaults to 4.0):
232
+ TODO: Add description.
233
+ joint_attention_kwargs (`None`, *optional*):
234
+ TODO: Add description.
235
+
236
+ Outputs:
237
+ latents (`Tensor`):
238
+ Denoised latents.
239
+ """
240
+
241
+ model_name = "flux2"
242
+
243
+ block_classes = Flux2ImageConditionedCoreDenoiseBlocks.values()
244
+ block_names = Flux2ImageConditionedCoreDenoiseBlocks.keys()
245
+
246
+ @property
247
+ def description(self):
248
+ return "Core denoise step that performs the denoising process for Flux2-dev with image conditioning."
249
+
250
+ @property
251
+ def outputs(self):
252
+ return [
253
+ OutputParam.template("latents"),
254
+ ]
255
+
256
+
257
+ class Flux2AutoCoreDenoiseStep(AutoPipelineBlocks):
258
+ model_name = "flux2"
259
+
260
+ block_classes = [Flux2ImageConditionedCoreDenoiseStep, Flux2CoreDenoiseStep]
261
+ block_names = ["image_conditioned", "text2image"]
262
+ block_trigger_inputs = ["image_latents", None]
263
+
264
+ @property
265
+ def description(self):
266
+ return (
267
+ "Auto core denoise step that performs the denoising process for Flux2-dev."
268
+ "This is an auto pipeline block that works for text-to-image and image-conditioned generation."
269
+ " - `Flux2CoreDenoiseStep` is used for text-to-image generation.\n"
270
+ " - `Flux2ImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
271
+ )
272
+
273
+
274
+ AUTO_BLOCKS = InsertableDict(
275
+ [
276
+ ("text_encoder", Flux2TextEncoderStep()),
277
+ ("vae_encoder", Flux2AutoVaeEncoderStep()),
278
+ ("denoise", Flux2AutoCoreDenoiseStep()),
279
+ ("decode", Flux2DecodeStep()),
280
+ ]
281
+ )
282
+
283
+
284
+ # auto_docstring
285
+ class Flux2AutoBlocks(SequentialPipelineBlocks):
286
+ """
287
+ Auto Modular pipeline for text-to-image and image-conditioned generation using Flux2.
288
+
289
+ Supported workflows:
290
+ - `text2image`: requires `prompt`
291
+ - `image_conditioned`: requires `image`, `prompt`
292
+
293
+ Components:
294
+ text_encoder (`Mistral3ForConditionalGeneration`) tokenizer (`AutoProcessor`) image_processor
295
+ (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
296
+ (`Flux2Transformer2DModel`)
297
+
298
+ Inputs:
299
+ prompt (`None`, *optional*):
300
+ TODO: Add description.
301
+ max_sequence_length (`int`, *optional*, defaults to 512):
302
+ TODO: Add description.
303
+ text_encoder_out_layers (`tuple`, *optional*, defaults to (10, 20, 30)):
304
+ TODO: Add description.
305
+ image (`None`, *optional*):
306
+ TODO: Add description.
307
+ height (`None`, *optional*):
308
+ TODO: Add description.
309
+ width (`None`, *optional*):
310
+ TODO: Add description.
311
+ generator (`None`, *optional*):
312
+ TODO: Add description.
313
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
314
+ TODO: Add description.
315
+ image_latents (`list`, *optional*):
316
+ TODO: Add description.
317
+ latents (`Tensor | NoneType`):
318
+ TODO: Add description.
319
+ num_inference_steps (`None`):
320
+ TODO: Add description.
321
+ timesteps (`None`):
322
+ TODO: Add description.
323
+ sigmas (`None`, *optional*):
324
+ TODO: Add description.
325
+ guidance_scale (`None`, *optional*, defaults to 4.0):
326
+ TODO: Add description.
327
+ joint_attention_kwargs (`None`, *optional*):
328
+ TODO: Add description.
329
+ image_latent_ids (`Tensor`, *optional*):
330
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
331
+ output_type (`None`, *optional*, defaults to pil):
332
+ TODO: Add description.
333
+
334
+ Outputs:
335
+ images (`list`):
336
+ Generated images.
337
+ """
338
+
339
+ model_name = "flux2"
340
+
341
+ block_classes = AUTO_BLOCKS.values()
342
+ block_names = AUTO_BLOCKS.keys()
343
+ _workflow_map = {
344
+ "text2image": {"prompt": True},
345
+ "image_conditioned": {"image": True, "prompt": True},
346
+ }
347
+
348
+ @property
349
+ def description(self):
350
+ return "Auto Modular pipeline for text-to-image and image-conditioned generation using Flux2."
351
+
352
+ @property
353
+ def outputs(self):
354
+ return [
355
+ OutputParam.template("images"),
356
+ ]
diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Flux2PrepareImageLatentsStep,
21
+ Flux2PrepareLatentsStep,
22
+ Flux2RoPEInputsStep,
23
+ Flux2SetTimestepsStep,
24
+ )
25
+ from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
26
+ from .denoise import Flux2KleinDenoiseStep
27
+ from .encoders import (
28
+ Flux2KleinTextEncoderStep,
29
+ Flux2VaeEncoderStep,
30
+ )
31
+ from .inputs import (
32
+ Flux2ProcessImagesInputStep,
33
+ Flux2TextInputStep,
34
+ )
35
+
36
+
37
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
38
+
39
+ ################
40
+ # VAE encoder
41
+ ################
42
+
43
+
44
+ # auto_docstring
45
+ class Flux2KleinVaeEncoderSequentialStep(SequentialPipelineBlocks):
46
+ """
47
+ VAE encoder step that preprocesses and encodes the image inputs into their latent representations.
48
+
49
+ Components:
50
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
51
+
52
+ Inputs:
53
+ image (`None`, *optional*):
54
+ TODO: Add description.
55
+ height (`None`, *optional*):
56
+ TODO: Add description.
57
+ width (`None`, *optional*):
58
+ TODO: Add description.
59
+ generator (`None`, *optional*):
60
+ TODO: Add description.
61
+
62
+ Outputs:
63
+ condition_images (`list`):
64
+ TODO: Add description.
65
+ image_latents (`list`):
66
+ List of latent representations for each reference image
67
+ """
68
+
69
+ model_name = "flux2-klein"
70
+
71
+ block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
72
+ block_names = ["preprocess", "encode"]
73
+
74
+ @property
75
+ def description(self) -> str:
76
+ return "VAE encoder step that preprocesses and encodes the image inputs into their latent representations."
77
+
78
+
79
+ # auto_docstring
80
+ class Flux2KleinAutoVaeEncoderStep(AutoPipelineBlocks):
81
+ """
82
+ VAE encoder step that encodes the image inputs into their latent representations.
83
+ This is an auto pipeline block that works for image conditioning tasks.
84
+ - `Flux2KleinVaeEncoderSequentialStep` is used when `image` is provided.
85
+ - If `image` is not provided, step will be skipped.
86
+
87
+ Components:
88
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
89
+
90
+ Inputs:
91
+ image (`None`, *optional*):
92
+ TODO: Add description.
93
+ height (`None`, *optional*):
94
+ TODO: Add description.
95
+ width (`None`, *optional*):
96
+ TODO: Add description.
97
+ generator (`None`, *optional*):
98
+ TODO: Add description.
99
+
100
+ Outputs:
101
+ condition_images (`list`):
102
+ TODO: Add description.
103
+ image_latents (`list`):
104
+ List of latent representations for each reference image
105
+ """
106
+
107
+ model_name = "flux2-klein"
108
+
109
+ block_classes = [Flux2KleinVaeEncoderSequentialStep]
110
+ block_names = ["img_conditioning"]
111
+ block_trigger_inputs = ["image"]
112
+
113
+ @property
114
+ def description(self):
115
+ return (
116
+ "VAE encoder step that encodes the image inputs into their latent representations.\n"
117
+ "This is an auto pipeline block that works for image conditioning tasks.\n"
118
+ " - `Flux2KleinVaeEncoderSequentialStep` is used when `image` is provided.\n"
119
+ " - If `image` is not provided, step will be skipped."
120
+ )
121
+
122
+
123
+ ###
124
+ ### Core denoise
125
+ ###
126
+
127
+ Flux2KleinCoreDenoiseBlocks = InsertableDict(
128
+ [
129
+ ("input", Flux2TextInputStep()),
130
+ ("prepare_latents", Flux2PrepareLatentsStep()),
131
+ ("set_timesteps", Flux2SetTimestepsStep()),
132
+ ("prepare_rope_inputs", Flux2RoPEInputsStep()),
133
+ ("denoise", Flux2KleinDenoiseStep()),
134
+ ("after_denoise", Flux2UnpackLatentsStep()),
135
+ ]
136
+ )
137
+
138
+
139
+ # auto_docstring
140
+ class Flux2KleinCoreDenoiseStep(SequentialPipelineBlocks):
141
+ """
142
+ Core denoise step that performs the denoising process for Flux2-Klein (distilled model), for text-to-image
143
+ generation.
144
+
145
+ Components:
146
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
147
+
148
+ Inputs:
149
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
150
+ TODO: Add description.
151
+ prompt_embeds (`Tensor`):
152
+ Pre-generated text embeddings. Can be generated from text_encoder step.
153
+ height (`int`, *optional*):
154
+ TODO: Add description.
155
+ width (`int`, *optional*):
156
+ TODO: Add description.
157
+ latents (`Tensor | NoneType`, *optional*):
158
+ TODO: Add description.
159
+ generator (`None`, *optional*):
160
+ TODO: Add description.
161
+ num_inference_steps (`None`, *optional*, defaults to 50):
162
+ TODO: Add description.
163
+ timesteps (`None`, *optional*):
164
+ TODO: Add description.
165
+ sigmas (`None`, *optional*):
166
+ TODO: Add description.
167
+ joint_attention_kwargs (`None`, *optional*):
168
+ TODO: Add description.
169
+ image_latents (`Tensor`, *optional*):
170
+ Packed image latents for conditioning. Shape: (B, img_seq_len, C)
171
+ image_latent_ids (`Tensor`, *optional*):
172
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
173
+
174
+ Outputs:
175
+ latents (`Tensor`):
176
+ Denoised latents.
177
+ """
178
+
179
+ model_name = "flux2-klein"
180
+
181
+ block_classes = Flux2KleinCoreDenoiseBlocks.values()
182
+ block_names = Flux2KleinCoreDenoiseBlocks.keys()
183
+
184
+ @property
185
+ def description(self):
186
+ return "Core denoise step that performs the denoising process for Flux2-Klein (distilled model), for text-to-image generation."
187
+
188
+ @property
189
+ def outputs(self):
190
+ return [
191
+ OutputParam.template("latents"),
192
+ ]
193
+
194
+
195
+ Flux2KleinImageConditionedCoreDenoiseBlocks = InsertableDict(
196
+ [
197
+ ("input", Flux2TextInputStep()),
198
+ ("prepare_image_latents", Flux2PrepareImageLatentsStep()),
199
+ ("prepare_latents", Flux2PrepareLatentsStep()),
200
+ ("set_timesteps", Flux2SetTimestepsStep()),
201
+ ("prepare_rope_inputs", Flux2RoPEInputsStep()),
202
+ ("denoise", Flux2KleinDenoiseStep()),
203
+ ("after_denoise", Flux2UnpackLatentsStep()),
204
+ ]
205
+ )
206
+
207
+
208
+ # auto_docstring
209
+ class Flux2KleinImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
210
+ """
211
+ Core denoise step that performs the denoising process for Flux2-Klein (distilled model) with image conditioning.
212
+
213
+ Components:
214
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
215
+
216
+ Inputs:
217
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
218
+ TODO: Add description.
219
+ prompt_embeds (`Tensor`):
220
+ Pre-generated text embeddings. Can be generated from text_encoder step.
221
+ image_latents (`list`, *optional*):
222
+ TODO: Add description.
223
+ height (`int`, *optional*):
224
+ TODO: Add description.
225
+ width (`int`, *optional*):
226
+ TODO: Add description.
227
+ latents (`Tensor | NoneType`, *optional*):
228
+ TODO: Add description.
229
+ generator (`None`, *optional*):
230
+ TODO: Add description.
231
+ num_inference_steps (`None`, *optional*, defaults to 50):
232
+ TODO: Add description.
233
+ timesteps (`None`, *optional*):
234
+ TODO: Add description.
235
+ sigmas (`None`, *optional*):
236
+ TODO: Add description.
237
+ joint_attention_kwargs (`None`, *optional*):
238
+ TODO: Add description.
239
+
240
+ Outputs:
241
+ latents (`Tensor`):
242
+ Denoised latents.
243
+ """
244
+
245
+ model_name = "flux2-klein"
246
+
247
+ block_classes = Flux2KleinImageConditionedCoreDenoiseBlocks.values()
248
+ block_names = Flux2KleinImageConditionedCoreDenoiseBlocks.keys()
249
+
250
+ @property
251
+ def description(self):
252
+ return "Core denoise step that performs the denoising process for Flux2-Klein (distilled model) with image conditioning."
253
+
254
+ @property
255
+ def outputs(self):
256
+ return [
257
+ OutputParam.template("latents"),
258
+ ]
259
+
260
+
261
+ # auto_docstring
262
+ class Flux2KleinAutoCoreDenoiseStep(AutoPipelineBlocks):
263
+ """
264
+ Auto core denoise step that performs the denoising process for Flux2-Klein.
265
+ This is an auto pipeline block that works for text-to-image and image-conditioned generation.
266
+ - `Flux2KleinCoreDenoiseStep` is used for text-to-image generation.
267
+ - `Flux2KleinImageConditionedCoreDenoiseStep` is used for image-conditioned generation.
268
+
269
+ Components:
270
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
271
+
272
+ Inputs:
273
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
274
+ TODO: Add description.
275
+ prompt_embeds (`Tensor`):
276
+ Pre-generated text embeddings. Can be generated from text_encoder step.
277
+ image_latents (`list`, *optional*):
278
+ TODO: Add description.
279
+ height (`int`, *optional*):
280
+ TODO: Add description.
281
+ width (`int`, *optional*):
282
+ TODO: Add description.
283
+ latents (`Tensor | NoneType`):
284
+ TODO: Add description.
285
+ generator (`None`, *optional*):
286
+ TODO: Add description.
287
+ num_inference_steps (`None`):
288
+ TODO: Add description.
289
+ timesteps (`None`):
290
+ TODO: Add description.
291
+ sigmas (`None`, *optional*):
292
+ TODO: Add description.
293
+ joint_attention_kwargs (`None`, *optional*):
294
+ TODO: Add description.
295
+ image_latent_ids (`Tensor`, *optional*):
296
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
297
+
298
+ Outputs:
299
+ latents (`Tensor`):
300
+ Denoised latents.
301
+ """
302
+
303
+ model_name = "flux2-klein"
304
+ block_classes = [Flux2KleinImageConditionedCoreDenoiseStep, Flux2KleinCoreDenoiseStep]
305
+ block_names = ["image_conditioned", "text2image"]
306
+ block_trigger_inputs = ["image_latents", None]
307
+
308
+ @property
309
+ def description(self):
310
+ return (
311
+ "Auto core denoise step that performs the denoising process for Flux2-Klein.\n"
312
+ "This is an auto pipeline block that works for text-to-image and image-conditioned generation.\n"
313
+ " - `Flux2KleinCoreDenoiseStep` is used for text-to-image generation.\n"
314
+ " - `Flux2KleinImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
315
+ )
316
+
317
+
318
+ ###
319
+ ### Auto blocks
320
+ ###
321
+
322
+
323
+ # auto_docstring
324
+ class Flux2KleinAutoBlocks(SequentialPipelineBlocks):
325
+ """
326
+ Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein.
327
+
328
+ Supported workflows:
329
+ - `text2image`: requires `prompt`
330
+ - `image_conditioned`: requires `image`, `prompt`
331
+
332
+ Components:
333
+ text_encoder (`Qwen3ForCausalLM`) tokenizer (`Qwen2Tokenizer`) image_processor (`Flux2ImageProcessor`) vae
334
+ (`AutoencoderKLFlux2`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
335
+
336
+ Configs:
337
+ is_distilled (default: True)
338
+
339
+ Inputs:
340
+ prompt (`None`, *optional*):
341
+ TODO: Add description.
342
+ max_sequence_length (`int`, *optional*, defaults to 512):
343
+ TODO: Add description.
344
+ text_encoder_out_layers (`tuple`, *optional*, defaults to (9, 18, 27)):
345
+ TODO: Add description.
346
+ image (`None`, *optional*):
347
+ TODO: Add description.
348
+ height (`None`, *optional*):
349
+ TODO: Add description.
350
+ width (`None`, *optional*):
351
+ TODO: Add description.
352
+ generator (`None`, *optional*):
353
+ TODO: Add description.
354
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
355
+ TODO: Add description.
356
+ image_latents (`list`, *optional*):
357
+ TODO: Add description.
358
+ latents (`Tensor | NoneType`):
359
+ TODO: Add description.
360
+ num_inference_steps (`None`):
361
+ TODO: Add description.
362
+ timesteps (`None`):
363
+ TODO: Add description.
364
+ sigmas (`None`, *optional*):
365
+ TODO: Add description.
366
+ joint_attention_kwargs (`None`, *optional*):
367
+ TODO: Add description.
368
+ image_latent_ids (`Tensor`, *optional*):
369
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
370
+ output_type (`None`, *optional*, defaults to pil):
371
+ TODO: Add description.
372
+
373
+ Outputs:
374
+ images (`list`):
375
+ Generated images.
376
+ """
377
+
378
+ model_name = "flux2-klein"
379
+ block_classes = [
380
+ Flux2KleinTextEncoderStep(),
381
+ Flux2KleinAutoVaeEncoderStep(),
382
+ Flux2KleinAutoCoreDenoiseStep(),
383
+ Flux2DecodeStep(),
384
+ ]
385
+ block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
386
+ _workflow_map = {
387
+ "text2image": {"prompt": True},
388
+ "image_conditioned": {"image": True, "prompt": True},
389
+ }
390
+
391
+ @property
392
+ def description(self):
393
+ return "Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein."
394
+
395
+ @property
396
+ def outputs(self):
397
+ return [
398
+ OutputParam.template("images"),
399
+ ]
diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein_base.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Flux2KleinBaseRoPEInputsStep,
21
+ Flux2PrepareImageLatentsStep,
22
+ Flux2PrepareLatentsStep,
23
+ Flux2SetTimestepsStep,
24
+ )
25
+ from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
26
+ from .denoise import Flux2KleinBaseDenoiseStep
27
+ from .encoders import (
28
+ Flux2KleinBaseTextEncoderStep,
29
+ Flux2VaeEncoderStep,
30
+ )
31
+ from .inputs import (
32
+ Flux2KleinBaseTextInputStep,
33
+ Flux2ProcessImagesInputStep,
34
+ )
35
+
36
+
37
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
38
+
39
+ ################
40
+ # VAE encoder
41
+ ################
42
+
43
+
44
+ # auto_docstring
45
+ class Flux2KleinBaseVaeEncoderSequentialStep(SequentialPipelineBlocks):
46
+ """
47
+ VAE encoder step that preprocesses and encodes the image inputs into their latent representations.
48
+
49
+ Components:
50
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
51
+
52
+ Inputs:
53
+ image (`None`, *optional*):
54
+ TODO: Add description.
55
+ height (`None`, *optional*):
56
+ TODO: Add description.
57
+ width (`None`, *optional*):
58
+ TODO: Add description.
59
+ generator (`None`, *optional*):
60
+ TODO: Add description.
61
+
62
+ Outputs:
63
+ condition_images (`list`):
64
+ TODO: Add description.
65
+ image_latents (`list`):
66
+ List of latent representations for each reference image
67
+ """
68
+
69
+ model_name = "flux2"
70
+
71
+ block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
72
+ block_names = ["preprocess", "encode"]
73
+
74
+ @property
75
+ def description(self) -> str:
76
+ return "VAE encoder step that preprocesses and encodes the image inputs into their latent representations."
77
+
78
+
79
+ # auto_docstring
80
+ class Flux2KleinBaseAutoVaeEncoderStep(AutoPipelineBlocks):
81
+ """
82
+ VAE encoder step that encodes the image inputs into their latent representations.
83
+ This is an auto pipeline block that works for image conditioning tasks.
84
+ - `Flux2KleinBaseVaeEncoderSequentialStep` is used when `image` is provided.
85
+ - If `image` is not provided, step will be skipped.
86
+
87
+ Components:
88
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
89
+
90
+ Inputs:
91
+ image (`None`, *optional*):
92
+ TODO: Add description.
93
+ height (`None`, *optional*):
94
+ TODO: Add description.
95
+ width (`None`, *optional*):
96
+ TODO: Add description.
97
+ generator (`None`, *optional*):
98
+ TODO: Add description.
99
+
100
+ Outputs:
101
+ condition_images (`list`):
102
+ TODO: Add description.
103
+ image_latents (`list`):
104
+ List of latent representations for each reference image
105
+ """
106
+
107
+ block_classes = [Flux2KleinBaseVaeEncoderSequentialStep]
108
+ block_names = ["img_conditioning"]
109
+ block_trigger_inputs = ["image"]
110
+
111
+ @property
112
+ def description(self):
113
+ return (
114
+ "VAE encoder step that encodes the image inputs into their latent representations.\n"
115
+ "This is an auto pipeline block that works for image conditioning tasks.\n"
116
+ " - `Flux2KleinBaseVaeEncoderSequentialStep` is used when `image` is provided.\n"
117
+ " - If `image` is not provided, step will be skipped."
118
+ )
119
+
120
+
121
+ ###
122
+ ### Core denoise
123
+ ###
124
+
125
+ Flux2KleinBaseCoreDenoiseBlocks = InsertableDict(
126
+ [
127
+ ("input", Flux2KleinBaseTextInputStep()),
128
+ ("prepare_latents", Flux2PrepareLatentsStep()),
129
+ ("set_timesteps", Flux2SetTimestepsStep()),
130
+ ("prepare_rope_inputs", Flux2KleinBaseRoPEInputsStep()),
131
+ ("denoise", Flux2KleinBaseDenoiseStep()),
132
+ ("after_denoise", Flux2UnpackLatentsStep()),
133
+ ]
134
+ )
135
+
136
+
137
+ # auto_docstring
138
+ class Flux2KleinBaseCoreDenoiseStep(SequentialPipelineBlocks):
139
+ """
140
+ Core denoise step that performs the denoising process for Flux2-Klein (base model), for text-to-image generation.
141
+
142
+ Components:
143
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
144
+ (`ClassifierFreeGuidance`)
145
+
146
+ Configs:
147
+ is_distilled (default: False)
148
+
149
+ Inputs:
150
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
151
+ TODO: Add description.
152
+ prompt_embeds (`Tensor`):
153
+ Pre-generated text embeddings. Can be generated from text_encoder step.
154
+ negative_prompt_embeds (`Tensor`, *optional*):
155
+ Pre-generated negative text embeddings. Can be generated from text_encoder step.
156
+ height (`int`, *optional*):
157
+ TODO: Add description.
158
+ width (`int`, *optional*):
159
+ TODO: Add description.
160
+ latents (`Tensor | NoneType`, *optional*):
161
+ TODO: Add description.
162
+ generator (`None`, *optional*):
163
+ TODO: Add description.
164
+ num_inference_steps (`None`, *optional*, defaults to 50):
165
+ TODO: Add description.
166
+ timesteps (`None`, *optional*):
167
+ TODO: Add description.
168
+ sigmas (`None`, *optional*):
169
+ TODO: Add description.
170
+ joint_attention_kwargs (`None`, *optional*):
171
+ TODO: Add description.
172
+ image_latents (`Tensor`, *optional*):
173
+ Packed image latents for conditioning. Shape: (B, img_seq_len, C)
174
+ image_latent_ids (`Tensor`, *optional*):
175
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
176
+
177
+ Outputs:
178
+ latents (`Tensor`):
179
+ Denoised latents.
180
+ """
181
+
182
+ model_name = "flux2-klein"
183
+ block_classes = Flux2KleinBaseCoreDenoiseBlocks.values()
184
+ block_names = Flux2KleinBaseCoreDenoiseBlocks.keys()
185
+
186
+ @property
187
+ def description(self):
188
+ return "Core denoise step that performs the denoising process for Flux2-Klein (base model), for text-to-image generation."
189
+
190
+ @property
191
+ def outputs(self):
192
+ return [
193
+ OutputParam.template("latents"),
194
+ ]
195
+
196
+
197
+ Flux2KleinBaseImageConditionedCoreDenoiseBlocks = InsertableDict(
198
+ [
199
+ ("input", Flux2KleinBaseTextInputStep()),
200
+ ("prepare_latents", Flux2PrepareLatentsStep()),
201
+ ("prepare_image_latents", Flux2PrepareImageLatentsStep()),
202
+ ("set_timesteps", Flux2SetTimestepsStep()),
203
+ ("prepare_rope_inputs", Flux2KleinBaseRoPEInputsStep()),
204
+ ("denoise", Flux2KleinBaseDenoiseStep()),
205
+ ("after_denoise", Flux2UnpackLatentsStep()),
206
+ ]
207
+ )
208
+
209
+
210
+ # auto_docstring
211
+ class Flux2KleinBaseImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
212
+ """
213
+ Core denoise step that performs the denoising process for Flux2-Klein (base model) with image conditioning.
214
+
215
+ Components:
216
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
217
+ (`ClassifierFreeGuidance`)
218
+
219
+ Configs:
220
+ is_distilled (default: False)
221
+
222
+ Inputs:
223
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
224
+ TODO: Add description.
225
+ prompt_embeds (`Tensor`):
226
+ Pre-generated text embeddings. Can be generated from text_encoder step.
227
+ negative_prompt_embeds (`Tensor`, *optional*):
228
+ Pre-generated negative text embeddings. Can be generated from text_encoder step.
229
+ height (`int`, *optional*):
230
+ TODO: Add description.
231
+ width (`int`, *optional*):
232
+ TODO: Add description.
233
+ latents (`Tensor | NoneType`, *optional*):
234
+ TODO: Add description.
235
+ generator (`None`, *optional*):
236
+ TODO: Add description.
237
+ image_latents (`list`, *optional*):
238
+ TODO: Add description.
239
+ num_inference_steps (`None`, *optional*, defaults to 50):
240
+ TODO: Add description.
241
+ timesteps (`None`, *optional*):
242
+ TODO: Add description.
243
+ sigmas (`None`, *optional*):
244
+ TODO: Add description.
245
+ joint_attention_kwargs (`None`, *optional*):
246
+ TODO: Add description.
247
+
248
+ Outputs:
249
+ latents (`Tensor`):
250
+ Denoised latents.
251
+ """
252
+
253
+ model_name = "flux2-klein"
254
+ block_classes = Flux2KleinBaseImageConditionedCoreDenoiseBlocks.values()
255
+ block_names = Flux2KleinBaseImageConditionedCoreDenoiseBlocks.keys()
256
+
257
+ @property
258
+ def description(self):
259
+ return "Core denoise step that performs the denoising process for Flux2-Klein (base model) with image conditioning."
260
+
261
+ @property
262
+ def outputs(self):
263
+ return [
264
+ OutputParam.template("latents"),
265
+ ]
266
+
267
+
268
+ # auto_docstring
269
+ class Flux2KleinBaseAutoCoreDenoiseStep(AutoPipelineBlocks):
270
+ """
271
+ Auto core denoise step that performs the denoising process for Flux2-Klein (base model).
272
+ This is an auto pipeline block that works for text-to-image and image-conditioned generation.
273
+ - `Flux2KleinBaseCoreDenoiseStep` is used for text-to-image generation.
274
+ - `Flux2KleinBaseImageConditionedCoreDenoiseStep` is used for image-conditioned generation.
275
+
276
+ Components:
277
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
278
+ (`ClassifierFreeGuidance`)
279
+
280
+ Configs:
281
+ is_distilled (default: False)
282
+
283
+ Inputs:
284
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
285
+ TODO: Add description.
286
+ prompt_embeds (`Tensor`):
287
+ Pre-generated text embeddings. Can be generated from text_encoder step.
288
+ negative_prompt_embeds (`Tensor`, *optional*):
289
+ Pre-generated negative text embeddings. Can be generated from text_encoder step.
290
+ height (`int`, *optional*):
291
+ TODO: Add description.
292
+ width (`int`, *optional*):
293
+ TODO: Add description.
294
+ latents (`Tensor | NoneType`):
295
+ TODO: Add description.
296
+ generator (`None`, *optional*):
297
+ TODO: Add description.
298
+ image_latents (`list`, *optional*):
299
+ TODO: Add description.
300
+ num_inference_steps (`None`):
301
+ TODO: Add description.
302
+ timesteps (`None`):
303
+ TODO: Add description.
304
+ sigmas (`None`, *optional*):
305
+ TODO: Add description.
306
+ joint_attention_kwargs (`None`, *optional*):
307
+ TODO: Add description.
308
+ image_latent_ids (`Tensor`, *optional*):
309
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
310
+
311
+ Outputs:
312
+ latents (`Tensor`):
313
+ Denoised latents.
314
+ """
315
+
316
+ model_name = "flux2-klein"
317
+ block_classes = [Flux2KleinBaseImageConditionedCoreDenoiseStep, Flux2KleinBaseCoreDenoiseStep]
318
+ block_names = ["image_conditioned", "text2image"]
319
+ block_trigger_inputs = ["image_latents", None]
320
+
321
+ @property
322
+ def description(self):
323
+ return (
324
+ "Auto core denoise step that performs the denoising process for Flux2-Klein (base model).\n"
325
+ "This is an auto pipeline block that works for text-to-image and image-conditioned generation.\n"
326
+ " - `Flux2KleinBaseCoreDenoiseStep` is used for text-to-image generation.\n"
327
+ " - `Flux2KleinBaseImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
328
+ )
329
+
330
+
331
+ ###
332
+ ### Auto blocks
333
+ ###
334
+
335
+
336
+ # auto_docstring
337
+ class Flux2KleinBaseAutoBlocks(SequentialPipelineBlocks):
338
+ """
339
+ Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein (base model).
340
+
341
+ Supported workflows:
342
+ - `text2image`: requires `prompt`
343
+ - `image_conditioned`: requires `image`, `prompt`
344
+
345
+ Components:
346
+ text_encoder (`Qwen3ForCausalLM`) tokenizer (`Qwen2Tokenizer`) guider (`ClassifierFreeGuidance`)
347
+ image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`) scheduler
348
+ (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
349
+
350
+ Configs:
351
+ is_distilled (default: False)
352
+
353
+ Inputs:
354
+ prompt (`None`, *optional*):
355
+ TODO: Add description.
356
+ max_sequence_length (`int`, *optional*, defaults to 512):
357
+ TODO: Add description.
358
+ text_encoder_out_layers (`tuple`, *optional*, defaults to (9, 18, 27)):
359
+ TODO: Add description.
360
+ image (`None`, *optional*):
361
+ TODO: Add description.
362
+ height (`None`, *optional*):
363
+ TODO: Add description.
364
+ width (`None`, *optional*):
365
+ TODO: Add description.
366
+ generator (`None`, *optional*):
367
+ TODO: Add description.
368
+ num_images_per_prompt (`None`, *optional*, defaults to 1):
369
+ TODO: Add description.
370
+ latents (`Tensor | NoneType`):
371
+ TODO: Add description.
372
+ image_latents (`list`, *optional*):
373
+ TODO: Add description.
374
+ num_inference_steps (`None`):
375
+ TODO: Add description.
376
+ timesteps (`None`):
377
+ TODO: Add description.
378
+ sigmas (`None`, *optional*):
379
+ TODO: Add description.
380
+ joint_attention_kwargs (`None`, *optional*):
381
+ TODO: Add description.
382
+ image_latent_ids (`Tensor`, *optional*):
383
+ Position IDs for image latents. Shape: (B, img_seq_len, 4)
384
+ output_type (`None`, *optional*, defaults to pil):
385
+ TODO: Add description.
386
+
387
+ Outputs:
388
+ images (`list`):
389
+ Generated images.
390
+ """
391
+
392
+ model_name = "flux2-klein"
393
+ block_classes = [
394
+ Flux2KleinBaseTextEncoderStep(),
395
+ Flux2KleinBaseAutoVaeEncoderStep(),
396
+ Flux2KleinBaseAutoCoreDenoiseStep(),
397
+ Flux2DecodeStep(),
398
+ ]
399
+ block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
400
+ _workflow_map = {
401
+ "text2image": {"prompt": True},
402
+ "image_conditioned": {"image": True, "prompt": True},
403
+ }
404
+
405
+ @property
406
+ def description(self):
407
+ return "Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein (base model)."
408
+
409
+ @property
410
+ def outputs(self):
411
+ return [
412
+ OutputParam.template("images"),
413
+ ]
diffusers/modular_pipelines/flux2/modular_pipeline.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...loaders import Flux2LoraLoaderMixin
17
+ from ...utils import logging
18
+ from ..modular_pipeline import ModularPipeline
19
+
20
+
21
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
22
+
23
+
24
+ class Flux2ModularPipeline(ModularPipeline, Flux2LoraLoaderMixin):
25
+ """
26
+ A ModularPipeline for Flux2.
27
+
28
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
29
+ """
30
+
31
+ default_blocks_name = "Flux2AutoBlocks"
32
+
33
+ @property
34
+ def default_height(self):
35
+ return self.default_sample_size * self.vae_scale_factor
36
+
37
+ @property
38
+ def default_width(self):
39
+ return self.default_sample_size * self.vae_scale_factor
40
+
41
+ @property
42
+ def default_sample_size(self):
43
+ return 128
44
+
45
+ @property
46
+ def vae_scale_factor(self):
47
+ vae_scale_factor = 8
48
+ if getattr(self, "vae", None) is not None:
49
+ vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
50
+ return vae_scale_factor
51
+
52
+ @property
53
+ def num_channels_latents(self):
54
+ num_channels_latents = 32
55
+ if getattr(self, "transformer", None):
56
+ num_channels_latents = self.transformer.config.in_channels // 4
57
+ return num_channels_latents
58
+
59
+
60
+ class Flux2KleinModularPipeline(Flux2ModularPipeline):
61
+ """
62
+ A ModularPipeline for Flux2-Klein (distilled model).
63
+
64
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
65
+ """
66
+
67
+ default_blocks_name = "Flux2KleinAutoBlocks"
68
+
69
+ @property
70
+ def requires_unconditional_embeds(self):
71
+ if hasattr(self.config, "is_distilled") and self.config.is_distilled:
72
+ return False
73
+
74
+ requires_unconditional_embeds = False
75
+ if hasattr(self, "guider") and self.guider is not None:
76
+ requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
77
+
78
+ return requires_unconditional_embeds
79
+
80
+
81
+ class Flux2KleinBaseModularPipeline(Flux2ModularPipeline):
82
+ """
83
+ A ModularPipeline for Flux2-Klein (base model).
84
+
85
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
86
+ """
87
+
88
+ default_blocks_name = "Flux2KleinBaseAutoBlocks"
89
+
90
+ @property
91
+ def requires_unconditional_embeds(self):
92
+ if hasattr(self.config, "is_distilled") and self.config.is_distilled:
93
+ return False
94
+
95
+ requires_unconditional_embeds = False
96
+ if hasattr(self, "guider") and self.guider is not None:
97
+ requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
98
+
99
+ return requires_unconditional_embeds
diffusers/modular_pipelines/helios/__init__.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_helios"] = ["HeliosAutoBlocks"]
25
+ _import_structure["modular_blocks_helios_pyramid"] = ["HeliosPyramidAutoBlocks"]
26
+ _import_structure["modular_blocks_helios_pyramid_distilled"] = ["HeliosPyramidDistilledAutoBlocks"]
27
+ _import_structure["modular_pipeline"] = [
28
+ "HeliosModularPipeline",
29
+ "HeliosPyramidDistilledModularPipeline",
30
+ "HeliosPyramidModularPipeline",
31
+ ]
32
+
33
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
34
+ try:
35
+ if not (is_transformers_available() and is_torch_available()):
36
+ raise OptionalDependencyNotAvailable()
37
+ except OptionalDependencyNotAvailable:
38
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
39
+ else:
40
+ from .modular_blocks_helios import HeliosAutoBlocks
41
+ from .modular_blocks_helios_pyramid import HeliosPyramidAutoBlocks
42
+ from .modular_blocks_helios_pyramid_distilled import HeliosPyramidDistilledAutoBlocks
43
+ from .modular_pipeline import (
44
+ HeliosModularPipeline,
45
+ HeliosPyramidDistilledModularPipeline,
46
+ HeliosPyramidModularPipeline,
47
+ )
48
+ else:
49
+ import sys
50
+
51
+ sys.modules[__name__] = _LazyModule(
52
+ __name__,
53
+ globals()["__file__"],
54
+ _import_structure,
55
+ module_spec=__spec__,
56
+ )
57
+
58
+ for name, value in _dummy_objects.items():
59
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/helios/before_denoise.py ADDED
@@ -0,0 +1,836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import numpy as np
16
+ import torch
17
+
18
+ from ...models import HeliosTransformer3DModel
19
+ from ...schedulers import HeliosScheduler
20
+ from ...utils import logging
21
+ from ...utils.torch_utils import randn_tensor
22
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+ from .modular_pipeline import HeliosModularPipeline
25
+
26
+
27
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+
30
+ # Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift
31
+ def calculate_shift(
32
+ image_seq_len,
33
+ base_seq_len: int = 256,
34
+ max_seq_len: int = 4096,
35
+ base_shift: float = 0.5,
36
+ max_shift: float = 1.15,
37
+ ):
38
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
39
+ b = base_shift - m * base_seq_len
40
+ mu = image_seq_len * m + b
41
+ return mu
42
+
43
+
44
+ class HeliosTextInputStep(ModularPipelineBlocks):
45
+ model_name = "helios"
46
+
47
+ @property
48
+ def description(self) -> str:
49
+ return (
50
+ "Input processing step that:\n"
51
+ " 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
52
+ " 2. Adjusts input tensor shapes based on `batch_size` (number of prompts) and `num_videos_per_prompt`\n\n"
53
+ "All input tensors are expected to have either batch_size=1 or match the batch_size\n"
54
+ "of prompt_embeds. The tensors will be duplicated across the batch dimension to\n"
55
+ "have a final batch_size of batch_size * num_videos_per_prompt."
56
+ )
57
+
58
+ @property
59
+ def inputs(self) -> list[InputParam]:
60
+ return [
61
+ InputParam(
62
+ "num_videos_per_prompt",
63
+ default=1,
64
+ type_hint=int,
65
+ description="Number of videos to generate per prompt.",
66
+ ),
67
+ InputParam.template("prompt_embeds"),
68
+ InputParam.template("negative_prompt_embeds"),
69
+ ]
70
+
71
+ @property
72
+ def intermediate_outputs(self) -> list[str]:
73
+ return [
74
+ OutputParam(
75
+ "batch_size",
76
+ type_hint=int,
77
+ description="Number of prompts, the final batch size of model inputs should be batch_size * num_videos_per_prompt",
78
+ ),
79
+ OutputParam(
80
+ "dtype",
81
+ type_hint=torch.dtype,
82
+ description="Data type of model tensor inputs (determined by `prompt_embeds.dtype`)",
83
+ ),
84
+ ]
85
+
86
+ def check_inputs(self, components, block_state):
87
+ if block_state.prompt_embeds is not None and block_state.negative_prompt_embeds is not None:
88
+ if block_state.prompt_embeds.shape != block_state.negative_prompt_embeds.shape:
89
+ raise ValueError(
90
+ "`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but"
91
+ f" got: `prompt_embeds` {block_state.prompt_embeds.shape} != `negative_prompt_embeds`"
92
+ f" {block_state.negative_prompt_embeds.shape}."
93
+ )
94
+
95
+ @torch.no_grad()
96
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
97
+ block_state = self.get_block_state(state)
98
+ self.check_inputs(components, block_state)
99
+
100
+ block_state.batch_size = block_state.prompt_embeds.shape[0]
101
+ block_state.dtype = block_state.prompt_embeds.dtype
102
+
103
+ _, seq_len, _ = block_state.prompt_embeds.shape
104
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_videos_per_prompt, 1)
105
+ block_state.prompt_embeds = block_state.prompt_embeds.view(
106
+ block_state.batch_size * block_state.num_videos_per_prompt, seq_len, -1
107
+ )
108
+
109
+ if block_state.negative_prompt_embeds is not None:
110
+ _, seq_len, _ = block_state.negative_prompt_embeds.shape
111
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(
112
+ 1, block_state.num_videos_per_prompt, 1
113
+ )
114
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
115
+ block_state.batch_size * block_state.num_videos_per_prompt, seq_len, -1
116
+ )
117
+
118
+ self.set_block_state(state, block_state)
119
+
120
+ return components, state
121
+
122
+
123
+ # Copied from diffusers.modular_pipelines.wan.before_denoise.repeat_tensor_to_batch_size
124
+ def repeat_tensor_to_batch_size(
125
+ input_name: str,
126
+ input_tensor: torch.Tensor,
127
+ batch_size: int,
128
+ num_videos_per_prompt: int = 1,
129
+ ) -> torch.Tensor:
130
+ """Repeat tensor elements to match the final batch size.
131
+
132
+ This function expands a tensor's batch dimension to match the final batch size (batch_size * num_videos_per_prompt)
133
+ by repeating each element along dimension 0.
134
+
135
+ The input tensor must have batch size 1 or batch_size. The function will:
136
+ - If batch size is 1: repeat each element (batch_size * num_videos_per_prompt) times
137
+ - If batch size equals batch_size: repeat each element num_videos_per_prompt times
138
+
139
+ Args:
140
+ input_name (str): Name of the input tensor (used for error messages)
141
+ input_tensor (torch.Tensor): The tensor to repeat. Must have batch size 1 or batch_size.
142
+ batch_size (int): The base batch size (number of prompts)
143
+ num_videos_per_prompt (int, optional): Number of videos to generate per prompt. Defaults to 1.
144
+
145
+ Returns:
146
+ torch.Tensor: The repeated tensor with final batch size (batch_size * num_videos_per_prompt)
147
+
148
+ Raises:
149
+ ValueError: If input_tensor is not a torch.Tensor or has invalid batch size
150
+
151
+ Examples:
152
+ tensor = torch.tensor([[1, 2, 3]]) # shape: [1, 3] repeated = repeat_tensor_to_batch_size("image", tensor,
153
+ batch_size=2, num_videos_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) - shape:
154
+ [4, 3]
155
+
156
+ tensor = torch.tensor([[1, 2, 3], [4, 5, 6]]) # shape: [2, 3] repeated = repeat_tensor_to_batch_size("image",
157
+ tensor, batch_size=2, num_videos_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [4, 5, 6], [4, 5, 6]])
158
+ - shape: [4, 3]
159
+ """
160
+ # make sure input is a tensor
161
+ if not isinstance(input_tensor, torch.Tensor):
162
+ raise ValueError(f"`{input_name}` must be a tensor")
163
+
164
+ # make sure input tensor e.g. image_latents has batch size 1 or batch_size same as prompts
165
+ if input_tensor.shape[0] == 1:
166
+ repeat_by = batch_size * num_videos_per_prompt
167
+ elif input_tensor.shape[0] == batch_size:
168
+ repeat_by = num_videos_per_prompt
169
+ else:
170
+ raise ValueError(
171
+ f"`{input_name}` must have have batch size 1 or {batch_size}, but got {input_tensor.shape[0]}"
172
+ )
173
+
174
+ # expand the tensor to match the batch_size * num_videos_per_prompt
175
+ input_tensor = input_tensor.repeat_interleave(repeat_by, dim=0)
176
+
177
+ return input_tensor
178
+
179
+
180
+ # Copied from diffusers.modular_pipelines.wan.before_denoise.calculate_dimension_from_latents
181
+ def calculate_dimension_from_latents(
182
+ latents: torch.Tensor, vae_scale_factor_temporal: int, vae_scale_factor_spatial: int
183
+ ) -> tuple[int, int]:
184
+ """Calculate image dimensions from latent tensor dimensions.
185
+
186
+ This function converts latent temporal and spatial dimensions to image temporal and spatial dimensions by
187
+ multiplying the latent num_frames/height/width by the VAE scale factor.
188
+
189
+ Args:
190
+ latents (torch.Tensor): The latent tensor. Must have 4 or 5 dimensions.
191
+ Expected shapes: [batch, channels, height, width] or [batch, channels, frames, height, width]
192
+ vae_scale_factor_temporal (int): The scale factor used by the VAE to compress temporal dimension.
193
+ Typically 4 for most VAEs (video is 4x larger than latents in temporal dimension)
194
+ vae_scale_factor_spatial (int): The scale factor used by the VAE to compress spatial dimension.
195
+ Typically 8 for most VAEs (image is 8x larger than latents in each dimension)
196
+
197
+ Returns:
198
+ tuple[int, int]: The calculated image dimensions as (height, width)
199
+
200
+ Raises:
201
+ ValueError: If latents tensor doesn't have 4 or 5 dimensions
202
+
203
+ """
204
+ if latents.ndim != 5:
205
+ raise ValueError(f"latents must have 5 dimensions, but got {latents.ndim}")
206
+
207
+ _, _, num_latent_frames, latent_height, latent_width = latents.shape
208
+
209
+ num_frames = (num_latent_frames - 1) * vae_scale_factor_temporal + 1
210
+ height = latent_height * vae_scale_factor_spatial
211
+ width = latent_width * vae_scale_factor_spatial
212
+
213
+ return num_frames, height, width
214
+
215
+
216
+ class HeliosAdditionalInputsStep(ModularPipelineBlocks):
217
+ """Configurable step that standardizes inputs for the denoising step.
218
+
219
+ This step handles:
220
+ 1. For encoded image latents: Computes height/width from latents and expands batch size
221
+ 2. For additional_batch_inputs: Expands batch dimensions to match final batch size
222
+ """
223
+
224
+ model_name = "helios"
225
+
226
+ def __init__(
227
+ self,
228
+ image_latent_inputs: list[InputParam] | None = None,
229
+ additional_batch_inputs: list[InputParam] | None = None,
230
+ ):
231
+ if image_latent_inputs is None:
232
+ image_latent_inputs = [InputParam.template("image_latents")]
233
+ if additional_batch_inputs is None:
234
+ additional_batch_inputs = []
235
+
236
+ if not isinstance(image_latent_inputs, list):
237
+ raise ValueError(f"image_latent_inputs must be a list, but got {type(image_latent_inputs)}")
238
+ else:
239
+ for input_param in image_latent_inputs:
240
+ if not isinstance(input_param, InputParam):
241
+ raise ValueError(f"image_latent_inputs must be a list of InputParam, but got {type(input_param)}")
242
+
243
+ if not isinstance(additional_batch_inputs, list):
244
+ raise ValueError(f"additional_batch_inputs must be a list, but got {type(additional_batch_inputs)}")
245
+ else:
246
+ for input_param in additional_batch_inputs:
247
+ if not isinstance(input_param, InputParam):
248
+ raise ValueError(
249
+ f"additional_batch_inputs must be a list of InputParam, but got {type(input_param)}"
250
+ )
251
+
252
+ self._image_latent_inputs = image_latent_inputs
253
+ self._additional_batch_inputs = additional_batch_inputs
254
+ super().__init__()
255
+
256
+ @property
257
+ def description(self) -> str:
258
+ summary_section = (
259
+ "Input processing step that:\n"
260
+ " 1. For image latent inputs: Computes height/width from latents and expands batch size\n"
261
+ " 2. For additional batch inputs: Expands batch dimensions to match final batch size"
262
+ )
263
+
264
+ inputs_info = ""
265
+ if self._image_latent_inputs or self._additional_batch_inputs:
266
+ inputs_info = "\n\nConfigured inputs:"
267
+ if self._image_latent_inputs:
268
+ inputs_info += f"\n - Image latent inputs: {[p.name for p in self._image_latent_inputs]}"
269
+ if self._additional_batch_inputs:
270
+ inputs_info += f"\n - Additional batch inputs: {[p.name for p in self._additional_batch_inputs]}"
271
+
272
+ placement_section = "\n\nThis block should be placed after the encoder steps and the text input step."
273
+
274
+ return summary_section + inputs_info + placement_section
275
+
276
+ @property
277
+ def inputs(self) -> list[InputParam]:
278
+ inputs = [
279
+ InputParam(name="num_videos_per_prompt", default=1),
280
+ InputParam(name="batch_size", required=True),
281
+ ]
282
+ inputs += self._image_latent_inputs + self._additional_batch_inputs
283
+
284
+ return inputs
285
+
286
+ @property
287
+ def intermediate_outputs(self) -> list[OutputParam]:
288
+ outputs = [
289
+ OutputParam("height", type_hint=int),
290
+ OutputParam("width", type_hint=int),
291
+ ]
292
+
293
+ for input_param in self._image_latent_inputs:
294
+ outputs.append(OutputParam(input_param.name, type_hint=torch.Tensor))
295
+
296
+ for input_param in self._additional_batch_inputs:
297
+ outputs.append(OutputParam(input_param.name, type_hint=torch.Tensor))
298
+
299
+ return outputs
300
+
301
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
302
+ block_state = self.get_block_state(state)
303
+
304
+ for input_param in self._image_latent_inputs:
305
+ image_latent_tensor = getattr(block_state, input_param.name)
306
+ if image_latent_tensor is None:
307
+ continue
308
+
309
+ # Calculate height/width from latents
310
+ _, height, width = calculate_dimension_from_latents(
311
+ image_latent_tensor, components.vae_scale_factor_temporal, components.vae_scale_factor_spatial
312
+ )
313
+ block_state.height = height
314
+ block_state.width = width
315
+
316
+ # Expand batch size
317
+ image_latent_tensor = repeat_tensor_to_batch_size(
318
+ input_name=input_param.name,
319
+ input_tensor=image_latent_tensor,
320
+ num_videos_per_prompt=block_state.num_videos_per_prompt,
321
+ batch_size=block_state.batch_size,
322
+ )
323
+
324
+ setattr(block_state, input_param.name, image_latent_tensor)
325
+
326
+ for input_param in self._additional_batch_inputs:
327
+ input_tensor = getattr(block_state, input_param.name)
328
+ if input_tensor is None:
329
+ continue
330
+
331
+ input_tensor = repeat_tensor_to_batch_size(
332
+ input_name=input_param.name,
333
+ input_tensor=input_tensor,
334
+ num_videos_per_prompt=block_state.num_videos_per_prompt,
335
+ batch_size=block_state.batch_size,
336
+ )
337
+
338
+ setattr(block_state, input_param.name, input_tensor)
339
+
340
+ self.set_block_state(state, block_state)
341
+ return components, state
342
+
343
+
344
+ class HeliosAddNoiseToImageLatentsStep(ModularPipelineBlocks):
345
+ """Adds noise to image_latents and fake_image_latents for I2V conditioning.
346
+
347
+ Applies single-sigma noise to image_latents (using image_noise_sigma range) and single-sigma noise to
348
+ fake_image_latents (using video_noise_sigma range).
349
+ """
350
+
351
+ model_name = "helios"
352
+
353
+ @property
354
+ def description(self) -> str:
355
+ return (
356
+ "Adds noise to image_latents and fake_image_latents for I2V conditioning. "
357
+ "Uses random sigma from configured ranges for each."
358
+ )
359
+
360
+ @property
361
+ def inputs(self) -> list[InputParam]:
362
+ return [
363
+ InputParam.template("image_latents"),
364
+ InputParam(
365
+ "fake_image_latents",
366
+ required=True,
367
+ type_hint=torch.Tensor,
368
+ description="Fake image latents used as history seed for I2V generation.",
369
+ ),
370
+ InputParam(
371
+ "image_noise_sigma_min",
372
+ default=0.111,
373
+ type_hint=float,
374
+ description="Minimum sigma for image latent noise.",
375
+ ),
376
+ InputParam(
377
+ "image_noise_sigma_max",
378
+ default=0.135,
379
+ type_hint=float,
380
+ description="Maximum sigma for image latent noise.",
381
+ ),
382
+ InputParam(
383
+ "video_noise_sigma_min",
384
+ default=0.111,
385
+ type_hint=float,
386
+ description="Minimum sigma for video/fake-image latent noise.",
387
+ ),
388
+ InputParam(
389
+ "video_noise_sigma_max",
390
+ default=0.135,
391
+ type_hint=float,
392
+ description="Maximum sigma for video/fake-image latent noise.",
393
+ ),
394
+ InputParam.template("generator"),
395
+ ]
396
+
397
+ @property
398
+ def intermediate_outputs(self) -> list[OutputParam]:
399
+ return [
400
+ OutputParam.template("image_latents"),
401
+ OutputParam("fake_image_latents", type_hint=torch.Tensor, description="Noisy fake image latents"),
402
+ ]
403
+
404
+ @torch.no_grad()
405
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
406
+ block_state = self.get_block_state(state)
407
+
408
+ device = components._execution_device
409
+ image_latents = block_state.image_latents
410
+ fake_image_latents = block_state.fake_image_latents
411
+
412
+ # Add noise to image_latents
413
+ image_noise_sigma = (
414
+ torch.rand(1, device=device, generator=block_state.generator)
415
+ * (block_state.image_noise_sigma_max - block_state.image_noise_sigma_min)
416
+ + block_state.image_noise_sigma_min
417
+ )
418
+ image_latents = (
419
+ image_noise_sigma * randn_tensor(image_latents.shape, generator=block_state.generator, device=device)
420
+ + (1 - image_noise_sigma) * image_latents
421
+ )
422
+
423
+ # Add noise to fake_image_latents
424
+ fake_image_noise_sigma = (
425
+ torch.rand(1, device=device, generator=block_state.generator)
426
+ * (block_state.video_noise_sigma_max - block_state.video_noise_sigma_min)
427
+ + block_state.video_noise_sigma_min
428
+ )
429
+ fake_image_latents = (
430
+ fake_image_noise_sigma
431
+ * randn_tensor(fake_image_latents.shape, generator=block_state.generator, device=device)
432
+ + (1 - fake_image_noise_sigma) * fake_image_latents
433
+ )
434
+
435
+ block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
436
+ block_state.fake_image_latents = fake_image_latents.to(device=device, dtype=torch.float32)
437
+
438
+ self.set_block_state(state, block_state)
439
+ return components, state
440
+
441
+
442
+ class HeliosAddNoiseToVideoLatentsStep(ModularPipelineBlocks):
443
+ """Adds noise to image_latents and video_latents for V2V conditioning.
444
+
445
+ Applies single-sigma noise to image_latents (using image_noise_sigma range) and per-frame noise to video_latents in
446
+ chunks (using video_noise_sigma range).
447
+ """
448
+
449
+ model_name = "helios"
450
+
451
+ @property
452
+ def description(self) -> str:
453
+ return (
454
+ "Adds noise to image_latents and video_latents for V2V conditioning. "
455
+ "Uses single-sigma noise for image_latents and per-frame noise for video chunks."
456
+ )
457
+
458
+ @property
459
+ def inputs(self) -> list[InputParam]:
460
+ return [
461
+ InputParam.template("image_latents"),
462
+ InputParam(
463
+ "video_latents",
464
+ required=True,
465
+ type_hint=torch.Tensor,
466
+ description="Encoded video latents for V2V generation.",
467
+ ),
468
+ InputParam(
469
+ "num_latent_frames_per_chunk",
470
+ default=9,
471
+ type_hint=int,
472
+ description="Number of latent frames per temporal chunk.",
473
+ ),
474
+ InputParam(
475
+ "image_noise_sigma_min",
476
+ default=0.111,
477
+ type_hint=float,
478
+ description="Minimum sigma for image latent noise.",
479
+ ),
480
+ InputParam(
481
+ "image_noise_sigma_max",
482
+ default=0.135,
483
+ type_hint=float,
484
+ description="Maximum sigma for image latent noise.",
485
+ ),
486
+ InputParam(
487
+ "video_noise_sigma_min",
488
+ default=0.111,
489
+ type_hint=float,
490
+ description="Minimum sigma for video latent noise.",
491
+ ),
492
+ InputParam(
493
+ "video_noise_sigma_max",
494
+ default=0.135,
495
+ type_hint=float,
496
+ description="Maximum sigma for video latent noise.",
497
+ ),
498
+ InputParam.template("generator"),
499
+ ]
500
+
501
+ @property
502
+ def intermediate_outputs(self) -> list[OutputParam]:
503
+ return [
504
+ OutputParam.template("image_latents"),
505
+ OutputParam("video_latents", type_hint=torch.Tensor, description="Noisy video latents"),
506
+ ]
507
+
508
+ @torch.no_grad()
509
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
510
+ block_state = self.get_block_state(state)
511
+
512
+ device = components._execution_device
513
+ image_latents = block_state.image_latents
514
+ video_latents = block_state.video_latents
515
+ num_latent_frames_per_chunk = block_state.num_latent_frames_per_chunk
516
+
517
+ # Add noise to first frame (single sigma)
518
+ image_noise_sigma = (
519
+ torch.rand(1, device=device, generator=block_state.generator)
520
+ * (block_state.image_noise_sigma_max - block_state.image_noise_sigma_min)
521
+ + block_state.image_noise_sigma_min
522
+ )
523
+ image_latents = (
524
+ image_noise_sigma * randn_tensor(image_latents.shape, generator=block_state.generator, device=device)
525
+ + (1 - image_noise_sigma) * image_latents
526
+ )
527
+
528
+ # Add per-frame noise to video chunks
529
+ noisy_latents_chunks = []
530
+ num_latent_chunks = video_latents.shape[2] // num_latent_frames_per_chunk
531
+ for i in range(num_latent_chunks):
532
+ chunk_start = i * num_latent_frames_per_chunk
533
+ chunk_end = chunk_start + num_latent_frames_per_chunk
534
+ latent_chunk = video_latents[:, :, chunk_start:chunk_end, :, :]
535
+
536
+ chunk_frames = latent_chunk.shape[2]
537
+ frame_sigmas = (
538
+ torch.rand(chunk_frames, device=device, generator=block_state.generator)
539
+ * (block_state.video_noise_sigma_max - block_state.video_noise_sigma_min)
540
+ + block_state.video_noise_sigma_min
541
+ )
542
+ frame_sigmas = frame_sigmas.view(1, 1, chunk_frames, 1, 1)
543
+
544
+ noisy_chunk = (
545
+ frame_sigmas * randn_tensor(latent_chunk.shape, generator=block_state.generator, device=device)
546
+ + (1 - frame_sigmas) * latent_chunk
547
+ )
548
+ noisy_latents_chunks.append(noisy_chunk)
549
+ video_latents = torch.cat(noisy_latents_chunks, dim=2)
550
+
551
+ block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
552
+ block_state.video_latents = video_latents.to(device=device, dtype=torch.float32)
553
+
554
+ self.set_block_state(state, block_state)
555
+ return components, state
556
+
557
+
558
+ class HeliosPrepareHistoryStep(ModularPipelineBlocks):
559
+ """Prepares chunk/history indices and initializes history state for the chunk loop."""
560
+
561
+ model_name = "helios"
562
+
563
+ @property
564
+ def description(self) -> str:
565
+ return (
566
+ "Prepares the chunk loop by computing latent dimensions, number of chunks, "
567
+ "history indices, and initializing history state (history_latents, image_latents, latent_chunks)."
568
+ )
569
+
570
+ @property
571
+ def expected_components(self) -> list[ComponentSpec]:
572
+ return [
573
+ ComponentSpec("transformer", HeliosTransformer3DModel),
574
+ ]
575
+
576
+ @property
577
+ def inputs(self) -> list[InputParam]:
578
+ return [
579
+ InputParam.template("height", default=384),
580
+ InputParam.template("width", default=640),
581
+ InputParam(
582
+ "num_frames", default=132, type_hint=int, description="Total number of video frames to generate."
583
+ ),
584
+ InputParam("batch_size", required=True, type_hint=int),
585
+ InputParam(
586
+ "num_latent_frames_per_chunk",
587
+ default=9,
588
+ type_hint=int,
589
+ description="Number of latent frames per temporal chunk.",
590
+ ),
591
+ InputParam(
592
+ "history_sizes",
593
+ default=[16, 2, 1],
594
+ type_hint=list,
595
+ description="Sizes of long/mid/short history buffers for temporal context.",
596
+ ),
597
+ InputParam(
598
+ "keep_first_frame",
599
+ default=True,
600
+ type_hint=bool,
601
+ description="Whether to keep the first frame as a prefix in history.",
602
+ ),
603
+ ]
604
+
605
+ @property
606
+ def intermediate_outputs(self) -> list[OutputParam]:
607
+ return [
608
+ OutputParam("num_latent_chunk", type_hint=int, description="Number of temporal chunks"),
609
+ OutputParam("latent_shape", type_hint=tuple, description="Shape of latent tensor per chunk"),
610
+ OutputParam("history_sizes", type_hint=list, description="Adjusted history sizes (sorted, descending)"),
611
+ OutputParam("indices_hidden_states", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
612
+ OutputParam("indices_latents_history_short", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
613
+ OutputParam("indices_latents_history_mid", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
614
+ OutputParam("indices_latents_history_long", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
615
+ OutputParam("history_latents", type_hint=torch.Tensor, description="Initialized zero history latents"),
616
+ ]
617
+
618
+ @torch.no_grad()
619
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
620
+ block_state = self.get_block_state(state)
621
+
622
+ batch_size = block_state.batch_size
623
+ device = components._execution_device
624
+
625
+ block_state.num_frames = max(block_state.num_frames, 1)
626
+ history_sizes = sorted(block_state.history_sizes, reverse=True)
627
+
628
+ num_channels_latents = components.num_channels_latents
629
+ h_latent = block_state.height // components.vae_scale_factor_spatial
630
+ w_latent = block_state.width // components.vae_scale_factor_spatial
631
+
632
+ # Compute number of chunks
633
+ block_state.window_num_frames = (
634
+ block_state.num_latent_frames_per_chunk - 1
635
+ ) * components.vae_scale_factor_temporal + 1
636
+ block_state.num_latent_chunk = max(
637
+ 1, (block_state.num_frames + block_state.window_num_frames - 1) // block_state.window_num_frames
638
+ )
639
+
640
+ # Modify history_sizes for non-keep_first_frame (matching pipeline behavior)
641
+ if not block_state.keep_first_frame:
642
+ history_sizes = history_sizes.copy()
643
+ history_sizes[-1] = history_sizes[-1] + 1
644
+
645
+ # Compute indices ONCE (same structure for all chunks)
646
+ if block_state.keep_first_frame:
647
+ indices = torch.arange(0, sum([1, *history_sizes, block_state.num_latent_frames_per_chunk]))
648
+ (
649
+ indices_prefix,
650
+ indices_latents_history_long,
651
+ indices_latents_history_mid,
652
+ indices_latents_history_1x,
653
+ indices_hidden_states,
654
+ ) = indices.split([1, *history_sizes, block_state.num_latent_frames_per_chunk], dim=0)
655
+ indices_latents_history_short = torch.cat([indices_prefix, indices_latents_history_1x], dim=0)
656
+ else:
657
+ indices = torch.arange(0, sum([*history_sizes, block_state.num_latent_frames_per_chunk]))
658
+ (
659
+ indices_latents_history_long,
660
+ indices_latents_history_mid,
661
+ indices_latents_history_short,
662
+ indices_hidden_states,
663
+ ) = indices.split([*history_sizes, block_state.num_latent_frames_per_chunk], dim=0)
664
+
665
+ # Latent shape per chunk
666
+ block_state.latent_shape = (
667
+ batch_size,
668
+ num_channels_latents,
669
+ block_state.num_latent_frames_per_chunk,
670
+ h_latent,
671
+ w_latent,
672
+ )
673
+
674
+ # Set outputs
675
+ block_state.history_sizes = history_sizes
676
+ block_state.indices_hidden_states = indices_hidden_states.unsqueeze(0)
677
+ block_state.indices_latents_history_short = indices_latents_history_short.unsqueeze(0)
678
+ block_state.indices_latents_history_mid = indices_latents_history_mid.unsqueeze(0)
679
+ block_state.indices_latents_history_long = indices_latents_history_long.unsqueeze(0)
680
+ block_state.history_latents = torch.zeros(
681
+ batch_size,
682
+ num_channels_latents,
683
+ sum(history_sizes),
684
+ h_latent,
685
+ w_latent,
686
+ device=device,
687
+ dtype=torch.float32,
688
+ )
689
+
690
+ self.set_block_state(state, block_state)
691
+
692
+ return components, state
693
+
694
+
695
+ class HeliosI2VSeedHistoryStep(ModularPipelineBlocks):
696
+ """Seeds history_latents with fake_image_latents for I2V pipelines.
697
+
698
+ This small additive step runs after HeliosPrepareHistoryStep and appends fake_image_latents to the initialized
699
+ history_latents tensor.
700
+ """
701
+
702
+ model_name = "helios"
703
+
704
+ @property
705
+ def description(self) -> str:
706
+ return "I2V history seeding: appends fake_image_latents to history_latents."
707
+
708
+ @property
709
+ def inputs(self) -> list[InputParam]:
710
+ return [
711
+ InputParam("history_latents", required=True, type_hint=torch.Tensor),
712
+ InputParam("fake_image_latents", required=True, type_hint=torch.Tensor),
713
+ ]
714
+
715
+ @property
716
+ def intermediate_outputs(self) -> list[OutputParam]:
717
+ return [
718
+ OutputParam(
719
+ "history_latents", type_hint=torch.Tensor, description="History latents seeded with fake_image_latents"
720
+ ),
721
+ ]
722
+
723
+ @torch.no_grad()
724
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
725
+ block_state = self.get_block_state(state)
726
+
727
+ block_state.history_latents = torch.cat([block_state.history_latents, block_state.fake_image_latents], dim=2)
728
+
729
+ self.set_block_state(state, block_state)
730
+ return components, state
731
+
732
+
733
+ class HeliosV2VSeedHistoryStep(ModularPipelineBlocks):
734
+ """Seeds history_latents with video_latents for V2V pipelines.
735
+
736
+ This step runs after HeliosPrepareHistoryStep and replaces the tail of history_latents with video_latents. If the
737
+ video has fewer frames than the history, the beginning of history is preserved.
738
+ """
739
+
740
+ model_name = "helios"
741
+
742
+ @property
743
+ def description(self) -> str:
744
+ return "V2V history seeding: replaces the tail of history_latents with video_latents."
745
+
746
+ @property
747
+ def inputs(self) -> list[InputParam]:
748
+ return [
749
+ InputParam("history_latents", required=True, type_hint=torch.Tensor),
750
+ InputParam("video_latents", required=True, type_hint=torch.Tensor),
751
+ ]
752
+
753
+ @property
754
+ def intermediate_outputs(self) -> list[OutputParam]:
755
+ return [
756
+ OutputParam(
757
+ "history_latents", type_hint=torch.Tensor, description="History latents seeded with video_latents"
758
+ ),
759
+ ]
760
+
761
+ @torch.no_grad()
762
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
763
+ block_state = self.get_block_state(state)
764
+
765
+ history_latents = block_state.history_latents
766
+ video_latents = block_state.video_latents
767
+
768
+ history_frames = history_latents.shape[2]
769
+ video_frames = video_latents.shape[2]
770
+ if video_frames < history_frames:
771
+ keep_frames = history_frames - video_frames
772
+ history_latents = torch.cat([history_latents[:, :, :keep_frames, :, :], video_latents], dim=2)
773
+ else:
774
+ history_latents = video_latents
775
+
776
+ block_state.history_latents = history_latents
777
+
778
+ self.set_block_state(state, block_state)
779
+ return components, state
780
+
781
+
782
+ class HeliosSetTimestepsStep(ModularPipelineBlocks):
783
+ """Computes scheduler parameters (mu, sigmas) for the chunk loop."""
784
+
785
+ model_name = "helios"
786
+
787
+ @property
788
+ def description(self) -> str:
789
+ return "Computes scheduler shift parameter (mu) and default sigmas for the Helios chunk loop."
790
+
791
+ @property
792
+ def expected_components(self) -> list[ComponentSpec]:
793
+ return [
794
+ ComponentSpec("transformer", HeliosTransformer3DModel),
795
+ ComponentSpec("scheduler", HeliosScheduler),
796
+ ]
797
+
798
+ @property
799
+ def inputs(self) -> list[InputParam]:
800
+ return [
801
+ InputParam("latent_shape", required=True, type_hint=tuple),
802
+ InputParam.template("num_inference_steps"),
803
+ InputParam.template("sigmas"),
804
+ ]
805
+
806
+ @property
807
+ def intermediate_outputs(self) -> list[OutputParam]:
808
+ return [
809
+ OutputParam("mu", type_hint=float, description="Scheduler shift parameter"),
810
+ OutputParam("sigmas", type_hint=list, description="Sigma schedule for diffusion"),
811
+ ]
812
+
813
+ @torch.no_grad()
814
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
815
+ block_state = self.get_block_state(state)
816
+
817
+ patch_size = components.transformer.config.patch_size
818
+ latent_shape = block_state.latent_shape
819
+ image_seq_len = (latent_shape[-1] * latent_shape[-2] * latent_shape[-3]) // (
820
+ patch_size[0] * patch_size[1] * patch_size[2]
821
+ )
822
+
823
+ if block_state.sigmas is None:
824
+ block_state.sigmas = np.linspace(0.999, 0.0, block_state.num_inference_steps + 1)[:-1]
825
+
826
+ block_state.mu = calculate_shift(
827
+ image_seq_len,
828
+ components.scheduler.config.get("base_image_seq_len", 256),
829
+ components.scheduler.config.get("max_image_seq_len", 4096),
830
+ components.scheduler.config.get("base_shift", 0.5),
831
+ components.scheduler.config.get("max_shift", 1.15),
832
+ )
833
+
834
+ self.set_block_state(state, block_state)
835
+
836
+ return components, state
diffusers/modular_pipelines/helios/decoders.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import numpy as np
16
+ import PIL
17
+ import torch
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...models import AutoencoderKLWan
21
+ from ...utils import logging
22
+ from ...video_processor import VideoProcessor
23
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
24
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
25
+
26
+
27
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+
30
+ class HeliosDecodeStep(ModularPipelineBlocks):
31
+ """Decode all chunk latents with VAE, trim frames, and postprocess into final video output."""
32
+
33
+ model_name = "helios"
34
+
35
+ @property
36
+ def description(self) -> str:
37
+ return (
38
+ "Decodes all chunk latents with the VAE, concatenates them, "
39
+ "trims to the target frame count, and postprocesses into the final video output."
40
+ )
41
+
42
+ @property
43
+ def expected_components(self) -> list[ComponentSpec]:
44
+ return [
45
+ ComponentSpec("vae", AutoencoderKLWan),
46
+ ComponentSpec(
47
+ "video_processor",
48
+ VideoProcessor,
49
+ config=FrozenDict({"vae_scale_factor": 8}),
50
+ default_creation_method="from_config",
51
+ ),
52
+ ]
53
+
54
+ @property
55
+ def inputs(self) -> list[InputParam]:
56
+ return [
57
+ InputParam(
58
+ "latent_chunks", required=True, type_hint=list, description="List of per-chunk denoised latent tensors"
59
+ ),
60
+ InputParam("num_frames", required=True, type_hint=int, description="The target number of output frames"),
61
+ InputParam.template("output_type", default="np"),
62
+ ]
63
+
64
+ @property
65
+ def intermediate_outputs(self) -> list[OutputParam]:
66
+ return [
67
+ OutputParam(
68
+ "videos",
69
+ type_hint=list[list[PIL.Image.Image]] | list[torch.Tensor] | list[np.ndarray],
70
+ description="The generated videos, can be a PIL.Image.Image, torch.Tensor or a numpy array",
71
+ ),
72
+ ]
73
+
74
+ @torch.no_grad()
75
+ def __call__(self, components, state: PipelineState) -> PipelineState:
76
+ block_state = self.get_block_state(state)
77
+
78
+ vae = components.vae
79
+ device = components._execution_device
80
+ decode_dtype = vae.dtype
81
+
82
+ latents_mean = (
83
+ torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(device, decode_dtype)
84
+ )
85
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
86
+ device, decode_dtype
87
+ )
88
+
89
+ history_video = None
90
+ for chunk_latents in block_state.latent_chunks:
91
+ current_latents = chunk_latents.to(device=device, dtype=decode_dtype) / latents_std + latents_mean
92
+ current_video = vae.decode(current_latents, return_dict=False)[0]
93
+
94
+ if history_video is None:
95
+ history_video = current_video
96
+ else:
97
+ history_video = torch.cat([history_video, current_video], dim=2)
98
+
99
+ # Trim to proper frame count
100
+ generated_frames = history_video.size(2)
101
+ generated_frames = (
102
+ generated_frames - 1
103
+ ) // components.vae_scale_factor_temporal * components.vae_scale_factor_temporal + 1
104
+ history_video = history_video[:, :, :generated_frames]
105
+
106
+ block_state.videos = components.video_processor.postprocess_video(
107
+ history_video, output_type=block_state.output_type
108
+ )
109
+
110
+ self.set_block_state(state, block_state)
111
+
112
+ return components, state
diffusers/modular_pipelines/helios/denoise.py ADDED
@@ -0,0 +1,1069 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import math
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+ from tqdm.auto import tqdm
21
+
22
+ from ...configuration_utils import FrozenDict
23
+ from ...guiders import ClassifierFreeGuidance, ClassifierFreeZeroStarGuidance
24
+ from ...models import HeliosTransformer3DModel
25
+ from ...schedulers import HeliosScheduler
26
+ from ...utils import logging
27
+ from ...utils.torch_utils import randn_tensor
28
+ from ..modular_pipeline import (
29
+ BlockState,
30
+ LoopSequentialPipelineBlocks,
31
+ ModularPipelineBlocks,
32
+ PipelineState,
33
+ )
34
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
35
+ from .before_denoise import calculate_shift
36
+ from .modular_pipeline import HeliosModularPipeline
37
+
38
+
39
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
40
+
41
+
42
+ def sample_block_noise(
43
+ batch_size,
44
+ channel,
45
+ num_frames,
46
+ height,
47
+ width,
48
+ gamma,
49
+ patch_size=(1, 2, 2),
50
+ device=None,
51
+ generator=None,
52
+ ):
53
+ """Generate spatially-correlated block noise for pyramid upsampling correction.
54
+
55
+ Uses a multivariate normal distribution with covariance based on `gamma` to produce noise with block structure,
56
+ matching the upsampling artifacts that need correction.
57
+ """
58
+ # NOTE: A generator must be provided to ensure correct and reproducible results.
59
+ # Creating a default generator here is a fallback only — without a fixed seed,
60
+ # the output will be non-deterministic and may produce incorrect results in CP context.
61
+ if generator is None:
62
+ generator = torch.Generator(device=device)
63
+ elif isinstance(generator, list):
64
+ generator = generator[0]
65
+
66
+ _, ph, pw = patch_size
67
+ block_size = ph * pw
68
+
69
+ cov = (
70
+ torch.eye(block_size, device=device) * (1 + gamma) - torch.ones(block_size, block_size, device=device) * gamma
71
+ )
72
+ cov += torch.eye(block_size, device=device) * 1e-8
73
+ cov = cov.float() # Upcast to fp32 for numerical stability — cholesky is unreliable in fp16/bf16.
74
+
75
+ L = torch.linalg.cholesky(cov)
76
+ block_number = batch_size * channel * num_frames * (height // ph) * (width // pw)
77
+ z = torch.randn(block_number, block_size, device=generator.device, generator=generator).to(device)
78
+ noise = z @ L.T
79
+
80
+ noise = noise.view(batch_size, channel, num_frames, height // ph, width // pw, ph, pw)
81
+ noise = noise.permute(0, 1, 2, 3, 5, 4, 6).reshape(batch_size, channel, num_frames, height, width)
82
+ return noise
83
+
84
+
85
+ # ========================================
86
+ # Chunk Loop Leaf Blocks
87
+ # ========================================
88
+
89
+
90
+ class HeliosChunkHistorySliceStep(ModularPipelineBlocks):
91
+ """Slices history latents into short/mid/long for a T2V chunk.
92
+
93
+ At k==0 with no image_latents, creates a zero prefix. Otherwise uses image_latents (either provided or captured
94
+ from first chunk by HeliosChunkUpdateStep).
95
+ """
96
+
97
+ model_name = "helios"
98
+
99
+ @property
100
+ def description(self) -> str:
101
+ return (
102
+ "T2V history slice: splits history into long/mid/short. At k==0 with no image_latents, "
103
+ "creates a zero prefix; otherwise uses image_latents as prefix for short history."
104
+ )
105
+
106
+ @property
107
+ def inputs(self) -> list[InputParam]:
108
+ return [
109
+ InputParam(
110
+ "keep_first_frame",
111
+ default=True,
112
+ type_hint=bool,
113
+ description="Whether to keep the first frame as a prefix in history.",
114
+ ),
115
+ InputParam(
116
+ "history_sizes",
117
+ required=True,
118
+ type_hint=list,
119
+ description="Sizes of long/mid/short history buffers for temporal context.",
120
+ ),
121
+ InputParam(
122
+ "history_latents",
123
+ required=True,
124
+ type_hint=torch.Tensor,
125
+ description="Accumulated history latents from previous chunks.",
126
+ ),
127
+ InputParam("latent_shape", required=True, type_hint=tuple),
128
+ ]
129
+
130
+ @property
131
+ def intermediate_outputs(self) -> list[OutputParam]:
132
+ return []
133
+
134
+ @torch.no_grad()
135
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
136
+ keep_first_frame = block_state.keep_first_frame
137
+ history_sizes = block_state.history_sizes
138
+ image_latents = block_state.image_latents
139
+ device = components._execution_device
140
+
141
+ batch_size, num_channels_latents, _, h_latent, w_latent = block_state.latent_shape
142
+
143
+ if keep_first_frame:
144
+ latents_history_long, latents_history_mid, latents_history_1x = block_state.history_latents[
145
+ :, :, -sum(history_sizes) :
146
+ ].split(history_sizes, dim=2)
147
+ if image_latents is None and k == 0:
148
+ latents_prefix = torch.zeros(
149
+ batch_size,
150
+ num_channels_latents,
151
+ 1,
152
+ h_latent,
153
+ w_latent,
154
+ device=device,
155
+ dtype=torch.float32,
156
+ )
157
+ else:
158
+ latents_prefix = image_latents
159
+ latents_history_short = torch.cat([latents_prefix, latents_history_1x], dim=2)
160
+ else:
161
+ latents_history_long, latents_history_mid, latents_history_short = block_state.history_latents[
162
+ :, :, -sum(history_sizes) :
163
+ ].split(history_sizes, dim=2)
164
+
165
+ block_state.latents_history_short = latents_history_short
166
+ block_state.latents_history_mid = latents_history_mid
167
+ block_state.latents_history_long = latents_history_long
168
+
169
+ return components, block_state
170
+
171
+
172
+ class HeliosI2VChunkHistorySliceStep(ModularPipelineBlocks):
173
+ """Slices history latents into short/mid/long for an I2V chunk.
174
+
175
+ Always uses image_latents as prefix (assumes history pre-seeded with fake_image_latents).
176
+ """
177
+
178
+ model_name = "helios"
179
+
180
+ @property
181
+ def description(self) -> str:
182
+ return (
183
+ "I2V history slice: splits pre-seeded history into long/mid/short, "
184
+ "always using image_latents as prefix for short history."
185
+ )
186
+
187
+ @property
188
+ def inputs(self) -> list[InputParam]:
189
+ return [
190
+ InputParam(
191
+ "keep_first_frame",
192
+ default=True,
193
+ type_hint=bool,
194
+ description="Whether to keep the first frame as a prefix in history.",
195
+ ),
196
+ InputParam(
197
+ "history_sizes",
198
+ required=True,
199
+ type_hint=list,
200
+ description="Sizes of long/mid/short history buffers for temporal context.",
201
+ ),
202
+ InputParam(
203
+ "history_latents",
204
+ required=True,
205
+ type_hint=torch.Tensor,
206
+ description="Accumulated history latents from previous chunks.",
207
+ ),
208
+ InputParam(
209
+ "image_latents",
210
+ required=True,
211
+ type_hint=torch.Tensor,
212
+ description="First-frame latents used as prefix for short history.",
213
+ ),
214
+ ]
215
+
216
+ @property
217
+ def intermediate_outputs(self) -> list[OutputParam]:
218
+ return []
219
+
220
+ @torch.no_grad()
221
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
222
+ keep_first_frame = block_state.keep_first_frame
223
+ history_sizes = block_state.history_sizes
224
+ image_latents = block_state.image_latents
225
+
226
+ if keep_first_frame:
227
+ latents_history_long, latents_history_mid, latents_history_1x = block_state.history_latents[
228
+ :, :, -sum(history_sizes) :
229
+ ].split(history_sizes, dim=2)
230
+ latents_history_short = torch.cat([image_latents, latents_history_1x], dim=2)
231
+ else:
232
+ latents_history_long, latents_history_mid, latents_history_short = block_state.history_latents[
233
+ :, :, -sum(history_sizes) :
234
+ ].split(history_sizes, dim=2)
235
+
236
+ block_state.latents_history_short = latents_history_short
237
+ block_state.latents_history_mid = latents_history_mid
238
+ block_state.latents_history_long = latents_history_long
239
+
240
+ return components, block_state
241
+
242
+
243
+ class HeliosChunkNoiseGenStep(ModularPipelineBlocks):
244
+ """Generates noise latents for a chunk using randn_tensor."""
245
+
246
+ model_name = "helios"
247
+
248
+ @property
249
+ def description(self) -> str:
250
+ return "Generates random noise latents at full resolution for a single chunk."
251
+
252
+ @property
253
+ def inputs(self) -> list[InputParam]:
254
+ return [
255
+ InputParam("latent_shape", required=True, type_hint=tuple),
256
+ InputParam.template("generator"),
257
+ ]
258
+
259
+ @torch.no_grad()
260
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
261
+ device = components._execution_device
262
+ block_state.latents = randn_tensor(
263
+ block_state.latent_shape, generator=block_state.generator, device=device, dtype=torch.float32
264
+ )
265
+ return components, block_state
266
+
267
+
268
+ class HeliosPyramidChunkNoiseGenStep(ModularPipelineBlocks):
269
+ """Generates noise latents and downsamples to smallest pyramid level."""
270
+
271
+ model_name = "helios-pyramid"
272
+
273
+ @property
274
+ def description(self) -> str:
275
+ return (
276
+ "Generates random noise at full resolution, then downsamples to the smallest "
277
+ "pyramid level via bilinear interpolation."
278
+ )
279
+
280
+ @property
281
+ def inputs(self) -> list[InputParam]:
282
+ return [
283
+ InputParam("latent_shape", required=True, type_hint=tuple),
284
+ InputParam(
285
+ "pyramid_num_inference_steps_list",
286
+ default=[10, 10, 10],
287
+ type_hint=list,
288
+ description="Number of denoising steps per pyramid stage.",
289
+ ),
290
+ InputParam.template("generator"),
291
+ ]
292
+
293
+ @torch.no_grad()
294
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
295
+ device = components._execution_device
296
+ batch_size, num_channels_latents, num_latent_frames, h_latent, w_latent = block_state.latent_shape
297
+
298
+ latents = randn_tensor(
299
+ block_state.latent_shape, generator=block_state.generator, device=device, dtype=torch.float32
300
+ )
301
+
302
+ # Downsample to smallest pyramid level
303
+ h, w = h_latent, w_latent
304
+ latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_latent_frames, num_channels_latents, h, w)
305
+ for _ in range(len(block_state.pyramid_num_inference_steps_list) - 1):
306
+ h //= 2
307
+ w //= 2
308
+ latents = F.interpolate(latents, size=(h, w), mode="bilinear") * 2
309
+ block_state.latents = latents.reshape(batch_size, num_latent_frames, num_channels_latents, h, w).permute(
310
+ 0, 2, 1, 3, 4
311
+ )
312
+
313
+ return components, block_state
314
+
315
+
316
+ class HeliosChunkSchedulerResetStep(ModularPipelineBlocks):
317
+ """Resets the scheduler with timesteps for a single chunk."""
318
+
319
+ model_name = "helios"
320
+
321
+ @property
322
+ def description(self) -> str:
323
+ return "Resets the scheduler with the correct timesteps and shift parameter (mu) for this chunk."
324
+
325
+ @property
326
+ def expected_components(self) -> list[ComponentSpec]:
327
+ return [
328
+ ComponentSpec("scheduler", HeliosScheduler),
329
+ ]
330
+
331
+ @property
332
+ def inputs(self) -> list[InputParam]:
333
+ return [
334
+ InputParam("mu", required=True, type_hint=float),
335
+ InputParam.template("sigmas", required=True),
336
+ InputParam.template("num_inference_steps"),
337
+ ]
338
+
339
+ @torch.no_grad()
340
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
341
+ device = components._execution_device
342
+ components.scheduler.set_timesteps(
343
+ block_state.num_inference_steps, device=device, sigmas=block_state.sigmas, mu=block_state.mu
344
+ )
345
+ block_state.timesteps = components.scheduler.timesteps
346
+
347
+ return components, block_state
348
+
349
+
350
+ # ========================================
351
+ # Inner Denoising Blocks
352
+ # ========================================
353
+
354
+
355
+ class HeliosChunkDenoiseInner(ModularPipelineBlocks):
356
+ """Inner timestep loop for denoising a single chunk, using guider for guidance."""
357
+
358
+ model_name = "helios"
359
+
360
+ @property
361
+ def description(self) -> str:
362
+ return (
363
+ "Inner denoising loop that iterates over timesteps for a single chunk. "
364
+ "Uses the guider to manage conditional/unconditional forward passes with cache_context, "
365
+ "applies guidance, and runs scheduler step."
366
+ )
367
+
368
+ @property
369
+ def expected_components(self) -> list[ComponentSpec]:
370
+ return [
371
+ ComponentSpec("transformer", HeliosTransformer3DModel),
372
+ ComponentSpec("scheduler", HeliosScheduler),
373
+ ComponentSpec(
374
+ "guider",
375
+ ClassifierFreeGuidance,
376
+ config=FrozenDict({"guidance_scale": 5.0}),
377
+ default_creation_method="from_config",
378
+ ),
379
+ ]
380
+
381
+ @property
382
+ def inputs(self) -> list[InputParam]:
383
+ return [
384
+ InputParam.template("latents"),
385
+ InputParam.template("timesteps"),
386
+ InputParam("prompt_embeds", type_hint=torch.Tensor),
387
+ InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
388
+ InputParam.template("denoiser_input_fields"),
389
+ InputParam.template("num_inference_steps"),
390
+ InputParam.template("attention_kwargs"),
391
+ InputParam.template("generator"),
392
+ ]
393
+
394
+ @torch.no_grad()
395
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
396
+ latents = block_state.latents
397
+ timesteps = block_state.timesteps
398
+ num_inference_steps = block_state.num_inference_steps
399
+
400
+ transformer_dtype = components.transformer.dtype
401
+ num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
402
+
403
+ # Guider inputs: only encoder_hidden_states differs between cond/uncond
404
+ guider_inputs = {
405
+ "encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
406
+ }
407
+
408
+ # Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
409
+ transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
410
+ shared_kwargs = {}
411
+ for field_name, field_value in block_state.denoiser_input_fields.items():
412
+ if field_name in transformer_args and field_name not in guider_inputs:
413
+ shared_kwargs[field_name] = field_value
414
+
415
+ # Add loop-internal history latents with dtype casting
416
+ shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
417
+ shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
418
+ shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
419
+ shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
420
+
421
+ with tqdm(total=num_inference_steps) as progress_bar:
422
+ for i, t in enumerate(timesteps):
423
+ timestep = t.expand(latents.shape[0]).to(torch.int64)
424
+ latent_model_input = latents.to(transformer_dtype)
425
+
426
+ components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
427
+ guider_state = components.guider.prepare_inputs(guider_inputs)
428
+
429
+ for guider_state_batch in guider_state:
430
+ components.guider.prepare_models(components.transformer)
431
+ cond_kwargs = {k: getattr(guider_state_batch, k) for k in guider_inputs.keys()}
432
+
433
+ context_name = getattr(guider_state_batch, components.guider._identifier_key)
434
+ with components.transformer.cache_context(context_name):
435
+ guider_state_batch.noise_pred = components.transformer(
436
+ hidden_states=latent_model_input,
437
+ timestep=timestep,
438
+ return_dict=False,
439
+ **cond_kwargs,
440
+ **shared_kwargs,
441
+ )[0]
442
+ components.guider.cleanup_models(components.transformer)
443
+
444
+ noise_pred = components.guider(guider_state)[0]
445
+
446
+ # Scheduler step
447
+ latents = components.scheduler.step(
448
+ noise_pred,
449
+ t,
450
+ latents,
451
+ generator=block_state.generator,
452
+ return_dict=False,
453
+ )[0]
454
+
455
+ if i == len(timesteps) - 1 or (
456
+ (i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
457
+ ):
458
+ progress_bar.update()
459
+
460
+ block_state.latents = latents
461
+ return components, block_state
462
+
463
+
464
+ class HeliosPyramidChunkDenoiseInner(ModularPipelineBlocks):
465
+ """Nested pyramid stage loop with inner timestep denoising.
466
+
467
+ For each pyramid stage (small -> full resolution):
468
+ 1. Upsample latents + block noise correction (stages > 0)
469
+ 2. Compute mu from current resolution, set scheduler timesteps
470
+ 3. Run timestep denoising loop (same logic as HeliosChunkDenoiseInner)
471
+ """
472
+
473
+ model_name = "helios-pyramid"
474
+
475
+ @property
476
+ def description(self) -> str:
477
+ return (
478
+ "Pyramid denoising inner block: loops over pyramid stages from smallest to full resolution. "
479
+ "Each stage upsamples latents (with block noise correction), recomputes scheduler parameters, "
480
+ "and runs the timestep denoising loop."
481
+ )
482
+
483
+ @property
484
+ def expected_components(self) -> list[ComponentSpec]:
485
+ return [
486
+ ComponentSpec("transformer", HeliosTransformer3DModel),
487
+ ComponentSpec("scheduler", HeliosScheduler),
488
+ ComponentSpec(
489
+ "guider",
490
+ ClassifierFreeZeroStarGuidance,
491
+ config=FrozenDict({"guidance_scale": 5.0, "zero_init_steps": 2}),
492
+ default_creation_method="from_config",
493
+ ),
494
+ ]
495
+
496
+ @property
497
+ def inputs(self) -> list[InputParam]:
498
+ return [
499
+ InputParam.template("latents"),
500
+ InputParam("prompt_embeds", type_hint=torch.Tensor),
501
+ InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
502
+ InputParam.template("denoiser_input_fields"),
503
+ InputParam(
504
+ "pyramid_num_inference_steps_list",
505
+ default=[10, 10, 10],
506
+ type_hint=list,
507
+ description="Number of denoising steps per pyramid stage.",
508
+ ),
509
+ InputParam.template("attention_kwargs"),
510
+ InputParam.template("generator"),
511
+ ]
512
+
513
+ @torch.no_grad()
514
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
515
+ device = components._execution_device
516
+ transformer_dtype = components.transformer.dtype
517
+ latents = block_state.latents
518
+ pyramid_num_stages = len(block_state.pyramid_num_inference_steps_list)
519
+
520
+ # Guider inputs: only encoder_hidden_states differs between cond/uncond
521
+ guider_inputs = {
522
+ "encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
523
+ }
524
+
525
+ # Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
526
+ transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
527
+ shared_kwargs = {}
528
+ for field_name, field_value in block_state.denoiser_input_fields.items():
529
+ if field_name in transformer_args and field_name not in guider_inputs:
530
+ shared_kwargs[field_name] = field_value
531
+
532
+ # Add loop-internal history latents with dtype casting
533
+ shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
534
+ shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
535
+ shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
536
+ shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
537
+
538
+ # Save original zero_init_steps if the guider supports it (e.g. ClassifierFreeZeroStarGuidance).
539
+ # Helios only applies zero init in pyramid stage 0 (lowest resolution), so we disable it
540
+ # for subsequent stages by temporarily setting zero_init_steps=0.
541
+ orig_zero_init_steps = getattr(components.guider, "zero_init_steps", None)
542
+
543
+ for i_s in range(pyramid_num_stages):
544
+ # --- Stage setup ---
545
+
546
+ # Disable zero init for stages > 0 (only stage 0 should have zero init)
547
+ if orig_zero_init_steps is not None and i_s > 0:
548
+ components.guider.zero_init_steps = 0
549
+
550
+ # a. Compute mu from current resolution (before upsample, matching standard pipeline)
551
+ patch_size = components.transformer.config.patch_size
552
+ image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
553
+ patch_size[0] * patch_size[1] * patch_size[2]
554
+ )
555
+ mu = calculate_shift(
556
+ image_seq_len,
557
+ components.scheduler.config.get("base_image_seq_len", 256),
558
+ components.scheduler.config.get("max_image_seq_len", 4096),
559
+ components.scheduler.config.get("base_shift", 0.5),
560
+ components.scheduler.config.get("max_shift", 1.15),
561
+ )
562
+
563
+ # b. Set scheduler timesteps for this stage
564
+ num_inference_steps = block_state.pyramid_num_inference_steps_list[i_s]
565
+ components.scheduler.set_timesteps(
566
+ num_inference_steps,
567
+ i_s,
568
+ device=device,
569
+ mu=mu,
570
+ )
571
+ timesteps = components.scheduler.timesteps
572
+
573
+ # c. Upsample + block noise correction for stages > 0
574
+ if i_s > 0:
575
+ batch_size, num_channels_latents, num_frames, current_h, current_w = latents.shape
576
+ new_h = current_h * 2
577
+ new_w = current_w * 2
578
+
579
+ latents = latents.permute(0, 2, 1, 3, 4).reshape(
580
+ batch_size * num_frames, num_channels_latents, current_h, current_w
581
+ )
582
+ latents = F.interpolate(latents, size=(new_h, new_w), mode="nearest")
583
+ latents = latents.reshape(batch_size, num_frames, num_channels_latents, new_h, new_w).permute(
584
+ 0, 2, 1, 3, 4
585
+ )
586
+
587
+ # Block noise correction
588
+ ori_sigma = 1 - components.scheduler.ori_start_sigmas[i_s]
589
+ gamma = components.scheduler.config.gamma
590
+ alpha = 1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
591
+ beta = alpha * (1 - ori_sigma) / math.sqrt(gamma)
592
+
593
+ batch_size, num_channels_latents, num_frames, h, w = latents.shape
594
+ noise = sample_block_noise(
595
+ batch_size,
596
+ num_channels_latents,
597
+ num_frames,
598
+ h,
599
+ w,
600
+ gamma,
601
+ patch_size,
602
+ device=device,
603
+ generator=block_state.generator,
604
+ )
605
+ noise = noise.to(dtype=transformer_dtype)
606
+ latents = alpha * latents + beta * noise
607
+
608
+ # --- Timestep denoising loop ---
609
+ num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
610
+
611
+ with tqdm(total=num_inference_steps) as progress_bar:
612
+ for i, t in enumerate(timesteps):
613
+ timestep = t.expand(latents.shape[0]).to(torch.int64)
614
+ latent_model_input = latents.to(transformer_dtype)
615
+
616
+ components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
617
+ guider_state = components.guider.prepare_inputs(guider_inputs)
618
+
619
+ for guider_state_batch in guider_state:
620
+ components.guider.prepare_models(components.transformer)
621
+ cond_kwargs = {kk: getattr(guider_state_batch, kk) for kk in guider_inputs.keys()}
622
+
623
+ context_name = getattr(guider_state_batch, components.guider._identifier_key)
624
+ with components.transformer.cache_context(context_name):
625
+ guider_state_batch.noise_pred = components.transformer(
626
+ hidden_states=latent_model_input,
627
+ timestep=timestep,
628
+ return_dict=False,
629
+ **cond_kwargs,
630
+ **shared_kwargs,
631
+ )[0]
632
+ components.guider.cleanup_models(components.transformer)
633
+
634
+ noise_pred = components.guider(guider_state)[0]
635
+
636
+ # Scheduler step
637
+ latents = components.scheduler.step(
638
+ noise_pred,
639
+ t,
640
+ latents,
641
+ generator=block_state.generator,
642
+ return_dict=False,
643
+ )[0]
644
+
645
+ if i == len(timesteps) - 1 or (
646
+ (i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
647
+ ):
648
+ progress_bar.update()
649
+
650
+ # Restore original zero_init_steps
651
+ if orig_zero_init_steps is not None:
652
+ components.guider.zero_init_steps = orig_zero_init_steps
653
+
654
+ block_state.latents = latents
655
+ return components, block_state
656
+
657
+
658
+ # ========================================
659
+ # Post-Denoise Update
660
+ # ========================================
661
+
662
+
663
+ class HeliosChunkUpdateStep(ModularPipelineBlocks):
664
+ """Updates chunk collection and history after denoising a single chunk."""
665
+
666
+ model_name = "helios"
667
+
668
+ @property
669
+ def description(self) -> str:
670
+ return (
671
+ "Post-denoising update step: appends the denoised latents to the chunk list, "
672
+ "captures image_latents from the first chunk if needed, and extends history_latents."
673
+ )
674
+
675
+ @property
676
+ def expected_components(self) -> list[ComponentSpec]:
677
+ return []
678
+
679
+ @property
680
+ def inputs(self) -> list[InputParam]:
681
+ return [
682
+ InputParam("latents", type_hint=torch.Tensor),
683
+ InputParam("history_latents", type_hint=torch.Tensor),
684
+ InputParam("keep_first_frame", default=True, type_hint=bool),
685
+ ]
686
+
687
+ @torch.no_grad()
688
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
689
+ # e. Collect denoised latents for this chunk
690
+ block_state.latent_chunks.append(block_state.latents)
691
+
692
+ # f. Update history
693
+ if block_state.keep_first_frame and k == 0 and block_state.image_latents is None:
694
+ block_state.image_latents = block_state.latents[:, :, 0:1, :, :]
695
+
696
+ block_state.history_latents = torch.cat([block_state.history_latents, block_state.latents], dim=2)
697
+
698
+ return components, block_state
699
+
700
+
701
+ # ========================================
702
+ # Chunk Loop Wrapper
703
+ # ========================================
704
+
705
+
706
+ class HeliosChunkLoopWrapper(LoopSequentialPipelineBlocks):
707
+ """Outer chunk loop that iterates over temporal chunks.
708
+
709
+ History indices, scheduler params, and history state are prepared by HeliosPrepareHistoryStep and
710
+ HeliosSetTimestepsStep before this block runs. Sub-blocks handle per-chunk preparation, denoising, and history
711
+ updates.
712
+ """
713
+
714
+ model_name = "helios"
715
+
716
+ @property
717
+ def description(self) -> str:
718
+ return (
719
+ "Pipeline block that iterates over temporal chunks for progressive video generation. "
720
+ "At each chunk iteration, it runs sub-blocks for preparation, denoising, and history updates."
721
+ )
722
+
723
+ @property
724
+ def loop_inputs(self) -> list[InputParam]:
725
+ return [
726
+ InputParam("num_latent_chunk", required=True, type_hint=int),
727
+ ]
728
+
729
+ @property
730
+ def loop_intermediate_outputs(self) -> list[OutputParam]:
731
+ return [
732
+ OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors"),
733
+ ]
734
+
735
+ @torch.no_grad()
736
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
737
+ block_state = self.get_block_state(state)
738
+ block_state.latent_chunks = []
739
+
740
+ if not hasattr(block_state, "image_latents"):
741
+ block_state.image_latents = None
742
+
743
+ for k in range(block_state.num_latent_chunk):
744
+ components, block_state = self.loop_step(components, block_state, k=k)
745
+
746
+ self.set_block_state(state, block_state)
747
+
748
+ return components, state
749
+
750
+
751
+ # ========================================
752
+ # Composed Chunk Denoise Steps
753
+ # ========================================
754
+
755
+
756
+ class HeliosChunkDenoiseStep(HeliosChunkLoopWrapper):
757
+ """T2V chunk-based denoising: history slice -> noise gen -> scheduler reset -> denoise -> update."""
758
+
759
+ block_classes = [
760
+ HeliosChunkHistorySliceStep,
761
+ HeliosChunkNoiseGenStep,
762
+ HeliosChunkSchedulerResetStep,
763
+ HeliosChunkDenoiseInner,
764
+ HeliosChunkUpdateStep,
765
+ ]
766
+ block_names = ["history_slice", "noise_gen", "scheduler_reset", "denoise_inner", "update_chunk"]
767
+
768
+ @property
769
+ def description(self) -> str:
770
+ return (
771
+ "T2V chunk denoise step that iterates over temporal chunks.\n"
772
+ "At each chunk: history_slice -> noise_gen -> scheduler_reset -> denoise_inner -> update_chunk."
773
+ )
774
+
775
+
776
+ class HeliosI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
777
+ """I2V chunk-based denoising: I2V history slice -> noise gen -> scheduler reset -> denoise -> update."""
778
+
779
+ block_classes = [
780
+ HeliosI2VChunkHistorySliceStep,
781
+ HeliosChunkNoiseGenStep,
782
+ HeliosChunkSchedulerResetStep,
783
+ HeliosChunkDenoiseInner,
784
+ HeliosChunkUpdateStep,
785
+ ]
786
+ block_names = ["history_slice", "noise_gen", "scheduler_reset", "denoise_inner", "update_chunk"]
787
+
788
+ @property
789
+ def description(self) -> str:
790
+ return (
791
+ "I2V chunk denoise step that iterates over temporal chunks.\n"
792
+ "At each chunk: history_slice (I2V) -> noise_gen -> scheduler_reset -> denoise_inner -> update_chunk."
793
+ )
794
+
795
+
796
+ class HeliosPyramidDistilledChunkDenoiseInner(ModularPipelineBlocks):
797
+ """Nested pyramid stage loop with DMD denoising for distilled checkpoints.
798
+
799
+ Same progressive multi-resolution strategy as HeliosPyramidChunkDenoiseInner, but:
800
+ - Guidance is disabled (guidance_scale=1.0, no unconditional pass)
801
+ - Supports is_amplify_first_chunk (doubles first chunk's timesteps via scheduler)
802
+ - Tracks start_point_list and passes DMD-specific args to scheduler.step()
803
+ """
804
+
805
+ model_name = "helios-pyramid"
806
+
807
+ @property
808
+ def description(self) -> str:
809
+ return (
810
+ "Distilled pyramid denoising inner block for DMD checkpoints. Loops over pyramid stages "
811
+ "from smallest to full resolution with guidance disabled and DMD scheduler support."
812
+ )
813
+
814
+ @property
815
+ def expected_components(self) -> list[ComponentSpec]:
816
+ return [
817
+ ComponentSpec("transformer", HeliosTransformer3DModel),
818
+ ComponentSpec("scheduler", HeliosScheduler),
819
+ ComponentSpec(
820
+ "guider",
821
+ ClassifierFreeGuidance,
822
+ config=FrozenDict({"guidance_scale": 1.0}),
823
+ default_creation_method="from_config",
824
+ ),
825
+ ]
826
+
827
+ @property
828
+ def inputs(self) -> list[InputParam]:
829
+ return [
830
+ InputParam.template("latents"),
831
+ InputParam("prompt_embeds", type_hint=torch.Tensor),
832
+ InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
833
+ InputParam.template("denoiser_input_fields"),
834
+ InputParam(
835
+ "pyramid_num_inference_steps_list",
836
+ default=[2, 2, 2],
837
+ type_hint=list,
838
+ description="Number of denoising steps per pyramid stage.",
839
+ ),
840
+ InputParam(
841
+ "is_amplify_first_chunk",
842
+ default=True,
843
+ type_hint=bool,
844
+ description="Whether to double the first chunk's timesteps via the scheduler for amplified generation.",
845
+ ),
846
+ InputParam.template("attention_kwargs"),
847
+ InputParam.template("generator"),
848
+ ]
849
+
850
+ @torch.no_grad()
851
+ def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
852
+ device = components._execution_device
853
+ transformer_dtype = components.transformer.dtype
854
+ latents = block_state.latents
855
+ pyramid_num_stages = len(block_state.pyramid_num_inference_steps_list)
856
+ is_first_chunk = k == 0
857
+
858
+ # Track start points for DMD scheduler
859
+ start_point_list = [latents]
860
+
861
+ # Guider inputs: only encoder_hidden_states differs between cond/uncond
862
+ guider_inputs = {
863
+ "encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
864
+ }
865
+
866
+ # Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
867
+ transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
868
+ shared_kwargs = {}
869
+ for field_name, field_value in block_state.denoiser_input_fields.items():
870
+ if field_name in transformer_args and field_name not in guider_inputs:
871
+ shared_kwargs[field_name] = field_value
872
+
873
+ # Add loop-internal history latents with dtype casting
874
+ shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
875
+ shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
876
+ shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
877
+ shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
878
+
879
+ for i_s in range(pyramid_num_stages):
880
+ # --- Stage setup ---
881
+ patch_size = components.transformer.config.patch_size
882
+
883
+ # a. Compute mu from current resolution (before upsample, matching standard pipeline)
884
+ image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
885
+ patch_size[0] * patch_size[1] * patch_size[2]
886
+ )
887
+ mu = calculate_shift(
888
+ image_seq_len,
889
+ components.scheduler.config.get("base_image_seq_len", 256),
890
+ components.scheduler.config.get("max_image_seq_len", 4096),
891
+ components.scheduler.config.get("base_shift", 0.5),
892
+ components.scheduler.config.get("max_shift", 1.15),
893
+ )
894
+
895
+ # b. Set scheduler timesteps for this stage (with DMD amplification)
896
+ num_inference_steps = block_state.pyramid_num_inference_steps_list[i_s]
897
+ components.scheduler.set_timesteps(
898
+ num_inference_steps,
899
+ i_s,
900
+ device=device,
901
+ mu=mu,
902
+ is_amplify_first_chunk=block_state.is_amplify_first_chunk and is_first_chunk,
903
+ )
904
+ timesteps = components.scheduler.timesteps
905
+
906
+ # c. Upsample + block noise correction for stages > 0
907
+ if i_s > 0:
908
+ batch_size, num_channels_latents, num_frames, current_h, current_w = latents.shape
909
+ new_h = current_h * 2
910
+ new_w = current_w * 2
911
+
912
+ latents = latents.permute(0, 2, 1, 3, 4).reshape(
913
+ batch_size * num_frames, num_channels_latents, current_h, current_w
914
+ )
915
+ latents = F.interpolate(latents, size=(new_h, new_w), mode="nearest")
916
+ latents = latents.reshape(batch_size, num_frames, num_channels_latents, new_h, new_w).permute(
917
+ 0, 2, 1, 3, 4
918
+ )
919
+
920
+ # Block noise correction
921
+ ori_sigma = 1 - components.scheduler.ori_start_sigmas[i_s]
922
+ gamma = components.scheduler.config.gamma
923
+ alpha = 1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
924
+ beta = alpha * (1 - ori_sigma) / math.sqrt(gamma)
925
+
926
+ batch_size, num_channels_latents, num_frames, h, w = latents.shape
927
+ noise = sample_block_noise(
928
+ batch_size,
929
+ num_channels_latents,
930
+ num_frames,
931
+ h,
932
+ w,
933
+ gamma,
934
+ patch_size,
935
+ device=device,
936
+ generator=block_state.generator,
937
+ )
938
+ noise = noise.to(dtype=transformer_dtype)
939
+ latents = alpha * latents + beta * noise
940
+
941
+ start_point_list.append(latents)
942
+
943
+ # --- Timestep denoising loop ---
944
+ num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
945
+
946
+ with tqdm(total=num_inference_steps) as progress_bar:
947
+ for i, t in enumerate(timesteps):
948
+ timestep = t.expand(latents.shape[0]).to(torch.int64)
949
+ latent_model_input = latents.to(transformer_dtype)
950
+
951
+ components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
952
+ guider_state = components.guider.prepare_inputs(guider_inputs)
953
+
954
+ for guider_state_batch in guider_state:
955
+ components.guider.prepare_models(components.transformer)
956
+ cond_kwargs = {k: getattr(guider_state_batch, k) for k in guider_inputs.keys()}
957
+
958
+ context_name = getattr(guider_state_batch, components.guider._identifier_key)
959
+ with components.transformer.cache_context(context_name):
960
+ guider_state_batch.noise_pred = components.transformer(
961
+ hidden_states=latent_model_input,
962
+ timestep=timestep,
963
+ return_dict=False,
964
+ **cond_kwargs,
965
+ **shared_kwargs,
966
+ )[0]
967
+ components.guider.cleanup_models(components.transformer)
968
+
969
+ noise_pred = components.guider(guider_state)[0]
970
+
971
+ # Scheduler step with DMD args
972
+ latents = components.scheduler.step(
973
+ noise_pred,
974
+ t,
975
+ latents,
976
+ generator=block_state.generator,
977
+ return_dict=False,
978
+ cur_sampling_step=i,
979
+ dmd_noisy_tensor=start_point_list[i_s],
980
+ dmd_sigmas=components.scheduler.sigmas,
981
+ dmd_timesteps=components.scheduler.timesteps,
982
+ all_timesteps=timesteps,
983
+ )[0]
984
+
985
+ if i == len(timesteps) - 1 or (
986
+ (i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
987
+ ):
988
+ progress_bar.update()
989
+
990
+ block_state.latents = latents
991
+ return components, block_state
992
+
993
+
994
+ class HeliosPyramidChunkDenoiseStep(HeliosChunkLoopWrapper):
995
+ """T2V pyramid chunk denoising: history slice -> pyramid noise gen -> pyramid denoise inner -> update."""
996
+
997
+ block_classes = [
998
+ HeliosChunkHistorySliceStep,
999
+ HeliosPyramidChunkNoiseGenStep,
1000
+ HeliosPyramidChunkDenoiseInner,
1001
+ HeliosChunkUpdateStep,
1002
+ ]
1003
+ block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
1004
+
1005
+ @property
1006
+ def description(self) -> str:
1007
+ return (
1008
+ "T2V pyramid chunk denoise step that iterates over temporal chunks.\n"
1009
+ "At each chunk: history_slice -> noise_gen (pyramid) -> denoise_inner (pyramid stages) -> update_chunk.\n"
1010
+ "Denoising starts at the smallest resolution and progressively upsamples."
1011
+ )
1012
+
1013
+
1014
+ class HeliosPyramidI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
1015
+ """I2V pyramid chunk denoising: I2V history slice -> pyramid noise gen -> pyramid denoise inner -> update."""
1016
+
1017
+ block_classes = [
1018
+ HeliosI2VChunkHistorySliceStep,
1019
+ HeliosPyramidChunkNoiseGenStep,
1020
+ HeliosPyramidChunkDenoiseInner,
1021
+ HeliosChunkUpdateStep,
1022
+ ]
1023
+ block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
1024
+
1025
+ @property
1026
+ def description(self) -> str:
1027
+ return (
1028
+ "I2V pyramid chunk denoise step that iterates over temporal chunks.\n"
1029
+ "At each chunk: history_slice (I2V) -> noise_gen (pyramid) -> denoise_inner (pyramid stages) -> update_chunk.\n"
1030
+ "Denoising starts at the smallest resolution and progressively upsamples."
1031
+ )
1032
+
1033
+
1034
+ class HeliosPyramidDistilledChunkDenoiseStep(HeliosChunkLoopWrapper):
1035
+ """T2V distilled pyramid chunk denoising with DMD scheduler and no CFG."""
1036
+
1037
+ block_classes = [
1038
+ HeliosChunkHistorySliceStep,
1039
+ HeliosPyramidChunkNoiseGenStep,
1040
+ HeliosPyramidDistilledChunkDenoiseInner,
1041
+ HeliosChunkUpdateStep,
1042
+ ]
1043
+ block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
1044
+
1045
+ @property
1046
+ def description(self) -> str:
1047
+ return (
1048
+ "T2V distilled pyramid chunk denoise step with DMD scheduler.\n"
1049
+ "At each chunk: history_slice -> noise_gen (pyramid) -> denoise_inner (distilled/DMD) -> update_chunk."
1050
+ )
1051
+
1052
+
1053
+ class HeliosPyramidDistilledI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
1054
+ """I2V distilled pyramid chunk denoising with DMD scheduler and no CFG."""
1055
+
1056
+ block_classes = [
1057
+ HeliosI2VChunkHistorySliceStep,
1058
+ HeliosPyramidChunkNoiseGenStep,
1059
+ HeliosPyramidDistilledChunkDenoiseInner,
1060
+ HeliosChunkUpdateStep,
1061
+ ]
1062
+ block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
1063
+
1064
+ @property
1065
+ def description(self) -> str:
1066
+ return (
1067
+ "I2V distilled pyramid chunk denoise step with DMD scheduler.\n"
1068
+ "At each chunk: history_slice (I2V) -> noise_gen (pyramid) -> denoise_inner (distilled/DMD) -> update_chunk."
1069
+ )
diffusers/modular_pipelines/helios/encoders.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import html
16
+
17
+ import regex as re
18
+ import torch
19
+ from transformers import AutoTokenizer, UMT5EncoderModel
20
+
21
+ from ...configuration_utils import FrozenDict
22
+ from ...guiders import ClassifierFreeGuidance
23
+ from ...models import AutoencoderKLWan
24
+ from ...utils import is_ftfy_available, logging
25
+ from ...video_processor import VideoProcessor
26
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
27
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
28
+ from .modular_pipeline import HeliosModularPipeline
29
+
30
+
31
+ if is_ftfy_available():
32
+ import ftfy
33
+
34
+
35
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
36
+
37
+
38
+ def basic_clean(text):
39
+ text = ftfy.fix_text(text)
40
+ text = html.unescape(html.unescape(text))
41
+ return text.strip()
42
+
43
+
44
+ def whitespace_clean(text):
45
+ text = re.sub(r"\s+", " ", text)
46
+ text = text.strip()
47
+ return text
48
+
49
+
50
+ def prompt_clean(text):
51
+ text = whitespace_clean(basic_clean(text))
52
+ return text
53
+
54
+
55
+ def get_t5_prompt_embeds(
56
+ text_encoder: UMT5EncoderModel,
57
+ tokenizer: AutoTokenizer,
58
+ prompt: str | list[str],
59
+ max_sequence_length: int,
60
+ device: torch.device,
61
+ dtype: torch.dtype | None = None,
62
+ ):
63
+ """Encode text prompts into T5 embeddings for Helios.
64
+
65
+ Args:
66
+ text_encoder: The T5 text encoder model.
67
+ tokenizer: The tokenizer for the text encoder.
68
+ prompt: The prompt or prompts to encode.
69
+ max_sequence_length: Maximum sequence length for tokenization.
70
+ device: Device to place tensors on.
71
+ dtype: Optional dtype override. Defaults to `text_encoder.dtype`.
72
+
73
+ Returns:
74
+ A tuple of `(prompt_embeds, attention_mask)` where `prompt_embeds` is the encoded text embeddings and
75
+ `attention_mask` is a boolean mask.
76
+ """
77
+ dtype = dtype or text_encoder.dtype
78
+
79
+ prompt = [prompt] if isinstance(prompt, str) else prompt
80
+ prompt = [prompt_clean(u) for u in prompt]
81
+
82
+ text_inputs = tokenizer(
83
+ prompt,
84
+ padding="max_length",
85
+ max_length=max_sequence_length,
86
+ truncation=True,
87
+ add_special_tokens=True,
88
+ return_attention_mask=True,
89
+ return_tensors="pt",
90
+ )
91
+ text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
92
+ seq_lens = mask.gt(0).sum(dim=1).long()
93
+
94
+ prompt_embeds = text_encoder(text_input_ids.to(device), mask.to(device)).last_hidden_state
95
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
96
+ prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
97
+ prompt_embeds = torch.stack(
98
+ [torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds], dim=0
99
+ )
100
+
101
+ return prompt_embeds, text_inputs.attention_mask.bool()
102
+
103
+
104
+ class HeliosTextEncoderStep(ModularPipelineBlocks):
105
+ model_name = "helios"
106
+
107
+ @property
108
+ def description(self) -> str:
109
+ return "Text Encoder step that generates text embeddings to guide the video generation"
110
+
111
+ @property
112
+ def expected_components(self) -> list[ComponentSpec]:
113
+ return [
114
+ ComponentSpec("text_encoder", UMT5EncoderModel),
115
+ ComponentSpec("tokenizer", AutoTokenizer),
116
+ ComponentSpec(
117
+ "guider",
118
+ ClassifierFreeGuidance,
119
+ config=FrozenDict({"guidance_scale": 5.0}),
120
+ default_creation_method="from_config",
121
+ ),
122
+ ]
123
+
124
+ @property
125
+ def inputs(self) -> list[InputParam]:
126
+ return [
127
+ InputParam.template("prompt"),
128
+ InputParam.template("negative_prompt"),
129
+ InputParam.template("max_sequence_length"),
130
+ ]
131
+
132
+ @property
133
+ def intermediate_outputs(self) -> list[OutputParam]:
134
+ return [
135
+ OutputParam.template("prompt_embeds"),
136
+ OutputParam.template("negative_prompt_embeds"),
137
+ ]
138
+
139
+ @staticmethod
140
+ def check_inputs(prompt, negative_prompt):
141
+ if prompt is not None and not isinstance(prompt, (str, list)):
142
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
143
+
144
+ if negative_prompt is not None and not isinstance(negative_prompt, (str, list)):
145
+ raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
146
+
147
+ if prompt is not None and negative_prompt is not None:
148
+ prompt_list = [prompt] if isinstance(prompt, str) else prompt
149
+ neg_list = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
150
+ if type(prompt_list) is not type(neg_list):
151
+ raise TypeError(
152
+ f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
153
+ f" {type(prompt)}."
154
+ )
155
+ if len(prompt_list) != len(neg_list):
156
+ raise ValueError(
157
+ f"`negative_prompt` has batch size {len(neg_list)}, but `prompt` has batch size"
158
+ f" {len(prompt_list)}. Please make sure that passed `negative_prompt` matches"
159
+ " the batch size of `prompt`."
160
+ )
161
+
162
+ @torch.no_grad()
163
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
164
+ block_state = self.get_block_state(state)
165
+
166
+ prompt = block_state.prompt
167
+ negative_prompt = block_state.negative_prompt
168
+ max_sequence_length = block_state.max_sequence_length
169
+ device = components._execution_device
170
+
171
+ self.check_inputs(prompt, negative_prompt)
172
+
173
+ # Encode prompt
174
+ block_state.prompt_embeds, _ = get_t5_prompt_embeds(
175
+ text_encoder=components.text_encoder,
176
+ tokenizer=components.tokenizer,
177
+ prompt=prompt,
178
+ max_sequence_length=max_sequence_length,
179
+ device=device,
180
+ )
181
+
182
+ # Encode negative prompt
183
+ block_state.negative_prompt_embeds = None
184
+ if components.requires_unconditional_embeds:
185
+ negative_prompt = negative_prompt or ""
186
+ if isinstance(prompt, list) and isinstance(negative_prompt, str):
187
+ negative_prompt = len(prompt) * [negative_prompt]
188
+
189
+ block_state.negative_prompt_embeds, _ = get_t5_prompt_embeds(
190
+ text_encoder=components.text_encoder,
191
+ tokenizer=components.tokenizer,
192
+ prompt=negative_prompt,
193
+ max_sequence_length=max_sequence_length,
194
+ device=device,
195
+ )
196
+
197
+ self.set_block_state(state, block_state)
198
+ return components, state
199
+
200
+
201
+ class HeliosImageVaeEncoderStep(ModularPipelineBlocks):
202
+ """Encodes an input image into VAE latent space for image-to-video generation."""
203
+
204
+ model_name = "helios"
205
+
206
+ @property
207
+ def description(self) -> str:
208
+ return (
209
+ "Image Encoder step that encodes an input image into VAE latent space, "
210
+ "producing image_latents (first frame prefix) and fake_image_latents (history seed) "
211
+ "for image-to-video generation."
212
+ )
213
+
214
+ @property
215
+ def expected_components(self) -> list[ComponentSpec]:
216
+ return [
217
+ ComponentSpec("vae", AutoencoderKLWan),
218
+ ComponentSpec(
219
+ "video_processor",
220
+ VideoProcessor,
221
+ config=FrozenDict({"vae_scale_factor": 8}),
222
+ default_creation_method="from_config",
223
+ ),
224
+ ]
225
+
226
+ @property
227
+ def inputs(self) -> list[InputParam]:
228
+ return [
229
+ InputParam.template("image"),
230
+ InputParam.template("height", default=384),
231
+ InputParam.template("width", default=640),
232
+ InputParam(
233
+ "num_latent_frames_per_chunk",
234
+ default=9,
235
+ type_hint=int,
236
+ description="Number of latent frames per temporal chunk.",
237
+ ),
238
+ InputParam.template("generator"),
239
+ ]
240
+
241
+ @property
242
+ def intermediate_outputs(self) -> list[OutputParam]:
243
+ return [
244
+ OutputParam.template("image_latents"),
245
+ OutputParam(
246
+ "fake_image_latents", type_hint=torch.Tensor, description="Fake image latents for history seeding"
247
+ ),
248
+ ]
249
+
250
+ @torch.no_grad()
251
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
252
+ block_state = self.get_block_state(state)
253
+
254
+ vae = components.vae
255
+ device = components._execution_device
256
+
257
+ latents_mean = (
258
+ torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype)
259
+ )
260
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
261
+ vae.device, vae.dtype
262
+ )
263
+
264
+ # Preprocess image to 4D tensor (B, C, H, W)
265
+ image = components.video_processor.preprocess(
266
+ block_state.image, height=block_state.height, width=block_state.width
267
+ )
268
+ image_5d = image.unsqueeze(2).to(device=device, dtype=vae.dtype) # (B, C, 1, H, W)
269
+
270
+ # Encode image to get image_latents
271
+ image_latents = vae.encode(image_5d).latent_dist.sample(generator=block_state.generator)
272
+ image_latents = (image_latents - latents_mean) * latents_std
273
+
274
+ # Encode fake video to get fake_image_latents
275
+ min_frames = (block_state.num_latent_frames_per_chunk - 1) * components.vae_scale_factor_temporal + 1
276
+ fake_video = image_5d.repeat(1, 1, min_frames, 1, 1) # (B, C, min_frames, H, W)
277
+ fake_latents_full = vae.encode(fake_video).latent_dist.sample(generator=block_state.generator)
278
+ fake_latents_full = (fake_latents_full - latents_mean) * latents_std
279
+ fake_image_latents = fake_latents_full[:, :, -1:, :, :]
280
+
281
+ block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
282
+ block_state.fake_image_latents = fake_image_latents.to(device=device, dtype=torch.float32)
283
+
284
+ self.set_block_state(state, block_state)
285
+ return components, state
286
+
287
+
288
+ class HeliosVideoVaeEncoderStep(ModularPipelineBlocks):
289
+ """Encodes an input video into VAE latent space for video-to-video generation.
290
+
291
+ Produces `image_latents` (first frame) and `video_latents` (remaining frames encoded in chunks).
292
+ """
293
+
294
+ model_name = "helios"
295
+
296
+ @property
297
+ def description(self) -> str:
298
+ return (
299
+ "Video Encoder step that encodes an input video into VAE latent space, "
300
+ "producing image_latents (first frame) and video_latents (chunked video frames) "
301
+ "for video-to-video generation."
302
+ )
303
+
304
+ @property
305
+ def expected_components(self) -> list[ComponentSpec]:
306
+ return [
307
+ ComponentSpec("vae", AutoencoderKLWan),
308
+ ComponentSpec(
309
+ "video_processor",
310
+ VideoProcessor,
311
+ config=FrozenDict({"vae_scale_factor": 8}),
312
+ default_creation_method="from_config",
313
+ ),
314
+ ]
315
+
316
+ @property
317
+ def inputs(self) -> list[InputParam]:
318
+ return [
319
+ InputParam("video", required=True, description="Input video for video-to-video generation"),
320
+ InputParam.template("height", default=384),
321
+ InputParam.template("width", default=640),
322
+ InputParam(
323
+ "num_latent_frames_per_chunk",
324
+ default=9,
325
+ type_hint=int,
326
+ description="Number of latent frames per temporal chunk.",
327
+ ),
328
+ InputParam.template("generator"),
329
+ ]
330
+
331
+ @property
332
+ def intermediate_outputs(self) -> list[OutputParam]:
333
+ return [
334
+ OutputParam.template("image_latents"),
335
+ OutputParam("video_latents", type_hint=torch.Tensor, description="Encoded video latents (chunked)"),
336
+ ]
337
+
338
+ @torch.no_grad()
339
+ def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
340
+ block_state = self.get_block_state(state)
341
+
342
+ vae = components.vae
343
+ device = components._execution_device
344
+ num_latent_frames_per_chunk = block_state.num_latent_frames_per_chunk
345
+
346
+ latents_mean = (
347
+ torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype)
348
+ )
349
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
350
+ vae.device, vae.dtype
351
+ )
352
+
353
+ # Preprocess video
354
+ video = components.video_processor.preprocess_video(
355
+ block_state.video, height=block_state.height, width=block_state.width
356
+ )
357
+ video = video.to(device=device, dtype=vae.dtype)
358
+
359
+ # Encode video into latents
360
+ num_frames = video.shape[2]
361
+ min_frames = (num_latent_frames_per_chunk - 1) * 4 + 1
362
+ num_chunks = num_frames // min_frames
363
+ if num_chunks == 0:
364
+ raise ValueError(
365
+ f"Video must have at least {min_frames} frames "
366
+ f"(got {num_frames} frames). "
367
+ f"Required: (num_latent_frames_per_chunk - 1) * 4 + 1 = ({num_latent_frames_per_chunk} - 1) * 4 + 1 = {min_frames}"
368
+ )
369
+ total_valid_frames = num_chunks * min_frames
370
+ start_frame = num_frames - total_valid_frames
371
+
372
+ # Encode first frame
373
+ first_frame = video[:, :, 0:1, :, :]
374
+ image_latents = vae.encode(first_frame).latent_dist.sample(generator=block_state.generator)
375
+ image_latents = (image_latents - latents_mean) * latents_std
376
+
377
+ # Encode remaining frames in chunks
378
+ latents_chunks = []
379
+ for i in range(num_chunks):
380
+ chunk_start = start_frame + i * min_frames
381
+ chunk_end = chunk_start + min_frames
382
+ video_chunk = video[:, :, chunk_start:chunk_end, :, :]
383
+ chunk_latents = vae.encode(video_chunk).latent_dist.sample(generator=block_state.generator)
384
+ chunk_latents = (chunk_latents - latents_mean) * latents_std
385
+ latents_chunks.append(chunk_latents)
386
+ video_latents = torch.cat(latents_chunks, dim=2)
387
+
388
+ block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
389
+ block_state.video_latents = video_latents.to(device=device, dtype=torch.float32)
390
+
391
+ self.set_block_state(state, block_state)
392
+ return components, state
diffusers/modular_pipelines/helios/modular_blocks_helios.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ...utils import logging
18
+ from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
19
+ from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
20
+ from .before_denoise import (
21
+ HeliosAdditionalInputsStep,
22
+ HeliosAddNoiseToImageLatentsStep,
23
+ HeliosAddNoiseToVideoLatentsStep,
24
+ HeliosI2VSeedHistoryStep,
25
+ HeliosPrepareHistoryStep,
26
+ HeliosSetTimestepsStep,
27
+ HeliosTextInputStep,
28
+ HeliosV2VSeedHistoryStep,
29
+ )
30
+ from .decoders import HeliosDecodeStep
31
+ from .denoise import HeliosChunkDenoiseStep, HeliosI2VChunkDenoiseStep
32
+ from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
33
+
34
+
35
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
36
+
37
+
38
+ # ====================
39
+ # 1. Vae Encoder
40
+ # ====================
41
+
42
+
43
+ # auto_docstring
44
+ class HeliosAutoVaeEncoderStep(AutoPipelineBlocks):
45
+ """
46
+ Encoder step that encodes video or image inputs. This is an auto pipeline block.
47
+ - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
48
+ - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
49
+ - If neither is provided, step will be skipped.
50
+
51
+ Components:
52
+ vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
53
+
54
+ Inputs:
55
+ video (`None`, *optional*):
56
+ Input video for video-to-video generation
57
+ height (`int`, *optional*, defaults to 384):
58
+ The height in pixels of the generated image.
59
+ width (`int`, *optional*, defaults to 640):
60
+ The width in pixels of the generated image.
61
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
62
+ Number of latent frames per temporal chunk.
63
+ generator (`Generator`, *optional*):
64
+ Torch generator for deterministic generation.
65
+ image (`Image | list`, *optional*):
66
+ Reference image(s) for denoising. Can be a single image or list of images.
67
+
68
+ Outputs:
69
+ image_latents (`Tensor`):
70
+ The latent representation of the input image.
71
+ video_latents (`Tensor`):
72
+ Encoded video latents (chunked)
73
+ fake_image_latents (`Tensor`):
74
+ Fake image latents for history seeding
75
+ """
76
+
77
+ block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
78
+ block_names = ["video_encoder", "image_encoder"]
79
+ block_trigger_inputs = ["video", "image"]
80
+
81
+ @property
82
+ def description(self):
83
+ return (
84
+ "Encoder step that encodes video or image inputs. This is an auto pipeline block.\n"
85
+ " - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
86
+ " - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
87
+ " - If neither is provided, step will be skipped."
88
+ )
89
+
90
+
91
+ # ====================
92
+ # 2. DENOISE
93
+ # ====================
94
+
95
+
96
+ # DENOISE (T2V)
97
+ # auto_docstring
98
+ class HeliosCoreDenoiseStep(SequentialPipelineBlocks):
99
+ """
100
+ Denoise block that takes encoded conditions and runs the chunk-based denoising process.
101
+
102
+ Components:
103
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
104
+
105
+ Inputs:
106
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
107
+ Number of videos to generate per prompt.
108
+ prompt_embeds (`Tensor`):
109
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
110
+ negative_prompt_embeds (`Tensor`, *optional*):
111
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
112
+ height (`int`, *optional*, defaults to 384):
113
+ The height in pixels of the generated image.
114
+ width (`int`, *optional*, defaults to 640):
115
+ The width in pixels of the generated image.
116
+ num_frames (`int`, *optional*, defaults to 132):
117
+ Total number of video frames to generate.
118
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
119
+ Number of latent frames per temporal chunk.
120
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
121
+ Sizes of long/mid/short history buffers for temporal context.
122
+ keep_first_frame (`bool`, *optional*, defaults to True):
123
+ Whether to keep the first frame as a prefix in history.
124
+ num_inference_steps (`int`, *optional*, defaults to 50):
125
+ The number of denoising steps.
126
+ sigmas (`list`, *optional*):
127
+ Custom sigmas for the denoising process.
128
+ generator (`Generator`, *optional*):
129
+ Torch generator for deterministic generation.
130
+ latents (`Tensor`, *optional*):
131
+ Pre-generated noisy latents for image generation.
132
+ timesteps (`Tensor`, *optional*):
133
+ Timesteps for the denoising process.
134
+ **denoiser_input_fields (`None`, *optional*):
135
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
136
+ attention_kwargs (`dict`, *optional*):
137
+ Additional kwargs for attention processors.
138
+
139
+ Outputs:
140
+ latent_chunks (`list`):
141
+ List of per-chunk denoised latent tensors
142
+ """
143
+
144
+ model_name = "helios"
145
+ block_classes = [
146
+ HeliosTextInputStep,
147
+ HeliosPrepareHistoryStep,
148
+ HeliosSetTimestepsStep,
149
+ HeliosChunkDenoiseStep,
150
+ ]
151
+ block_names = ["input", "prepare_history", "set_timesteps", "chunk_denoise"]
152
+
153
+ @property
154
+ def description(self):
155
+ return "Denoise block that takes encoded conditions and runs the chunk-based denoising process."
156
+
157
+ @property
158
+ def outputs(self):
159
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
160
+
161
+
162
+ # DENOISE (I2V)
163
+ # auto_docstring
164
+ class HeliosI2VCoreDenoiseStep(SequentialPipelineBlocks):
165
+ """
166
+ I2V denoise block that seeds history with image latents and uses I2V-aware chunk preparation.
167
+
168
+ Components:
169
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
170
+
171
+ Inputs:
172
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
173
+ Number of videos to generate per prompt.
174
+ prompt_embeds (`Tensor`):
175
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
176
+ negative_prompt_embeds (`Tensor`, *optional*):
177
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
178
+ image_latents (`Tensor`):
179
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
180
+ fake_image_latents (`Tensor`, *optional*):
181
+ Fake image latents used as history seed for I2V generation.
182
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
183
+ Minimum sigma for image latent noise.
184
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
185
+ Maximum sigma for image latent noise.
186
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
187
+ Minimum sigma for video/fake-image latent noise.
188
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
189
+ Maximum sigma for video/fake-image latent noise.
190
+ generator (`Generator`, *optional*):
191
+ Torch generator for deterministic generation.
192
+ num_frames (`int`, *optional*, defaults to 132):
193
+ Total number of video frames to generate.
194
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
195
+ Number of latent frames per temporal chunk.
196
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
197
+ Sizes of long/mid/short history buffers for temporal context.
198
+ keep_first_frame (`bool`, *optional*, defaults to True):
199
+ Whether to keep the first frame as a prefix in history.
200
+ num_inference_steps (`int`, *optional*, defaults to 50):
201
+ The number of denoising steps.
202
+ sigmas (`list`, *optional*):
203
+ Custom sigmas for the denoising process.
204
+ latents (`Tensor`, *optional*):
205
+ Pre-generated noisy latents for image generation.
206
+ timesteps (`Tensor`, *optional*):
207
+ Timesteps for the denoising process.
208
+ **denoiser_input_fields (`None`, *optional*):
209
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
210
+ attention_kwargs (`dict`, *optional*):
211
+ Additional kwargs for attention processors.
212
+
213
+ Outputs:
214
+ latent_chunks (`list`):
215
+ List of per-chunk denoised latent tensors
216
+ """
217
+
218
+ model_name = "helios"
219
+ block_classes = [
220
+ HeliosTextInputStep,
221
+ HeliosAdditionalInputsStep(
222
+ image_latent_inputs=[InputParam.template("image_latents")],
223
+ additional_batch_inputs=[
224
+ InputParam(
225
+ "fake_image_latents",
226
+ type_hint=torch.Tensor,
227
+ description="Fake image latents used as history seed for I2V generation.",
228
+ ),
229
+ ],
230
+ ),
231
+ HeliosAddNoiseToImageLatentsStep,
232
+ HeliosPrepareHistoryStep,
233
+ HeliosI2VSeedHistoryStep,
234
+ HeliosSetTimestepsStep,
235
+ HeliosI2VChunkDenoiseStep,
236
+ ]
237
+ block_names = [
238
+ "input",
239
+ "additional_inputs",
240
+ "add_noise_image",
241
+ "prepare_history",
242
+ "seed_history",
243
+ "set_timesteps",
244
+ "chunk_denoise",
245
+ ]
246
+
247
+ @property
248
+ def description(self):
249
+ return "I2V denoise block that seeds history with image latents and uses I2V-aware chunk preparation."
250
+
251
+ @property
252
+ def outputs(self):
253
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
254
+
255
+
256
+ # DENOISE (V2V)
257
+ # auto_docstring
258
+ class HeliosV2VCoreDenoiseStep(SequentialPipelineBlocks):
259
+ """
260
+ V2V denoise block that seeds history with video latents and uses I2V-aware chunk preparation.
261
+
262
+ Components:
263
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
264
+
265
+ Inputs:
266
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
267
+ Number of videos to generate per prompt.
268
+ prompt_embeds (`Tensor`):
269
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
270
+ negative_prompt_embeds (`Tensor`, *optional*):
271
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
272
+ image_latents (`Tensor`, *optional*):
273
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
274
+ video_latents (`Tensor`, *optional*):
275
+ Encoded video latents for V2V generation.
276
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
277
+ Number of latent frames per temporal chunk.
278
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
279
+ Minimum sigma for image latent noise.
280
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
281
+ Maximum sigma for image latent noise.
282
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
283
+ Minimum sigma for video latent noise.
284
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
285
+ Maximum sigma for video latent noise.
286
+ generator (`Generator`, *optional*):
287
+ Torch generator for deterministic generation.
288
+ num_frames (`int`, *optional*, defaults to 132):
289
+ Total number of video frames to generate.
290
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
291
+ Sizes of long/mid/short history buffers for temporal context.
292
+ keep_first_frame (`bool`, *optional*, defaults to True):
293
+ Whether to keep the first frame as a prefix in history.
294
+ num_inference_steps (`int`, *optional*, defaults to 50):
295
+ The number of denoising steps.
296
+ sigmas (`list`, *optional*):
297
+ Custom sigmas for the denoising process.
298
+ latents (`Tensor`, *optional*):
299
+ Pre-generated noisy latents for image generation.
300
+ timesteps (`Tensor`, *optional*):
301
+ Timesteps for the denoising process.
302
+ **denoiser_input_fields (`None`, *optional*):
303
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
304
+ attention_kwargs (`dict`, *optional*):
305
+ Additional kwargs for attention processors.
306
+
307
+ Outputs:
308
+ latent_chunks (`list`):
309
+ List of per-chunk denoised latent tensors
310
+ """
311
+
312
+ model_name = "helios"
313
+ block_classes = [
314
+ HeliosTextInputStep,
315
+ HeliosAdditionalInputsStep(
316
+ image_latent_inputs=[InputParam.template("image_latents")],
317
+ additional_batch_inputs=[
318
+ InputParam(
319
+ "video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
320
+ ),
321
+ ],
322
+ ),
323
+ HeliosAddNoiseToVideoLatentsStep,
324
+ HeliosPrepareHistoryStep,
325
+ HeliosV2VSeedHistoryStep,
326
+ HeliosSetTimestepsStep,
327
+ HeliosI2VChunkDenoiseStep,
328
+ ]
329
+ block_names = [
330
+ "input",
331
+ "additional_inputs",
332
+ "add_noise_video",
333
+ "prepare_history",
334
+ "seed_history",
335
+ "set_timesteps",
336
+ "chunk_denoise",
337
+ ]
338
+
339
+ @property
340
+ def description(self):
341
+ return "V2V denoise block that seeds history with video latents and uses I2V-aware chunk preparation."
342
+
343
+ @property
344
+ def outputs(self):
345
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
346
+
347
+
348
+ # AUTO DENOISE
349
+ # auto_docstring
350
+ class HeliosAutoCoreDenoiseStep(ConditionalPipelineBlocks):
351
+ """
352
+ Core denoise step that selects the appropriate denoising block.
353
+ - `HeliosV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
354
+ - `HeliosI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
355
+ - `HeliosCoreDenoiseStep` (text2video) for text-to-video tasks.
356
+
357
+ Components:
358
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
359
+
360
+ Inputs:
361
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
362
+ Number of videos to generate per prompt.
363
+ prompt_embeds (`Tensor`):
364
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
365
+ negative_prompt_embeds (`Tensor`, *optional*):
366
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
367
+ image_latents (`Tensor`, *optional*):
368
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
369
+ video_latents (`Tensor`, *optional*):
370
+ Encoded video latents for V2V generation.
371
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
372
+ Number of latent frames per temporal chunk.
373
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
374
+ Minimum sigma for image latent noise.
375
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
376
+ Maximum sigma for image latent noise.
377
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
378
+ Minimum sigma for video latent noise.
379
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
380
+ Maximum sigma for video latent noise.
381
+ generator (`Generator`, *optional*):
382
+ Torch generator for deterministic generation.
383
+ num_frames (`int`, *optional*, defaults to 132):
384
+ Total number of video frames to generate.
385
+ history_sizes (`list`):
386
+ Sizes of long/mid/short history buffers for temporal context.
387
+ keep_first_frame (`bool`, *optional*, defaults to True):
388
+ Whether to keep the first frame as a prefix in history.
389
+ num_inference_steps (`int`, *optional*, defaults to 50):
390
+ The number of denoising steps.
391
+ sigmas (`list`):
392
+ Custom sigmas for the denoising process.
393
+ latents (`Tensor`, *optional*):
394
+ Pre-generated noisy latents for image generation.
395
+ timesteps (`Tensor`, *optional*):
396
+ Timesteps for the denoising process.
397
+ **denoiser_input_fields (`None`, *optional*):
398
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
399
+ attention_kwargs (`dict`, *optional*):
400
+ Additional kwargs for attention processors.
401
+ fake_image_latents (`Tensor`, *optional*):
402
+ Fake image latents used as history seed for I2V generation.
403
+ height (`int`, *optional*, defaults to 384):
404
+ The height in pixels of the generated image.
405
+ width (`int`, *optional*, defaults to 640):
406
+ The width in pixels of the generated image.
407
+
408
+ Outputs:
409
+ latent_chunks (`list`):
410
+ List of per-chunk denoised latent tensors
411
+ """
412
+
413
+ block_classes = [HeliosV2VCoreDenoiseStep, HeliosI2VCoreDenoiseStep, HeliosCoreDenoiseStep]
414
+ block_names = ["video2video", "image2video", "text2video"]
415
+ block_trigger_inputs = ["video_latents", "fake_image_latents"]
416
+ default_block_name = "text2video"
417
+
418
+ def select_block(self, video_latents=None, fake_image_latents=None):
419
+ if video_latents is not None:
420
+ return "video2video"
421
+ elif fake_image_latents is not None:
422
+ return "image2video"
423
+ return None
424
+
425
+ @property
426
+ def description(self):
427
+ return (
428
+ "Core denoise step that selects the appropriate denoising block.\n"
429
+ " - `HeliosV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
430
+ " - `HeliosI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
431
+ " - `HeliosCoreDenoiseStep` (text2video) for text-to-video tasks."
432
+ )
433
+
434
+
435
+ AUTO_BLOCKS = InsertableDict(
436
+ [
437
+ ("text_encoder", HeliosTextEncoderStep()),
438
+ ("vae_encoder", HeliosAutoVaeEncoderStep()),
439
+ ("denoise", HeliosAutoCoreDenoiseStep()),
440
+ ("decode", HeliosDecodeStep()),
441
+ ]
442
+ )
443
+
444
+ # ====================
445
+ # 3. Auto Blocks
446
+ # ====================
447
+
448
+
449
+ # auto_docstring
450
+ class HeliosAutoBlocks(SequentialPipelineBlocks):
451
+ """
452
+ Auto Modular pipeline for text-to-video, image-to-video, and video-to-video tasks using Helios.
453
+
454
+ Supported workflows:
455
+ - `text2video`: requires `prompt`
456
+ - `image2video`: requires `prompt`, `image`
457
+ - `video2video`: requires `prompt`, `video`
458
+
459
+ Components:
460
+ text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
461
+ (`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
462
+ (`HeliosScheduler`)
463
+
464
+ Inputs:
465
+ prompt (`str`):
466
+ The prompt or prompts to guide image generation.
467
+ negative_prompt (`str`, *optional*):
468
+ The prompt or prompts not to guide the image generation.
469
+ max_sequence_length (`int`, *optional*, defaults to 512):
470
+ Maximum sequence length for prompt encoding.
471
+ video (`None`, *optional*):
472
+ Input video for video-to-video generation
473
+ height (`int`, *optional*, defaults to 384):
474
+ The height in pixels of the generated image.
475
+ width (`int`, *optional*, defaults to 640):
476
+ The width in pixels of the generated image.
477
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
478
+ Number of latent frames per temporal chunk.
479
+ generator (`Generator`, *optional*):
480
+ Torch generator for deterministic generation.
481
+ image (`Image | list`, *optional*):
482
+ Reference image(s) for denoising. Can be a single image or list of images.
483
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
484
+ Number of videos to generate per prompt.
485
+ image_latents (`Tensor`, *optional*):
486
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
487
+ video_latents (`Tensor`, *optional*):
488
+ Encoded video latents for V2V generation.
489
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
490
+ Minimum sigma for image latent noise.
491
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
492
+ Maximum sigma for image latent noise.
493
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
494
+ Minimum sigma for video latent noise.
495
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
496
+ Maximum sigma for video latent noise.
497
+ num_frames (`int`, *optional*, defaults to 132):
498
+ Total number of video frames to generate.
499
+ history_sizes (`list`):
500
+ Sizes of long/mid/short history buffers for temporal context.
501
+ keep_first_frame (`bool`, *optional*, defaults to True):
502
+ Whether to keep the first frame as a prefix in history.
503
+ num_inference_steps (`int`, *optional*, defaults to 50):
504
+ The number of denoising steps.
505
+ sigmas (`list`):
506
+ Custom sigmas for the denoising process.
507
+ latents (`Tensor`, *optional*):
508
+ Pre-generated noisy latents for image generation.
509
+ timesteps (`Tensor`, *optional*):
510
+ Timesteps for the denoising process.
511
+ **denoiser_input_fields (`None`, *optional*):
512
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
513
+ attention_kwargs (`dict`, *optional*):
514
+ Additional kwargs for attention processors.
515
+ fake_image_latents (`Tensor`, *optional*):
516
+ Fake image latents used as history seed for I2V generation.
517
+ output_type (`str`, *optional*, defaults to np):
518
+ Output format: 'pil', 'np', 'pt'.
519
+
520
+ Outputs:
521
+ videos (`list`):
522
+ The generated videos.
523
+ """
524
+
525
+ model_name = "helios"
526
+
527
+ block_classes = AUTO_BLOCKS.values()
528
+ block_names = AUTO_BLOCKS.keys()
529
+
530
+ _workflow_map = {
531
+ "text2video": {"prompt": True},
532
+ "image2video": {"prompt": True, "image": True},
533
+ "video2video": {"prompt": True, "video": True},
534
+ }
535
+
536
+ @property
537
+ def description(self):
538
+ return "Auto Modular pipeline for text-to-video, image-to-video, and video-to-video tasks using Helios."
539
+
540
+ @property
541
+ def outputs(self):
542
+ return [OutputParam.template("videos")]
diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid.py ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ...utils import logging
18
+ from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
19
+ from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
20
+ from .before_denoise import (
21
+ HeliosAdditionalInputsStep,
22
+ HeliosAddNoiseToImageLatentsStep,
23
+ HeliosAddNoiseToVideoLatentsStep,
24
+ HeliosI2VSeedHistoryStep,
25
+ HeliosPrepareHistoryStep,
26
+ HeliosTextInputStep,
27
+ HeliosV2VSeedHistoryStep,
28
+ )
29
+ from .decoders import HeliosDecodeStep
30
+ from .denoise import HeliosPyramidChunkDenoiseStep, HeliosPyramidI2VChunkDenoiseStep
31
+ from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
32
+
33
+
34
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
35
+
36
+
37
+ # ====================
38
+ # 1. Vae Encoder
39
+ # ====================
40
+
41
+
42
+ # auto_docstring
43
+ class HeliosPyramidAutoVaeEncoderStep(AutoPipelineBlocks):
44
+ """
45
+ Encoder step that encodes video or image inputs. This is an auto pipeline block.
46
+ - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
47
+ - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
48
+ - If neither is provided, step will be skipped.
49
+
50
+ Components:
51
+ vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
52
+
53
+ Inputs:
54
+ video (`None`, *optional*):
55
+ Input video for video-to-video generation
56
+ height (`int`, *optional*, defaults to 384):
57
+ The height in pixels of the generated image.
58
+ width (`int`, *optional*, defaults to 640):
59
+ The width in pixels of the generated image.
60
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
61
+ Number of latent frames per temporal chunk.
62
+ generator (`Generator`, *optional*):
63
+ Torch generator for deterministic generation.
64
+ image (`Image | list`, *optional*):
65
+ Reference image(s) for denoising. Can be a single image or list of images.
66
+
67
+ Outputs:
68
+ image_latents (`Tensor`):
69
+ The latent representation of the input image.
70
+ video_latents (`Tensor`):
71
+ Encoded video latents (chunked)
72
+ fake_image_latents (`Tensor`):
73
+ Fake image latents for history seeding
74
+ """
75
+
76
+ block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
77
+ block_names = ["video_encoder", "image_encoder"]
78
+ block_trigger_inputs = ["video", "image"]
79
+
80
+ @property
81
+ def description(self):
82
+ return (
83
+ "Encoder step that encodes video or image inputs. This is an auto pipeline block.\n"
84
+ " - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
85
+ " - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
86
+ " - If neither is provided, step will be skipped."
87
+ )
88
+
89
+
90
+ # ====================
91
+ # 2. DENOISE
92
+ # ====================
93
+
94
+
95
+ # DENOISE (T2V)
96
+ # auto_docstring
97
+ class HeliosPyramidCoreDenoiseStep(SequentialPipelineBlocks):
98
+ """
99
+ T2V pyramid denoise block with progressive multi-resolution denoising.
100
+
101
+ Components:
102
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
103
+ (`ClassifierFreeZeroStarGuidance`)
104
+
105
+ Inputs:
106
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
107
+ Number of videos to generate per prompt.
108
+ prompt_embeds (`Tensor`):
109
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
110
+ negative_prompt_embeds (`Tensor`, *optional*):
111
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
112
+ height (`int`, *optional*, defaults to 384):
113
+ The height in pixels of the generated image.
114
+ width (`int`, *optional*, defaults to 640):
115
+ The width in pixels of the generated image.
116
+ num_frames (`int`, *optional*, defaults to 132):
117
+ Total number of video frames to generate.
118
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
119
+ Number of latent frames per temporal chunk.
120
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
121
+ Sizes of long/mid/short history buffers for temporal context.
122
+ keep_first_frame (`bool`, *optional*, defaults to True):
123
+ Whether to keep the first frame as a prefix in history.
124
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
125
+ Number of denoising steps per pyramid stage.
126
+ generator (`Generator`, *optional*):
127
+ Torch generator for deterministic generation.
128
+ latents (`Tensor`, *optional*):
129
+ Pre-generated noisy latents for image generation.
130
+ **denoiser_input_fields (`None`, *optional*):
131
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
132
+ attention_kwargs (`dict`, *optional*):
133
+ Additional kwargs for attention processors.
134
+
135
+ Outputs:
136
+ latent_chunks (`list`):
137
+ List of per-chunk denoised latent tensors
138
+ """
139
+
140
+ model_name = "helios-pyramid"
141
+ block_classes = [
142
+ HeliosTextInputStep,
143
+ HeliosPrepareHistoryStep,
144
+ HeliosPyramidChunkDenoiseStep,
145
+ ]
146
+ block_names = ["input", "prepare_history", "pyramid_chunk_denoise"]
147
+
148
+ @property
149
+ def description(self):
150
+ return "T2V pyramid denoise block with progressive multi-resolution denoising."
151
+
152
+ @property
153
+ def outputs(self):
154
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
155
+
156
+
157
+ # DENOISE (I2V)
158
+ # auto_docstring
159
+ class HeliosPyramidI2VCoreDenoiseStep(SequentialPipelineBlocks):
160
+ """
161
+ I2V pyramid denoise block with progressive multi-resolution denoising.
162
+
163
+ Components:
164
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
165
+ (`ClassifierFreeZeroStarGuidance`)
166
+
167
+ Inputs:
168
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
169
+ Number of videos to generate per prompt.
170
+ prompt_embeds (`Tensor`):
171
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
172
+ negative_prompt_embeds (`Tensor`, *optional*):
173
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
174
+ image_latents (`Tensor`):
175
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
176
+ fake_image_latents (`Tensor`, *optional*):
177
+ Fake image latents used as history seed for I2V generation.
178
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
179
+ Minimum sigma for image latent noise.
180
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
181
+ Maximum sigma for image latent noise.
182
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
183
+ Minimum sigma for video/fake-image latent noise.
184
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
185
+ Maximum sigma for video/fake-image latent noise.
186
+ generator (`Generator`, *optional*):
187
+ Torch generator for deterministic generation.
188
+ num_frames (`int`, *optional*, defaults to 132):
189
+ Total number of video frames to generate.
190
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
191
+ Number of latent frames per temporal chunk.
192
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
193
+ Sizes of long/mid/short history buffers for temporal context.
194
+ keep_first_frame (`bool`, *optional*, defaults to True):
195
+ Whether to keep the first frame as a prefix in history.
196
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
197
+ Number of denoising steps per pyramid stage.
198
+ latents (`Tensor`, *optional*):
199
+ Pre-generated noisy latents for image generation.
200
+ **denoiser_input_fields (`None`, *optional*):
201
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
202
+ attention_kwargs (`dict`, *optional*):
203
+ Additional kwargs for attention processors.
204
+
205
+ Outputs:
206
+ latent_chunks (`list`):
207
+ List of per-chunk denoised latent tensors
208
+ """
209
+
210
+ model_name = "helios-pyramid"
211
+ block_classes = [
212
+ HeliosTextInputStep,
213
+ HeliosAdditionalInputsStep(
214
+ image_latent_inputs=[InputParam.template("image_latents")],
215
+ additional_batch_inputs=[
216
+ InputParam(
217
+ "fake_image_latents",
218
+ type_hint=torch.Tensor,
219
+ description="Fake image latents used as history seed for I2V generation.",
220
+ ),
221
+ ],
222
+ ),
223
+ HeliosAddNoiseToImageLatentsStep,
224
+ HeliosPrepareHistoryStep,
225
+ HeliosI2VSeedHistoryStep,
226
+ HeliosPyramidI2VChunkDenoiseStep,
227
+ ]
228
+ block_names = [
229
+ "input",
230
+ "additional_inputs",
231
+ "add_noise_image",
232
+ "prepare_history",
233
+ "seed_history",
234
+ "pyramid_chunk_denoise",
235
+ ]
236
+
237
+ @property
238
+ def description(self):
239
+ return "I2V pyramid denoise block with progressive multi-resolution denoising."
240
+
241
+ @property
242
+ def outputs(self):
243
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
244
+
245
+
246
+ # DENOISE (V2V)
247
+ # auto_docstring
248
+ class HeliosPyramidV2VCoreDenoiseStep(SequentialPipelineBlocks):
249
+ """
250
+ V2V pyramid denoise block with progressive multi-resolution denoising.
251
+
252
+ Components:
253
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
254
+ (`ClassifierFreeZeroStarGuidance`)
255
+
256
+ Inputs:
257
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
258
+ Number of videos to generate per prompt.
259
+ prompt_embeds (`Tensor`):
260
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
261
+ negative_prompt_embeds (`Tensor`, *optional*):
262
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
263
+ image_latents (`Tensor`, *optional*):
264
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
265
+ video_latents (`Tensor`, *optional*):
266
+ Encoded video latents for V2V generation.
267
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
268
+ Number of latent frames per temporal chunk.
269
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
270
+ Minimum sigma for image latent noise.
271
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
272
+ Maximum sigma for image latent noise.
273
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
274
+ Minimum sigma for video latent noise.
275
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
276
+ Maximum sigma for video latent noise.
277
+ generator (`Generator`, *optional*):
278
+ Torch generator for deterministic generation.
279
+ num_frames (`int`, *optional*, defaults to 132):
280
+ Total number of video frames to generate.
281
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
282
+ Sizes of long/mid/short history buffers for temporal context.
283
+ keep_first_frame (`bool`, *optional*, defaults to True):
284
+ Whether to keep the first frame as a prefix in history.
285
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
286
+ Number of denoising steps per pyramid stage.
287
+ latents (`Tensor`, *optional*):
288
+ Pre-generated noisy latents for image generation.
289
+ **denoiser_input_fields (`None`, *optional*):
290
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
291
+ attention_kwargs (`dict`, *optional*):
292
+ Additional kwargs for attention processors.
293
+
294
+ Outputs:
295
+ latent_chunks (`list`):
296
+ List of per-chunk denoised latent tensors
297
+ """
298
+
299
+ model_name = "helios-pyramid"
300
+ block_classes = [
301
+ HeliosTextInputStep,
302
+ HeliosAdditionalInputsStep(
303
+ image_latent_inputs=[InputParam.template("image_latents")],
304
+ additional_batch_inputs=[
305
+ InputParam(
306
+ "video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
307
+ ),
308
+ ],
309
+ ),
310
+ HeliosAddNoiseToVideoLatentsStep,
311
+ HeliosPrepareHistoryStep,
312
+ HeliosV2VSeedHistoryStep,
313
+ HeliosPyramidI2VChunkDenoiseStep,
314
+ ]
315
+ block_names = [
316
+ "input",
317
+ "additional_inputs",
318
+ "add_noise_video",
319
+ "prepare_history",
320
+ "seed_history",
321
+ "pyramid_chunk_denoise",
322
+ ]
323
+
324
+ @property
325
+ def description(self):
326
+ return "V2V pyramid denoise block with progressive multi-resolution denoising."
327
+
328
+ @property
329
+ def outputs(self):
330
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
331
+
332
+
333
+ # AUTO DENOISE
334
+ # auto_docstring
335
+ class HeliosPyramidAutoCoreDenoiseStep(ConditionalPipelineBlocks):
336
+ """
337
+ Pyramid core denoise step that selects the appropriate denoising block.
338
+ - `HeliosPyramidV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
339
+ - `HeliosPyramidI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
340
+ - `HeliosPyramidCoreDenoiseStep` (text2video) for text-to-video tasks.
341
+
342
+ Components:
343
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
344
+ (`ClassifierFreeZeroStarGuidance`)
345
+
346
+ Inputs:
347
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
348
+ Number of videos to generate per prompt.
349
+ prompt_embeds (`Tensor`):
350
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
351
+ negative_prompt_embeds (`Tensor`, *optional*):
352
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
353
+ image_latents (`Tensor`, *optional*):
354
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
355
+ video_latents (`Tensor`, *optional*):
356
+ Encoded video latents for V2V generation.
357
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
358
+ Number of latent frames per temporal chunk.
359
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
360
+ Minimum sigma for image latent noise.
361
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
362
+ Maximum sigma for image latent noise.
363
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
364
+ Minimum sigma for video latent noise.
365
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
366
+ Maximum sigma for video latent noise.
367
+ generator (`Generator`, *optional*):
368
+ Torch generator for deterministic generation.
369
+ num_frames (`int`, *optional*, defaults to 132):
370
+ Total number of video frames to generate.
371
+ history_sizes (`list`):
372
+ Sizes of long/mid/short history buffers for temporal context.
373
+ keep_first_frame (`bool`, *optional*, defaults to True):
374
+ Whether to keep the first frame as a prefix in history.
375
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
376
+ Number of denoising steps per pyramid stage.
377
+ latents (`Tensor`, *optional*):
378
+ Pre-generated noisy latents for image generation.
379
+ **denoiser_input_fields (`None`, *optional*):
380
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
381
+ attention_kwargs (`dict`, *optional*):
382
+ Additional kwargs for attention processors.
383
+ fake_image_latents (`Tensor`, *optional*):
384
+ Fake image latents used as history seed for I2V generation.
385
+ height (`int`, *optional*, defaults to 384):
386
+ The height in pixels of the generated image.
387
+ width (`int`, *optional*, defaults to 640):
388
+ The width in pixels of the generated image.
389
+
390
+ Outputs:
391
+ latent_chunks (`list`):
392
+ List of per-chunk denoised latent tensors
393
+ """
394
+
395
+ block_classes = [HeliosPyramidV2VCoreDenoiseStep, HeliosPyramidI2VCoreDenoiseStep, HeliosPyramidCoreDenoiseStep]
396
+ block_names = ["video2video", "image2video", "text2video"]
397
+ block_trigger_inputs = ["video_latents", "fake_image_latents"]
398
+ default_block_name = "text2video"
399
+
400
+ def select_block(self, video_latents=None, fake_image_latents=None):
401
+ if video_latents is not None:
402
+ return "video2video"
403
+ elif fake_image_latents is not None:
404
+ return "image2video"
405
+ return None
406
+
407
+ @property
408
+ def description(self):
409
+ return (
410
+ "Pyramid core denoise step that selects the appropriate denoising block.\n"
411
+ " - `HeliosPyramidV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
412
+ " - `HeliosPyramidI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
413
+ " - `HeliosPyramidCoreDenoiseStep` (text2video) for text-to-video tasks."
414
+ )
415
+
416
+
417
+ # ====================
418
+ # 3. Auto Blocks
419
+ # ====================
420
+
421
+ PYRAMID_AUTO_BLOCKS = InsertableDict(
422
+ [
423
+ ("text_encoder", HeliosTextEncoderStep()),
424
+ ("vae_encoder", HeliosPyramidAutoVaeEncoderStep()),
425
+ ("denoise", HeliosPyramidAutoCoreDenoiseStep()),
426
+ ("decode", HeliosDecodeStep()),
427
+ ]
428
+ )
429
+
430
+
431
+ # auto_docstring
432
+ class HeliosPyramidAutoBlocks(SequentialPipelineBlocks):
433
+ """
434
+ Auto Modular pipeline for pyramid progressive generation (T2V/I2V/V2V) using Helios.
435
+
436
+ Supported workflows:
437
+ - `text2video`: requires `prompt`
438
+ - `image2video`: requires `prompt`, `image`
439
+ - `video2video`: requires `prompt`, `video`
440
+
441
+ Components:
442
+ text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
443
+ (`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
444
+ (`HeliosScheduler`)
445
+
446
+ Inputs:
447
+ prompt (`str`):
448
+ The prompt or prompts to guide image generation.
449
+ negative_prompt (`str`, *optional*):
450
+ The prompt or prompts not to guide the image generation.
451
+ max_sequence_length (`int`, *optional*, defaults to 512):
452
+ Maximum sequence length for prompt encoding.
453
+ video (`None`, *optional*):
454
+ Input video for video-to-video generation
455
+ height (`int`, *optional*, defaults to 384):
456
+ The height in pixels of the generated image.
457
+ width (`int`, *optional*, defaults to 640):
458
+ The width in pixels of the generated image.
459
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
460
+ Number of latent frames per temporal chunk.
461
+ generator (`Generator`, *optional*):
462
+ Torch generator for deterministic generation.
463
+ image (`Image | list`, *optional*):
464
+ Reference image(s) for denoising. Can be a single image or list of images.
465
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
466
+ Number of videos to generate per prompt.
467
+ image_latents (`Tensor`, *optional*):
468
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
469
+ video_latents (`Tensor`, *optional*):
470
+ Encoded video latents for V2V generation.
471
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
472
+ Minimum sigma for image latent noise.
473
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
474
+ Maximum sigma for image latent noise.
475
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
476
+ Minimum sigma for video latent noise.
477
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
478
+ Maximum sigma for video latent noise.
479
+ num_frames (`int`, *optional*, defaults to 132):
480
+ Total number of video frames to generate.
481
+ history_sizes (`list`):
482
+ Sizes of long/mid/short history buffers for temporal context.
483
+ keep_first_frame (`bool`, *optional*, defaults to True):
484
+ Whether to keep the first frame as a prefix in history.
485
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
486
+ Number of denoising steps per pyramid stage.
487
+ latents (`Tensor`, *optional*):
488
+ Pre-generated noisy latents for image generation.
489
+ **denoiser_input_fields (`None`, *optional*):
490
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
491
+ attention_kwargs (`dict`, *optional*):
492
+ Additional kwargs for attention processors.
493
+ fake_image_latents (`Tensor`, *optional*):
494
+ Fake image latents used as history seed for I2V generation.
495
+ output_type (`str`, *optional*, defaults to np):
496
+ Output format: 'pil', 'np', 'pt'.
497
+
498
+ Outputs:
499
+ videos (`list`):
500
+ The generated videos.
501
+ """
502
+
503
+ model_name = "helios-pyramid"
504
+
505
+ block_classes = PYRAMID_AUTO_BLOCKS.values()
506
+ block_names = PYRAMID_AUTO_BLOCKS.keys()
507
+
508
+ _workflow_map = {
509
+ "text2video": {"prompt": True},
510
+ "image2video": {"prompt": True, "image": True},
511
+ "video2video": {"prompt": True, "video": True},
512
+ }
513
+
514
+ @property
515
+ def description(self):
516
+ return "Auto Modular pipeline for pyramid progressive generation (T2V/I2V/V2V) using Helios."
517
+
518
+ @property
519
+ def outputs(self):
520
+ return [OutputParam.template("videos")]
diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid_distilled.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ...utils import logging
18
+ from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
19
+ from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
20
+ from .before_denoise import (
21
+ HeliosAdditionalInputsStep,
22
+ HeliosAddNoiseToImageLatentsStep,
23
+ HeliosAddNoiseToVideoLatentsStep,
24
+ HeliosI2VSeedHistoryStep,
25
+ HeliosPrepareHistoryStep,
26
+ HeliosTextInputStep,
27
+ HeliosV2VSeedHistoryStep,
28
+ )
29
+ from .decoders import HeliosDecodeStep
30
+ from .denoise import HeliosPyramidDistilledChunkDenoiseStep, HeliosPyramidDistilledI2VChunkDenoiseStep
31
+ from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
32
+
33
+
34
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
35
+
36
+
37
+ # ====================
38
+ # 1. Vae Encoder
39
+ # ====================
40
+
41
+
42
+ # auto_docstring
43
+ class HeliosPyramidDistilledAutoVaeEncoderStep(AutoPipelineBlocks):
44
+ """
45
+ Encoder step for distilled pyramid pipeline.
46
+ - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
47
+ - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
48
+ - If neither is provided, step will be skipped.
49
+
50
+ Components:
51
+ vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
52
+
53
+ Inputs:
54
+ video (`None`, *optional*):
55
+ Input video for video-to-video generation
56
+ height (`int`, *optional*, defaults to 384):
57
+ The height in pixels of the generated image.
58
+ width (`int`, *optional*, defaults to 640):
59
+ The width in pixels of the generated image.
60
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
61
+ Number of latent frames per temporal chunk.
62
+ generator (`Generator`, *optional*):
63
+ Torch generator for deterministic generation.
64
+ image (`Image | list`, *optional*):
65
+ Reference image(s) for denoising. Can be a single image or list of images.
66
+
67
+ Outputs:
68
+ image_latents (`Tensor`):
69
+ The latent representation of the input image.
70
+ video_latents (`Tensor`):
71
+ Encoded video latents (chunked)
72
+ fake_image_latents (`Tensor`):
73
+ Fake image latents for history seeding
74
+ """
75
+
76
+ block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
77
+ block_names = ["video_encoder", "image_encoder"]
78
+ block_trigger_inputs = ["video", "image"]
79
+
80
+ @property
81
+ def description(self):
82
+ return (
83
+ "Encoder step for distilled pyramid pipeline.\n"
84
+ " - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
85
+ " - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
86
+ " - If neither is provided, step will be skipped."
87
+ )
88
+
89
+
90
+ # ====================
91
+ # 2. DENOISE
92
+ # ====================
93
+
94
+
95
+ # DENOISE (T2V)
96
+ # auto_docstring
97
+ class HeliosPyramidDistilledCoreDenoiseStep(SequentialPipelineBlocks):
98
+ """
99
+ T2V distilled pyramid denoise block with DMD scheduler and no CFG.
100
+
101
+ Components:
102
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
103
+
104
+ Inputs:
105
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
106
+ Number of videos to generate per prompt.
107
+ prompt_embeds (`Tensor`):
108
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
109
+ negative_prompt_embeds (`Tensor`, *optional*):
110
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
111
+ height (`int`, *optional*, defaults to 384):
112
+ The height in pixels of the generated image.
113
+ width (`int`, *optional*, defaults to 640):
114
+ The width in pixels of the generated image.
115
+ num_frames (`int`, *optional*, defaults to 132):
116
+ Total number of video frames to generate.
117
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
118
+ Number of latent frames per temporal chunk.
119
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
120
+ Sizes of long/mid/short history buffers for temporal context.
121
+ keep_first_frame (`bool`, *optional*, defaults to True):
122
+ Whether to keep the first frame as a prefix in history.
123
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
124
+ Number of denoising steps per pyramid stage.
125
+ generator (`Generator`, *optional*):
126
+ Torch generator for deterministic generation.
127
+ latents (`Tensor`, *optional*):
128
+ Pre-generated noisy latents for image generation.
129
+ **denoiser_input_fields (`None`, *optional*):
130
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
131
+ is_amplify_first_chunk (`bool`, *optional*, defaults to True):
132
+ Whether to double the first chunk's timesteps via the scheduler for amplified generation.
133
+ attention_kwargs (`dict`, *optional*):
134
+ Additional kwargs for attention processors.
135
+
136
+ Outputs:
137
+ latent_chunks (`list`):
138
+ List of per-chunk denoised latent tensors
139
+ """
140
+
141
+ model_name = "helios-pyramid"
142
+ block_classes = [
143
+ HeliosTextInputStep,
144
+ HeliosPrepareHistoryStep,
145
+ HeliosPyramidDistilledChunkDenoiseStep,
146
+ ]
147
+ block_names = ["input", "prepare_history", "pyramid_chunk_denoise"]
148
+
149
+ @property
150
+ def description(self):
151
+ return "T2V distilled pyramid denoise block with DMD scheduler and no CFG."
152
+
153
+ @property
154
+ def outputs(self):
155
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
156
+
157
+
158
+ # DENOISE (I2V)
159
+ # auto_docstring
160
+ class HeliosPyramidDistilledI2VCoreDenoiseStep(SequentialPipelineBlocks):
161
+ """
162
+ I2V distilled pyramid denoise block with DMD scheduler and no CFG.
163
+
164
+ Components:
165
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
166
+
167
+ Inputs:
168
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
169
+ Number of videos to generate per prompt.
170
+ prompt_embeds (`Tensor`):
171
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
172
+ negative_prompt_embeds (`Tensor`, *optional*):
173
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
174
+ image_latents (`Tensor`):
175
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
176
+ fake_image_latents (`Tensor`, *optional*):
177
+ Fake image latents used as history seed for I2V generation.
178
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
179
+ Minimum sigma for image latent noise.
180
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
181
+ Maximum sigma for image latent noise.
182
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
183
+ Minimum sigma for video/fake-image latent noise.
184
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
185
+ Maximum sigma for video/fake-image latent noise.
186
+ generator (`Generator`, *optional*):
187
+ Torch generator for deterministic generation.
188
+ num_frames (`int`, *optional*, defaults to 132):
189
+ Total number of video frames to generate.
190
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
191
+ Number of latent frames per temporal chunk.
192
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
193
+ Sizes of long/mid/short history buffers for temporal context.
194
+ keep_first_frame (`bool`, *optional*, defaults to True):
195
+ Whether to keep the first frame as a prefix in history.
196
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
197
+ Number of denoising steps per pyramid stage.
198
+ latents (`Tensor`, *optional*):
199
+ Pre-generated noisy latents for image generation.
200
+ **denoiser_input_fields (`None`, *optional*):
201
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
202
+ is_amplify_first_chunk (`bool`, *optional*, defaults to True):
203
+ Whether to double the first chunk's timesteps via the scheduler for amplified generation.
204
+ attention_kwargs (`dict`, *optional*):
205
+ Additional kwargs for attention processors.
206
+
207
+ Outputs:
208
+ latent_chunks (`list`):
209
+ List of per-chunk denoised latent tensors
210
+ """
211
+
212
+ model_name = "helios-pyramid"
213
+ block_classes = [
214
+ HeliosTextInputStep,
215
+ HeliosAdditionalInputsStep(
216
+ image_latent_inputs=[InputParam.template("image_latents")],
217
+ additional_batch_inputs=[
218
+ InputParam(
219
+ "fake_image_latents",
220
+ type_hint=torch.Tensor,
221
+ description="Fake image latents used as history seed for I2V generation.",
222
+ ),
223
+ ],
224
+ ),
225
+ HeliosAddNoiseToImageLatentsStep,
226
+ HeliosPrepareHistoryStep,
227
+ HeliosI2VSeedHistoryStep,
228
+ HeliosPyramidDistilledI2VChunkDenoiseStep,
229
+ ]
230
+ block_names = [
231
+ "input",
232
+ "additional_inputs",
233
+ "add_noise_image",
234
+ "prepare_history",
235
+ "seed_history",
236
+ "pyramid_chunk_denoise",
237
+ ]
238
+
239
+ @property
240
+ def description(self):
241
+ return "I2V distilled pyramid denoise block with DMD scheduler and no CFG."
242
+
243
+ @property
244
+ def outputs(self):
245
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
246
+
247
+
248
+ # DENOISE (V2V)
249
+ # auto_docstring
250
+ class HeliosPyramidDistilledV2VCoreDenoiseStep(SequentialPipelineBlocks):
251
+ """
252
+ V2V distilled pyramid denoise block with DMD scheduler and no CFG.
253
+
254
+ Components:
255
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
256
+
257
+ Inputs:
258
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
259
+ Number of videos to generate per prompt.
260
+ prompt_embeds (`Tensor`):
261
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
262
+ negative_prompt_embeds (`Tensor`, *optional*):
263
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
264
+ image_latents (`Tensor`, *optional*):
265
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
266
+ video_latents (`Tensor`, *optional*):
267
+ Encoded video latents for V2V generation.
268
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
269
+ Number of latent frames per temporal chunk.
270
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
271
+ Minimum sigma for image latent noise.
272
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
273
+ Maximum sigma for image latent noise.
274
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
275
+ Minimum sigma for video latent noise.
276
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
277
+ Maximum sigma for video latent noise.
278
+ generator (`Generator`, *optional*):
279
+ Torch generator for deterministic generation.
280
+ num_frames (`int`, *optional*, defaults to 132):
281
+ Total number of video frames to generate.
282
+ history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
283
+ Sizes of long/mid/short history buffers for temporal context.
284
+ keep_first_frame (`bool`, *optional*, defaults to True):
285
+ Whether to keep the first frame as a prefix in history.
286
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
287
+ Number of denoising steps per pyramid stage.
288
+ latents (`Tensor`, *optional*):
289
+ Pre-generated noisy latents for image generation.
290
+ **denoiser_input_fields (`None`, *optional*):
291
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
292
+ is_amplify_first_chunk (`bool`, *optional*, defaults to True):
293
+ Whether to double the first chunk's timesteps via the scheduler for amplified generation.
294
+ attention_kwargs (`dict`, *optional*):
295
+ Additional kwargs for attention processors.
296
+
297
+ Outputs:
298
+ latent_chunks (`list`):
299
+ List of per-chunk denoised latent tensors
300
+ """
301
+
302
+ model_name = "helios-pyramid"
303
+ block_classes = [
304
+ HeliosTextInputStep,
305
+ HeliosAdditionalInputsStep(
306
+ image_latent_inputs=[InputParam.template("image_latents")],
307
+ additional_batch_inputs=[
308
+ InputParam(
309
+ "video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
310
+ ),
311
+ ],
312
+ ),
313
+ HeliosAddNoiseToVideoLatentsStep,
314
+ HeliosPrepareHistoryStep,
315
+ HeliosV2VSeedHistoryStep,
316
+ HeliosPyramidDistilledI2VChunkDenoiseStep,
317
+ ]
318
+ block_names = [
319
+ "input",
320
+ "additional_inputs",
321
+ "add_noise_video",
322
+ "prepare_history",
323
+ "seed_history",
324
+ "pyramid_chunk_denoise",
325
+ ]
326
+
327
+ @property
328
+ def description(self):
329
+ return "V2V distilled pyramid denoise block with DMD scheduler and no CFG."
330
+
331
+ @property
332
+ def outputs(self):
333
+ return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
334
+
335
+
336
+ # AUTO DENOISE
337
+ # auto_docstring
338
+ class HeliosPyramidDistilledAutoCoreDenoiseStep(ConditionalPipelineBlocks):
339
+ """
340
+ Distilled pyramid core denoise step that selects the appropriate denoising block.
341
+ - `HeliosPyramidDistilledV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
342
+ - `HeliosPyramidDistilledI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
343
+ - `HeliosPyramidDistilledCoreDenoiseStep` (text2video) for text-to-video tasks.
344
+
345
+ Components:
346
+ transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
347
+
348
+ Inputs:
349
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
350
+ Number of videos to generate per prompt.
351
+ prompt_embeds (`Tensor`):
352
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
353
+ negative_prompt_embeds (`Tensor`, *optional*):
354
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
355
+ image_latents (`Tensor`, *optional*):
356
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
357
+ video_latents (`Tensor`, *optional*):
358
+ Encoded video latents for V2V generation.
359
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
360
+ Number of latent frames per temporal chunk.
361
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
362
+ Minimum sigma for image latent noise.
363
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
364
+ Maximum sigma for image latent noise.
365
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
366
+ Minimum sigma for video latent noise.
367
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
368
+ Maximum sigma for video latent noise.
369
+ generator (`Generator`, *optional*):
370
+ Torch generator for deterministic generation.
371
+ num_frames (`int`, *optional*, defaults to 132):
372
+ Total number of video frames to generate.
373
+ history_sizes (`list`):
374
+ Sizes of long/mid/short history buffers for temporal context.
375
+ keep_first_frame (`bool`, *optional*, defaults to True):
376
+ Whether to keep the first frame as a prefix in history.
377
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
378
+ Number of denoising steps per pyramid stage.
379
+ latents (`Tensor`, *optional*):
380
+ Pre-generated noisy latents for image generation.
381
+ **denoiser_input_fields (`None`, *optional*):
382
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
383
+ is_amplify_first_chunk (`bool`, *optional*, defaults to True):
384
+ Whether to double the first chunk's timesteps via the scheduler for amplified generation.
385
+ attention_kwargs (`dict`, *optional*):
386
+ Additional kwargs for attention processors.
387
+ fake_image_latents (`Tensor`, *optional*):
388
+ Fake image latents used as history seed for I2V generation.
389
+ height (`int`, *optional*, defaults to 384):
390
+ The height in pixels of the generated image.
391
+ width (`int`, *optional*, defaults to 640):
392
+ The width in pixels of the generated image.
393
+
394
+ Outputs:
395
+ latent_chunks (`list`):
396
+ List of per-chunk denoised latent tensors
397
+ """
398
+
399
+ block_classes = [
400
+ HeliosPyramidDistilledV2VCoreDenoiseStep,
401
+ HeliosPyramidDistilledI2VCoreDenoiseStep,
402
+ HeliosPyramidDistilledCoreDenoiseStep,
403
+ ]
404
+ block_names = ["video2video", "image2video", "text2video"]
405
+ block_trigger_inputs = ["video_latents", "fake_image_latents"]
406
+ default_block_name = "text2video"
407
+
408
+ def select_block(self, video_latents=None, fake_image_latents=None):
409
+ if video_latents is not None:
410
+ return "video2video"
411
+ elif fake_image_latents is not None:
412
+ return "image2video"
413
+ return None
414
+
415
+ @property
416
+ def description(self):
417
+ return (
418
+ "Distilled pyramid core denoise step that selects the appropriate denoising block.\n"
419
+ " - `HeliosPyramidDistilledV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
420
+ " - `HeliosPyramidDistilledI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
421
+ " - `HeliosPyramidDistilledCoreDenoiseStep` (text2video) for text-to-video tasks."
422
+ )
423
+
424
+
425
+ # ====================
426
+ # 3. Auto Blocks
427
+ # ====================
428
+
429
+ DISTILLED_PYRAMID_AUTO_BLOCKS = InsertableDict(
430
+ [
431
+ ("text_encoder", HeliosTextEncoderStep()),
432
+ ("vae_encoder", HeliosPyramidDistilledAutoVaeEncoderStep()),
433
+ ("denoise", HeliosPyramidDistilledAutoCoreDenoiseStep()),
434
+ ("decode", HeliosDecodeStep()),
435
+ ]
436
+ )
437
+
438
+
439
+ # auto_docstring
440
+ class HeliosPyramidDistilledAutoBlocks(SequentialPipelineBlocks):
441
+ """
442
+ Auto Modular pipeline for distilled pyramid progressive generation (T2V/I2V/V2V) using Helios.
443
+
444
+ Supported workflows:
445
+ - `text2video`: requires `prompt`
446
+ - `image2video`: requires `prompt`, `image`
447
+ - `video2video`: requires `prompt`, `video`
448
+
449
+ Components:
450
+ text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
451
+ (`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
452
+ (`HeliosScheduler`)
453
+
454
+ Inputs:
455
+ prompt (`str`):
456
+ The prompt or prompts to guide image generation.
457
+ negative_prompt (`str`, *optional*):
458
+ The prompt or prompts not to guide the image generation.
459
+ max_sequence_length (`int`, *optional*, defaults to 512):
460
+ Maximum sequence length for prompt encoding.
461
+ video (`None`, *optional*):
462
+ Input video for video-to-video generation
463
+ height (`int`, *optional*, defaults to 384):
464
+ The height in pixels of the generated image.
465
+ width (`int`, *optional*, defaults to 640):
466
+ The width in pixels of the generated image.
467
+ num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
468
+ Number of latent frames per temporal chunk.
469
+ generator (`Generator`, *optional*):
470
+ Torch generator for deterministic generation.
471
+ image (`Image | list`, *optional*):
472
+ Reference image(s) for denoising. Can be a single image or list of images.
473
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
474
+ Number of videos to generate per prompt.
475
+ image_latents (`Tensor`, *optional*):
476
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
477
+ video_latents (`Tensor`, *optional*):
478
+ Encoded video latents for V2V generation.
479
+ image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
480
+ Minimum sigma for image latent noise.
481
+ image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
482
+ Maximum sigma for image latent noise.
483
+ video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
484
+ Minimum sigma for video latent noise.
485
+ video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
486
+ Maximum sigma for video latent noise.
487
+ num_frames (`int`, *optional*, defaults to 132):
488
+ Total number of video frames to generate.
489
+ history_sizes (`list`):
490
+ Sizes of long/mid/short history buffers for temporal context.
491
+ keep_first_frame (`bool`, *optional*, defaults to True):
492
+ Whether to keep the first frame as a prefix in history.
493
+ pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
494
+ Number of denoising steps per pyramid stage.
495
+ latents (`Tensor`, *optional*):
496
+ Pre-generated noisy latents for image generation.
497
+ **denoiser_input_fields (`None`, *optional*):
498
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
499
+ is_amplify_first_chunk (`bool`, *optional*, defaults to True):
500
+ Whether to double the first chunk's timesteps via the scheduler for amplified generation.
501
+ attention_kwargs (`dict`, *optional*):
502
+ Additional kwargs for attention processors.
503
+ fake_image_latents (`Tensor`, *optional*):
504
+ Fake image latents used as history seed for I2V generation.
505
+ output_type (`str`, *optional*, defaults to np):
506
+ Output format: 'pil', 'np', 'pt'.
507
+
508
+ Outputs:
509
+ videos (`list`):
510
+ The generated videos.
511
+ """
512
+
513
+ model_name = "helios-pyramid"
514
+
515
+ block_classes = DISTILLED_PYRAMID_AUTO_BLOCKS.values()
516
+ block_names = DISTILLED_PYRAMID_AUTO_BLOCKS.keys()
517
+
518
+ _workflow_map = {
519
+ "text2video": {"prompt": True},
520
+ "image2video": {"prompt": True, "image": True},
521
+ "video2video": {"prompt": True, "video": True},
522
+ }
523
+
524
+ @property
525
+ def description(self):
526
+ return "Auto Modular pipeline for distilled pyramid progressive generation (T2V/I2V/V2V) using Helios."
527
+
528
+ @property
529
+ def outputs(self):
530
+ return [OutputParam.template("videos")]
diffusers/modular_pipelines/helios/modular_pipeline.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...loaders import HeliosLoraLoaderMixin
17
+ from ...utils import logging
18
+ from ..modular_pipeline import ModularPipeline
19
+
20
+
21
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
22
+
23
+
24
+ class HeliosModularPipeline(
25
+ ModularPipeline,
26
+ HeliosLoraLoaderMixin,
27
+ ):
28
+ """
29
+ A ModularPipeline for Helios text-to-video generation.
30
+
31
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
32
+ """
33
+
34
+ default_blocks_name = "HeliosAutoBlocks"
35
+
36
+ @property
37
+ def vae_scale_factor_spatial(self):
38
+ vae_scale_factor = 8
39
+ if hasattr(self, "vae") and self.vae is not None:
40
+ vae_scale_factor = self.vae.config.scale_factor_spatial
41
+ return vae_scale_factor
42
+
43
+ @property
44
+ def vae_scale_factor_temporal(self):
45
+ vae_scale_factor = 4
46
+ if hasattr(self, "vae") and self.vae is not None:
47
+ vae_scale_factor = self.vae.config.scale_factor_temporal
48
+ return vae_scale_factor
49
+
50
+ @property
51
+ def num_channels_latents(self):
52
+ # YiYi TODO: find out default value
53
+ num_channels_latents = 16
54
+ if hasattr(self, "transformer") and self.transformer is not None:
55
+ num_channels_latents = self.transformer.config.in_channels
56
+ return num_channels_latents
57
+
58
+ @property
59
+ def requires_unconditional_embeds(self):
60
+ requires_unconditional_embeds = False
61
+
62
+ if hasattr(self, "guider") and self.guider is not None:
63
+ requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
64
+
65
+ return requires_unconditional_embeds
66
+
67
+
68
+ class HeliosPyramidModularPipeline(HeliosModularPipeline):
69
+ """
70
+ A ModularPipeline for Helios pyramid (progressive resolution) video generation.
71
+
72
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
73
+ """
74
+
75
+ default_blocks_name = "HeliosPyramidAutoBlocks"
76
+
77
+
78
+ class HeliosPyramidDistilledModularPipeline(HeliosModularPipeline):
79
+ """
80
+ A ModularPipeline for Helios distilled pyramid video generation using DMD scheduler.
81
+
82
+ Uses guidance_scale=1.0 (no CFG) and supports is_amplify_first_chunk for the DMD scheduler.
83
+
84
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
85
+ """
86
+
87
+ default_blocks_name = "HeliosPyramidDistilledAutoBlocks"
diffusers/modular_pipelines/hunyuan_video1_5/__init__.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_hunyuan_video1_5"] = [
25
+ "HunyuanVideo15AutoBlocks",
26
+ ]
27
+ _import_structure["modular_pipeline"] = ["HunyuanVideo15ModularPipeline"]
28
+
29
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
30
+ try:
31
+ if not (is_transformers_available() and is_torch_available()):
32
+ raise OptionalDependencyNotAvailable()
33
+ except OptionalDependencyNotAvailable:
34
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
35
+ else:
36
+ from .modular_blocks_hunyuan_video1_5 import HunyuanVideo15AutoBlocks
37
+ from .modular_pipeline import HunyuanVideo15ModularPipeline
38
+ else:
39
+ import sys
40
+
41
+ sys.modules[__name__] = _LazyModule(
42
+ __name__,
43
+ globals()["__file__"],
44
+ _import_structure,
45
+ module_spec=__spec__,
46
+ )
47
+
48
+ for name, value in _dummy_objects.items():
49
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/hunyuan_video1_5/before_denoise.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ from ...configuration_utils import FrozenDict
21
+ from ...models import HunyuanVideo15Transformer3DModel
22
+ from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
23
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
24
+ from ...utils import logging
25
+ from ...utils.torch_utils import randn_tensor
26
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
27
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
28
+ from .modular_pipeline import HunyuanVideo15ModularPipeline
29
+
30
+
31
+ logger = logging.get_logger(__name__)
32
+
33
+
34
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
35
+ def retrieve_timesteps(
36
+ scheduler,
37
+ num_inference_steps: int | None = None,
38
+ device: str | torch.device | None = None,
39
+ timesteps: list[int] | None = None,
40
+ sigmas: list[float] | None = None,
41
+ **kwargs,
42
+ ):
43
+ r"""
44
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
45
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
46
+
47
+ Args:
48
+ scheduler (`SchedulerMixin`):
49
+ The scheduler to get timesteps from.
50
+ num_inference_steps (`int`):
51
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
52
+ must be `None`.
53
+ device (`str` or `torch.device`, *optional*):
54
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
55
+ timesteps (`list[int]`, *optional*):
56
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
57
+ `num_inference_steps` and `sigmas` must be `None`.
58
+ sigmas (`list[float]`, *optional*):
59
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
60
+ `num_inference_steps` and `timesteps` must be `None`.
61
+
62
+ Returns:
63
+ `tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
64
+ second element is the number of inference steps.
65
+ """
66
+ if timesteps is not None and sigmas is not None:
67
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
68
+ if timesteps is not None:
69
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
70
+ if not accepts_timesteps:
71
+ raise ValueError(
72
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
73
+ f" timestep schedules. Please check whether you are using the correct scheduler."
74
+ )
75
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
76
+ timesteps = scheduler.timesteps
77
+ num_inference_steps = len(timesteps)
78
+ elif sigmas is not None:
79
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
80
+ if not accept_sigmas:
81
+ raise ValueError(
82
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
83
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
84
+ )
85
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
86
+ timesteps = scheduler.timesteps
87
+ num_inference_steps = len(timesteps)
88
+ else:
89
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
90
+ timesteps = scheduler.timesteps
91
+ return timesteps, num_inference_steps
92
+
93
+
94
+ class HunyuanVideo15TextInputStep(ModularPipelineBlocks):
95
+ model_name = "hunyuan-video-1.5"
96
+
97
+ @property
98
+ def description(self) -> str:
99
+ return "Input processing step that determines batch_size"
100
+
101
+ @property
102
+ def inputs(self) -> list[InputParam]:
103
+ return [
104
+ InputParam.template("prompt_embeds"),
105
+ InputParam.template("batch_size", default=None),
106
+ ]
107
+
108
+ @property
109
+ def intermediate_outputs(self) -> list[OutputParam]:
110
+ return [
111
+ OutputParam("batch_size", type_hint=int),
112
+ ]
113
+
114
+ @torch.no_grad()
115
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
116
+ block_state = self.get_block_state(state)
117
+ block_state.batch_size = getattr(block_state, "batch_size", None) or block_state.prompt_embeds.shape[0]
118
+ self.set_block_state(state, block_state)
119
+ return components, state
120
+
121
+
122
+ class HunyuanVideo15SetTimestepsStep(ModularPipelineBlocks):
123
+ model_name = "hunyuan-video-1.5"
124
+
125
+ @property
126
+ def expected_components(self) -> list[ComponentSpec]:
127
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
128
+
129
+ @property
130
+ def description(self) -> str:
131
+ return "Step that sets the scheduler's timesteps for inference"
132
+
133
+ @property
134
+ def inputs(self) -> list[InputParam]:
135
+ return [
136
+ InputParam.template("num_inference_steps"),
137
+ InputParam.template("sigmas"),
138
+ ]
139
+
140
+ @property
141
+ def intermediate_outputs(self) -> list[OutputParam]:
142
+ return [
143
+ OutputParam("timesteps", type_hint=torch.Tensor),
144
+ OutputParam("num_inference_steps", type_hint=int),
145
+ ]
146
+
147
+ @torch.no_grad()
148
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
149
+ block_state = self.get_block_state(state)
150
+ device = components._execution_device
151
+
152
+ sigmas = block_state.sigmas
153
+ if sigmas is None:
154
+ sigmas = np.linspace(1.0, 0.0, block_state.num_inference_steps + 1)[:-1]
155
+
156
+ block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
157
+ components.scheduler, block_state.num_inference_steps, device, sigmas=sigmas
158
+ )
159
+
160
+ self.set_block_state(state, block_state)
161
+ return components, state
162
+
163
+
164
+ class HunyuanVideo15PrepareLatentsStep(ModularPipelineBlocks):
165
+ model_name = "hunyuan-video-1.5"
166
+
167
+ @property
168
+ def description(self) -> str:
169
+ return "Prepare latents, conditioning latents, mask, and image_embeds for T2V"
170
+
171
+ @property
172
+ def expected_components(self) -> list[ComponentSpec]:
173
+ return [
174
+ ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
175
+ ComponentSpec(
176
+ "video_processor",
177
+ HunyuanVideo15ImageProcessor,
178
+ config=FrozenDict({"vae_scale_factor": 16}),
179
+ default_creation_method="from_config",
180
+ ),
181
+ ]
182
+
183
+ @property
184
+ def inputs(self) -> list[InputParam]:
185
+ return [
186
+ InputParam.template("height"),
187
+ InputParam.template("width"),
188
+ InputParam("num_frames", type_hint=int, default=121, description="Number of video frames to generate."),
189
+ InputParam.template("latents"),
190
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
191
+ InputParam.template("generator"),
192
+ InputParam.template("batch_size", required=True, default=None),
193
+ ]
194
+
195
+ @property
196
+ def intermediate_outputs(self) -> list[OutputParam]:
197
+ return [
198
+ OutputParam("latents", type_hint=torch.Tensor, description="Pure noise latents"),
199
+ OutputParam("cond_latents_concat", type_hint=torch.Tensor),
200
+ OutputParam("mask_concat", type_hint=torch.Tensor),
201
+ OutputParam("image_embeds", type_hint=torch.Tensor),
202
+ ]
203
+
204
+ @torch.no_grad()
205
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
206
+ block_state = self.get_block_state(state)
207
+ device = components._execution_device
208
+ dtype = components.transformer.dtype
209
+
210
+ height = block_state.height
211
+ width = block_state.width
212
+ if height is None and width is None:
213
+ height, width = components.video_processor.calculate_default_height_width(
214
+ components.default_aspect_ratio[1], components.default_aspect_ratio[0], components.target_size
215
+ )
216
+
217
+ batch_size = block_state.batch_size * block_state.num_videos_per_prompt
218
+ num_frames = block_state.num_frames
219
+
220
+ latents = block_state.latents
221
+ if latents is not None:
222
+ latents = latents.to(device=device, dtype=dtype)
223
+ else:
224
+ shape = (
225
+ batch_size,
226
+ components.num_channels_latents,
227
+ (num_frames - 1) // components.vae_scale_factor_temporal + 1,
228
+ int(height) // components.vae_scale_factor_spatial,
229
+ int(width) // components.vae_scale_factor_spatial,
230
+ )
231
+ if isinstance(block_state.generator, list) and len(block_state.generator) != batch_size:
232
+ raise ValueError(
233
+ f"You have passed a list of generators of length {len(block_state.generator)}, but requested an effective batch"
234
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
235
+ )
236
+ latents = randn_tensor(shape, generator=block_state.generator, device=device, dtype=dtype)
237
+
238
+ block_state.latents = latents
239
+
240
+ b, c, f, h, w = latents.shape
241
+ block_state.cond_latents_concat = torch.zeros(b, c, f, h, w, dtype=dtype, device=device)
242
+ block_state.mask_concat = torch.zeros(b, 1, f, h, w, dtype=dtype, device=device)
243
+
244
+ block_state.image_embeds = torch.zeros(
245
+ block_state.batch_size,
246
+ components.vision_num_semantic_tokens,
247
+ components.vision_states_dim,
248
+ dtype=dtype,
249
+ device=device,
250
+ )
251
+
252
+ self.set_block_state(state, block_state)
253
+ return components, state
254
+
255
+
256
+ class HunyuanVideo15Image2VideoPrepareLatentsStep(ModularPipelineBlocks):
257
+ model_name = "hunyuan-video-1.5"
258
+
259
+ @property
260
+ def description(self) -> str:
261
+ return (
262
+ "Prepare I2V conditioning from image_latents and image_embeds. "
263
+ "Expects pure noise `latents` from HunyuanVideo15PrepareLatentsStep. "
264
+ "Builds cond_latents_concat and mask_concat for the denoiser."
265
+ )
266
+
267
+ @property
268
+ def expected_components(self) -> list[ComponentSpec]:
269
+ return [ComponentSpec("transformer", HunyuanVideo15Transformer3DModel)]
270
+
271
+ @property
272
+ def inputs(self) -> list[InputParam]:
273
+ return [
274
+ InputParam(
275
+ "image_latents",
276
+ type_hint=torch.Tensor,
277
+ required=True,
278
+ description="Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.",
279
+ ),
280
+ InputParam(
281
+ "image_embeds",
282
+ type_hint=torch.Tensor,
283
+ required=True,
284
+ description="Siglip image embeddings from the image encoder step, used as extra conditioning for I2V.",
285
+ ),
286
+ InputParam.template("latents", required=True),
287
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
288
+ InputParam.template("batch_size", required=True, default=None),
289
+ ]
290
+
291
+ @property
292
+ def intermediate_outputs(self) -> list[OutputParam]:
293
+ return [
294
+ OutputParam("cond_latents_concat", type_hint=torch.Tensor),
295
+ OutputParam("mask_concat", type_hint=torch.Tensor),
296
+ OutputParam("image_embeds", type_hint=torch.Tensor),
297
+ ]
298
+
299
+ @torch.no_grad()
300
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
301
+ block_state = self.get_block_state(state)
302
+ device = components._execution_device
303
+ dtype = components.transformer.dtype
304
+
305
+ batch_size = block_state.batch_size * block_state.num_videos_per_prompt
306
+
307
+ b, c, f, h, w = block_state.latents.shape
308
+
309
+ latent_condition = block_state.image_latents.to(device=device, dtype=dtype)
310
+ latent_condition = latent_condition.repeat(batch_size, 1, f, 1, 1)
311
+ latent_condition[:, :, 1:, :, :] = 0
312
+ block_state.cond_latents_concat = latent_condition
313
+
314
+ latent_mask = torch.zeros(b, 1, f, h, w, dtype=dtype, device=device)
315
+ latent_mask[:, :, 0, :, :] = 1.0
316
+ block_state.mask_concat = latent_mask
317
+
318
+ image_embeds = block_state.image_embeds.to(device=device, dtype=dtype)
319
+ if image_embeds.shape[0] == 1 and batch_size > 1:
320
+ image_embeds = image_embeds.repeat(batch_size, 1, 1)
321
+ block_state.image_embeds = image_embeds
322
+
323
+ self.set_block_state(state, block_state)
324
+ return components, state
diffusers/modular_pipelines/hunyuan_video1_5/decoders.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...models import AutoencoderKLHunyuanVideo15
20
+ from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
21
+ from ...utils import logging
22
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+
25
+
26
+ logger = logging.get_logger(__name__)
27
+
28
+
29
+ class HunyuanVideo15VaeDecoderStep(ModularPipelineBlocks):
30
+ model_name = "hunyuan-video-1.5"
31
+
32
+ @property
33
+ def expected_components(self) -> list[ComponentSpec]:
34
+ return [
35
+ ComponentSpec("vae", AutoencoderKLHunyuanVideo15),
36
+ ComponentSpec(
37
+ "video_processor",
38
+ HunyuanVideo15ImageProcessor,
39
+ config=FrozenDict({"vae_scale_factor": 16}),
40
+ default_creation_method="from_config",
41
+ ),
42
+ ]
43
+
44
+ @property
45
+ def description(self) -> str:
46
+ return "Step that decodes the denoised latents into videos"
47
+
48
+ @property
49
+ def inputs(self) -> list[InputParam]:
50
+ return [
51
+ InputParam.template("latents", required=True),
52
+ InputParam.template("output_type", default="np"),
53
+ ]
54
+
55
+ @property
56
+ def intermediate_outputs(self) -> list[OutputParam]:
57
+ return [
58
+ OutputParam.template("videos"),
59
+ ]
60
+
61
+ @torch.no_grad()
62
+ def __call__(self, components, state: PipelineState) -> PipelineState:
63
+ block_state = self.get_block_state(state)
64
+
65
+ latents = block_state.latents.to(components.vae.dtype) / components.vae.config.scaling_factor
66
+ video = components.vae.decode(latents, return_dict=False)[0]
67
+ block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type)
68
+
69
+ self.set_block_state(state, block_state)
70
+ return components, state
diffusers/modular_pipelines/hunyuan_video1_5/denoise.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...guiders import ClassifierFreeGuidance
20
+ from ...models import HunyuanVideo15Transformer3DModel
21
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
22
+ from ...utils import logging
23
+ from ..modular_pipeline import (
24
+ BlockState,
25
+ LoopSequentialPipelineBlocks,
26
+ ModularPipelineBlocks,
27
+ PipelineState,
28
+ )
29
+ from ..modular_pipeline_utils import ComponentSpec, InputParam
30
+ from .modular_pipeline import HunyuanVideo15ModularPipeline
31
+
32
+
33
+ logger = logging.get_logger(__name__)
34
+
35
+
36
+ class HunyuanVideo15LoopBeforeDenoiser(ModularPipelineBlocks):
37
+ model_name = "hunyuan-video-1.5"
38
+
39
+ @property
40
+ def description(self) -> str:
41
+ return "Step within the denoising loop that prepares the latent input"
42
+
43
+ @property
44
+ def inputs(self) -> list[InputParam]:
45
+ return [
46
+ InputParam.template("latents", required=True),
47
+ InputParam("cond_latents_concat", required=True, type_hint=torch.Tensor),
48
+ InputParam("mask_concat", required=True, type_hint=torch.Tensor),
49
+ ]
50
+
51
+ @torch.no_grad()
52
+ def __call__(self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
53
+ block_state.latent_model_input = torch.cat(
54
+ [block_state.latents, block_state.cond_latents_concat, block_state.mask_concat], dim=1
55
+ )
56
+ return components, block_state
57
+
58
+
59
+ class HunyuanVideo15LoopDenoiser(ModularPipelineBlocks):
60
+ model_name = "hunyuan-video-1.5"
61
+
62
+ def __init__(self, guider_input_fields=None):
63
+ if guider_input_fields is None:
64
+ guider_input_fields = {
65
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
66
+ "encoder_attention_mask": ("prompt_embeds_mask", "negative_prompt_embeds_mask"),
67
+ "encoder_hidden_states_2": ("prompt_embeds_2", "negative_prompt_embeds_2"),
68
+ "encoder_attention_mask_2": ("prompt_embeds_mask_2", "negative_prompt_embeds_mask_2"),
69
+ }
70
+ if not isinstance(guider_input_fields, dict):
71
+ raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
72
+ self._guider_input_fields = guider_input_fields
73
+ super().__init__()
74
+
75
+ @property
76
+ def expected_components(self) -> list[ComponentSpec]:
77
+ return [
78
+ ComponentSpec(
79
+ "guider",
80
+ ClassifierFreeGuidance,
81
+ config=FrozenDict({"guidance_scale": 7.5}),
82
+ default_creation_method="from_config",
83
+ ),
84
+ ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
85
+ ]
86
+
87
+ @property
88
+ def description(self) -> str:
89
+ return "Step within the denoising loop that denoises the latents with guidance"
90
+
91
+ @property
92
+ def inputs(self) -> list[InputParam]:
93
+ inputs = [
94
+ InputParam.template("attention_kwargs"),
95
+ InputParam.template("num_inference_steps", required=True, default=None),
96
+ InputParam(
97
+ "image_embeds",
98
+ type_hint=torch.Tensor,
99
+ description="Siglip image embeddings used as extra conditioning for I2V. Zero-filled for T2V.",
100
+ ),
101
+ ]
102
+ for value in self._guider_input_fields.values():
103
+ if isinstance(value, tuple):
104
+ inputs.append(
105
+ InputParam(
106
+ name=value[0],
107
+ required=True,
108
+ type_hint=torch.Tensor,
109
+ description=f"Positive branch of the {value[0]!r} field fed into the guider.",
110
+ )
111
+ )
112
+ for neg_name in value[1:]:
113
+ inputs.append(
114
+ InputParam(
115
+ name=neg_name,
116
+ type_hint=torch.Tensor,
117
+ description=f"Negative branch of the {neg_name!r} field fed into the guider.",
118
+ )
119
+ )
120
+ else:
121
+ inputs.append(
122
+ InputParam(
123
+ name=value,
124
+ required=True,
125
+ type_hint=torch.Tensor,
126
+ description=f"{value!r} field fed into the guider.",
127
+ )
128
+ )
129
+ return inputs
130
+
131
+ @torch.no_grad()
132
+ def __call__(
133
+ self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
134
+ ) -> PipelineState:
135
+ timestep = t.expand(block_state.latent_model_input.shape[0]).to(block_state.latent_model_input.dtype)
136
+
137
+ # Step 1: Collect model inputs
138
+ guider_inputs = {
139
+ input_name: tuple(getattr(block_state, v) for v in value)
140
+ if isinstance(value, tuple)
141
+ else getattr(block_state, value)
142
+ for input_name, value in self._guider_input_fields.items()
143
+ }
144
+
145
+ # Step 2: Update guider state
146
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
147
+
148
+ # Step 3: Prepare batched inputs
149
+ guider_state = components.guider.prepare_inputs(guider_inputs)
150
+
151
+ # Step 4: Run denoiser for each batch
152
+ for guider_state_batch in guider_state:
153
+ components.guider.prepare_models(components.transformer)
154
+
155
+ cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
156
+
157
+ context_name = getattr(guider_state_batch, components.guider._identifier_key)
158
+ with components.transformer.cache_context(context_name):
159
+ guider_state_batch.noise_pred = components.transformer(
160
+ hidden_states=block_state.latent_model_input,
161
+ image_embeds=block_state.image_embeds,
162
+ timestep=timestep,
163
+ attention_kwargs=block_state.attention_kwargs,
164
+ return_dict=False,
165
+ **cond_kwargs,
166
+ )[0]
167
+
168
+ components.guider.cleanup_models(components.transformer)
169
+
170
+ # Step 5: Combine predictions
171
+ block_state.noise_pred = components.guider(guider_state)[0]
172
+
173
+ return components, block_state
174
+
175
+
176
+ class HunyuanVideo15LoopAfterDenoiser(ModularPipelineBlocks):
177
+ model_name = "hunyuan-video-1.5"
178
+
179
+ @property
180
+ def expected_components(self) -> list[ComponentSpec]:
181
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
182
+
183
+ @property
184
+ def description(self) -> str:
185
+ return "Step within the denoising loop that updates the latents"
186
+
187
+ @torch.no_grad()
188
+ def __call__(self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
189
+ latents_dtype = block_state.latents.dtype
190
+ block_state.latents = components.scheduler.step(
191
+ block_state.noise_pred, t, block_state.latents, return_dict=False
192
+ )[0]
193
+
194
+ if block_state.latents.dtype != latents_dtype:
195
+ if torch.backends.mps.is_available():
196
+ block_state.latents = block_state.latents.to(latents_dtype)
197
+
198
+ return components, block_state
199
+
200
+
201
+ class HunyuanVideo15DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
202
+ model_name = "hunyuan-video-1.5"
203
+
204
+ @property
205
+ def description(self) -> str:
206
+ return "Pipeline block that iteratively denoises the latents over timesteps"
207
+
208
+ @property
209
+ def loop_expected_components(self) -> list[ComponentSpec]:
210
+ return [
211
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
212
+ ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
213
+ ]
214
+
215
+ @property
216
+ def loop_inputs(self) -> list[InputParam]:
217
+ return [
218
+ InputParam.template("timesteps", required=True),
219
+ InputParam.template("num_inference_steps", required=True, default=None),
220
+ ]
221
+
222
+ @torch.no_grad()
223
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
224
+ block_state = self.get_block_state(state)
225
+
226
+ block_state.num_warmup_steps = max(
227
+ len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
228
+ )
229
+
230
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
231
+ for i, t in enumerate(block_state.timesteps):
232
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
233
+ if i == len(block_state.timesteps) - 1 or (
234
+ (i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
235
+ ):
236
+ progress_bar.update()
237
+
238
+ self.set_block_state(state, block_state)
239
+ return components, state
240
+
241
+
242
+ class HunyuanVideo15DenoiseStep(HunyuanVideo15DenoiseLoopWrapper):
243
+ block_classes = [
244
+ HunyuanVideo15LoopBeforeDenoiser,
245
+ HunyuanVideo15LoopDenoiser(),
246
+ HunyuanVideo15LoopAfterDenoiser,
247
+ ]
248
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
249
+
250
+ @property
251
+ def description(self) -> str:
252
+ return (
253
+ "Denoise step that iteratively denoises the latents.\n"
254
+ "At each iteration:\n"
255
+ " - `HunyuanVideo15LoopBeforeDenoiser`\n"
256
+ " - `HunyuanVideo15LoopDenoiser`\n"
257
+ " - `HunyuanVideo15LoopAfterDenoiser`\n"
258
+ "This block supports text-to-video tasks."
259
+ )
260
+
261
+
262
+ class HunyuanVideo15Image2VideoLoopDenoiser(ModularPipelineBlocks):
263
+ model_name = "hunyuan-video-1.5"
264
+
265
+ def __init__(self, guider_input_fields=None):
266
+ if guider_input_fields is None:
267
+ guider_input_fields = {
268
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
269
+ "encoder_attention_mask": ("prompt_embeds_mask", "negative_prompt_embeds_mask"),
270
+ "encoder_hidden_states_2": ("prompt_embeds_2", "negative_prompt_embeds_2"),
271
+ "encoder_attention_mask_2": ("prompt_embeds_mask_2", "negative_prompt_embeds_mask_2"),
272
+ }
273
+ if not isinstance(guider_input_fields, dict):
274
+ raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
275
+ self._guider_input_fields = guider_input_fields
276
+ super().__init__()
277
+
278
+ @property
279
+ def expected_components(self) -> list[ComponentSpec]:
280
+ return [
281
+ ComponentSpec(
282
+ "guider",
283
+ ClassifierFreeGuidance,
284
+ config=FrozenDict({"guidance_scale": 7.5}),
285
+ default_creation_method="from_config",
286
+ ),
287
+ ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
288
+ ]
289
+
290
+ @property
291
+ def description(self) -> str:
292
+ return "I2V denoiser with MeanFlow timestep_r support"
293
+
294
+ @property
295
+ def inputs(self) -> list[InputParam]:
296
+ inputs = [
297
+ InputParam.template("attention_kwargs"),
298
+ InputParam.template("num_inference_steps", required=True, default=None),
299
+ InputParam(
300
+ "image_embeds",
301
+ type_hint=torch.Tensor,
302
+ description="Siglip image embeddings used as extra conditioning for I2V. Zero-filled for T2V.",
303
+ ),
304
+ InputParam.template("timesteps", required=True),
305
+ ]
306
+ for value in self._guider_input_fields.values():
307
+ if isinstance(value, tuple):
308
+ inputs.append(
309
+ InputParam(
310
+ name=value[0],
311
+ required=True,
312
+ type_hint=torch.Tensor,
313
+ description=f"Positive branch of the {value[0]!r} field fed into the guider.",
314
+ )
315
+ )
316
+ for neg_name in value[1:]:
317
+ inputs.append(
318
+ InputParam(
319
+ name=neg_name,
320
+ type_hint=torch.Tensor,
321
+ description=f"Negative branch of the {neg_name!r} field fed into the guider.",
322
+ )
323
+ )
324
+ else:
325
+ inputs.append(
326
+ InputParam(
327
+ name=value,
328
+ required=True,
329
+ type_hint=torch.Tensor,
330
+ description=f"{value!r} field fed into the guider.",
331
+ )
332
+ )
333
+ return inputs
334
+
335
+ @torch.no_grad()
336
+ def __call__(
337
+ self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
338
+ ) -> PipelineState:
339
+ timestep = t.expand(block_state.latent_model_input.shape[0]).to(block_state.latent_model_input.dtype)
340
+
341
+ # MeanFlow timestep_r (lines 855-862)
342
+ if components.transformer.config.use_meanflow:
343
+ if i == len(block_state.timesteps) - 1:
344
+ timestep_r = torch.tensor([0.0], device=timestep.device)
345
+ else:
346
+ timestep_r = block_state.timesteps[i + 1]
347
+ timestep_r = timestep_r.expand(block_state.latents.shape[0]).to(block_state.latents.dtype)
348
+ else:
349
+ timestep_r = None
350
+
351
+ guider_inputs = {
352
+ input_name: tuple(getattr(block_state, v) for v in value)
353
+ if isinstance(value, tuple)
354
+ else getattr(block_state, value)
355
+ for input_name, value in self._guider_input_fields.items()
356
+ }
357
+
358
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
359
+ guider_state = components.guider.prepare_inputs(guider_inputs)
360
+
361
+ for guider_state_batch in guider_state:
362
+ components.guider.prepare_models(components.transformer)
363
+
364
+ cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
365
+
366
+ context_name = getattr(guider_state_batch, components.guider._identifier_key)
367
+ with components.transformer.cache_context(context_name):
368
+ guider_state_batch.noise_pred = components.transformer(
369
+ hidden_states=block_state.latent_model_input,
370
+ image_embeds=block_state.image_embeds,
371
+ timestep=timestep,
372
+ timestep_r=timestep_r,
373
+ attention_kwargs=block_state.attention_kwargs,
374
+ return_dict=False,
375
+ **cond_kwargs,
376
+ )[0]
377
+
378
+ components.guider.cleanup_models(components.transformer)
379
+
380
+ block_state.noise_pred = components.guider(guider_state)[0]
381
+
382
+ return components, block_state
383
+
384
+
385
+ class HunyuanVideo15Image2VideoDenoiseStep(HunyuanVideo15DenoiseLoopWrapper):
386
+ block_classes = [
387
+ HunyuanVideo15LoopBeforeDenoiser,
388
+ HunyuanVideo15Image2VideoLoopDenoiser(),
389
+ HunyuanVideo15LoopAfterDenoiser,
390
+ ]
391
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
392
+
393
+ @property
394
+ def description(self) -> str:
395
+ return (
396
+ "Denoise step for image-to-video with MeanFlow support.\n"
397
+ "At each iteration:\n"
398
+ " - `HunyuanVideo15LoopBeforeDenoiser`\n"
399
+ " - `HunyuanVideo15Image2VideoLoopDenoiser`\n"
400
+ " - `HunyuanVideo15LoopAfterDenoiser`"
401
+ )
diffusers/modular_pipelines/hunyuan_video1_5/encoders.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import re
16
+
17
+ import torch
18
+ from transformers import (
19
+ ByT5Tokenizer,
20
+ Qwen2_5_VLTextModel,
21
+ Qwen2TokenizerFast,
22
+ SiglipImageProcessor,
23
+ SiglipVisionModel,
24
+ T5EncoderModel,
25
+ )
26
+
27
+ from ...configuration_utils import FrozenDict
28
+ from ...guiders import ClassifierFreeGuidance
29
+ from ...models import AutoencoderKLHunyuanVideo15
30
+ from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
31
+ from ...utils import logging
32
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
33
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
34
+ from .modular_pipeline import HunyuanVideo15ModularPipeline
35
+
36
+
37
+ logger = logging.get_logger(__name__)
38
+
39
+
40
+ def format_text_input(prompt, system_message):
41
+ return [
42
+ [{"role": "system", "content": system_message}, {"role": "user", "content": p if p else " "}] for p in prompt
43
+ ]
44
+
45
+
46
+ def extract_glyph_texts(prompt):
47
+ pattern = r"\"(.*?)\"|\"(.*?)\""
48
+ matches = re.findall(pattern, prompt)
49
+ result = [match[0] or match[1] for match in matches]
50
+ result = list(dict.fromkeys(result)) if len(result) > 1 else result
51
+ if result:
52
+ formatted_result = ". ".join([f'Text "{text}"' for text in result]) + ". "
53
+ else:
54
+ formatted_result = None
55
+ return formatted_result
56
+
57
+
58
+ def _get_mllm_prompt_embeds(
59
+ text_encoder,
60
+ tokenizer,
61
+ prompt,
62
+ device,
63
+ tokenizer_max_length=1000,
64
+ num_hidden_layers_to_skip=2,
65
+ # fmt: off
66
+ system_message="You are a helpful assistant. Describe the video by detailing the following aspects: \
67
+ 1. The main content and theme of the video. \
68
+ 2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects. \
69
+ 3. Actions, events, behaviors temporal relationships, physical movement changes of the objects. \
70
+ 4. background environment, light, style and atmosphere. \
71
+ 5. camera angles, movements, and transitions used in the video.",
72
+ # fmt: on
73
+ crop_start=108,
74
+ ):
75
+ prompt = [prompt] if isinstance(prompt, str) else prompt
76
+ prompt = format_text_input(prompt, system_message)
77
+
78
+ text_inputs = tokenizer.apply_chat_template(
79
+ prompt,
80
+ add_generation_prompt=True,
81
+ tokenize=True,
82
+ return_dict=True,
83
+ padding="max_length",
84
+ max_length=tokenizer_max_length + crop_start,
85
+ truncation=True,
86
+ return_tensors="pt",
87
+ )
88
+
89
+ text_input_ids = text_inputs.input_ids.to(device=device)
90
+ prompt_attention_mask = text_inputs.attention_mask.to(device=device)
91
+
92
+ prompt_embeds = text_encoder(
93
+ input_ids=text_input_ids,
94
+ attention_mask=prompt_attention_mask,
95
+ output_hidden_states=True,
96
+ ).hidden_states[-(num_hidden_layers_to_skip + 1)]
97
+
98
+ if crop_start is not None and crop_start > 0:
99
+ prompt_embeds = prompt_embeds[:, crop_start:]
100
+ prompt_attention_mask = prompt_attention_mask[:, crop_start:]
101
+
102
+ return prompt_embeds, prompt_attention_mask
103
+
104
+
105
+ def _get_byt5_prompt_embeds(tokenizer, text_encoder, prompt, device, tokenizer_max_length=256):
106
+ prompt = [prompt] if isinstance(prompt, str) else prompt
107
+ glyph_texts = [extract_glyph_texts(p) for p in prompt]
108
+
109
+ prompt_embeds_list = []
110
+ prompt_embeds_mask_list = []
111
+
112
+ for glyph_text in glyph_texts:
113
+ if glyph_text is None:
114
+ glyph_text_embeds = torch.zeros(
115
+ (1, tokenizer_max_length, text_encoder.config.d_model), device=device, dtype=text_encoder.dtype
116
+ )
117
+ glyph_text_embeds_mask = torch.zeros((1, tokenizer_max_length), device=device, dtype=torch.int64)
118
+ else:
119
+ txt_tokens = tokenizer(
120
+ glyph_text,
121
+ padding="max_length",
122
+ max_length=tokenizer_max_length,
123
+ truncation=True,
124
+ add_special_tokens=True,
125
+ return_tensors="pt",
126
+ ).to(device)
127
+
128
+ glyph_text_embeds = text_encoder(
129
+ input_ids=txt_tokens.input_ids,
130
+ attention_mask=txt_tokens.attention_mask.float(),
131
+ )[0]
132
+ glyph_text_embeds = glyph_text_embeds.to(device=device)
133
+ glyph_text_embeds_mask = txt_tokens.attention_mask.to(device=device)
134
+
135
+ prompt_embeds_list.append(glyph_text_embeds)
136
+ prompt_embeds_mask_list.append(glyph_text_embeds_mask)
137
+
138
+ return torch.cat(prompt_embeds_list, dim=0), torch.cat(prompt_embeds_mask_list, dim=0)
139
+
140
+
141
+ class HunyuanVideo15TextEncoderStep(ModularPipelineBlocks):
142
+ model_name = "hunyuan-video-1.5"
143
+
144
+ @property
145
+ def description(self) -> str:
146
+ return "Dual text encoder step using Qwen2.5-VL (MLLM) and ByT5 (glyph text)"
147
+
148
+ @property
149
+ def expected_components(self) -> list[ComponentSpec]:
150
+ return [
151
+ ComponentSpec("text_encoder", Qwen2_5_VLTextModel),
152
+ ComponentSpec("tokenizer", Qwen2TokenizerFast),
153
+ ComponentSpec("text_encoder_2", T5EncoderModel),
154
+ ComponentSpec("tokenizer_2", ByT5Tokenizer),
155
+ ComponentSpec(
156
+ "guider",
157
+ ClassifierFreeGuidance,
158
+ config=FrozenDict({"guidance_scale": 7.5}),
159
+ default_creation_method="from_config",
160
+ ),
161
+ ]
162
+
163
+ @property
164
+ def inputs(self) -> list[InputParam]:
165
+ return [
166
+ InputParam.template("prompt", required=False),
167
+ InputParam.template("negative_prompt"),
168
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
169
+ ]
170
+
171
+ @property
172
+ def intermediate_outputs(self) -> list[OutputParam]:
173
+ return [
174
+ OutputParam.template("prompt_embeds"),
175
+ OutputParam.template("prompt_embeds_mask"),
176
+ OutputParam.template("negative_prompt_embeds"),
177
+ OutputParam.template("negative_prompt_embeds_mask"),
178
+ OutputParam(
179
+ "prompt_embeds_2",
180
+ type_hint=torch.Tensor,
181
+ kwargs_type="denoiser_input_fields",
182
+ description="ByT5 glyph-text embeddings used as a second conditioning stream for the transformer.",
183
+ ),
184
+ OutputParam(
185
+ "prompt_embeds_mask_2",
186
+ type_hint=torch.Tensor,
187
+ kwargs_type="denoiser_input_fields",
188
+ description="Attention mask for the ByT5 glyph-text embeddings.",
189
+ ),
190
+ OutputParam(
191
+ "negative_prompt_embeds_2",
192
+ type_hint=torch.Tensor,
193
+ kwargs_type="denoiser_input_fields",
194
+ description="ByT5 glyph-text negative embeddings for classifier-free guidance.",
195
+ ),
196
+ OutputParam(
197
+ "negative_prompt_embeds_mask_2",
198
+ type_hint=torch.Tensor,
199
+ kwargs_type="denoiser_input_fields",
200
+ description="Attention mask for the ByT5 glyph-text negative embeddings.",
201
+ ),
202
+ ]
203
+
204
+ @staticmethod
205
+ def encode_prompt(
206
+ components,
207
+ prompt,
208
+ device=None,
209
+ dtype=None,
210
+ batch_size=1,
211
+ num_videos_per_prompt=1,
212
+ ):
213
+ device = device or components._execution_device
214
+ dtype = dtype or components.text_encoder.dtype
215
+
216
+ if prompt is None:
217
+ prompt = [""] * batch_size
218
+ prompt = [prompt] if isinstance(prompt, str) else prompt
219
+
220
+ prompt_embeds, prompt_embeds_mask = _get_mllm_prompt_embeds(
221
+ tokenizer=components.tokenizer,
222
+ text_encoder=components.text_encoder,
223
+ prompt=prompt,
224
+ device=device,
225
+ tokenizer_max_length=components.tokenizer_max_length,
226
+ system_message=components.system_message,
227
+ crop_start=components.prompt_template_encode_start_idx,
228
+ )
229
+
230
+ prompt_embeds_2, prompt_embeds_mask_2 = _get_byt5_prompt_embeds(
231
+ tokenizer=components.tokenizer_2,
232
+ text_encoder=components.text_encoder_2,
233
+ prompt=prompt,
234
+ device=device,
235
+ tokenizer_max_length=components.tokenizer_2_max_length,
236
+ )
237
+
238
+ _, seq_len, _ = prompt_embeds.shape
239
+ prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1).view(
240
+ batch_size * num_videos_per_prompt, seq_len, -1
241
+ )
242
+ prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_videos_per_prompt, 1).view(
243
+ batch_size * num_videos_per_prompt, seq_len
244
+ )
245
+
246
+ _, seq_len_2, _ = prompt_embeds_2.shape
247
+ prompt_embeds_2 = prompt_embeds_2.repeat(1, num_videos_per_prompt, 1).view(
248
+ batch_size * num_videos_per_prompt, seq_len_2, -1
249
+ )
250
+ prompt_embeds_mask_2 = prompt_embeds_mask_2.repeat(1, num_videos_per_prompt, 1).view(
251
+ batch_size * num_videos_per_prompt, seq_len_2
252
+ )
253
+
254
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
255
+ prompt_embeds_mask = prompt_embeds_mask.to(dtype=dtype, device=device)
256
+ prompt_embeds_2 = prompt_embeds_2.to(dtype=dtype, device=device)
257
+ prompt_embeds_mask_2 = prompt_embeds_mask_2.to(dtype=dtype, device=device)
258
+
259
+ return prompt_embeds, prompt_embeds_mask, prompt_embeds_2, prompt_embeds_mask_2
260
+
261
+ @torch.no_grad()
262
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
263
+ block_state = self.get_block_state(state)
264
+ device = components._execution_device
265
+ dtype = components.transformer.dtype
266
+
267
+ prompt = block_state.prompt
268
+ negative_prompt = block_state.negative_prompt
269
+ num_videos_per_prompt = block_state.num_videos_per_prompt
270
+
271
+ if prompt is not None and isinstance(prompt, str):
272
+ batch_size = 1
273
+ elif prompt is not None and isinstance(prompt, list):
274
+ batch_size = len(prompt)
275
+ else:
276
+ batch_size = 1
277
+
278
+ (
279
+ block_state.prompt_embeds,
280
+ block_state.prompt_embeds_mask,
281
+ block_state.prompt_embeds_2,
282
+ block_state.prompt_embeds_mask_2,
283
+ ) = self.encode_prompt(
284
+ components,
285
+ prompt=prompt,
286
+ device=device,
287
+ dtype=dtype,
288
+ batch_size=batch_size,
289
+ num_videos_per_prompt=num_videos_per_prompt,
290
+ )
291
+
292
+ if components.requires_unconditional_embeds:
293
+ (
294
+ block_state.negative_prompt_embeds,
295
+ block_state.negative_prompt_embeds_mask,
296
+ block_state.negative_prompt_embeds_2,
297
+ block_state.negative_prompt_embeds_mask_2,
298
+ ) = self.encode_prompt(
299
+ components,
300
+ prompt=negative_prompt,
301
+ device=device,
302
+ dtype=dtype,
303
+ batch_size=batch_size,
304
+ num_videos_per_prompt=num_videos_per_prompt,
305
+ )
306
+
307
+ state.set("batch_size", batch_size)
308
+
309
+ self.set_block_state(state, block_state)
310
+ return components, state
311
+
312
+
313
+ def retrieve_latents(
314
+ encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
315
+ ):
316
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
317
+ return encoder_output.latent_dist.sample(generator)
318
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
319
+ return encoder_output.latent_dist.mode()
320
+ elif hasattr(encoder_output, "latents"):
321
+ return encoder_output.latents
322
+ else:
323
+ raise AttributeError("Could not access latents of provided encoder_output")
324
+
325
+
326
+ class HunyuanVideo15VaeEncoderStep(ModularPipelineBlocks):
327
+ model_name = "hunyuan-video-1.5"
328
+
329
+ @property
330
+ def description(self) -> str:
331
+ return "VAE Encoder step that encodes an input image into latent space for image-to-video generation"
332
+
333
+ @property
334
+ def expected_components(self) -> list[ComponentSpec]:
335
+ return [
336
+ ComponentSpec("vae", AutoencoderKLHunyuanVideo15),
337
+ ComponentSpec(
338
+ "video_processor",
339
+ HunyuanVideo15ImageProcessor,
340
+ config=FrozenDict({"vae_scale_factor": 16}),
341
+ default_creation_method="from_config",
342
+ ),
343
+ ]
344
+
345
+ @property
346
+ def inputs(self) -> list[InputParam]:
347
+ return [
348
+ InputParam.template("image", required=True),
349
+ InputParam.template("height"),
350
+ InputParam.template("width"),
351
+ ]
352
+
353
+ @property
354
+ def intermediate_outputs(self) -> list[OutputParam]:
355
+ return [
356
+ OutputParam(
357
+ "image_latents",
358
+ type_hint=torch.Tensor,
359
+ description="Encoded image latents from the VAE encoder",
360
+ ),
361
+ OutputParam("height", type_hint=int, description="Target height resolved from image"),
362
+ OutputParam("width", type_hint=int, description="Target width resolved from image"),
363
+ ]
364
+
365
+ @torch.no_grad()
366
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
367
+ block_state = self.get_block_state(state)
368
+ device = components._execution_device
369
+
370
+ image = block_state.image
371
+ height = block_state.height
372
+ width = block_state.width
373
+ if height is None or width is None:
374
+ height, width = components.video_processor.calculate_default_height_width(
375
+ height=image.size[1], width=image.size[0], target_size=components.target_size
376
+ )
377
+ image = components.video_processor.resize(image, height=height, width=width, resize_mode="crop")
378
+
379
+ vae_dtype = components.vae.dtype
380
+ image_tensor = components.video_processor.preprocess(image, height=height, width=width).to(
381
+ device=device, dtype=vae_dtype
382
+ )
383
+ image_tensor = image_tensor.unsqueeze(2)
384
+ image_latents = retrieve_latents(components.vae.encode(image_tensor), sample_mode="argmax")
385
+ image_latents = image_latents * components.vae.config.scaling_factor
386
+
387
+ block_state.image_latents = image_latents
388
+ block_state.height = height
389
+ block_state.width = width
390
+ state.set("image", image)
391
+
392
+ self.set_block_state(state, block_state)
393
+ return components, state
394
+
395
+
396
+ class HunyuanVideo15ImageEncoderStep(ModularPipelineBlocks):
397
+ model_name = "hunyuan-video-1.5"
398
+
399
+ @property
400
+ def description(self) -> str:
401
+ return "Siglip image encoder step that produces image_embeds for image-to-video generation"
402
+
403
+ @property
404
+ def expected_components(self) -> list[ComponentSpec]:
405
+ return [
406
+ ComponentSpec("image_encoder", SiglipVisionModel),
407
+ ComponentSpec("feature_extractor", SiglipImageProcessor),
408
+ ]
409
+
410
+ @property
411
+ def inputs(self) -> list[InputParam]:
412
+ return [
413
+ InputParam.template("image", required=True),
414
+ ]
415
+
416
+ @property
417
+ def intermediate_outputs(self) -> list[OutputParam]:
418
+ return [
419
+ OutputParam(
420
+ "image_embeds",
421
+ type_hint=torch.Tensor,
422
+ description="Image embeddings from the Siglip vision encoder",
423
+ ),
424
+ ]
425
+
426
+ @torch.no_grad()
427
+ def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
428
+ block_state = self.get_block_state(state)
429
+ device = components._execution_device
430
+
431
+ image_encoder_dtype = next(components.image_encoder.parameters()).dtype
432
+ image_inputs = components.feature_extractor.preprocess(
433
+ images=block_state.image, do_resize=True, return_tensors="pt", do_convert_rgb=True
434
+ )
435
+ image_inputs = image_inputs.to(device=device, dtype=image_encoder_dtype)
436
+ image_embeds = components.image_encoder(**image_inputs).last_hidden_state
437
+
438
+ block_state.image_embeds = image_embeds
439
+
440
+ self.set_block_state(state, block_state)
441
+ return components, state
diffusers/modular_pipelines/hunyuan_video1_5/modular_blocks_hunyuan_video1_5.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ...utils import logging
16
+ from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
17
+ from ..modular_pipeline_utils import OutputParam
18
+ from .before_denoise import (
19
+ HunyuanVideo15Image2VideoPrepareLatentsStep,
20
+ HunyuanVideo15PrepareLatentsStep,
21
+ HunyuanVideo15SetTimestepsStep,
22
+ HunyuanVideo15TextInputStep,
23
+ )
24
+ from .decoders import HunyuanVideo15VaeDecoderStep
25
+ from .denoise import HunyuanVideo15DenoiseStep, HunyuanVideo15Image2VideoDenoiseStep
26
+ from .encoders import (
27
+ HunyuanVideo15ImageEncoderStep,
28
+ HunyuanVideo15TextEncoderStep,
29
+ HunyuanVideo15VaeEncoderStep,
30
+ )
31
+
32
+
33
+ logger = logging.get_logger(__name__)
34
+
35
+
36
+ # auto_docstring
37
+ class HunyuanVideo15CoreDenoiseStep(SequentialPipelineBlocks):
38
+ """
39
+ Denoise block that takes encoded conditions and runs the denoising process.
40
+
41
+ Components:
42
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
43
+ video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
44
+
45
+ Inputs:
46
+ prompt_embeds (`Tensor`):
47
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
48
+ batch_size (`int`, *optional*):
49
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
50
+ be generated in input step.
51
+ num_inference_steps (`int`, *optional*, defaults to 50):
52
+ The number of denoising steps.
53
+ sigmas (`list`, *optional*):
54
+ Custom sigmas for the denoising process.
55
+ height (`int`, *optional*):
56
+ The height in pixels of the generated image.
57
+ width (`int`, *optional*):
58
+ The width in pixels of the generated image.
59
+ num_frames (`int`, *optional*, defaults to 121):
60
+ Number of video frames to generate.
61
+ latents (`Tensor`, *optional*):
62
+ Pre-generated noisy latents for image generation.
63
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
64
+ The number of images to generate per prompt.
65
+ generator (`Generator`, *optional*):
66
+ Torch generator for deterministic generation.
67
+ attention_kwargs (`dict`, *optional*):
68
+ Additional kwargs for attention processors.
69
+ negative_prompt_embeds (`Tensor`, *optional*):
70
+ Negative branch of the 'negative_prompt_embeds' field fed into the guider.
71
+ prompt_embeds_mask (`Tensor`):
72
+ Positive branch of the 'prompt_embeds_mask' field fed into the guider.
73
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
74
+ Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
75
+ prompt_embeds_2 (`Tensor`):
76
+ Positive branch of the 'prompt_embeds_2' field fed into the guider.
77
+ negative_prompt_embeds_2 (`Tensor`, *optional*):
78
+ Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
79
+ prompt_embeds_mask_2 (`Tensor`):
80
+ Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
81
+ negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
82
+ Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
83
+
84
+ Outputs:
85
+ latents (`Tensor`):
86
+ Denoised latents.
87
+ """
88
+
89
+ model_name = "hunyuan-video-1.5"
90
+ block_classes = [
91
+ HunyuanVideo15TextInputStep,
92
+ HunyuanVideo15SetTimestepsStep,
93
+ HunyuanVideo15PrepareLatentsStep,
94
+ HunyuanVideo15DenoiseStep,
95
+ ]
96
+ block_names = ["input", "set_timesteps", "prepare_latents", "denoise"]
97
+
98
+ @property
99
+ def description(self):
100
+ return "Denoise block that takes encoded conditions and runs the denoising process."
101
+
102
+ @property
103
+ def outputs(self):
104
+ return [OutputParam.template("latents")]
105
+
106
+
107
+ # auto_docstring
108
+ class HunyuanVideo15Blocks(SequentialPipelineBlocks):
109
+ """
110
+ Modular pipeline blocks for HunyuanVideo 1.5 text-to-video.
111
+
112
+ Components:
113
+ text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
114
+ tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) scheduler (`FlowMatchEulerDiscreteScheduler`)
115
+ transformer (`HunyuanVideo15Transformer3DModel`) video_processor (`HunyuanVideo15ImageProcessor`) vae
116
+ (`AutoencoderKLHunyuanVideo15`)
117
+
118
+ Inputs:
119
+ prompt (`str`, *optional*):
120
+ The prompt or prompts to guide image generation.
121
+ negative_prompt (`str`, *optional*):
122
+ The prompt or prompts not to guide the image generation.
123
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
124
+ The number of images to generate per prompt.
125
+ batch_size (`int`, *optional*):
126
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
127
+ be generated in input step.
128
+ num_inference_steps (`int`, *optional*, defaults to 50):
129
+ The number of denoising steps.
130
+ sigmas (`list`, *optional*):
131
+ Custom sigmas for the denoising process.
132
+ height (`int`, *optional*):
133
+ The height in pixels of the generated image.
134
+ width (`int`, *optional*):
135
+ The width in pixels of the generated image.
136
+ num_frames (`int`, *optional*, defaults to 121):
137
+ Number of video frames to generate.
138
+ latents (`Tensor`, *optional*):
139
+ Pre-generated noisy latents for image generation.
140
+ generator (`Generator`, *optional*):
141
+ Torch generator for deterministic generation.
142
+ attention_kwargs (`dict`, *optional*):
143
+ Additional kwargs for attention processors.
144
+ output_type (`str`, *optional*, defaults to np):
145
+ Output format: 'pil', 'np', 'pt'.
146
+
147
+ Outputs:
148
+ videos (`list`):
149
+ The generated videos.
150
+ """
151
+
152
+ model_name = "hunyuan-video-1.5"
153
+ block_classes = [
154
+ HunyuanVideo15TextEncoderStep,
155
+ HunyuanVideo15CoreDenoiseStep,
156
+ HunyuanVideo15VaeDecoderStep,
157
+ ]
158
+ block_names = ["text_encoder", "denoise", "decode"]
159
+
160
+ @property
161
+ def description(self):
162
+ return "Modular pipeline blocks for HunyuanVideo 1.5 text-to-video."
163
+
164
+ @property
165
+ def outputs(self):
166
+ return [OutputParam.template("videos")]
167
+
168
+
169
+ # auto_docstring
170
+ class HunyuanVideo15Image2VideoCoreDenoiseStep(SequentialPipelineBlocks):
171
+ """
172
+ Denoise block for image-to-video that takes encoded conditions and runs the denoising process.
173
+
174
+ Components:
175
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
176
+ video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
177
+
178
+ Inputs:
179
+ prompt_embeds (`Tensor`):
180
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
181
+ batch_size (`int`, *optional*):
182
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
183
+ be generated in input step.
184
+ num_inference_steps (`int`, *optional*, defaults to 50):
185
+ The number of denoising steps.
186
+ sigmas (`list`, *optional*):
187
+ Custom sigmas for the denoising process.
188
+ height (`int`, *optional*):
189
+ The height in pixels of the generated image.
190
+ width (`int`, *optional*):
191
+ The width in pixels of the generated image.
192
+ num_frames (`int`, *optional*, defaults to 121):
193
+ Number of video frames to generate.
194
+ latents (`Tensor`, *optional*):
195
+ Pre-generated noisy latents for image generation.
196
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
197
+ The number of images to generate per prompt.
198
+ generator (`Generator`, *optional*):
199
+ Torch generator for deterministic generation.
200
+ image_latents (`Tensor`):
201
+ Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
202
+ attention_kwargs (`dict`, *optional*):
203
+ Additional kwargs for attention processors.
204
+ negative_prompt_embeds (`Tensor`, *optional*):
205
+ Negative branch of the 'negative_prompt_embeds' field fed into the guider.
206
+ prompt_embeds_mask (`Tensor`):
207
+ Positive branch of the 'prompt_embeds_mask' field fed into the guider.
208
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
209
+ Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
210
+ prompt_embeds_2 (`Tensor`):
211
+ Positive branch of the 'prompt_embeds_2' field fed into the guider.
212
+ negative_prompt_embeds_2 (`Tensor`, *optional*):
213
+ Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
214
+ prompt_embeds_mask_2 (`Tensor`):
215
+ Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
216
+ negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
217
+ Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
218
+
219
+ Outputs:
220
+ latents (`Tensor`):
221
+ Denoised latents.
222
+ """
223
+
224
+ model_name = "hunyuan-video-1.5"
225
+ block_classes = [
226
+ HunyuanVideo15TextInputStep,
227
+ HunyuanVideo15SetTimestepsStep,
228
+ HunyuanVideo15PrepareLatentsStep,
229
+ HunyuanVideo15Image2VideoPrepareLatentsStep,
230
+ HunyuanVideo15Image2VideoDenoiseStep,
231
+ ]
232
+ block_names = ["input", "set_timesteps", "prepare_latents", "prepare_i2v_latents", "denoise"]
233
+
234
+ @property
235
+ def description(self):
236
+ return "Denoise block for image-to-video that takes encoded conditions and runs the denoising process."
237
+
238
+ @property
239
+ def outputs(self):
240
+ return [OutputParam.template("latents")]
241
+
242
+
243
+ # auto_docstring
244
+ class HunyuanVideo15AutoVaeEncoderStep(AutoPipelineBlocks):
245
+ """
246
+ VAE encoder step that encodes the image input into its latent representation.
247
+ This is an auto pipeline block that works for image-to-video tasks.
248
+ - `HunyuanVideo15VaeEncoderStep` is used when `image` is provided.
249
+ - If `image` is not provided, step will be skipped.
250
+
251
+ Components:
252
+ vae (`AutoencoderKLHunyuanVideo15`) video_processor (`HunyuanVideo15ImageProcessor`)
253
+
254
+ Inputs:
255
+ image (`Image | list`, *optional*):
256
+ Reference image(s) for denoising. Can be a single image or list of images.
257
+ height (`int`, *optional*):
258
+ The height in pixels of the generated image.
259
+ width (`int`, *optional*):
260
+ The width in pixels of the generated image.
261
+
262
+ Outputs:
263
+ image_latents (`Tensor`):
264
+ Encoded image latents from the VAE encoder
265
+ height (`int`):
266
+ Target height resolved from image
267
+ width (`int`):
268
+ Target width resolved from image
269
+ """
270
+
271
+ model_name = "hunyuan-video-1.5"
272
+ block_classes = [HunyuanVideo15VaeEncoderStep]
273
+ block_names = ["vae_encoder"]
274
+ block_trigger_inputs = ["image"]
275
+
276
+ @property
277
+ def description(self):
278
+ return (
279
+ "VAE encoder step that encodes the image input into its latent representation.\n"
280
+ "This is an auto pipeline block that works for image-to-video tasks.\n"
281
+ " - `HunyuanVideo15VaeEncoderStep` is used when `image` is provided.\n"
282
+ " - If `image` is not provided, step will be skipped."
283
+ )
284
+
285
+
286
+ # auto_docstring
287
+ class HunyuanVideo15AutoImageEncoderStep(AutoPipelineBlocks):
288
+ """
289
+ Siglip image encoder step that produces image_embeds.
290
+ This is an auto pipeline block that works for image-to-video tasks.
291
+ - `HunyuanVideo15ImageEncoderStep` is used when `image` is provided.
292
+ - If `image` is not provided, step will be skipped.
293
+
294
+ Components:
295
+ image_encoder (`SiglipVisionModel`) feature_extractor (`SiglipImageProcessor`)
296
+
297
+ Inputs:
298
+ image (`Image | list`, *optional*):
299
+ Reference image(s) for denoising. Can be a single image or list of images.
300
+
301
+ Outputs:
302
+ image_embeds (`Tensor`):
303
+ Image embeddings from the Siglip vision encoder
304
+ """
305
+
306
+ model_name = "hunyuan-video-1.5"
307
+ block_classes = [HunyuanVideo15ImageEncoderStep]
308
+ block_names = ["image_encoder"]
309
+ block_trigger_inputs = ["image"]
310
+
311
+ @property
312
+ def description(self):
313
+ return (
314
+ "Siglip image encoder step that produces image_embeds.\n"
315
+ "This is an auto pipeline block that works for image-to-video tasks.\n"
316
+ " - `HunyuanVideo15ImageEncoderStep` is used when `image` is provided.\n"
317
+ " - If `image` is not provided, step will be skipped."
318
+ )
319
+
320
+
321
+ # auto_docstring
322
+ class HunyuanVideo15AutoCoreDenoiseStep(AutoPipelineBlocks):
323
+ """
324
+ Auto denoise block that selects the appropriate denoise pipeline based on inputs.
325
+ - `HunyuanVideo15Image2VideoCoreDenoiseStep` is used when `image_latents` is provided.
326
+ - `HunyuanVideo15CoreDenoiseStep` is used otherwise (text-to-video).
327
+
328
+ Components:
329
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
330
+ video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
331
+
332
+ Inputs:
333
+ prompt_embeds (`Tensor`):
334
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
335
+ batch_size (`int`):
336
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
337
+ be generated in input step.
338
+ num_inference_steps (`int`):
339
+ The number of denoising steps.
340
+ sigmas (`list`, *optional*):
341
+ Custom sigmas for the denoising process.
342
+ height (`int`, *optional*):
343
+ The height in pixels of the generated image.
344
+ width (`int`, *optional*):
345
+ The width in pixels of the generated image.
346
+ num_frames (`int`, *optional*, defaults to 121):
347
+ Number of video frames to generate.
348
+ latents (`Tensor`):
349
+ Pre-generated noisy latents for image generation.
350
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
351
+ The number of images to generate per prompt.
352
+ generator (`Generator`, *optional*):
353
+ Torch generator for deterministic generation.
354
+ image_latents (`Tensor`, *optional*):
355
+ Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
356
+ attention_kwargs (`dict`, *optional*):
357
+ Additional kwargs for attention processors.
358
+ negative_prompt_embeds (`Tensor`, *optional*):
359
+ Negative branch of the 'negative_prompt_embeds' field fed into the guider.
360
+ prompt_embeds_mask (`Tensor`):
361
+ Positive branch of the 'prompt_embeds_mask' field fed into the guider.
362
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
363
+ Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
364
+ prompt_embeds_2 (`Tensor`):
365
+ Positive branch of the 'prompt_embeds_2' field fed into the guider.
366
+ negative_prompt_embeds_2 (`Tensor`, *optional*):
367
+ Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
368
+ prompt_embeds_mask_2 (`Tensor`):
369
+ Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
370
+ negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
371
+ Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
372
+
373
+ Outputs:
374
+ latents (`Tensor`):
375
+ Denoised latents.
376
+ """
377
+
378
+ model_name = "hunyuan-video-1.5"
379
+ block_classes = [HunyuanVideo15Image2VideoCoreDenoiseStep, HunyuanVideo15CoreDenoiseStep]
380
+ block_names = ["image2video", "text2video"]
381
+ block_trigger_inputs = ["image_latents", None]
382
+
383
+ @property
384
+ def description(self):
385
+ return (
386
+ "Auto denoise block that selects the appropriate denoise pipeline based on inputs.\n"
387
+ " - `HunyuanVideo15Image2VideoCoreDenoiseStep` is used when `image_latents` is provided.\n"
388
+ " - `HunyuanVideo15CoreDenoiseStep` is used otherwise (text-to-video)."
389
+ )
390
+
391
+
392
+ # auto_docstring
393
+ class HunyuanVideo15AutoBlocks(SequentialPipelineBlocks):
394
+ """
395
+ Auto blocks for HunyuanVideo 1.5 that support both text-to-video and image-to-video workflows.
396
+
397
+ Supported workflows:
398
+ - `text2video`: requires `prompt`
399
+ - `image2video`: requires `image`, `prompt`
400
+
401
+ Components:
402
+ text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
403
+ tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) vae (`AutoencoderKLHunyuanVideo15`)
404
+ video_processor (`HunyuanVideo15ImageProcessor`) image_encoder (`SiglipVisionModel`) feature_extractor
405
+ (`SiglipImageProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
406
+ (`HunyuanVideo15Transformer3DModel`)
407
+
408
+ Inputs:
409
+ prompt (`str`, *optional*):
410
+ The prompt or prompts to guide image generation.
411
+ negative_prompt (`str`, *optional*):
412
+ The prompt or prompts not to guide the image generation.
413
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
414
+ The number of images to generate per prompt.
415
+ image (`Image | list`, *optional*):
416
+ Reference image(s) for denoising. Can be a single image or list of images.
417
+ height (`int`, *optional*):
418
+ The height in pixels of the generated image.
419
+ width (`int`, *optional*):
420
+ The width in pixels of the generated image.
421
+ batch_size (`int`):
422
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
423
+ be generated in input step.
424
+ num_inference_steps (`int`):
425
+ The number of denoising steps.
426
+ sigmas (`list`, *optional*):
427
+ Custom sigmas for the denoising process.
428
+ num_frames (`int`, *optional*, defaults to 121):
429
+ Number of video frames to generate.
430
+ latents (`Tensor`):
431
+ Pre-generated noisy latents for image generation.
432
+ generator (`Generator`, *optional*):
433
+ Torch generator for deterministic generation.
434
+ image_latents (`Tensor`, *optional*):
435
+ Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
436
+ attention_kwargs (`dict`, *optional*):
437
+ Additional kwargs for attention processors.
438
+ output_type (`str`, *optional*, defaults to np):
439
+ Output format: 'pil', 'np', 'pt'.
440
+
441
+ Outputs:
442
+ videos (`list`):
443
+ The generated videos.
444
+ """
445
+
446
+ model_name = "hunyuan-video-1.5"
447
+ block_classes = [
448
+ HunyuanVideo15TextEncoderStep,
449
+ HunyuanVideo15AutoVaeEncoderStep,
450
+ HunyuanVideo15AutoImageEncoderStep,
451
+ HunyuanVideo15AutoCoreDenoiseStep,
452
+ HunyuanVideo15VaeDecoderStep,
453
+ ]
454
+ block_names = ["text_encoder", "vae_encoder", "image_encoder", "denoise", "decode"]
455
+ _workflow_map = {
456
+ "text2video": {"prompt": True},
457
+ "image2video": {"image": True, "prompt": True},
458
+ }
459
+
460
+ @property
461
+ def description(self):
462
+ return "Auto blocks for HunyuanVideo 1.5 that support both text-to-video and image-to-video workflows."
463
+
464
+ @property
465
+ def outputs(self):
466
+ return [OutputParam.template("videos")]
467
+
468
+
469
+ # auto_docstring
470
+ class HunyuanVideo15Image2VideoBlocks(SequentialPipelineBlocks):
471
+ """
472
+ Modular pipeline blocks for HunyuanVideo 1.5 image-to-video.
473
+
474
+ Components:
475
+ text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
476
+ tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) vae (`AutoencoderKLHunyuanVideo15`)
477
+ video_processor (`HunyuanVideo15ImageProcessor`) image_encoder (`SiglipVisionModel`) feature_extractor
478
+ (`SiglipImageProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
479
+ (`HunyuanVideo15Transformer3DModel`)
480
+
481
+ Inputs:
482
+ prompt (`str`, *optional*):
483
+ The prompt or prompts to guide image generation.
484
+ negative_prompt (`str`, *optional*):
485
+ The prompt or prompts not to guide the image generation.
486
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
487
+ The number of images to generate per prompt.
488
+ image (`Image | list`, *optional*):
489
+ Reference image(s) for denoising. Can be a single image or list of images.
490
+ height (`int`, *optional*):
491
+ The height in pixels of the generated image.
492
+ width (`int`, *optional*):
493
+ The width in pixels of the generated image.
494
+ batch_size (`int`, *optional*):
495
+ Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
496
+ be generated in input step.
497
+ num_inference_steps (`int`, *optional*, defaults to 50):
498
+ The number of denoising steps.
499
+ sigmas (`list`, *optional*):
500
+ Custom sigmas for the denoising process.
501
+ num_frames (`int`, *optional*, defaults to 121):
502
+ Number of video frames to generate.
503
+ latents (`Tensor`, *optional*):
504
+ Pre-generated noisy latents for image generation.
505
+ generator (`Generator`, *optional*):
506
+ Torch generator for deterministic generation.
507
+ image_latents (`Tensor`):
508
+ Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
509
+ attention_kwargs (`dict`, *optional*):
510
+ Additional kwargs for attention processors.
511
+ output_type (`str`, *optional*, defaults to np):
512
+ Output format: 'pil', 'np', 'pt'.
513
+
514
+ Outputs:
515
+ videos (`list`):
516
+ The generated videos.
517
+ """
518
+
519
+ model_name = "hunyuan-video-1.5"
520
+ block_classes = [
521
+ HunyuanVideo15TextEncoderStep,
522
+ HunyuanVideo15AutoVaeEncoderStep,
523
+ HunyuanVideo15AutoImageEncoderStep,
524
+ HunyuanVideo15Image2VideoCoreDenoiseStep,
525
+ HunyuanVideo15VaeDecoderStep,
526
+ ]
527
+ block_names = ["text_encoder", "vae_encoder", "image_encoder", "denoise", "decode"]
528
+
529
+ @property
530
+ def description(self):
531
+ return "Modular pipeline blocks for HunyuanVideo 1.5 image-to-video."
532
+
533
+ @property
534
+ def outputs(self):
535
+ return [OutputParam.template("videos")]
diffusers/modular_pipelines/hunyuan_video1_5/modular_pipeline.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ...loaders import HunyuanVideoLoraLoaderMixin
16
+ from ...utils import logging
17
+ from ..modular_pipeline import ModularPipeline
18
+
19
+
20
+ logger = logging.get_logger(__name__)
21
+
22
+
23
+ class HunyuanVideo15ModularPipeline(
24
+ ModularPipeline,
25
+ HunyuanVideoLoraLoaderMixin,
26
+ ):
27
+ """
28
+ A ModularPipeline for HunyuanVideo 1.5.
29
+
30
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
31
+ """
32
+
33
+ default_blocks_name = "HunyuanVideo15AutoBlocks"
34
+
35
+ @property
36
+ def vae_scale_factor_spatial(self):
37
+ return self.vae.spatial_compression_ratio if getattr(self, "vae", None) else 16
38
+
39
+ @property
40
+ def vae_scale_factor_temporal(self):
41
+ return self.vae.temporal_compression_ratio if getattr(self, "vae", None) else 4
42
+
43
+ @property
44
+ def num_channels_latents(self):
45
+ return self.vae.config.latent_channels if getattr(self, "vae", None) else 32
46
+
47
+ @property
48
+ def target_size(self):
49
+ return self.transformer.config.target_size if getattr(self, "transformer", None) else 640
50
+
51
+ @property
52
+ def default_aspect_ratio(self):
53
+ return (16, 9)
54
+
55
+ @property
56
+ def vision_num_semantic_tokens(self):
57
+ return 729
58
+
59
+ @property
60
+ def vision_states_dim(self):
61
+ return self.transformer.config.image_embed_dim if getattr(self, "transformer", None) else 1152
62
+
63
+ @property
64
+ def tokenizer_max_length(self):
65
+ return 1000
66
+
67
+ @property
68
+ def tokenizer_2_max_length(self):
69
+ return 256
70
+
71
+ # fmt: off
72
+ @property
73
+ def system_message(self):
74
+ return "You are a helpful assistant. Describe the video by detailing the following aspects: \
75
+ 1. The main content and theme of the video. \
76
+ 2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects. \
77
+ 3. Actions, events, behaviors temporal relationships, physical movement changes of the objects. \
78
+ 4. background environment, light, style and atmosphere. \
79
+ 5. camera angles, movements, and transitions used in the video."
80
+ # fmt: on
81
+
82
+ @property
83
+ def prompt_template_encode_start_idx(self):
84
+ return 108
85
+
86
+ @property
87
+ def requires_unconditional_embeds(self):
88
+ if hasattr(self, "guider") and self.guider is not None:
89
+ return self.guider._enabled and self.guider.num_conditions > 1
90
+ return False
diffusers/modular_pipelines/ideogram4/__init__.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_ideogram4"] = ["Ideogram4AutoBlocks"]
25
+ _import_structure["modular_pipeline"] = ["Ideogram4ModularPipeline"]
26
+
27
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
28
+ try:
29
+ if not (is_transformers_available() and is_torch_available()):
30
+ raise OptionalDependencyNotAvailable()
31
+ except OptionalDependencyNotAvailable:
32
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
33
+ else:
34
+ from .modular_blocks_ideogram4 import Ideogram4AutoBlocks
35
+ from .modular_pipeline import Ideogram4ModularPipeline
36
+ else:
37
+ import sys
38
+
39
+ sys.modules[__name__] = _LazyModule(
40
+ __name__,
41
+ globals()["__file__"],
42
+ _import_structure,
43
+ module_spec=__spec__,
44
+ )
45
+
46
+ for name, value in _dummy_objects.items():
47
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/ideogram4/before_denoise.py ADDED
@@ -0,0 +1,558 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import math
17
+
18
+ import torch
19
+
20
+ from ...models.transformers.transformer_ideogram4 import (
21
+ IMAGE_POSITION_OFFSET,
22
+ LLM_TOKEN_INDICATOR,
23
+ OUTPUT_IMAGE_INDICATOR,
24
+ SEQUENCE_PADDING_INDICATOR,
25
+ Ideogram4Transformer2DModel,
26
+ )
27
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
28
+ from ...utils import logging
29
+ from ...utils.torch_utils import randn_tensor
30
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
31
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
32
+ from .modular_pipeline import Ideogram4ModularPipeline
33
+
34
+
35
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
36
+
37
+ # Default per-step guidance schedule (length must equal `num_inference_steps`): 7.0 for the main steps,
38
+ # dropping to 3.0 for the final 3 "polish" steps.
39
+ DEFAULT_GUIDANCE_SCHEDULE = (7.0,) * 45 + (3.0,) * 3
40
+
41
+
42
+ # Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._logit_normal_sigmas
43
+ def _logit_normal_sigmas(
44
+ num_inference_steps: int,
45
+ mu: float,
46
+ std: float = 1.0,
47
+ logsnr_min: float = -15.0,
48
+ logsnr_max: float = 18.0,
49
+ device: torch.device | None = None,
50
+ ) -> torch.Tensor:
51
+ r"""
52
+ Build a length-`num_inference_steps` sigma schedule using the Ideogram4 logit-normal flow-matching schedule.
53
+
54
+ Sigmas are returned in `[0, 1]` in decreasing order (sigma close to 1 corresponds to pure noise, sigma close to 0
55
+ to clean data), matching diffusers conventions.
56
+
57
+ The Ideogram4 schedule applies `sigma(s) = 1 - logit_normal_cdf_inverse(1 - s)` to `s = linspace(0, 1, N + 1)` and
58
+ keeps the first `N` entries; a terminal zero is appended downstream by the scheduler.
59
+ """
60
+ intervals = torch.linspace(0.0, 1.0, num_inference_steps + 1, dtype=torch.float64)
61
+ # Apply the inverse CDF of a normal then push through the logistic to obtain a logit-normal CDF inverse.
62
+ z = torch.special.ndtri(intervals)
63
+ y = mu + std * z
64
+ t = 1.0 - torch.special.expit(y)
65
+ t_min = 1.0 / (1.0 + math.exp(0.5 * logsnr_max))
66
+ t_max = 1.0 / (1.0 + math.exp(0.5 * logsnr_min))
67
+ t = t.clamp(t_min, t_max)
68
+ # Convert from model time (0 = noise, 1 = data) to diffusers sigma (1 = noise, 0 = data) and reverse.
69
+ sigmas = (1.0 - t).flip(0)
70
+ # Drop the trailing 0; FlowMatchEulerDiscreteScheduler.set_timesteps appends one back internally.
71
+ sigmas = sigmas[:-1].to(dtype=torch.float32, device=device)
72
+ return sigmas
73
+
74
+
75
+ # Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._resolution_aware_mu
76
+ def _resolution_aware_mu(
77
+ height: int,
78
+ width: int,
79
+ base_mu: float,
80
+ base_resolution: tuple[int, int] = (512, 512),
81
+ ) -> float:
82
+ """Shift the schedule mean as a function of image resolution."""
83
+ num_pixels = height * width
84
+ base_pixels = base_resolution[0] * base_resolution[1]
85
+ return base_mu + 0.5 * math.log(num_pixels / base_pixels)
86
+
87
+
88
+ # Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._expand_tensor_to_effective_batch
89
+ def _expand_tensor_to_effective_batch(
90
+ tensor: torch.Tensor,
91
+ batch_size: int,
92
+ num_per_prompt: int,
93
+ tensor_name: str | None = None,
94
+ ) -> torch.Tensor:
95
+ """Replicate `tensor` along dim 0 from `batch_size` (or 1) to `batch_size * num_per_prompt`."""
96
+ target_batch_size = batch_size * num_per_prompt
97
+
98
+ if tensor.shape[0] == target_batch_size:
99
+ return tensor
100
+
101
+ if tensor.shape[0] == 1:
102
+ repeat_by = target_batch_size
103
+ elif tensor.shape[0] == batch_size:
104
+ repeat_by = num_per_prompt
105
+ else:
106
+ tensor_name = f"`{tensor_name}`" if tensor_name is not None else "Tensor"
107
+ raise ValueError(
108
+ f"{tensor_name} batch size must be 1, `batch_size` ({batch_size}), or "
109
+ f"`batch_size * num_*_per_prompt` ({target_batch_size}), but got {tensor.shape[0]}."
110
+ )
111
+
112
+ return torch.repeat_interleave(tensor, repeats=repeat_by, dim=0, output_size=tensor.shape[0] * repeat_by)
113
+
114
+
115
+ # auto_docstring
116
+ class Ideogram4TextInputsStep(ModularPipelineBlocks):
117
+ """
118
+ Input step that determines `batch_size`/`dtype` from the per-prompt `text_features` and replicates the text outputs
119
+ to `batch_size * num_images_per_prompt`. Place after the text encoder.
120
+
121
+ Inputs:
122
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
123
+ The number of images to generate per prompt.
124
+ text_features (`Tensor`):
125
+ Per-prompt text features from the encoder.
126
+ text_lengths (`list`):
127
+ Per-prompt text-token counts from the encoder.
128
+
129
+ Outputs:
130
+ batch_size (`int`):
131
+ Effective batch size (num prompts * num_images_per_prompt).
132
+ dtype (`dtype`):
133
+ The dtype of the text features.
134
+ text_features (`Tensor`):
135
+ Text features, batch-expanded.
136
+ text_lengths (`list`):
137
+ Text-token counts, batch-expanded.
138
+ """
139
+
140
+ model_name = "ideogram4"
141
+
142
+ @property
143
+ def description(self) -> str:
144
+ return (
145
+ "Input step that determines `batch_size`/`dtype` from the per-prompt `text_features` and replicates the "
146
+ "text outputs to `batch_size * num_images_per_prompt`. Place after the text encoder."
147
+ )
148
+
149
+ @property
150
+ def inputs(self) -> list[InputParam]:
151
+ return [
152
+ InputParam.template("num_images_per_prompt", default=1),
153
+ InputParam(
154
+ name="text_features",
155
+ required=True,
156
+ type_hint=torch.Tensor,
157
+ description="Per-prompt text features from the encoder.",
158
+ ),
159
+ InputParam(
160
+ name="text_lengths",
161
+ required=True,
162
+ type_hint=list,
163
+ description="Per-prompt text-token counts from the encoder.",
164
+ ),
165
+ ]
166
+
167
+ @property
168
+ def intermediate_outputs(self) -> list[OutputParam]:
169
+ return [
170
+ OutputParam(
171
+ name="batch_size",
172
+ type_hint=int,
173
+ description="Effective batch size (num prompts * num_images_per_prompt).",
174
+ ),
175
+ OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
176
+ OutputParam(name="text_features", type_hint=torch.Tensor, description="Text features, batch-expanded."),
177
+ OutputParam(name="text_lengths", type_hint=list, description="Text-token counts, batch-expanded."),
178
+ ]
179
+
180
+ @torch.no_grad()
181
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
182
+ block_state = self.get_block_state(state)
183
+
184
+ prompt_batch = block_state.text_features.shape[0]
185
+ num_per_prompt = block_state.num_images_per_prompt
186
+
187
+ block_state.dtype = block_state.text_features.dtype
188
+ block_state.text_features = _expand_tensor_to_effective_batch(
189
+ block_state.text_features, prompt_batch, num_per_prompt, "text_features"
190
+ )
191
+ block_state.text_lengths = [n for n in block_state.text_lengths for _ in range(num_per_prompt)]
192
+ block_state.batch_size = prompt_batch * num_per_prompt
193
+
194
+ self.set_block_state(state, block_state)
195
+ return components, state
196
+
197
+
198
+ # auto_docstring
199
+ class Ideogram4PrepareLatentsStep(ModularPipelineBlocks):
200
+ """
201
+ Step that prepares the packed image latents (B, num_image_tokens, latent_dim) for the denoising loop.
202
+
203
+ Components:
204
+ transformer (`Ideogram4Transformer2DModel`)
205
+
206
+ Inputs:
207
+ latents (`Tensor`, *optional*):
208
+ Pre-generated noisy latents for image generation.
209
+ height (`int`):
210
+ The height in pixels of the generated image.
211
+ width (`int`):
212
+ The width in pixels of the generated image.
213
+ generator (`Generator`, *optional*):
214
+ Torch generator for deterministic generation.
215
+ batch_size (`int`):
216
+ Effective batch size.
217
+
218
+ Outputs:
219
+ latents (`Tensor`):
220
+ The initial packed image latents (B, num_image_tokens, latent_dim).
221
+ num_image_tokens (`int`):
222
+ Number of image tokens (grid_h * grid_w).
223
+ """
224
+
225
+ model_name = "ideogram4"
226
+
227
+ @property
228
+ def description(self) -> str:
229
+ return "Step that prepares the packed image latents (B, num_image_tokens, latent_dim) for the denoising loop."
230
+
231
+ @property
232
+ def expected_components(self) -> list[ComponentSpec]:
233
+ return [ComponentSpec("transformer", Ideogram4Transformer2DModel)]
234
+
235
+ @property
236
+ def inputs(self) -> list[InputParam]:
237
+ return [
238
+ InputParam.template("latents"),
239
+ InputParam.template("height", required=True),
240
+ InputParam.template("width", required=True),
241
+ InputParam.template("generator"),
242
+ InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
243
+ ]
244
+
245
+ @property
246
+ def intermediate_outputs(self) -> list[OutputParam]:
247
+ return [
248
+ OutputParam(
249
+ name="latents",
250
+ type_hint=torch.Tensor,
251
+ description="The initial packed image latents (B, num_image_tokens, latent_dim).",
252
+ ),
253
+ OutputParam(
254
+ name="num_image_tokens", type_hint=int, description="Number of image tokens (grid_h * grid_w)."
255
+ ),
256
+ ]
257
+
258
+ @torch.no_grad()
259
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
260
+ block_state = self.get_block_state(state)
261
+
262
+ device = components._execution_device
263
+ patch = components.patch_size
264
+ grid_h = block_state.height // (components.vae_scale_factor * patch)
265
+ grid_w = block_state.width // (components.vae_scale_factor * patch)
266
+ num_image_tokens = grid_h * grid_w
267
+ latent_dim = components.transformer.config.in_channels
268
+
269
+ shape = (block_state.batch_size, num_image_tokens, latent_dim)
270
+ if block_state.latents is None:
271
+ block_state.latents = randn_tensor(
272
+ shape, generator=block_state.generator, device=device, dtype=torch.float32
273
+ )
274
+ else:
275
+ block_state.latents = block_state.latents.to(device=device, dtype=torch.float32)
276
+
277
+ block_state.num_image_tokens = num_image_tokens
278
+
279
+ self.set_block_state(state, block_state)
280
+ return components, state
281
+
282
+
283
+ # auto_docstring
284
+ class Ideogram4SetTimestepsStep(ModularPipelineBlocks):
285
+ """
286
+ Step that sets the resolution-aware logit-normal sigma schedule on the scheduler and resolves the per-step guidance
287
+ weights.
288
+
289
+ Components:
290
+ scheduler (`FlowMatchEulerDiscreteScheduler`)
291
+
292
+ Inputs:
293
+ num_inference_steps (`int`, *optional*, defaults to 48):
294
+ The number of denoising steps.
295
+ height (`int`):
296
+ The height in pixels of the generated image.
297
+ width (`int`):
298
+ The width in pixels of the generated image.
299
+ mu (`float`, *optional*, defaults to 0.0):
300
+ Base mean of the logit-normal schedule.
301
+ std (`float`, *optional*, defaults to 1.5):
302
+ Std of the logit-normal schedule.
303
+ guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
304
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
305
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
306
+ Per-step guidance scale schedule (length num_inference_steps).
307
+
308
+ Outputs:
309
+ timesteps (`Tensor`):
310
+ The denoising timesteps.
311
+ gw (`Tensor`):
312
+ Per-step guidance weights (num_inference_steps,).
313
+ """
314
+
315
+ model_name = "ideogram4"
316
+
317
+ @property
318
+ def description(self) -> str:
319
+ return (
320
+ "Step that sets the resolution-aware logit-normal sigma schedule on the scheduler and resolves the "
321
+ "per-step guidance weights."
322
+ )
323
+
324
+ @property
325
+ def expected_components(self) -> list[ComponentSpec]:
326
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
327
+
328
+ @property
329
+ def inputs(self) -> list[InputParam]:
330
+ return [
331
+ InputParam.template("num_inference_steps", default=48),
332
+ InputParam.template("height", required=True),
333
+ InputParam.template("width", required=True),
334
+ InputParam(name="mu", default=0.0, type_hint=float, description="Base mean of the logit-normal schedule."),
335
+ InputParam(name="std", default=1.5, type_hint=float, description="Std of the logit-normal schedule."),
336
+ InputParam(
337
+ name="guidance_schedule",
338
+ default=DEFAULT_GUIDANCE_SCHEDULE,
339
+ type_hint=list,
340
+ description="Per-step guidance scale schedule (length num_inference_steps).",
341
+ ),
342
+ ]
343
+
344
+ @property
345
+ def intermediate_outputs(self) -> list[OutputParam]:
346
+ return [
347
+ OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps."),
348
+ OutputParam(
349
+ name="gw", type_hint=torch.Tensor, description="Per-step guidance weights (num_inference_steps,)."
350
+ ),
351
+ ]
352
+
353
+ @torch.no_grad()
354
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
355
+ block_state = self.get_block_state(state)
356
+
357
+ device = components._execution_device
358
+ if len(block_state.guidance_schedule) != block_state.num_inference_steps:
359
+ raise ValueError(
360
+ f"`guidance_schedule` must have length `num_inference_steps` ({block_state.num_inference_steps}), "
361
+ f"got {len(block_state.guidance_schedule)}."
362
+ )
363
+
364
+ schedule_mu = _resolution_aware_mu(height=block_state.height, width=block_state.width, base_mu=block_state.mu)
365
+ sigmas = _logit_normal_sigmas(block_state.num_inference_steps, schedule_mu, std=block_state.std, device=device)
366
+ components.scheduler.set_timesteps(sigmas=sigmas.tolist(), device=device)
367
+
368
+ block_state.timesteps = components.scheduler.timesteps
369
+ block_state.gw = torch.as_tensor(block_state.guidance_schedule, dtype=torch.float32, device=device)
370
+
371
+ self.set_block_state(state, block_state)
372
+ return components, state
373
+
374
+
375
+ # auto_docstring
376
+ class Ideogram4PrepareAdditionalInputsStep(ModularPipelineBlocks):
377
+ """
378
+ Step that prepares the additional denoiser inputs from the packed-sequence layout: the conditional
379
+ encoder_hidden_states (text features packed with image padding) and the position_ids/segment_ids/indicator, plus
380
+ the unconditional (image-only) counterparts. Place after prepare_latents.
381
+
382
+ Inputs:
383
+ height (`int`):
384
+ The height in pixels of the generated image.
385
+ width (`int`):
386
+ The width in pixels of the generated image.
387
+ text_features (`Tensor`):
388
+ Batch-expanded text features.
389
+ text_lengths (`list`):
390
+ Batch-expanded text-token counts.
391
+ batch_size (`int`):
392
+ Effective batch size.
393
+
394
+ Outputs:
395
+ prompt_embeds (`Tensor`):
396
+ Packed conditional encoder_hidden_states (B, total_seq, dim).
397
+ position_ids (`Tensor`):
398
+ Conditional 3-axis MRoPE position ids.
399
+ segment_ids (`Tensor`):
400
+ Conditional block-diagonal segment ids.
401
+ indicator (`Tensor`):
402
+ Conditional per-token text/image/pad role.
403
+ negative_prompt_embeds (`Tensor`):
404
+ Unconditional (zeroed) text features (B, num_image_tokens, dim).
405
+ negative_position_ids (`Tensor`):
406
+ Unconditional position ids (image region).
407
+ negative_segment_ids (`Tensor`):
408
+ Unconditional segment ids (image region).
409
+ negative_indicator (`Tensor`):
410
+ Unconditional indicator (image region).
411
+ """
412
+
413
+ model_name = "ideogram4"
414
+
415
+ @property
416
+ def description(self) -> str:
417
+ return (
418
+ "Step that prepares the additional denoiser inputs from the packed-sequence layout: the conditional "
419
+ "encoder_hidden_states (text features packed with image padding) and the position_ids/segment_ids/"
420
+ "indicator, plus the unconditional (image-only) counterparts. Place after prepare_latents."
421
+ )
422
+
423
+ @property
424
+ def inputs(self) -> list[InputParam]:
425
+ return [
426
+ InputParam.template("height", required=True),
427
+ InputParam.template("width", required=True),
428
+ InputParam(
429
+ name="text_features",
430
+ required=True,
431
+ type_hint=torch.Tensor,
432
+ description="Batch-expanded text features.",
433
+ ),
434
+ InputParam(
435
+ name="text_lengths", required=True, type_hint=list, description="Batch-expanded text-token counts."
436
+ ),
437
+ InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
438
+ ]
439
+
440
+ @property
441
+ def intermediate_outputs(self) -> list[OutputParam]:
442
+ return [
443
+ OutputParam(
444
+ name="prompt_embeds",
445
+ type_hint=torch.Tensor,
446
+ description="Packed conditional encoder_hidden_states (B, total_seq, dim).",
447
+ ),
448
+ OutputParam(
449
+ name="position_ids", type_hint=torch.Tensor, description="Conditional 3-axis MRoPE position ids."
450
+ ),
451
+ OutputParam(
452
+ name="segment_ids", type_hint=torch.Tensor, description="Conditional block-diagonal segment ids."
453
+ ),
454
+ OutputParam(
455
+ name="indicator", type_hint=torch.Tensor, description="Conditional per-token text/image/pad role."
456
+ ),
457
+ OutputParam(
458
+ name="negative_prompt_embeds",
459
+ type_hint=torch.Tensor,
460
+ description="Unconditional (zeroed) text features (B, num_image_tokens, dim).",
461
+ ),
462
+ OutputParam(
463
+ name="negative_position_ids",
464
+ type_hint=torch.Tensor,
465
+ description="Unconditional position ids (image region).",
466
+ ),
467
+ OutputParam(
468
+ name="negative_segment_ids",
469
+ type_hint=torch.Tensor,
470
+ description="Unconditional segment ids (image region).",
471
+ ),
472
+ OutputParam(
473
+ name="negative_indicator",
474
+ type_hint=torch.Tensor,
475
+ description="Unconditional indicator (image region).",
476
+ ),
477
+ ]
478
+
479
+ @staticmethod
480
+ # Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4.Ideogram4Pipeline._prepare_ids
481
+ def _prepare_ids(
482
+ text_lengths: list[int],
483
+ grid_h: int,
484
+ grid_w: int,
485
+ max_text_tokens: int,
486
+ device: torch.device,
487
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
488
+ """Build the packed `[left-pad][text][image]` layout from the per-prompt text lengths and the image grid.
489
+
490
+ Returns `position_ids` (3-axis MRoPE), `segment_ids` (block-diagonal attention) and `indicator` (per-token
491
+ text/image/pad role).
492
+ """
493
+ batch_size = len(text_lengths)
494
+ num_image_tokens = grid_h * grid_w
495
+ total_seq_len = max_text_tokens + num_image_tokens
496
+
497
+ # Image position ids (t=0, h, w); offset keeps them disjoint from text positions.
498
+ h_idx = torch.arange(grid_h).view(-1, 1).expand(grid_h, grid_w).reshape(-1)
499
+ w_idx = torch.arange(grid_w).view(1, -1).expand(grid_h, grid_w).reshape(-1)
500
+ t_idx = torch.zeros_like(h_idx)
501
+ image_pos = torch.stack([t_idx, h_idx, w_idx], dim=1) + IMAGE_POSITION_OFFSET
502
+
503
+ position_ids = torch.zeros(batch_size, total_seq_len, 3, dtype=torch.long)
504
+ segment_ids = torch.full((batch_size, total_seq_len), SEQUENCE_PADDING_INDICATOR, dtype=torch.long)
505
+ indicator = torch.zeros(batch_size, total_seq_len, dtype=torch.long)
506
+
507
+ for b, num_text in enumerate(text_lengths):
508
+ offset = max_text_tokens - num_text
509
+
510
+ text_pos = torch.arange(num_text)
511
+ text_pos_3d = torch.stack([text_pos, text_pos, text_pos], dim=1)
512
+ position_ids[b, offset : offset + num_text] = text_pos_3d
513
+ position_ids[b, offset + num_text :] = image_pos
514
+
515
+ indicator[b, offset : offset + num_text] = LLM_TOKEN_INDICATOR
516
+ indicator[b, offset + num_text :] = OUTPUT_IMAGE_INDICATOR
517
+
518
+ segment_ids[b, offset : offset + num_text + num_image_tokens] = 1
519
+
520
+ return position_ids.to(device), segment_ids.to(device), indicator.to(device)
521
+
522
+ @torch.no_grad()
523
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
524
+ block_state = self.get_block_state(state)
525
+
526
+ device = components._execution_device
527
+ patch = components.patch_size
528
+ grid_h = block_state.height // (components.vae_scale_factor * patch)
529
+ grid_w = block_state.width // (components.vae_scale_factor * patch)
530
+ num_image_tokens = grid_h * grid_w
531
+
532
+ text_features = block_state.text_features
533
+ max_text_tokens = text_features.shape[1]
534
+ feature_dim = text_features.shape[-1]
535
+
536
+ position_ids, segment_ids, indicator = self._prepare_ids(
537
+ block_state.text_lengths, grid_h, grid_w, max_text_tokens, device
538
+ )
539
+
540
+ # Pack the text features into the full sequence; image positions carry no text features.
541
+ image_feature_padding = torch.zeros(
542
+ block_state.batch_size, num_image_tokens, feature_dim, dtype=text_features.dtype, device=device
543
+ )
544
+ block_state.prompt_embeds = torch.cat([text_features, image_feature_padding], dim=1)
545
+
546
+ # Unconditional (image-only) branch, derived from the conditioning.
547
+ block_state.negative_prompt_embeds = torch.zeros(
548
+ block_state.batch_size, num_image_tokens, feature_dim, dtype=text_features.dtype, device=device
549
+ )
550
+ block_state.position_ids = position_ids
551
+ block_state.segment_ids = segment_ids
552
+ block_state.indicator = indicator
553
+ block_state.negative_position_ids = position_ids[:, max_text_tokens:]
554
+ block_state.negative_segment_ids = segment_ids[:, max_text_tokens:]
555
+ block_state.negative_indicator = indicator[:, max_text_tokens:]
556
+
557
+ self.set_block_state(state, block_state)
558
+ return components, state
diffusers/modular_pipelines/ideogram4/decoders.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...image_processor import VaeImageProcessor
20
+ from ...models import AutoencoderKLFlux2
21
+ from ...utils import logging
22
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+ from .modular_pipeline import Ideogram4ModularPipeline
25
+
26
+
27
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+
30
+ # auto_docstring
31
+ class Ideogram4DecodeStep(ModularPipelineBlocks):
32
+ """
33
+ Step that decodes the unpatchified (B, ae_channels, H, W) latents into images: de-normalizes with the VAE
34
+ batch-norm statistics and decodes through the VAE.
35
+
36
+ Components:
37
+ vae (`AutoencoderKLFlux2`) image_processor (`VaeImageProcessor`)
38
+
39
+ Inputs:
40
+ output_type (`str`, *optional*, defaults to pil):
41
+ Output format: 'pil', 'np', 'pt'.
42
+ latents (`Tensor`):
43
+ The unpatchified (B, ae_channels, H, W) latents to decode, from the after-denoise step.
44
+
45
+ Outputs:
46
+ images (`list`):
47
+ Generated images.
48
+ """
49
+
50
+ model_name = "ideogram4"
51
+
52
+ @property
53
+ def description(self) -> str:
54
+ return (
55
+ "Step that decodes the unpatchified (B, ae_channels, H, W) latents into images: de-normalizes with the "
56
+ "VAE batch-norm statistics and decodes through the VAE."
57
+ )
58
+
59
+ @property
60
+ def expected_components(self) -> list[ComponentSpec]:
61
+ return [
62
+ ComponentSpec("vae", AutoencoderKLFlux2),
63
+ ComponentSpec(
64
+ "image_processor",
65
+ VaeImageProcessor,
66
+ config=FrozenDict({"vae_scale_factor": 16}),
67
+ default_creation_method="from_config",
68
+ ),
69
+ ]
70
+
71
+ @property
72
+ def inputs(self) -> list[InputParam]:
73
+ return [
74
+ InputParam.template("output_type", default="pil"),
75
+ InputParam(
76
+ name="latents",
77
+ required=True,
78
+ type_hint=torch.Tensor,
79
+ description="The unpatchified (B, ae_channels, H, W) latents to decode, from the after-denoise step.",
80
+ ),
81
+ ]
82
+
83
+ @property
84
+ def intermediate_outputs(self) -> list[OutputParam]:
85
+ return [OutputParam.template("images")]
86
+
87
+ @torch.no_grad()
88
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
89
+ block_state = self.get_block_state(state)
90
+
91
+ z = block_state.latents
92
+ patch = components.patch_size
93
+ ae_channels = z.shape[1]
94
+ grid_h, grid_w = z.shape[2] // patch, z.shape[3] // patch
95
+
96
+ # VAE bn stores per-channel statistics over the packed channels, laid out as (patch_row, patch_col,
97
+ # ae_channel). Reshape them into an (ae_channels, patch, patch) tile and repeat across the grid so the
98
+ # denormalization on the unpatchified latents matches the packed-space statistics.
99
+ bn_mean = components.vae.bn.running_mean.view(patch, patch, ae_channels).permute(2, 0, 1)
100
+ bn_std = torch.sqrt(components.vae.bn.running_var + components.vae.config.batch_norm_eps)
101
+ bn_std = bn_std.view(patch, patch, ae_channels).permute(2, 0, 1)
102
+ bn_mean = bn_mean.repeat(1, grid_h, grid_w).to(device=z.device, dtype=z.dtype)
103
+ bn_std = bn_std.repeat(1, grid_h, grid_w).to(device=z.device, dtype=z.dtype)
104
+ z = z * bn_std + bn_mean
105
+
106
+ decoded = components.vae.decode(z.to(components.vae.dtype), return_dict=False)[0]
107
+ block_state.images = components.image_processor.postprocess(
108
+ decoded.float(), output_type=block_state.output_type
109
+ )
110
+
111
+ self.set_block_state(state, block_state)
112
+ return components, state
diffusers/modular_pipelines/ideogram4/denoise.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...models.transformers.transformer_ideogram4 import Ideogram4Transformer2DModel
19
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
20
+ from ...utils import logging
21
+ from ..modular_pipeline import (
22
+ BlockState,
23
+ LoopSequentialPipelineBlocks,
24
+ ModularPipelineBlocks,
25
+ PipelineState,
26
+ )
27
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
28
+ from .modular_pipeline import Ideogram4ModularPipeline
29
+
30
+
31
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
32
+
33
+
34
+ class Ideogram4LoopBeforeDenoiser(ModularPipelineBlocks):
35
+ model_name = "ideogram4"
36
+
37
+ @property
38
+ def description(self) -> str:
39
+ return (
40
+ "Within the denoising loop: build the conditional packed input `[text-padding][image latents]` and the "
41
+ "model timestep. Compose into the `sub_blocks` of `Ideogram4DenoiseLoopWrapper`."
42
+ )
43
+
44
+ @property
45
+ def expected_components(self) -> list[ComponentSpec]:
46
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
47
+
48
+ @property
49
+ def inputs(self) -> list[InputParam]:
50
+ return [
51
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
52
+ InputParam(
53
+ name="position_ids", required=True, type_hint=torch.Tensor, description="Conditional position ids."
54
+ ),
55
+ InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
56
+ ]
57
+
58
+ @torch.no_grad()
59
+ def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
60
+ # Conditional packed sequence is [text-padding][image latents]; text region length = total - image tokens.
61
+ max_text_tokens = block_state.position_ids.shape[1] - block_state.latents.shape[1]
62
+ text_z_padding = torch.zeros(
63
+ block_state.latents.shape[0],
64
+ max_text_tokens,
65
+ block_state.latents.shape[-1],
66
+ dtype=block_state.latents.dtype,
67
+ device=block_state.latents.device,
68
+ )
69
+ block_state.pos_z = torch.cat([text_z_padding, block_state.latents], dim=1)
70
+ block_state.max_text_tokens = max_text_tokens
71
+
72
+ # Map sigma-domain timestep to model time t in [0, 1] (0 = noise, 1 = clean data).
73
+ num_train_timesteps = components.scheduler.config.num_train_timesteps
74
+ t_model = 1.0 - (t.float() / num_train_timesteps)
75
+ block_state.t_model = t_model.expand(block_state.batch_size)
76
+ return components, block_state
77
+
78
+
79
+ class Ideogram4LoopDenoiser(ModularPipelineBlocks):
80
+ model_name = "ideogram4"
81
+
82
+ @property
83
+ def description(self) -> str:
84
+ return (
85
+ "Within the denoising loop: run the conditional `transformer` on the full packed sequence and the "
86
+ "`unconditional_transformer` on the image-only sequence, then blend with the per-step guidance weight "
87
+ "(asymmetric CFG, no guider). Compose into `Ideogram4DenoiseLoopWrapper`."
88
+ )
89
+
90
+ @property
91
+ def expected_components(self) -> list[ComponentSpec]:
92
+ return [
93
+ ComponentSpec("transformer", Ideogram4Transformer2DModel),
94
+ ComponentSpec("unconditional_transformer", Ideogram4Transformer2DModel),
95
+ ]
96
+
97
+ @property
98
+ def inputs(self) -> list[InputParam]:
99
+ return [
100
+ InputParam(
101
+ name="prompt_embeds",
102
+ required=True,
103
+ type_hint=torch.Tensor,
104
+ description="Packed conditional encoder_hidden_states.",
105
+ ),
106
+ InputParam(
107
+ name="position_ids",
108
+ required=True,
109
+ type_hint=torch.Tensor,
110
+ description="Conditional 3-axis MRoPE position ids.",
111
+ ),
112
+ InputParam(
113
+ name="segment_ids",
114
+ required=True,
115
+ type_hint=torch.Tensor,
116
+ description="Conditional block-diagonal segment ids.",
117
+ ),
118
+ InputParam(
119
+ name="indicator",
120
+ required=True,
121
+ type_hint=torch.Tensor,
122
+ description="Conditional per-token text/image/pad role.",
123
+ ),
124
+ InputParam(
125
+ name="negative_prompt_embeds",
126
+ required=True,
127
+ type_hint=torch.Tensor,
128
+ description="Unconditional (zeroed) text features.",
129
+ ),
130
+ InputParam(
131
+ name="negative_position_ids",
132
+ required=True,
133
+ type_hint=torch.Tensor,
134
+ description="Unconditional position ids (image region).",
135
+ ),
136
+ InputParam(
137
+ name="negative_segment_ids",
138
+ required=True,
139
+ type_hint=torch.Tensor,
140
+ description="Unconditional segment ids (image region).",
141
+ ),
142
+ InputParam(
143
+ name="negative_indicator",
144
+ required=True,
145
+ type_hint=torch.Tensor,
146
+ description="Unconditional indicator (image region).",
147
+ ),
148
+ InputParam(name="gw", required=True, type_hint=torch.Tensor, description="Per-step guidance weights."),
149
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
150
+ ]
151
+
152
+ @torch.no_grad()
153
+ def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
154
+ transformer = components.transformer
155
+ unconditional_transformer = components.unconditional_transformer
156
+
157
+ # Conditional pass operates on the full packed sequence; the velocity is the image-token region.
158
+ pos_out = transformer(
159
+ hidden_states=block_state.pos_z.to(transformer.dtype),
160
+ timestep=block_state.t_model.to(transformer.dtype),
161
+ encoder_hidden_states=block_state.prompt_embeds.to(transformer.dtype),
162
+ position_ids=block_state.position_ids,
163
+ segment_ids=block_state.segment_ids,
164
+ indicator=block_state.indicator,
165
+ return_dict=False,
166
+ )[0]
167
+ pos_v = pos_out[:, block_state.max_text_tokens :].to(torch.float32)
168
+
169
+ # Unconditional pass uses the image-only positions with zeroed text features.
170
+ neg_v = unconditional_transformer(
171
+ hidden_states=block_state.latents.to(unconditional_transformer.dtype),
172
+ timestep=block_state.t_model.to(unconditional_transformer.dtype),
173
+ encoder_hidden_states=block_state.negative_prompt_embeds.to(unconditional_transformer.dtype),
174
+ position_ids=block_state.negative_position_ids,
175
+ segment_ids=block_state.negative_segment_ids,
176
+ indicator=block_state.negative_indicator,
177
+ return_dict=False,
178
+ )[0].to(torch.float32)
179
+
180
+ gw_i = block_state.gw[i]
181
+ v = gw_i * pos_v + (1.0 - gw_i) * neg_v
182
+ # The scheduler integrates `-v` (Ideogram predicts velocity v = x0 - noise).
183
+ block_state.noise_pred = -v
184
+ return components, block_state
185
+
186
+
187
+ class Ideogram4LoopAfterDenoiser(ModularPipelineBlocks):
188
+ model_name = "ideogram4"
189
+
190
+ @property
191
+ def description(self) -> str:
192
+ return "Within the denoising loop: scheduler step. Compose into `Ideogram4DenoiseLoopWrapper`."
193
+
194
+ @property
195
+ def expected_components(self) -> list[ComponentSpec]:
196
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
197
+
198
+ @property
199
+ def intermediate_outputs(self) -> list[OutputParam]:
200
+ return [OutputParam(name="latents", type_hint=torch.Tensor, description="The denoised latents.")]
201
+
202
+ @torch.no_grad()
203
+ def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
204
+ block_state.latents = components.scheduler.step(
205
+ block_state.noise_pred, t, block_state.latents, return_dict=False
206
+ )[0]
207
+ return components, block_state
208
+
209
+
210
+ # auto_docstring
211
+ class Ideogram4DenoiseStep(LoopSequentialPipelineBlocks):
212
+ """
213
+ Denoising loop that iteratively denoises the packed image latents over `timesteps`, running both the conditional
214
+ and unconditional transformers and blending with the per-step guidance schedule.
215
+
216
+ Components:
217
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Ideogram4Transformer2DModel`)
218
+ unconditional_transformer (`Ideogram4Transformer2DModel`)
219
+
220
+ Inputs:
221
+ timesteps (`Tensor`):
222
+ Denoising timesteps from set_timesteps.
223
+ num_inference_steps (`int`, *optional*, defaults to 48):
224
+ The number of denoising steps.
225
+ latents (`Tensor`):
226
+ Packed image latents.
227
+ position_ids (`Tensor`):
228
+ Conditional position ids.
229
+ batch_size (`int`):
230
+ Effective batch size.
231
+ prompt_embeds (`Tensor`):
232
+ Packed conditional encoder_hidden_states.
233
+ position_ids (`Tensor`):
234
+ Conditional 3-axis MRoPE position ids.
235
+ segment_ids (`Tensor`):
236
+ Conditional block-diagonal segment ids.
237
+ indicator (`Tensor`):
238
+ Conditional per-token text/image/pad role.
239
+ negative_prompt_embeds (`Tensor`):
240
+ Unconditional (zeroed) text features.
241
+ negative_position_ids (`Tensor`):
242
+ Unconditional position ids (image region).
243
+ negative_segment_ids (`Tensor`):
244
+ Unconditional segment ids (image region).
245
+ negative_indicator (`Tensor`):
246
+ Unconditional indicator (image region).
247
+ gw (`Tensor`):
248
+ Per-step guidance weights.
249
+
250
+ Outputs:
251
+ latents (`Tensor`):
252
+ The denoised latents.
253
+ """
254
+
255
+ model_name = "ideogram4"
256
+ block_classes = [Ideogram4LoopBeforeDenoiser, Ideogram4LoopDenoiser, Ideogram4LoopAfterDenoiser]
257
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
258
+
259
+ @property
260
+ def description(self) -> str:
261
+ return (
262
+ "Denoising loop that iteratively denoises the packed image latents over `timesteps`, running both the "
263
+ "conditional and unconditional transformers and blending with the per-step guidance schedule."
264
+ )
265
+
266
+ @property
267
+ def loop_expected_components(self) -> list[ComponentSpec]:
268
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
269
+
270
+ @property
271
+ def loop_inputs(self) -> list[InputParam]:
272
+ return [
273
+ InputParam(
274
+ name="timesteps",
275
+ required=True,
276
+ type_hint=torch.Tensor,
277
+ description="Denoising timesteps from set_timesteps.",
278
+ ),
279
+ InputParam.template("num_inference_steps", default=48),
280
+ ]
281
+
282
+ @torch.no_grad()
283
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
284
+ block_state = self.get_block_state(state)
285
+
286
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
287
+ for i, t in enumerate(block_state.timesteps):
288
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
289
+ progress_bar.update()
290
+
291
+ self.set_block_state(state, block_state)
292
+ return components, state
293
+
294
+
295
+ # auto_docstring
296
+ class Ideogram4AfterDenoiseStep(ModularPipelineBlocks):
297
+ """
298
+ Step that runs after the denoising loop: unpatchifies the packed image latents (B, num_image_tokens, ae_channels *
299
+ patch ** 2) into a (B, ae_channels, H, W) latent for the decoder.
300
+
301
+ Inputs:
302
+ height (`int`):
303
+ The height in pixels of the generated image.
304
+ width (`int`):
305
+ The width in pixels of the generated image.
306
+ latents (`Tensor`):
307
+ The denoised packed image latents (B, num_image_tokens, latent_dim).
308
+
309
+ Outputs:
310
+ latents (`Tensor`):
311
+ Unpatchified latents (B, ae_channels, H, W) ready for the VAE decoder.
312
+ """
313
+
314
+ model_name = "ideogram4"
315
+
316
+ @property
317
+ def description(self) -> str:
318
+ return (
319
+ "Step that runs after the denoising loop: unpatchifies the packed image latents "
320
+ "(B, num_image_tokens, ae_channels * patch ** 2) into a (B, ae_channels, H, W) latent for the decoder."
321
+ )
322
+
323
+ @property
324
+ def inputs(self) -> list[InputParam]:
325
+ return [
326
+ InputParam.template("height", required=True),
327
+ InputParam.template("width", required=True),
328
+ InputParam(
329
+ name="latents",
330
+ required=True,
331
+ type_hint=torch.Tensor,
332
+ description="The denoised packed image latents (B, num_image_tokens, latent_dim).",
333
+ ),
334
+ ]
335
+
336
+ @property
337
+ def intermediate_outputs(self) -> list[OutputParam]:
338
+ return [
339
+ OutputParam(
340
+ name="latents",
341
+ type_hint=torch.Tensor,
342
+ description="Unpatchified latents (B, ae_channels, H, W) ready for the VAE decoder.",
343
+ )
344
+ ]
345
+
346
+ @torch.no_grad()
347
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
348
+ block_state = self.get_block_state(state)
349
+
350
+ z = block_state.latents
351
+ patch = components.patch_size
352
+ grid_h = block_state.height // (components.vae_scale_factor * patch)
353
+ grid_w = block_state.width // (components.vae_scale_factor * patch)
354
+
355
+ ae_channels = z.shape[-1] // (patch * patch)
356
+ z = z.view(z.shape[0], grid_h, grid_w, patch, patch, ae_channels)
357
+ z = z.permute(0, 5, 1, 3, 2, 4).contiguous()
358
+ z = z.view(z.shape[0], ae_channels, grid_h * patch, grid_w * patch)
359
+
360
+ block_state.latents = z
361
+
362
+ self.set_block_state(state, block_state)
363
+ return components, state
diffusers/modular_pipelines/ideogram4/encoders.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+ from transformers import Qwen2Tokenizer, Qwen3VLModel
18
+ from transformers.masking_utils import create_causal_mask
19
+
20
+ from ...pipelines.ideogram4.prompt_enhancer import (
21
+ PROMPT_UPSAMPLE_TEMPERATURE,
22
+ Ideogram4PromptEnhancerHead,
23
+ build_caption_logits_processor,
24
+ build_prompt_enhancer,
25
+ generate_captions,
26
+ )
27
+ from ...utils import is_outlines_available, logging
28
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
29
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
30
+ from .modular_pipeline import Ideogram4ModularPipeline
31
+
32
+
33
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+
36
+ # Hidden states of these Qwen3-VL decoder layers are concatenated to form the per-token
37
+ # text conditioning consumed by the Ideogram4 transformer.
38
+ QWEN3_VL_ACTIVATION_LAYERS = (0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 35)
39
+
40
+
41
+ # auto_docstring
42
+ class Ideogram4PromptUpsampleStep(ModularPipelineBlocks):
43
+ """
44
+ Optional step that rewrites the prompt(s) into Ideogram4's native structured JSON caption when
45
+ `prompt_upsampling=True` (the format the model is trained on). Requires a generative `text_encoder` (a
46
+ `Qwen3VLForConditionalGeneration`); install `outlines` for schema-constrained captions.
47
+
48
+ Components:
49
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
50
+ with the text encoder. prompt_enhancer_head (`Ideogram4PromptEnhancerHead`): LM head grafted onto the text
51
+ encoder for prompt upsampling.
52
+
53
+ Inputs:
54
+ prompt (`str`):
55
+ The prompt or prompts to guide image generation.
56
+ prompt_upsampling (`bool`, *optional*, defaults to False):
57
+ If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.
58
+ prompt_upsampling_temperature (`float`, *optional*, defaults to 1.0):
59
+ Sampling temperature for prompt upsampling.
60
+ height (`int`, *optional*):
61
+ The height in pixels of the generated image.
62
+ width (`int`, *optional*):
63
+ The width in pixels of the generated image.
64
+ max_sequence_length (`int`, *optional*, defaults to 2048):
65
+ Maximum sequence length for prompt encoding.
66
+ generator (`Generator`, *optional*):
67
+ Torch generator for deterministic generation.
68
+
69
+ Outputs:
70
+ prompt (`list`):
71
+ The (possibly upsampled) prompt forwarded to the text encoder.
72
+ """
73
+
74
+ model_name = "ideogram4"
75
+
76
+ def __init__(self):
77
+ # Built lazily on first upsample: the head-less encoder body + `prompt_enhancer_head`, combined.
78
+ self._prompt_enhancer = None
79
+ # Outlines logits processor for schema-constrained captions; built lazily on first upsample.
80
+ self._caption_logits_processor = None
81
+ super().__init__()
82
+
83
+ @property
84
+ def description(self) -> str:
85
+ return (
86
+ "Optional step that rewrites the prompt(s) into Ideogram4's native structured JSON caption when "
87
+ "`prompt_upsampling=True` (the format the model is trained on). Requires a generative `text_encoder` "
88
+ "(a `Qwen3VLForConditionalGeneration`); install `outlines` for schema-constrained captions."
89
+ )
90
+
91
+ @property
92
+ def expected_components(self) -> list[ComponentSpec]:
93
+ return [
94
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
95
+ ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer paired with the text encoder."),
96
+ ComponentSpec(
97
+ "prompt_enhancer_head",
98
+ Ideogram4PromptEnhancerHead,
99
+ description="LM head grafted onto the text encoder for prompt upsampling.",
100
+ ),
101
+ ]
102
+
103
+ @property
104
+ def inputs(self) -> list[InputParam]:
105
+ return [
106
+ InputParam.template("prompt", required=True),
107
+ InputParam(
108
+ name="prompt_upsampling",
109
+ type_hint=bool,
110
+ default=False,
111
+ description="If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.",
112
+ ),
113
+ InputParam(
114
+ name="prompt_upsampling_temperature",
115
+ type_hint=float,
116
+ default=PROMPT_UPSAMPLE_TEMPERATURE,
117
+ description="Sampling temperature for prompt upsampling.",
118
+ ),
119
+ InputParam.template("height"),
120
+ InputParam.template("width"),
121
+ InputParam.template("max_sequence_length", default=2048),
122
+ InputParam.template("generator"),
123
+ ]
124
+
125
+ @property
126
+ def intermediate_outputs(self) -> list[OutputParam]:
127
+ return [
128
+ OutputParam(
129
+ name="prompt",
130
+ type_hint=list,
131
+ description="The (possibly upsampled) prompt forwarded to the text encoder.",
132
+ ),
133
+ ]
134
+
135
+ @torch.no_grad()
136
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
137
+ block_state = self.get_block_state(state)
138
+
139
+ if block_state.prompt_upsampling:
140
+ if components.prompt_enhancer_head is None:
141
+ raise ValueError(
142
+ "Prompt upsampling requires the `prompt_enhancer_head` component, which is not loaded. Load an "
143
+ "`Ideogram4PromptEnhancerHead` and add it to the pipeline."
144
+ )
145
+ if self._prompt_enhancer is None:
146
+ self._prompt_enhancer = build_prompt_enhancer(components.text_encoder, components.prompt_enhancer_head)
147
+ if self._caption_logits_processor is None and is_outlines_available():
148
+ self._caption_logits_processor = build_caption_logits_processor(
149
+ self._prompt_enhancer, components.tokenizer
150
+ )
151
+ if self._caption_logits_processor is None:
152
+ logger.warning_once(
153
+ "`outlines` is not installed; prompt upsampling runs unconstrained and may not return "
154
+ "schema-valid JSON. Install with `pip install outlines` for structured captions."
155
+ )
156
+ height = block_state.height or components.default_height
157
+ width = block_state.width or components.default_width
158
+ block_state.prompt = generate_captions(
159
+ self._prompt_enhancer,
160
+ components.tokenizer,
161
+ self._caption_logits_processor,
162
+ block_state.prompt,
163
+ height,
164
+ width,
165
+ temperature=block_state.prompt_upsampling_temperature,
166
+ max_new_tokens=block_state.max_sequence_length,
167
+ generator=block_state.generator,
168
+ device=components._execution_device,
169
+ )
170
+
171
+ self.set_block_state(state, block_state)
172
+ return components, state
173
+
174
+
175
+ # auto_docstring
176
+ class Ideogram4TextEncoderStep(ModularPipelineBlocks):
177
+ """
178
+ Text encoder step that tokenizes the prompt(s) and runs the Qwen3-VL text encoder, returning the per-token text
179
+ features (concatenated from a fixed set of activation layers). Only the text tokens are encoded; the packed image
180
+ tokens are appended later (the encoder is causal with image after text, so they never affect the text features).
181
+
182
+ Components:
183
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
184
+ with the text encoder.
185
+
186
+ Inputs:
187
+ prompt (`str`):
188
+ The prompt or prompts to guide image generation.
189
+ max_sequence_length (`int`, *optional*, defaults to 2048):
190
+ Maximum sequence length for prompt encoding.
191
+
192
+ Outputs:
193
+ text_features (`Tensor`):
194
+ Per-prompt text features (B, max_sequence_length, llm_features_dim), padding zeroed.
195
+ text_lengths (`list`):
196
+ Per-prompt real text-token counts, used to lay out the packed sequence.
197
+ """
198
+
199
+ model_name = "ideogram4"
200
+
201
+ @property
202
+ def description(self) -> str:
203
+ return (
204
+ "Text encoder step that tokenizes the prompt(s) and runs the Qwen3-VL text encoder, returning the "
205
+ "per-token text features (concatenated from a fixed set of activation layers). Only the text tokens are "
206
+ "encoded; the packed image tokens are appended later (the encoder is causal with image after text, so "
207
+ "they never affect the text features)."
208
+ )
209
+
210
+ @property
211
+ def expected_components(self) -> list[ComponentSpec]:
212
+ return [
213
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
214
+ ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer paired with the text encoder."),
215
+ ]
216
+
217
+ @property
218
+ def inputs(self) -> list[InputParam]:
219
+ return [
220
+ InputParam.template("prompt", required=True),
221
+ InputParam.template("max_sequence_length", default=2048),
222
+ ]
223
+
224
+ @property
225
+ def intermediate_outputs(self) -> list[OutputParam]:
226
+ return [
227
+ OutputParam(
228
+ name="text_features",
229
+ type_hint=torch.Tensor,
230
+ description="Per-prompt text features (B, max_sequence_length, llm_features_dim), padding zeroed.",
231
+ ),
232
+ OutputParam(
233
+ name="text_lengths",
234
+ type_hint=list,
235
+ description="Per-prompt real text-token counts, used to lay out the packed sequence.",
236
+ ),
237
+ ]
238
+
239
+ @staticmethod
240
+ # Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4.Ideogram4Pipeline._get_text_encoder_hidden_states
241
+ def _get_text_encoder_hidden_states(
242
+ text_encoder,
243
+ token_ids: torch.Tensor,
244
+ attention_mask: torch.Tensor,
245
+ pos_2d: torch.Tensor,
246
+ ) -> list[torch.Tensor]:
247
+ """Run the text encoder's decoder layers, returning the hidden states tapped at each activation layer."""
248
+
249
+ language_model = text_encoder.language_model
250
+
251
+ inputs_embeds = language_model.embed_tokens(token_ids)
252
+
253
+ position_ids_4d = pos_2d[None, ...].expand(4, pos_2d.shape[0], -1)
254
+ text_position_ids = position_ids_4d[0]
255
+ mrope_position_ids = position_ids_4d[1:]
256
+
257
+ causal_mask = create_causal_mask(
258
+ config=language_model.config,
259
+ inputs_embeds=inputs_embeds,
260
+ attention_mask=attention_mask,
261
+ past_key_values=None,
262
+ position_ids=text_position_ids,
263
+ )
264
+ position_embeddings = language_model.rotary_emb(inputs_embeds, mrope_position_ids)
265
+
266
+ tap_set = set(QWEN3_VL_ACTIVATION_LAYERS)
267
+ captured: dict[int, torch.Tensor] = {}
268
+ hidden_states = inputs_embeds
269
+ for layer_idx, decoder_layer in enumerate(language_model.layers):
270
+ hidden_states = decoder_layer(
271
+ hidden_states,
272
+ attention_mask=causal_mask,
273
+ position_ids=text_position_ids,
274
+ past_key_values=None,
275
+ position_embeddings=position_embeddings,
276
+ )
277
+ if layer_idx in tap_set:
278
+ captured[layer_idx] = hidden_states
279
+
280
+ return [captured[i] for i in QWEN3_VL_ACTIVATION_LAYERS]
281
+
282
+ @torch.no_grad()
283
+ def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
284
+ block_state = self.get_block_state(state)
285
+
286
+ device = components._execution_device
287
+ tokenizer = components.tokenizer
288
+ max_text_tokens = block_state.max_sequence_length
289
+
290
+ prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
291
+ batch_size = len(prompts)
292
+
293
+ # Tokenize each chat-formatted prompt and left-pad to `max_sequence_length`.
294
+ token_ids = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
295
+ attention_mask = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
296
+ text_position_ids = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
297
+ text_lengths = []
298
+ for b, text_prompt in enumerate(prompts):
299
+ messages = [{"role": "user", "content": [{"type": "text", "text": text_prompt}]}]
300
+ text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
301
+ toks = tokenizer(text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
302
+ n = int(toks.shape[0])
303
+ if n > max_text_tokens:
304
+ raise ValueError(f"prompt has {n} tokens, exceeds max_sequence_length={max_text_tokens}")
305
+ text_lengths.append(n)
306
+ offset = max_text_tokens - n
307
+ token_ids[b, offset:] = toks
308
+ attention_mask[b, offset:] = 1
309
+ text_position_ids[b, offset:] = torch.arange(n)
310
+
311
+ token_ids = token_ids.to(device)
312
+ attention_mask = attention_mask.to(device)
313
+ text_position_ids = text_position_ids.to(device)
314
+
315
+ # Run the text encoder, tapping the activation-layer hidden states, then concatenate them into per-token
316
+ # text features (padding zeroed).
317
+ selected = self._get_text_encoder_hidden_states(
318
+ components.text_encoder, token_ids, attention_mask, text_position_ids
319
+ )
320
+ text_features = torch.stack(selected, dim=0).permute(1, 2, 3, 0).reshape(batch_size, max_text_tokens, -1)
321
+ text_features = (text_features * attention_mask.to(text_features.dtype).unsqueeze(-1)).to(torch.float32)
322
+
323
+ block_state.text_features = text_features
324
+ block_state.text_lengths = text_lengths
325
+
326
+ self.set_block_state(state, block_state)
327
+ return components, state
diffusers/modular_pipelines/ideogram4/modular_blocks_ideogram4.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Ideogram4PrepareAdditionalInputsStep,
21
+ Ideogram4PrepareLatentsStep,
22
+ Ideogram4SetTimestepsStep,
23
+ Ideogram4TextInputsStep,
24
+ )
25
+ from .decoders import Ideogram4DecodeStep
26
+ from .denoise import Ideogram4AfterDenoiseStep, Ideogram4DenoiseStep
27
+ from .encoders import Ideogram4PromptUpsampleStep, Ideogram4TextEncoderStep
28
+
29
+
30
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
31
+
32
+
33
+ # Core denoise: consumes the per-prompt text features and produces the unpatchified latents
34
+ # (batch/latents/timesteps/ids inputs -> denoising loop -> unpatchify).
35
+ CORE_DENOISE_BLOCKS = InsertableDict(
36
+ [
37
+ ("input", Ideogram4TextInputsStep()),
38
+ ("prepare_latents", Ideogram4PrepareLatentsStep()),
39
+ ("set_timesteps", Ideogram4SetTimestepsStep()),
40
+ ("prepare_additional_inputs", Ideogram4PrepareAdditionalInputsStep()),
41
+ ("denoise", Ideogram4DenoiseStep()),
42
+ ("after_denoise", Ideogram4AfterDenoiseStep()),
43
+ ]
44
+ )
45
+
46
+
47
+ # auto_docstring
48
+ class Ideogram4CoreDenoiseStep(SequentialPipelineBlocks):
49
+ """
50
+ Core denoising workflow for Ideogram4 text-to-image: prepares the batch/latents/timesteps and the packed denoiser
51
+ inputs, runs the asymmetric-CFG denoising loop over the conditional and unconditional transformers, and
52
+ unpatchifies the result for the decoder.
53
+
54
+ Components:
55
+ transformer (`Ideogram4Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
56
+ unconditional_transformer (`Ideogram4Transformer2DModel`)
57
+
58
+ Inputs:
59
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
60
+ The number of images to generate per prompt.
61
+ text_features (`Tensor`):
62
+ Per-prompt text features from the encoder.
63
+ text_lengths (`list`):
64
+ Per-prompt text-token counts from the encoder.
65
+ latents (`Tensor`, *optional*):
66
+ Pre-generated noisy latents for image generation.
67
+ height (`int`):
68
+ The height in pixels of the generated image.
69
+ width (`int`):
70
+ The width in pixels of the generated image.
71
+ generator (`Generator`, *optional*):
72
+ Torch generator for deterministic generation.
73
+ num_inference_steps (`int`, *optional*, defaults to 48):
74
+ The number of denoising steps.
75
+ mu (`float`, *optional*, defaults to 0.0):
76
+ Base mean of the logit-normal schedule.
77
+ std (`float`, *optional*, defaults to 1.5):
78
+ Std of the logit-normal schedule.
79
+ guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
80
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
81
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
82
+ Per-step guidance scale schedule (length num_inference_steps).
83
+
84
+ Outputs:
85
+ latents (`Tensor`):
86
+ Unpatchified (B, ae_channels, H, W) latents.
87
+ """
88
+
89
+ model_name = "ideogram4"
90
+ block_classes = list(CORE_DENOISE_BLOCKS.values())
91
+ block_names = list(CORE_DENOISE_BLOCKS.keys())
92
+
93
+ @property
94
+ def description(self) -> str:
95
+ return (
96
+ "Core denoising workflow for Ideogram4 text-to-image: prepares the batch/latents/timesteps and the packed "
97
+ "denoiser inputs, runs the asymmetric-CFG denoising loop over the conditional and unconditional "
98
+ "transformers, and unpatchifies the result for the decoder."
99
+ )
100
+
101
+ @property
102
+ def outputs(self) -> list[OutputParam]:
103
+ # The only meaningful product of the core step is the unpatchified latents; the batch/timesteps/packed-sequence
104
+ # inputs prepared along the way are consumed within the loop and are not updated by it.
105
+ return [OutputParam.template("latents", description="Unpatchified (B, ae_channels, H, W) latents.")]
106
+
107
+
108
+ # auto_docstring
109
+ class Ideogram4AutoBlocks(SequentialPipelineBlocks):
110
+ """
111
+ Auto Modular pipeline for text-to-image generation using Ideogram4: (optional) prompt upsampling -> encode text ->
112
+ core denoise (asymmetric CFG over two transformers) -> decode.
113
+
114
+ Supported workflows:
115
+ - `text2image`: requires `prompt`
116
+
117
+ Components:
118
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
119
+ with the text encoder. prompt_enhancer_head (`Ideogram4PromptEnhancerHead`): LM head grafted onto the text
120
+ encoder for prompt upsampling. transformer (`Ideogram4Transformer2DModel`) scheduler
121
+ (`FlowMatchEulerDiscreteScheduler`) unconditional_transformer (`Ideogram4Transformer2DModel`) vae
122
+ (`AutoencoderKLFlux2`) image_processor (`VaeImageProcessor`)
123
+
124
+ Inputs:
125
+ prompt (`str`):
126
+ The prompt or prompts to guide image generation.
127
+ prompt_upsampling (`bool`, *optional*, defaults to False):
128
+ If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.
129
+ prompt_upsampling_temperature (`float`, *optional*, defaults to 1.0):
130
+ Sampling temperature for prompt upsampling.
131
+ height (`int`, *optional*):
132
+ The height in pixels of the generated image.
133
+ width (`int`, *optional*):
134
+ The width in pixels of the generated image.
135
+ max_sequence_length (`int`, *optional*, defaults to 2048):
136
+ Maximum sequence length for prompt encoding.
137
+ generator (`Generator`, *optional*):
138
+ Torch generator for deterministic generation.
139
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
140
+ The number of images to generate per prompt.
141
+ latents (`Tensor`, *optional*):
142
+ Pre-generated noisy latents for image generation.
143
+ num_inference_steps (`int`, *optional*, defaults to 48):
144
+ The number of denoising steps.
145
+ mu (`float`, *optional*, defaults to 0.0):
146
+ Base mean of the logit-normal schedule.
147
+ std (`float`, *optional*, defaults to 1.5):
148
+ Std of the logit-normal schedule.
149
+ guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
150
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
151
+ 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
152
+ Per-step guidance scale schedule (length num_inference_steps).
153
+ output_type (`str`, *optional*, defaults to pil):
154
+ Output format: 'pil', 'np', 'pt'.
155
+
156
+ Outputs:
157
+ images (`list`):
158
+ Generated images.
159
+ """
160
+
161
+ model_name = "ideogram4"
162
+ block_classes = [
163
+ Ideogram4PromptUpsampleStep(),
164
+ Ideogram4TextEncoderStep(),
165
+ Ideogram4CoreDenoiseStep(),
166
+ Ideogram4DecodeStep(),
167
+ ]
168
+ block_names = ["prompt_upsample", "text_encoder", "denoise", "decode"]
169
+
170
+ # Workflow map declaring the trigger conditions for each supported workflow.
171
+ # `True` means the workflow triggers when the input is not None.
172
+ _workflow_map = {
173
+ "text2image": {"prompt": True},
174
+ }
175
+
176
+ @property
177
+ def description(self) -> str:
178
+ return (
179
+ "Auto Modular pipeline for text-to-image generation using Ideogram4: (optional) prompt upsampling -> "
180
+ "encode text -> core denoise (asymmetric CFG over two transformers) -> decode."
181
+ )
182
+
183
+ @property
184
+ def outputs(self) -> list[OutputParam]:
185
+ return [OutputParam.template("images")]
diffusers/modular_pipelines/ideogram4/modular_pipeline.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ...loaders import Ideogram4LoraLoaderMixin
16
+ from ..modular_pipeline import ModularPipeline
17
+
18
+
19
+ class Ideogram4ModularPipeline(ModularPipeline, Ideogram4LoraLoaderMixin):
20
+ """
21
+ A ModularPipeline for Ideogram4.
22
+
23
+ > [!WARNING] > This is an experimental feature!
24
+ """
25
+
26
+ default_blocks_name = "Ideogram4AutoBlocks"
27
+
28
+ # Ideogram4 patchifies the VAE output by a factor of 2 before feeding the transformer.
29
+ @property
30
+ def patch_size(self):
31
+ return 2
32
+
33
+ @property
34
+ def default_height(self):
35
+ return 2048
36
+
37
+ @property
38
+ def default_width(self):
39
+ return 2048
40
+
41
+ @property
42
+ def vae_scale_factor(self):
43
+ vae_scale_factor = 8
44
+ if getattr(self, "vae", None) is not None:
45
+ vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
46
+ return vae_scale_factor
diffusers/modular_pipelines/krea2/__init__.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_krea2"] = ["Krea2AutoBlocks"]
25
+ _import_structure["modular_blocks_krea2_turbo"] = ["Krea2TurboAutoBlocks"]
26
+ _import_structure["modular_pipeline"] = ["Krea2ModularPipeline", "Krea2TurboModularPipeline"]
27
+
28
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
29
+ try:
30
+ if not (is_transformers_available() and is_torch_available()):
31
+ raise OptionalDependencyNotAvailable()
32
+ except OptionalDependencyNotAvailable:
33
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
34
+ else:
35
+ from .modular_blocks_krea2 import Krea2AutoBlocks
36
+ from .modular_blocks_krea2_turbo import Krea2TurboAutoBlocks
37
+ from .modular_pipeline import Krea2ModularPipeline, Krea2TurboModularPipeline
38
+ else:
39
+ import sys
40
+
41
+ sys.modules[__name__] = _LazyModule(
42
+ __name__,
43
+ globals()["__file__"],
44
+ _import_structure,
45
+ module_spec=__spec__,
46
+ )
47
+
48
+ for name, value in _dummy_objects.items():
49
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/krea2/before_denoise.py ADDED
@@ -0,0 +1,590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import numpy as np
17
+ import torch
18
+
19
+ from ...models.transformers.transformer_krea2 import Krea2Transformer2DModel
20
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
21
+ from ...utils import logging
22
+ from ...utils.torch_utils import randn_tensor
23
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
24
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
25
+ from .modular_pipeline import Krea2ModularPipeline
26
+
27
+
28
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
29
+
30
+
31
+ # Copied from diffusers.pipelines.krea2.pipeline_krea2.calculate_shift
32
+ def calculate_shift(
33
+ image_seq_len,
34
+ base_seq_len: int = 256,
35
+ max_seq_len: int = 4096,
36
+ base_shift: float = 0.5,
37
+ max_shift: float = 1.15,
38
+ ):
39
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
40
+ b = base_shift - m * base_seq_len
41
+ mu = image_seq_len * m + b
42
+ return mu
43
+
44
+
45
+ # auto_docstring
46
+ class Krea2TextInputsStep(ModularPipelineBlocks):
47
+ """
48
+ Input step that determines `batch_size`/`dtype` from the per-prompt `prompt_embeds` and replicates the text
49
+ conditioning (and the optional negative branch) to `batch_size * num_images_per_prompt`. Place after the text
50
+ encoder.
51
+
52
+ Inputs:
53
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
54
+ The number of images to generate per prompt.
55
+ prompt_embeds (`Tensor`):
56
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
57
+ prompt_embeds_mask (`Tensor`):
58
+ Per-prompt boolean text mask (B, text_seq_len).
59
+ negative_prompt_embeds (`Tensor`, *optional*):
60
+ Per-prompt negative text features.
61
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
62
+ Per-prompt negative text mask.
63
+
64
+ Outputs:
65
+ batch_size (`int`):
66
+ Effective batch size (num prompts * num_images_per_prompt).
67
+ dtype (`dtype`):
68
+ The dtype of the text features.
69
+ prompt_embeds (`Tensor`):
70
+ Text features, batch-expanded.
71
+ prompt_embeds_mask (`Tensor`):
72
+ Text mask, batch-expanded.
73
+ negative_prompt_embeds (`Tensor`):
74
+ Negative text features, batch-expanded.
75
+ negative_prompt_embeds_mask (`Tensor`):
76
+ Negative text mask, batch-expanded.
77
+ """
78
+
79
+ model_name = "krea2"
80
+
81
+ @property
82
+ def description(self) -> str:
83
+ return (
84
+ "Input step that determines `batch_size`/`dtype` from the per-prompt `prompt_embeds` and replicates the "
85
+ "text conditioning (and the optional negative branch) to `batch_size * num_images_per_prompt`. Place after "
86
+ "the text encoder."
87
+ )
88
+
89
+ @property
90
+ def inputs(self) -> list[InputParam]:
91
+ return [
92
+ InputParam.template("num_images_per_prompt", default=1),
93
+ InputParam(
94
+ name="prompt_embeds",
95
+ required=True,
96
+ type_hint=torch.Tensor,
97
+ description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
98
+ ),
99
+ InputParam(
100
+ name="prompt_embeds_mask",
101
+ required=True,
102
+ type_hint=torch.Tensor,
103
+ description="Per-prompt boolean text mask (B, text_seq_len).",
104
+ ),
105
+ InputParam(
106
+ name="negative_prompt_embeds",
107
+ type_hint=torch.Tensor,
108
+ description="Per-prompt negative text features.",
109
+ ),
110
+ InputParam(
111
+ name="negative_prompt_embeds_mask",
112
+ type_hint=torch.Tensor,
113
+ description="Per-prompt negative text mask.",
114
+ ),
115
+ ]
116
+
117
+ @property
118
+ def intermediate_outputs(self) -> list[OutputParam]:
119
+ return [
120
+ OutputParam(
121
+ name="batch_size",
122
+ type_hint=int,
123
+ description="Effective batch size (num prompts * num_images_per_prompt).",
124
+ ),
125
+ OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
126
+ OutputParam(name="prompt_embeds", type_hint=torch.Tensor, description="Text features, batch-expanded."),
127
+ OutputParam(name="prompt_embeds_mask", type_hint=torch.Tensor, description="Text mask, batch-expanded."),
128
+ OutputParam(
129
+ name="negative_prompt_embeds",
130
+ type_hint=torch.Tensor,
131
+ description="Negative text features, batch-expanded.",
132
+ ),
133
+ OutputParam(
134
+ name="negative_prompt_embeds_mask",
135
+ type_hint=torch.Tensor,
136
+ description="Negative text mask, batch-expanded.",
137
+ ),
138
+ ]
139
+
140
+ @torch.no_grad()
141
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
142
+ block_state = self.get_block_state(state)
143
+
144
+ prompt_batch, seq_len, num_layers, dim = block_state.prompt_embeds.shape
145
+ n = block_state.num_images_per_prompt
146
+
147
+ block_state.dtype = block_state.prompt_embeds.dtype
148
+ block_state.batch_size = prompt_batch * n
149
+
150
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, n, 1, 1).view(
151
+ prompt_batch * n, seq_len, num_layers, dim
152
+ )
153
+ block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.repeat(1, n).view(prompt_batch * n, seq_len)
154
+
155
+ if block_state.negative_prompt_embeds is not None:
156
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(1, n, 1, 1).view(
157
+ prompt_batch * n, seq_len, num_layers, dim
158
+ )
159
+ block_state.negative_prompt_embeds_mask = block_state.negative_prompt_embeds_mask.repeat(1, n).view(
160
+ prompt_batch * n, seq_len
161
+ )
162
+
163
+ self.set_block_state(state, block_state)
164
+ return components, state
165
+
166
+
167
+ # auto_docstring
168
+ class Krea2TurboTextInputsStep(ModularPipelineBlocks):
169
+ """
170
+ Input step for the distilled Krea 2 turbo checkpoint that determines `batch_size`/`dtype` from the per-prompt
171
+ `prompt_embeds` and replicates the text conditioning to `batch_size * num_images_per_prompt`. The distilled
172
+ checkpoint runs without classifier-free guidance, so there is no negative branch. Place after the text encoder.
173
+
174
+ Inputs:
175
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
176
+ The number of images to generate per prompt.
177
+ prompt_embeds (`Tensor`):
178
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
179
+ prompt_embeds_mask (`Tensor`):
180
+ Per-prompt boolean text mask (B, text_seq_len).
181
+
182
+ Outputs:
183
+ batch_size (`int`):
184
+ Effective batch size (num prompts * num_images_per_prompt).
185
+ dtype (`dtype`):
186
+ The dtype of the text features.
187
+ prompt_embeds (`Tensor`):
188
+ Text features, batch-expanded.
189
+ prompt_embeds_mask (`Tensor`):
190
+ Text mask, batch-expanded.
191
+ """
192
+
193
+ model_name = "krea2"
194
+
195
+ @property
196
+ def description(self) -> str:
197
+ return (
198
+ "Input step for the distilled Krea 2 turbo checkpoint that determines `batch_size`/`dtype` from the "
199
+ "per-prompt `prompt_embeds` and replicates the text conditioning to `batch_size * num_images_per_prompt`. "
200
+ "The distilled checkpoint runs without classifier-free guidance, so there is no negative branch. Place "
201
+ "after the text encoder."
202
+ )
203
+
204
+ @property
205
+ def inputs(self) -> list[InputParam]:
206
+ return [
207
+ InputParam.template("num_images_per_prompt", default=1),
208
+ InputParam(
209
+ name="prompt_embeds",
210
+ required=True,
211
+ type_hint=torch.Tensor,
212
+ description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
213
+ ),
214
+ InputParam(
215
+ name="prompt_embeds_mask",
216
+ required=True,
217
+ type_hint=torch.Tensor,
218
+ description="Per-prompt boolean text mask (B, text_seq_len).",
219
+ ),
220
+ ]
221
+
222
+ @property
223
+ def intermediate_outputs(self) -> list[OutputParam]:
224
+ return [
225
+ OutputParam(
226
+ name="batch_size",
227
+ type_hint=int,
228
+ description="Effective batch size (num prompts * num_images_per_prompt).",
229
+ ),
230
+ OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
231
+ OutputParam(name="prompt_embeds", type_hint=torch.Tensor, description="Text features, batch-expanded."),
232
+ OutputParam(name="prompt_embeds_mask", type_hint=torch.Tensor, description="Text mask, batch-expanded."),
233
+ ]
234
+
235
+ @torch.no_grad()
236
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
237
+ block_state = self.get_block_state(state)
238
+
239
+ prompt_batch, seq_len, num_layers, dim = block_state.prompt_embeds.shape
240
+ n = block_state.num_images_per_prompt
241
+
242
+ block_state.dtype = block_state.prompt_embeds.dtype
243
+ block_state.batch_size = prompt_batch * n
244
+
245
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, n, 1, 1).view(
246
+ prompt_batch * n, seq_len, num_layers, dim
247
+ )
248
+ block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.repeat(1, n).view(prompt_batch * n, seq_len)
249
+
250
+ self.set_block_state(state, block_state)
251
+ return components, state
252
+
253
+
254
+ # auto_docstring
255
+ class Krea2PrepareLatentsStep(ModularPipelineBlocks):
256
+ """
257
+ Step that samples the spatial image latents and patch-packs them into (B, image_seq_len, in_channels) for the
258
+ denoising loop.
259
+
260
+ Components:
261
+ transformer (`Krea2Transformer2DModel`)
262
+
263
+ Inputs:
264
+ latents (`Tensor`, *optional*):
265
+ Pre-generated noisy latents for image generation.
266
+ height (`int`, *optional*, defaults to 1024):
267
+ The height in pixels of the generated image.
268
+ width (`int`, *optional*, defaults to 1024):
269
+ The width in pixels of the generated image.
270
+ generator (`Generator`, *optional*):
271
+ Torch generator for deterministic generation.
272
+ batch_size (`int`):
273
+ Effective batch size.
274
+ dtype (`dtype`):
275
+ The working dtype.
276
+
277
+ Outputs:
278
+ latents (`Tensor`):
279
+ The initial packed image latents (B, image_seq_len, in_channels).
280
+ image_seq_len (`int`):
281
+ Number of image tokens (grid_h * grid_w).
282
+ """
283
+
284
+ model_name = "krea2"
285
+
286
+ @property
287
+ def description(self) -> str:
288
+ return (
289
+ "Step that samples the spatial image latents and patch-packs them into (B, image_seq_len, in_channels) "
290
+ "for the denoising loop."
291
+ )
292
+
293
+ @property
294
+ def expected_components(self) -> list[ComponentSpec]:
295
+ return [ComponentSpec("transformer", Krea2Transformer2DModel)]
296
+
297
+ @property
298
+ def inputs(self) -> list[InputParam]:
299
+ return [
300
+ InputParam.template("latents"),
301
+ InputParam.template("height", default=1024),
302
+ InputParam.template("width", default=1024),
303
+ InputParam.template("generator"),
304
+ InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
305
+ InputParam(name="dtype", required=True, type_hint=torch.dtype, description="The working dtype."),
306
+ ]
307
+
308
+ @property
309
+ def intermediate_outputs(self) -> list[OutputParam]:
310
+ return [
311
+ OutputParam(
312
+ name="latents",
313
+ type_hint=torch.Tensor,
314
+ description="The initial packed image latents (B, image_seq_len, in_channels).",
315
+ ),
316
+ OutputParam(name="image_seq_len", type_hint=int, description="Number of image tokens (grid_h * grid_w)."),
317
+ ]
318
+
319
+ @torch.no_grad()
320
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
321
+ block_state = self.get_block_state(state)
322
+
323
+ device = components._execution_device
324
+ p = components.patch_size
325
+ num_channels_latents = components.transformer.config.in_channels // (p**2)
326
+
327
+ multiple = components.vae_scale_factor * components.patch_size
328
+ if block_state.height % multiple != 0 or block_state.width % multiple != 0:
329
+ rounded_height = ((block_state.height + multiple - 1) // multiple) * multiple
330
+ rounded_width = ((block_state.width + multiple - 1) // multiple) * multiple
331
+ logger.warning(
332
+ f"`height` and `width` must be multiples of {multiple}; rounding up from {block_state.height}x{block_state.width} to"
333
+ f" {rounded_height}x{rounded_width}."
334
+ )
335
+ block_state.height, block_state.width = rounded_height, rounded_width
336
+
337
+ latent_height = block_state.height // components.vae_scale_factor
338
+ latent_width = block_state.width // components.vae_scale_factor
339
+
340
+ if block_state.latents is not None:
341
+ block_state.latents = block_state.latents.to(device=device, dtype=block_state.dtype)
342
+ else:
343
+ latents = randn_tensor(
344
+ (block_state.batch_size, num_channels_latents, latent_height, latent_width),
345
+ generator=block_state.generator,
346
+ device=device,
347
+ dtype=block_state.dtype,
348
+ )
349
+ latents = latents.view(
350
+ block_state.batch_size, num_channels_latents, latent_height // p, p, latent_width // p, p
351
+ )
352
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
353
+ block_state.latents = latents.reshape(
354
+ block_state.batch_size, (latent_height // p) * (latent_width // p), num_channels_latents * p * p
355
+ )
356
+
357
+ block_state.image_seq_len = block_state.latents.shape[1]
358
+
359
+ self.set_block_state(state, block_state)
360
+ return components, state
361
+
362
+
363
+ # auto_docstring
364
+ class Krea2SetTimestepsStep(ModularPipelineBlocks):
365
+ """
366
+ Step that sets the Krea 2 flow-matching schedule on the scheduler: a linear sigma schedule with a resolution-aware
367
+ dynamic time shift `mu`.
368
+
369
+ Components:
370
+ scheduler (`FlowMatchEulerDiscreteScheduler`)
371
+
372
+ Inputs:
373
+ num_inference_steps (`int`, *optional*, defaults to 28):
374
+ The number of denoising steps.
375
+ sigmas (`list`, *optional*):
376
+ Custom sigma schedule (defaults to a linear ramp).
377
+ image_seq_len (`int`):
378
+ Number of image tokens, used to compute the resolution-aware shift.
379
+
380
+ Outputs:
381
+ timesteps (`Tensor`):
382
+ The denoising timesteps.
383
+ """
384
+
385
+ model_name = "krea2"
386
+
387
+ @property
388
+ def description(self) -> str:
389
+ return (
390
+ "Step that sets the Krea 2 flow-matching schedule on the scheduler: a linear sigma schedule with a "
391
+ "resolution-aware dynamic time shift `mu`."
392
+ )
393
+
394
+ @property
395
+ def expected_components(self) -> list[ComponentSpec]:
396
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
397
+
398
+ @property
399
+ def inputs(self) -> list[InputParam]:
400
+ return [
401
+ InputParam.template("num_inference_steps", default=28),
402
+ InputParam(
403
+ name="sigmas", type_hint=list, description="Custom sigma schedule (defaults to a linear ramp)."
404
+ ),
405
+ InputParam(
406
+ name="image_seq_len",
407
+ required=True,
408
+ type_hint=int,
409
+ description="Number of image tokens, used to compute the resolution-aware shift.",
410
+ ),
411
+ ]
412
+
413
+ @property
414
+ def intermediate_outputs(self) -> list[OutputParam]:
415
+ return [OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps.")]
416
+
417
+ @torch.no_grad()
418
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
419
+ block_state = self.get_block_state(state)
420
+
421
+ device = components._execution_device
422
+ num_inference_steps = block_state.num_inference_steps
423
+
424
+ sigmas = block_state.sigmas
425
+ if sigmas is None:
426
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
427
+ else:
428
+ block_state.num_inference_steps = len(sigmas)
429
+
430
+ config = components.scheduler.config
431
+ mu = calculate_shift(
432
+ block_state.image_seq_len,
433
+ config.get("base_image_seq_len", 256),
434
+ config.get("max_image_seq_len", 6400),
435
+ config.get("base_shift", 0.5),
436
+ config.get("max_shift", 1.15),
437
+ )
438
+
439
+ components.scheduler.set_timesteps(sigmas=sigmas, mu=mu, device=device)
440
+ components.scheduler.set_begin_index(0)
441
+ block_state.timesteps = components.scheduler.timesteps
442
+
443
+ self.set_block_state(state, block_state)
444
+ return components, state
445
+
446
+
447
+ # auto_docstring
448
+ class Krea2TurboSetTimestepsStep(ModularPipelineBlocks):
449
+ """
450
+ Step that sets the flow-matching schedule for the distilled Krea 2 turbo checkpoint on the scheduler: a linear
451
+ sigma schedule with the fixed time shift `mu=1.15` the checkpoint was distilled with.
452
+
453
+ Components:
454
+ scheduler (`FlowMatchEulerDiscreteScheduler`)
455
+
456
+ Inputs:
457
+ num_inference_steps (`int`, *optional*, defaults to 8):
458
+ The number of denoising steps.
459
+ sigmas (`list`, *optional*):
460
+ Custom sigma schedule (defaults to a linear ramp).
461
+
462
+ Outputs:
463
+ timesteps (`Tensor`):
464
+ The denoising timesteps.
465
+ """
466
+
467
+ model_name = "krea2"
468
+
469
+ @property
470
+ def description(self) -> str:
471
+ return (
472
+ "Step that sets the flow-matching schedule for the distilled Krea 2 turbo checkpoint on the scheduler: a "
473
+ "linear sigma schedule with the fixed time shift `mu=1.15` the checkpoint was distilled with."
474
+ )
475
+
476
+ @property
477
+ def expected_components(self) -> list[ComponentSpec]:
478
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
479
+
480
+ @property
481
+ def inputs(self) -> list[InputParam]:
482
+ return [
483
+ InputParam.template("num_inference_steps", default=8),
484
+ InputParam(
485
+ name="sigmas", type_hint=list, description="Custom sigma schedule (defaults to a linear ramp)."
486
+ ),
487
+ ]
488
+
489
+ @property
490
+ def intermediate_outputs(self) -> list[OutputParam]:
491
+ return [OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps.")]
492
+
493
+ @torch.no_grad()
494
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
495
+ block_state = self.get_block_state(state)
496
+
497
+ device = components._execution_device
498
+ num_inference_steps = block_state.num_inference_steps
499
+
500
+ sigmas = block_state.sigmas
501
+ if sigmas is None:
502
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
503
+ else:
504
+ block_state.num_inference_steps = len(sigmas)
505
+
506
+ components.scheduler.set_timesteps(sigmas=sigmas, mu=1.15, device=device)
507
+ components.scheduler.set_begin_index(0)
508
+ block_state.timesteps = components.scheduler.timesteps
509
+
510
+ self.set_block_state(state, block_state)
511
+ return components, state
512
+
513
+
514
+ # auto_docstring
515
+ class Krea2PreparePositionIdsStep(ModularPipelineBlocks):
516
+ """
517
+ Step that builds the shared rotary position ids for the combined [text | image] sequence: text at the origin, image
518
+ tokens at their (0, h, w) latent-grid coordinates. Place after prepare_latents.
519
+
520
+ Inputs:
521
+ height (`int`, *optional*, defaults to 1024):
522
+ The height in pixels of the generated image.
523
+ width (`int`, *optional*, defaults to 1024):
524
+ The width in pixels of the generated image.
525
+ prompt_embeds (`Tensor`):
526
+ Batch-expanded text features (only text_seq_len is used).
527
+
528
+ Outputs:
529
+ position_ids (`Tensor`):
530
+ Shared rotary coordinates (text_seq_len + grid_h * grid_w, 3).
531
+ """
532
+
533
+ model_name = "krea2"
534
+
535
+ @property
536
+ def description(self) -> str:
537
+ return (
538
+ "Step that builds the shared rotary position ids for the combined [text | image] sequence: text at the "
539
+ "origin, image tokens at their (0, h, w) latent-grid coordinates. Place after prepare_latents."
540
+ )
541
+
542
+ @property
543
+ def inputs(self) -> list[InputParam]:
544
+ return [
545
+ InputParam.template("height", default=1024),
546
+ InputParam.template("width", default=1024),
547
+ InputParam(
548
+ name="prompt_embeds",
549
+ required=True,
550
+ type_hint=torch.Tensor,
551
+ description="Batch-expanded text features (only text_seq_len is used).",
552
+ ),
553
+ ]
554
+
555
+ @property
556
+ def intermediate_outputs(self) -> list[OutputParam]:
557
+ return [
558
+ OutputParam(
559
+ name="position_ids",
560
+ type_hint=torch.Tensor,
561
+ description="Shared rotary coordinates (text_seq_len + grid_h * grid_w, 3).",
562
+ )
563
+ ]
564
+
565
+ @staticmethod
566
+ # Copied from diffusers.pipelines.krea2.pipeline_krea2.Krea2Pipeline.prepare_position_ids
567
+ def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
568
+ """Build the `(text_seq_len + grid_height * grid_width, 3)` rotary coordinates for the combined sequence:
569
+ text tokens sit at the origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
570
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
571
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
572
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
573
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
574
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
575
+ return torch.cat([text_ids, image_ids], dim=0)
576
+
577
+ @torch.no_grad()
578
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
579
+ block_state = self.get_block_state(state)
580
+
581
+ device = components._execution_device
582
+ p = components.patch_size
583
+ grid_h = block_state.height // (components.vae_scale_factor * p)
584
+ grid_w = block_state.width // (components.vae_scale_factor * p)
585
+ text_seq_len = block_state.prompt_embeds.shape[1]
586
+
587
+ block_state.position_ids = self.prepare_position_ids(text_seq_len, grid_h, grid_w, device)
588
+
589
+ self.set_block_state(state, block_state)
590
+ return components, state
diffusers/modular_pipelines/krea2/decoders.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...image_processor import VaeImageProcessor
20
+ from ...models import AutoencoderKLQwenImage
21
+ from ...utils import logging
22
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+ from .modular_pipeline import Krea2ModularPipeline
25
+
26
+
27
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+
30
+ # auto_docstring
31
+ class Krea2DecodeStep(ModularPipelineBlocks):
32
+ """
33
+ Step that unpacks the denoised packed latents back to the spatial grid, de-normalizes them with the VAE's
34
+ per-channel statistics, and decodes them through the Qwen-Image VAE into images.
35
+
36
+ Components:
37
+ vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
38
+
39
+ Inputs:
40
+ output_type (`str`, *optional*, defaults to pil):
41
+ Output format: 'pil', 'np', 'pt'.
42
+ height (`int`, *optional*, defaults to 1024):
43
+ The height in pixels of the generated image.
44
+ width (`int`, *optional*, defaults to 1024):
45
+ The width in pixels of the generated image.
46
+ latents (`Tensor`):
47
+ The denoised packed latents (B, image_seq_len, in_channels) from the denoising loop.
48
+
49
+ Outputs:
50
+ images (`list`):
51
+ Generated images.
52
+ """
53
+
54
+ model_name = "krea2"
55
+
56
+ @property
57
+ def description(self) -> str:
58
+ return (
59
+ "Step that unpacks the denoised packed latents back to the spatial grid, de-normalizes them with the "
60
+ "VAE's per-channel statistics, and decodes them through the Qwen-Image VAE into images."
61
+ )
62
+
63
+ @property
64
+ def expected_components(self) -> list[ComponentSpec]:
65
+ return [
66
+ ComponentSpec("vae", AutoencoderKLQwenImage),
67
+ ComponentSpec(
68
+ "image_processor",
69
+ VaeImageProcessor,
70
+ # Effective pixel-to-token downsampling factor: vae_scale_factor (8) * patch_size (2).
71
+ config=FrozenDict({"vae_scale_factor": 16}),
72
+ default_creation_method="from_config",
73
+ ),
74
+ ]
75
+
76
+ @property
77
+ def inputs(self) -> list[InputParam]:
78
+ return [
79
+ InputParam.template("output_type", default="pil"),
80
+ InputParam.template("height", default=1024),
81
+ InputParam.template("width", default=1024),
82
+ InputParam(
83
+ name="latents",
84
+ required=True,
85
+ type_hint=torch.Tensor,
86
+ description="The denoised packed latents (B, image_seq_len, in_channels) from the denoising loop.",
87
+ ),
88
+ ]
89
+
90
+ @property
91
+ def intermediate_outputs(self) -> list[OutputParam]:
92
+ return [OutputParam.template("images")]
93
+
94
+ @torch.no_grad()
95
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
96
+ block_state = self.get_block_state(state)
97
+
98
+ vae = components.vae
99
+ p = components.patch_size
100
+ latents = block_state.latents
101
+
102
+ batch_size, _, channels = latents.shape
103
+ height = p * (int(block_state.height) // (components.vae_scale_factor * p))
104
+ width = p * (int(block_state.width) // (components.vae_scale_factor * p))
105
+ latents = latents.view(batch_size, height // p, width // p, channels // (p * p), p, p)
106
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
107
+ latents = latents.reshape(batch_size, channels // (p * p), 1, height, width)
108
+
109
+ latents = latents.to(vae.dtype)
110
+ latents_mean = (
111
+ torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(latents.device, latents.dtype)
112
+ )
113
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
114
+ latents.device, latents.dtype
115
+ )
116
+ latents = latents / latents_std + latents_mean
117
+ image = vae.decode(latents, return_dict=False)[0][:, :, 0]
118
+ block_state.images = components.image_processor.postprocess(image, output_type=block_state.output_type)
119
+
120
+ self.set_block_state(state, block_state)
121
+ return components, state
diffusers/modular_pipelines/krea2/denoise.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...guiders import ClassifierFreeGuidance
20
+ from ...models.transformers.transformer_krea2 import Krea2Transformer2DModel
21
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
22
+ from ...utils import logging
23
+ from ..modular_pipeline import (
24
+ BlockState,
25
+ LoopSequentialPipelineBlocks,
26
+ ModularPipelineBlocks,
27
+ PipelineState,
28
+ )
29
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
30
+ from .modular_pipeline import Krea2ModularPipeline
31
+
32
+
33
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+
36
+ class Krea2LoopBeforeDenoiser(ModularPipelineBlocks):
37
+ model_name = "krea2"
38
+
39
+ @property
40
+ def description(self) -> str:
41
+ return (
42
+ "Within the denoising loop: normalize the scheduler timestep into the model's flow time and broadcast it "
43
+ "across the batch. Compose into the `sub_blocks` of a `Krea2DenoiseLoopWrapper`-based step."
44
+ )
45
+
46
+ @property
47
+ def expected_components(self) -> list[ComponentSpec]:
48
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
49
+
50
+ @property
51
+ def inputs(self) -> list[InputParam]:
52
+ return [
53
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
54
+ InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
55
+ ]
56
+
57
+ @torch.no_grad()
58
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
59
+ num_train_timesteps = components.scheduler.config.num_train_timesteps
60
+ block_state.timestep = (t / num_train_timesteps).expand(block_state.batch_size)
61
+ return components, block_state
62
+
63
+
64
+ class Krea2LoopDenoiser(ModularPipelineBlocks):
65
+ model_name = "krea2"
66
+
67
+ @property
68
+ def description(self) -> str:
69
+ return (
70
+ "Within the denoising loop: run the `transformer` on the conditional (and, when the guider enables CFG, "
71
+ "the negative) text features and combine them through the `guider`. Compose into `Krea2DenoiseStep`."
72
+ )
73
+
74
+ @property
75
+ def expected_components(self) -> list[ComponentSpec]:
76
+ return [
77
+ ComponentSpec(
78
+ "guider",
79
+ ClassifierFreeGuidance,
80
+ # Krea 2 uses cond-anchored CFG (`cond + scale * (cond - uncond)`), which is the
81
+ # `use_original_formulation` branch of ClassifierFreeGuidance; scale 0 disables it (distilled TDM).
82
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
83
+ default_creation_method="from_config",
84
+ ),
85
+ ComponentSpec("transformer", Krea2Transformer2DModel),
86
+ ]
87
+
88
+ @property
89
+ def inputs(self) -> list[InputParam]:
90
+ return [
91
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
92
+ InputParam.template("num_inference_steps", required=True),
93
+ InputParam(
94
+ name="prompt_embeds",
95
+ required=True,
96
+ type_hint=torch.Tensor,
97
+ description="Conditional stacked text features.",
98
+ ),
99
+ InputParam(
100
+ name="prompt_embeds_mask", required=True, type_hint=torch.Tensor, description="Conditional text mask."
101
+ ),
102
+ InputParam(
103
+ name="position_ids",
104
+ required=True,
105
+ type_hint=torch.Tensor,
106
+ description="Shared rotary coordinates for the [text | image] sequence.",
107
+ ),
108
+ InputParam(
109
+ name="negative_prompt_embeds", type_hint=torch.Tensor, description="Negative stacked text features."
110
+ ),
111
+ InputParam(name="negative_prompt_embeds_mask", type_hint=torch.Tensor, description="Negative text mask."),
112
+ InputParam.template("attention_kwargs"),
113
+ ]
114
+
115
+ @torch.no_grad()
116
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
117
+ transformer = components.transformer
118
+
119
+ latents = block_state.latents.to(transformer.dtype)
120
+ timestep = block_state.timestep.to(transformer.dtype)
121
+
122
+ guider_inputs = {
123
+ "encoder_hidden_states": (
124
+ block_state.prompt_embeds.to(transformer.dtype),
125
+ block_state.negative_prompt_embeds.to(transformer.dtype)
126
+ if block_state.negative_prompt_embeds is not None
127
+ else None,
128
+ ),
129
+ "encoder_attention_mask": (
130
+ block_state.prompt_embeds_mask,
131
+ block_state.negative_prompt_embeds_mask,
132
+ ),
133
+ }
134
+
135
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
136
+ guider_state = components.guider.prepare_inputs(guider_inputs)
137
+
138
+ for guider_state_batch in guider_state:
139
+ components.guider.prepare_models(components.transformer)
140
+ cond_kwargs = {name: getattr(guider_state_batch, name) for name in guider_inputs}
141
+ guider_state_batch.noise_pred = transformer(
142
+ hidden_states=latents,
143
+ timestep=timestep,
144
+ position_ids=block_state.position_ids,
145
+ attention_kwargs=block_state.attention_kwargs,
146
+ return_dict=False,
147
+ **cond_kwargs,
148
+ )[0]
149
+ components.guider.cleanup_models(components.transformer)
150
+
151
+ block_state.noise_pred = components.guider(guider_state).pred
152
+ return components, block_state
153
+
154
+
155
+ class Krea2TurboLoopDenoiser(ModularPipelineBlocks):
156
+ model_name = "krea2"
157
+
158
+ @property
159
+ def description(self) -> str:
160
+ return (
161
+ "Within the denoising loop: run the `transformer` on the conditional text features. The distilled Krea 2 "
162
+ "turbo checkpoint runs without classifier-free guidance, so there is no negative branch or guider. Compose "
163
+ "into the `sub_blocks` of `Krea2TurboDenoiseStep`."
164
+ )
165
+
166
+ @property
167
+ def expected_components(self) -> list[ComponentSpec]:
168
+ return [ComponentSpec("transformer", Krea2Transformer2DModel)]
169
+
170
+ @property
171
+ def inputs(self) -> list[InputParam]:
172
+ return [
173
+ InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
174
+ InputParam(
175
+ name="prompt_embeds",
176
+ required=True,
177
+ type_hint=torch.Tensor,
178
+ description="Conditional stacked text features.",
179
+ ),
180
+ InputParam(
181
+ name="prompt_embeds_mask", required=True, type_hint=torch.Tensor, description="Conditional text mask."
182
+ ),
183
+ InputParam(
184
+ name="position_ids",
185
+ required=True,
186
+ type_hint=torch.Tensor,
187
+ description="Shared rotary coordinates for the [text | image] sequence.",
188
+ ),
189
+ InputParam.template("attention_kwargs"),
190
+ ]
191
+
192
+ @torch.no_grad()
193
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
194
+ transformer = components.transformer
195
+
196
+ latents = block_state.latents.to(transformer.dtype)
197
+ timestep = block_state.timestep.to(transformer.dtype)
198
+
199
+ block_state.noise_pred = transformer(
200
+ hidden_states=latents,
201
+ timestep=timestep,
202
+ position_ids=block_state.position_ids,
203
+ attention_kwargs=block_state.attention_kwargs,
204
+ encoder_hidden_states=block_state.prompt_embeds.to(transformer.dtype),
205
+ encoder_attention_mask=block_state.prompt_embeds_mask,
206
+ return_dict=False,
207
+ )[0]
208
+ return components, block_state
209
+
210
+
211
+ class Krea2LoopAfterDenoiser(ModularPipelineBlocks):
212
+ model_name = "krea2"
213
+
214
+ @property
215
+ def description(self) -> str:
216
+ return "Within the denoising loop: scheduler step. Compose into a `Krea2DenoiseLoopWrapper`-based step."
217
+
218
+ @property
219
+ def expected_components(self) -> list[ComponentSpec]:
220
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
221
+
222
+ @property
223
+ def intermediate_outputs(self) -> list[OutputParam]:
224
+ return [OutputParam(name="latents", type_hint=torch.Tensor, description="The denoised latents.")]
225
+
226
+ @torch.no_grad()
227
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
228
+ latents_dtype = block_state.latents.dtype
229
+ block_state.latents = components.scheduler.step(
230
+ block_state.noise_pred, t, block_state.latents, return_dict=False
231
+ )[0]
232
+ block_state.latents = block_state.latents.to(latents_dtype)
233
+ return components, block_state
234
+
235
+
236
+ class Krea2DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
237
+ model_name = "krea2"
238
+
239
+ @property
240
+ def description(self) -> str:
241
+ return (
242
+ "Pipeline block that iteratively denoises the packed image latents over `timesteps`. "
243
+ "The specific steps within each iteration can be customized with the `sub_blocks` attribute."
244
+ )
245
+
246
+ @property
247
+ def loop_expected_components(self) -> list[ComponentSpec]:
248
+ return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
249
+
250
+ @property
251
+ def loop_inputs(self) -> list[InputParam]:
252
+ return [
253
+ InputParam(
254
+ name="timesteps",
255
+ required=True,
256
+ type_hint=torch.Tensor,
257
+ description="Denoising timesteps from set_timesteps.",
258
+ ),
259
+ InputParam.template("num_inference_steps", required=True),
260
+ InputParam.template("attention_kwargs"),
261
+ ]
262
+
263
+ @torch.no_grad()
264
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
265
+ block_state = self.get_block_state(state)
266
+
267
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
268
+ for i, t in enumerate(block_state.timesteps):
269
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
270
+ progress_bar.update()
271
+
272
+ self.set_block_state(state, block_state)
273
+ return components, state
274
+
275
+
276
+ # auto_docstring
277
+ class Krea2DenoiseStep(Krea2DenoiseLoopWrapper):
278
+ """
279
+ Denoising loop that iteratively denoises the packed image latents over `timesteps`, running the transformer on the
280
+ conditional (and, when the guider enables CFG, the negative) text features and combining them through the `guider`.
281
+
282
+ Components:
283
+ scheduler (`FlowMatchEulerDiscreteScheduler`) guider (`ClassifierFreeGuidance`) transformer
284
+ (`Krea2Transformer2DModel`)
285
+
286
+ Inputs:
287
+ timesteps (`Tensor`):
288
+ Denoising timesteps from set_timesteps.
289
+ num_inference_steps (`int`):
290
+ The number of denoising steps.
291
+ attention_kwargs (`dict`, *optional*):
292
+ Additional kwargs for attention processors.
293
+ latents (`Tensor`):
294
+ Packed image latents.
295
+ batch_size (`int`):
296
+ Effective batch size.
297
+ prompt_embeds (`Tensor`):
298
+ Conditional stacked text features.
299
+ prompt_embeds_mask (`Tensor`):
300
+ Conditional text mask.
301
+ position_ids (`Tensor`):
302
+ Shared rotary coordinates for the [text | image] sequence.
303
+ negative_prompt_embeds (`Tensor`, *optional*):
304
+ Negative stacked text features.
305
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
306
+ Negative text mask.
307
+
308
+ Outputs:
309
+ latents (`Tensor`):
310
+ The denoised latents.
311
+ """
312
+
313
+ model_name = "krea2"
314
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2LoopDenoiser, Krea2LoopAfterDenoiser]
315
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
316
+
317
+ @property
318
+ def description(self) -> str:
319
+ return (
320
+ "Denoising loop that iteratively denoises the packed image latents over `timesteps`, running the "
321
+ "transformer on the conditional (and, when the guider enables CFG, the negative) text features and "
322
+ "combining them through the `guider`."
323
+ )
324
+
325
+
326
+ # auto_docstring
327
+ class Krea2TurboDenoiseStep(Krea2DenoiseLoopWrapper):
328
+ """
329
+ Denoising loop for the distilled Krea 2 turbo checkpoint that iteratively denoises the packed image latents over
330
+ `timesteps`, running the transformer on the conditional text features. The distilled checkpoint runs without
331
+ classifier-free guidance.
332
+
333
+ Components:
334
+ scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Krea2Transformer2DModel`)
335
+
336
+ Inputs:
337
+ timesteps (`Tensor`):
338
+ Denoising timesteps from set_timesteps.
339
+ num_inference_steps (`int`):
340
+ The number of denoising steps.
341
+ attention_kwargs (`dict`, *optional*):
342
+ Additional kwargs for attention processors.
343
+ latents (`Tensor`):
344
+ Packed image latents.
345
+ batch_size (`int`):
346
+ Effective batch size.
347
+ prompt_embeds (`Tensor`):
348
+ Conditional stacked text features.
349
+ prompt_embeds_mask (`Tensor`):
350
+ Conditional text mask.
351
+ position_ids (`Tensor`):
352
+ Shared rotary coordinates for the [text | image] sequence.
353
+
354
+ Outputs:
355
+ latents (`Tensor`):
356
+ The denoised latents.
357
+ """
358
+
359
+ model_name = "krea2"
360
+ block_classes = [Krea2LoopBeforeDenoiser, Krea2TurboLoopDenoiser, Krea2LoopAfterDenoiser]
361
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
362
+
363
+ @property
364
+ def description(self) -> str:
365
+ return (
366
+ "Denoising loop for the distilled Krea 2 turbo checkpoint that iteratively denoises the packed image "
367
+ "latents over `timesteps`, running the transformer on the conditional text features. The distilled "
368
+ "checkpoint runs without classifier-free guidance."
369
+ )
diffusers/modular_pipelines/krea2/encoders.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+ from transformers import AutoTokenizer, Qwen3VLModel
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...guiders import ClassifierFreeGuidance
21
+ from ...utils import logging
22
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+ from .modular_pipeline import Krea2ModularPipeline
25
+
26
+
27
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
28
+
29
+
30
+ # Indices into the Qwen3-VL `hidden_states` tuple (0 is the embedding output) whose states are stacked per token as the
31
+ # transformer's text conditioning. Must have `transformer.config.num_text_layers` entries.
32
+ KREA2_TEXT_ENCODER_SELECT_LAYERS = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
33
+
34
+ # Krea 2 wraps the prompt in this Qwen-Image chat template before encoding. The prompt is padded to a fixed length
35
+ # first and the assistant suffix is appended *after* the padding (matching how the model was sampled at training time);
36
+ # the first `_PROMPT_TEMPLATE_ENCODE_START_IDX` (system prefix) tokens are dropped from the encoder outputs.
37
+ _PROMPT_TEMPLATE_ENCODE_PREFIX = (
38
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
39
+ "spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
40
+ )
41
+ _PROMPT_TEMPLATE_ENCODE_SUFFIX = "<|im_end|>\n<|im_start|>assistant\n"
42
+ _PROMPT_TEMPLATE_ENCODE_START_IDX = 34
43
+ _PROMPT_TEMPLATE_ENCODE_NUM_SUFFIX_TOKENS = 5
44
+
45
+
46
+ # auto_docstring
47
+ class Krea2TextEncoderStep(ModularPipelineBlocks):
48
+ """
49
+ Text encoder step that tokenizes the prompt(s) with the Krea 2 chat template, runs the Qwen3-VL text encoder, and
50
+ stacks a fixed set of decoder-layer hidden states per token as the transformer's text conditioning. The negative
51
+ prompt is encoded the same way when the guider enables CFG.
52
+
53
+ Components:
54
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
55
+ with the text encoder. guider (`ClassifierFreeGuidance`)
56
+
57
+ Inputs:
58
+ prompt (`str`):
59
+ The prompt or prompts to guide image generation.
60
+ negative_prompt (`str`, *optional*):
61
+ The negative prompt(s) for CFG.
62
+ max_sequence_length (`int`, *optional*, defaults to 512):
63
+ Maximum sequence length for prompt encoding.
64
+
65
+ Outputs:
66
+ prompt_embeds (`Tensor`):
67
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
68
+ prompt_embeds_mask (`Tensor`):
69
+ Per-prompt boolean text mask (B, text_seq_len).
70
+ negative_prompt_embeds (`Tensor`):
71
+ Per-prompt negative text features (only when guidance is enabled).
72
+ negative_prompt_embeds_mask (`Tensor`):
73
+ Per-prompt negative text mask (only when guidance is enabled).
74
+ """
75
+
76
+ model_name = "krea2"
77
+
78
+ @property
79
+ def description(self) -> str:
80
+ return (
81
+ "Text encoder step that tokenizes the prompt(s) with the Krea 2 chat template, runs the Qwen3-VL text "
82
+ "encoder, and stacks a fixed set of decoder-layer hidden states per token as the transformer's text "
83
+ "conditioning. The negative prompt is encoded the same way when the guider enables CFG."
84
+ )
85
+
86
+ @property
87
+ def expected_components(self) -> list[ComponentSpec]:
88
+ return [
89
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
90
+ ComponentSpec("tokenizer", AutoTokenizer, description="The tokenizer paired with the text encoder."),
91
+ ComponentSpec(
92
+ "guider",
93
+ ClassifierFreeGuidance,
94
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
95
+ default_creation_method="from_config",
96
+ ),
97
+ ]
98
+
99
+ @property
100
+ def inputs(self) -> list[InputParam]:
101
+ return [
102
+ InputParam.template("prompt", required=True),
103
+ InputParam(name="negative_prompt", type_hint=str, description="The negative prompt(s) for CFG."),
104
+ InputParam.template("max_sequence_length", default=512),
105
+ ]
106
+
107
+ @property
108
+ def intermediate_outputs(self) -> list[OutputParam]:
109
+ return [
110
+ OutputParam(
111
+ name="prompt_embeds",
112
+ type_hint=torch.Tensor,
113
+ description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
114
+ ),
115
+ OutputParam(
116
+ name="prompt_embeds_mask",
117
+ type_hint=torch.Tensor,
118
+ description="Per-prompt boolean text mask (B, text_seq_len).",
119
+ ),
120
+ OutputParam(
121
+ name="negative_prompt_embeds",
122
+ type_hint=torch.Tensor,
123
+ description="Per-prompt negative text features (only when guidance is enabled).",
124
+ ),
125
+ OutputParam(
126
+ name="negative_prompt_embeds_mask",
127
+ type_hint=torch.Tensor,
128
+ description="Per-prompt negative text mask (only when guidance is enabled).",
129
+ ),
130
+ ]
131
+
132
+ def _encode_prompt(self, components, prompt, max_sequence_length, device):
133
+ """Tokenize `prompt` into the fixed-length Krea 2 layout and tap the selected encoder hidden states.
134
+
135
+ Mirrors `Krea2Pipeline.get_text_hidden_states`. Returns a `(hidden_states, attention_mask)` tuple of shapes
136
+ `(batch_size, text_seq_len, num_text_layers, text_hidden_dim)` and `(batch_size, text_seq_len)` (bool).
137
+ """
138
+ tokenizer = components.tokenizer
139
+ prompt = [prompt] if isinstance(prompt, str) else prompt
140
+ prefix_idx = _PROMPT_TEMPLATE_ENCODE_START_IDX
141
+ text = [_PROMPT_TEMPLATE_ENCODE_PREFIX + e for e in prompt]
142
+ text_tokens = tokenizer(
143
+ text,
144
+ truncation=True,
145
+ padding="max_length",
146
+ max_length=max_sequence_length + prefix_idx - _PROMPT_TEMPLATE_ENCODE_NUM_SUFFIX_TOKENS,
147
+ return_tensors="pt",
148
+ ).to(device)
149
+ suffix_tokens = tokenizer([_PROMPT_TEMPLATE_ENCODE_SUFFIX] * len(text), return_tensors="pt").to(device)
150
+
151
+ input_ids = torch.cat([text_tokens.input_ids, suffix_tokens.input_ids], dim=1)
152
+ attention_mask = torch.cat([text_tokens.attention_mask, suffix_tokens.attention_mask], dim=1).bool()
153
+
154
+ # Krea 2 pads in the middle of the template (`[prefix | prompt | PAD | suffix]`), so the suffix tokens sit
155
+ # downstream of the padding. The text features must use positions that count only real tokens (padding does
156
+ # not consume a position) to match how the model was trained; otherwise the suffix gets a shifted mRoPE phase.
157
+ position_ids = (attention_mask.long().cumsum(dim=-1) - 1).clamp(min=0)
158
+ position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
159
+
160
+ outputs = components.text_encoder(
161
+ input_ids=input_ids,
162
+ attention_mask=attention_mask,
163
+ position_ids=position_ids,
164
+ output_hidden_states=True,
165
+ )
166
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in KREA2_TEXT_ENCODER_SELECT_LAYERS], dim=2)
167
+
168
+ hidden_states = hidden_states[:, prefix_idx:]
169
+ attention_mask = attention_mask[:, prefix_idx:]
170
+ return hidden_states, attention_mask
171
+
172
+ @torch.no_grad()
173
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
174
+ block_state = self.get_block_state(state)
175
+
176
+ device = components._execution_device
177
+ prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
178
+
179
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
180
+ components, prompts, block_state.max_sequence_length, device
181
+ )
182
+
183
+ block_state.negative_prompt_embeds = None
184
+ block_state.negative_prompt_embeds_mask = None
185
+ if components.requires_unconditional_embeds:
186
+ negative_prompt = block_state.negative_prompt
187
+ if negative_prompt is None:
188
+ negative_prompt = ""
189
+ if isinstance(negative_prompt, str):
190
+ negative_prompt = [negative_prompt] * len(prompts)
191
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = self._encode_prompt(
192
+ components, negative_prompt, block_state.max_sequence_length, device
193
+ )
194
+
195
+ self.set_block_state(state, block_state)
196
+ return components, state
197
+
198
+
199
+ # auto_docstring
200
+ class Krea2TurboTextEncoderStep(Krea2TextEncoderStep):
201
+ """
202
+ Text encoder step for the distilled Krea 2 turbo checkpoint that tokenizes the prompt(s) with the Krea 2 chat
203
+ template, runs the Qwen3-VL text encoder, and stacks a fixed set of decoder-layer hidden states per token as the
204
+ transformer's text conditioning. The distilled checkpoint runs without classifier-free guidance, so it takes no
205
+ negative prompt and has no guider.
206
+
207
+ Components:
208
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
209
+ with the text encoder.
210
+
211
+ Inputs:
212
+ prompt (`str`):
213
+ The prompt or prompts to guide image generation.
214
+ max_sequence_length (`int`, *optional*, defaults to 512):
215
+ Maximum sequence length for prompt encoding.
216
+
217
+ Outputs:
218
+ prompt_embeds (`Tensor`):
219
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
220
+ prompt_embeds_mask (`Tensor`):
221
+ Per-prompt boolean text mask (B, text_seq_len).
222
+ """
223
+
224
+ model_name = "krea2"
225
+
226
+ @property
227
+ def description(self) -> str:
228
+ return (
229
+ "Text encoder step for the distilled Krea 2 turbo checkpoint that tokenizes the prompt(s) with the Krea 2 "
230
+ "chat template, runs the Qwen3-VL text encoder, and stacks a fixed set of decoder-layer hidden states per "
231
+ "token as the transformer's text conditioning. The distilled checkpoint runs without classifier-free "
232
+ "guidance, so it takes no negative prompt and has no guider."
233
+ )
234
+
235
+ @property
236
+ def expected_components(self) -> list[ComponentSpec]:
237
+ return [
238
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
239
+ ComponentSpec("tokenizer", AutoTokenizer, description="The tokenizer paired with the text encoder."),
240
+ ]
241
+
242
+ @property
243
+ def inputs(self) -> list[InputParam]:
244
+ return [
245
+ InputParam.template("prompt", required=True),
246
+ InputParam.template("max_sequence_length", default=512),
247
+ ]
248
+
249
+ @property
250
+ def intermediate_outputs(self) -> list[OutputParam]:
251
+ return [
252
+ OutputParam(
253
+ name="prompt_embeds",
254
+ type_hint=torch.Tensor,
255
+ description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
256
+ ),
257
+ OutputParam(
258
+ name="prompt_embeds_mask",
259
+ type_hint=torch.Tensor,
260
+ description="Per-prompt boolean text mask (B, text_seq_len).",
261
+ ),
262
+ ]
263
+
264
+ @torch.no_grad()
265
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
266
+ block_state = self.get_block_state(state)
267
+
268
+ device = components._execution_device
269
+ prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
270
+
271
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
272
+ components, prompts, block_state.max_sequence_length, device
273
+ )
274
+
275
+ self.set_block_state(state, block_state)
276
+ return components, state
diffusers/modular_pipelines/krea2/modular_blocks_krea2.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Krea2PrepareLatentsStep,
21
+ Krea2PreparePositionIdsStep,
22
+ Krea2SetTimestepsStep,
23
+ Krea2TextInputsStep,
24
+ )
25
+ from .decoders import Krea2DecodeStep
26
+ from .denoise import Krea2DenoiseStep
27
+ from .encoders import Krea2TextEncoderStep
28
+
29
+
30
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
31
+
32
+
33
+ CORE_DENOISE_BLOCKS = InsertableDict(
34
+ [
35
+ ("input", Krea2TextInputsStep()),
36
+ ("prepare_latents", Krea2PrepareLatentsStep()),
37
+ ("set_timesteps", Krea2SetTimestepsStep()),
38
+ ("prepare_position_ids", Krea2PreparePositionIdsStep()),
39
+ ("denoise", Krea2DenoiseStep()),
40
+ ]
41
+ )
42
+
43
+
44
+ # auto_docstring
45
+ class Krea2CoreDenoiseStep(SequentialPipelineBlocks):
46
+ """
47
+ Core denoising workflow for Krea 2 text-to-image: prepares the batch/latents/timesteps and the shared position ids,
48
+ then runs the symmetric-CFG denoising loop, producing the denoised packed latents for the decoder.
49
+
50
+ Components:
51
+ transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`) guider
52
+ (`ClassifierFreeGuidance`)
53
+
54
+ Inputs:
55
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
56
+ The number of images to generate per prompt.
57
+ prompt_embeds (`Tensor`):
58
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
59
+ prompt_embeds_mask (`Tensor`):
60
+ Per-prompt boolean text mask (B, text_seq_len).
61
+ negative_prompt_embeds (`Tensor`, *optional*):
62
+ Per-prompt negative text features.
63
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
64
+ Per-prompt negative text mask.
65
+ latents (`Tensor`, *optional*):
66
+ Pre-generated noisy latents for image generation.
67
+ height (`int`, *optional*, defaults to 1024):
68
+ The height in pixels of the generated image.
69
+ width (`int`, *optional*, defaults to 1024):
70
+ The width in pixels of the generated image.
71
+ generator (`Generator`, *optional*):
72
+ Torch generator for deterministic generation.
73
+ num_inference_steps (`int`, *optional*, defaults to 28):
74
+ The number of denoising steps.
75
+ sigmas (`list`, *optional*):
76
+ Custom sigma schedule (defaults to a linear ramp).
77
+ attention_kwargs (`dict`, *optional*):
78
+ Additional kwargs for attention processors.
79
+
80
+ Outputs:
81
+ latents (`Tensor`):
82
+ The denoised packed latents (B, image_seq_len, in_channels).
83
+ """
84
+
85
+ model_name = "krea2"
86
+ block_classes = list(CORE_DENOISE_BLOCKS.values())
87
+ block_names = list(CORE_DENOISE_BLOCKS.keys())
88
+
89
+ @property
90
+ def description(self) -> str:
91
+ return (
92
+ "Core denoising workflow for Krea 2 text-to-image: prepares the batch/latents/timesteps and the shared "
93
+ "position ids, then runs the symmetric-CFG denoising loop, producing the denoised packed latents for the "
94
+ "decoder."
95
+ )
96
+
97
+ @property
98
+ def outputs(self) -> list[OutputParam]:
99
+ return [
100
+ OutputParam.template("latents", description="The denoised packed latents (B, image_seq_len, in_channels).")
101
+ ]
102
+
103
+
104
+ # auto_docstring
105
+ class Krea2AutoBlocks(SequentialPipelineBlocks):
106
+ """
107
+ Auto Modular pipeline for text-to-image generation using Krea 2: encode text -> core denoise (symmetric CFG) ->
108
+ decode.
109
+
110
+ Supported workflows:
111
+ - `text2image`: requires `prompt`
112
+
113
+ Components:
114
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
115
+ with the text encoder. guider (`ClassifierFreeGuidance`) transformer (`Krea2Transformer2DModel`) scheduler
116
+ (`FlowMatchEulerDiscreteScheduler`) vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
117
+
118
+ Inputs:
119
+ prompt (`str`):
120
+ The prompt or prompts to guide image generation.
121
+ negative_prompt (`str`, *optional*):
122
+ The negative prompt(s) for CFG.
123
+ max_sequence_length (`int`, *optional*, defaults to 512):
124
+ Maximum sequence length for prompt encoding.
125
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
126
+ The number of images to generate per prompt.
127
+ latents (`Tensor`, *optional*):
128
+ Pre-generated noisy latents for image generation.
129
+ height (`int`, *optional*, defaults to 1024):
130
+ The height in pixels of the generated image.
131
+ width (`int`, *optional*, defaults to 1024):
132
+ The width in pixels of the generated image.
133
+ generator (`Generator`, *optional*):
134
+ Torch generator for deterministic generation.
135
+ num_inference_steps (`int`, *optional*, defaults to 28):
136
+ The number of denoising steps.
137
+ sigmas (`list`, *optional*):
138
+ Custom sigma schedule (defaults to a linear ramp).
139
+ attention_kwargs (`dict`, *optional*):
140
+ Additional kwargs for attention processors.
141
+ output_type (`str`, *optional*, defaults to pil):
142
+ Output format: 'pil', 'np', 'pt'.
143
+
144
+ Outputs:
145
+ images (`list`):
146
+ Generated images.
147
+ """
148
+
149
+ model_name = "krea2"
150
+ block_classes = [
151
+ Krea2TextEncoderStep,
152
+ Krea2CoreDenoiseStep,
153
+ Krea2DecodeStep,
154
+ ]
155
+ block_names = ["text_encoder", "denoise", "decode"]
156
+
157
+ _workflow_map = {
158
+ "text2image": {"prompt": True},
159
+ }
160
+
161
+ @property
162
+ def description(self) -> str:
163
+ return (
164
+ "Auto Modular pipeline for text-to-image generation using Krea 2: encode text -> core denoise "
165
+ "(symmetric CFG) -> decode."
166
+ )
167
+
168
+ @property
169
+ def outputs(self) -> list[OutputParam]:
170
+ return [OutputParam.template("images")]
diffusers/modular_pipelines/krea2/modular_blocks_krea2_turbo.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ from ...utils import logging
17
+ from ..modular_pipeline import SequentialPipelineBlocks
18
+ from ..modular_pipeline_utils import InsertableDict, OutputParam
19
+ from .before_denoise import (
20
+ Krea2PrepareLatentsStep,
21
+ Krea2PreparePositionIdsStep,
22
+ Krea2TurboSetTimestepsStep,
23
+ Krea2TurboTextInputsStep,
24
+ )
25
+ from .decoders import Krea2DecodeStep
26
+ from .denoise import Krea2TurboDenoiseStep
27
+ from .encoders import Krea2TurboTextEncoderStep
28
+
29
+
30
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
31
+
32
+
33
+ CORE_DENOISE_BLOCKS = InsertableDict(
34
+ [
35
+ ("input", Krea2TurboTextInputsStep()),
36
+ ("prepare_latents", Krea2PrepareLatentsStep()),
37
+ ("set_timesteps", Krea2TurboSetTimestepsStep()),
38
+ ("prepare_position_ids", Krea2PreparePositionIdsStep()),
39
+ ("denoise", Krea2TurboDenoiseStep()),
40
+ ]
41
+ )
42
+
43
+
44
+ # auto_docstring
45
+ class Krea2TurboCoreDenoiseStep(SequentialPipelineBlocks):
46
+ """
47
+ Core denoising workflow for the distilled Krea 2 turbo text-to-image checkpoint: prepares the
48
+ batch/latents/timesteps and the shared position ids, then runs the guidance-free denoising loop, producing the
49
+ denoised packed latents for the decoder.
50
+
51
+ Components:
52
+ transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
53
+
54
+ Inputs:
55
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
56
+ The number of images to generate per prompt.
57
+ prompt_embeds (`Tensor`):
58
+ Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
59
+ prompt_embeds_mask (`Tensor`):
60
+ Per-prompt boolean text mask (B, text_seq_len).
61
+ latents (`Tensor`, *optional*):
62
+ Pre-generated noisy latents for image generation.
63
+ height (`int`, *optional*, defaults to 1024):
64
+ The height in pixels of the generated image.
65
+ width (`int`, *optional*, defaults to 1024):
66
+ The width in pixels of the generated image.
67
+ generator (`Generator`, *optional*):
68
+ Torch generator for deterministic generation.
69
+ num_inference_steps (`int`, *optional*, defaults to 8):
70
+ The number of denoising steps.
71
+ sigmas (`list`, *optional*):
72
+ Custom sigma schedule (defaults to a linear ramp).
73
+ attention_kwargs (`dict`, *optional*):
74
+ Additional kwargs for attention processors.
75
+
76
+ Outputs:
77
+ latents (`Tensor`):
78
+ The denoised packed latents (B, image_seq_len, in_channels).
79
+ """
80
+
81
+ model_name = "krea2"
82
+ block_classes = list(CORE_DENOISE_BLOCKS.values())
83
+ block_names = list(CORE_DENOISE_BLOCKS.keys())
84
+
85
+ @property
86
+ def description(self) -> str:
87
+ return (
88
+ "Core denoising workflow for the distilled Krea 2 turbo text-to-image checkpoint: prepares the "
89
+ "batch/latents/timesteps and the shared position ids, then runs the guidance-free denoising loop, "
90
+ "producing the denoised packed latents for the decoder."
91
+ )
92
+
93
+ @property
94
+ def outputs(self) -> list[OutputParam]:
95
+ return [
96
+ OutputParam.template("latents", description="The denoised packed latents (B, image_seq_len, in_channels).")
97
+ ]
98
+
99
+
100
+ # auto_docstring
101
+ class Krea2TurboAutoBlocks(SequentialPipelineBlocks):
102
+ """
103
+ Auto Modular pipeline for text-to-image generation using the distilled Krea 2 turbo checkpoint: encode text -> core
104
+ denoise (guidance-free) -> decode.
105
+
106
+ Supported workflows:
107
+ - `text2image`: requires `prompt`
108
+
109
+ Components:
110
+ text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
111
+ with the text encoder. transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
112
+ vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
113
+
114
+ Inputs:
115
+ prompt (`str`):
116
+ The prompt or prompts to guide image generation.
117
+ max_sequence_length (`int`, *optional*, defaults to 512):
118
+ Maximum sequence length for prompt encoding.
119
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
120
+ The number of images to generate per prompt.
121
+ latents (`Tensor`, *optional*):
122
+ Pre-generated noisy latents for image generation.
123
+ height (`int`, *optional*, defaults to 1024):
124
+ The height in pixels of the generated image.
125
+ width (`int`, *optional*, defaults to 1024):
126
+ The width in pixels of the generated image.
127
+ generator (`Generator`, *optional*):
128
+ Torch generator for deterministic generation.
129
+ num_inference_steps (`int`, *optional*, defaults to 8):
130
+ The number of denoising steps.
131
+ sigmas (`list`, *optional*):
132
+ Custom sigma schedule (defaults to a linear ramp).
133
+ attention_kwargs (`dict`, *optional*):
134
+ Additional kwargs for attention processors.
135
+ output_type (`str`, *optional*, defaults to pil):
136
+ Output format: 'pil', 'np', 'pt'.
137
+
138
+ Outputs:
139
+ images (`list`):
140
+ Generated images.
141
+ """
142
+
143
+ model_name = "krea2"
144
+ block_classes = [
145
+ Krea2TurboTextEncoderStep,
146
+ Krea2TurboCoreDenoiseStep,
147
+ Krea2DecodeStep,
148
+ ]
149
+ block_names = ["text_encoder", "denoise", "decode"]
150
+
151
+ _workflow_map = {
152
+ "text2image": {"prompt": True},
153
+ }
154
+
155
+ @property
156
+ def description(self) -> str:
157
+ return (
158
+ "Auto Modular pipeline for text-to-image generation using the distilled Krea 2 turbo checkpoint: encode "
159
+ "text -> core denoise (guidance-free) -> decode."
160
+ )
161
+
162
+ @property
163
+ def outputs(self) -> list[OutputParam]:
164
+ return [OutputParam.template("images")]
diffusers/modular_pipelines/krea2/modular_pipeline.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ...loaders import Krea2LoraLoaderMixin
16
+ from ..modular_pipeline import ModularPipeline
17
+
18
+
19
+ class Krea2ModularPipeline(ModularPipeline, Krea2LoraLoaderMixin):
20
+ """
21
+ A ModularPipeline for Krea 2.
22
+
23
+ > [!WARNING] > This is an experimental feature!
24
+ """
25
+
26
+ default_blocks_name = "Krea2AutoBlocks"
27
+
28
+ @property
29
+ def patch_size(self):
30
+ return 2
31
+
32
+ @property
33
+ def default_height(self):
34
+ return 1024
35
+
36
+ @property
37
+ def default_width(self):
38
+ return 1024
39
+
40
+ @property
41
+ def vae_scale_factor(self):
42
+ vae_scale_factor = 8
43
+ if getattr(self, "vae", None) is not None:
44
+ vae_scale_factor = 2 ** len(self.vae.temperal_downsample)
45
+ return vae_scale_factor
46
+
47
+ @property
48
+ def requires_unconditional_embeds(self):
49
+ requires_unconditional_embeds = False
50
+ if hasattr(self, "guider") and self.guider is not None:
51
+ requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
52
+ return requires_unconditional_embeds
53
+
54
+
55
+ class Krea2TurboModularPipeline(Krea2ModularPipeline):
56
+ """
57
+ A ModularPipeline for the distilled Krea 2 turbo (TDM) checkpoint. It runs without classifier-free guidance, so it
58
+ takes no negative prompt and has no guider.
59
+
60
+ > [!WARNING] > This is an experimental feature!
61
+ """
62
+
63
+ default_blocks_name = "Krea2TurboAutoBlocks"
64
+
65
+ @property
66
+ def requires_unconditional_embeds(self):
67
+ return False
diffusers/modular_pipelines/ltx/__init__.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_ltx"] = ["LTXAutoBlocks", "LTXBlocks", "LTXImage2VideoBlocks"]
25
+ _import_structure["modular_pipeline"] = ["LTXModularPipeline"]
26
+
27
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
28
+ try:
29
+ if not (is_transformers_available() and is_torch_available()):
30
+ raise OptionalDependencyNotAvailable()
31
+ except OptionalDependencyNotAvailable:
32
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
33
+ else:
34
+ from .modular_blocks_ltx import LTXAutoBlocks, LTXBlocks, LTXImage2VideoBlocks
35
+ from .modular_pipeline import LTXModularPipeline
36
+ else:
37
+ import sys
38
+
39
+ sys.modules[__name__] = _LazyModule(
40
+ __name__,
41
+ globals()["__file__"],
42
+ _import_structure,
43
+ module_spec=__spec__,
44
+ )
45
+
46
+ for name, value in _dummy_objects.items():
47
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/ltx/before_denoise.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ from ...configuration_utils import FrozenDict
21
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
22
+ from ...utils import logging
23
+ from ...utils.torch_utils import randn_tensor
24
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
25
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
26
+ from .modular_pipeline import LTXModularPipeline, LTXVideoPachifier
27
+
28
+
29
+ logger = logging.get_logger(__name__)
30
+
31
+
32
+ def calculate_shift(
33
+ image_seq_len,
34
+ base_seq_len: int = 256,
35
+ max_seq_len: int = 4096,
36
+ base_shift: float = 0.5,
37
+ max_shift: float = 1.15,
38
+ ):
39
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
40
+ b = base_shift - m * base_seq_len
41
+ mu = image_seq_len * m + b
42
+ return mu
43
+
44
+
45
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
46
+ def retrieve_timesteps(
47
+ scheduler,
48
+ num_inference_steps: int | None = None,
49
+ device: str | torch.device | None = None,
50
+ timesteps: list[int] | None = None,
51
+ sigmas: list[float] | None = None,
52
+ **kwargs,
53
+ ):
54
+ r"""
55
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
56
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
57
+
58
+ Args:
59
+ scheduler (`SchedulerMixin`):
60
+ The scheduler to get timesteps from.
61
+ num_inference_steps (`int`):
62
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
63
+ must be `None`.
64
+ device (`str` or `torch.device`, *optional*):
65
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
66
+ timesteps (`list[int]`, *optional*):
67
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
68
+ `num_inference_steps` and `sigmas` must be `None`.
69
+ sigmas (`list[float]`, *optional*):
70
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
71
+ `num_inference_steps` and `timesteps` must be `None`.
72
+
73
+ Returns:
74
+ `tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
75
+ second element is the number of inference steps.
76
+ """
77
+ if timesteps is not None and sigmas is not None:
78
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
79
+ if timesteps is not None:
80
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
81
+ if not accepts_timesteps:
82
+ raise ValueError(
83
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
84
+ f" timestep schedules. Please check whether you are using the correct scheduler."
85
+ )
86
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
87
+ timesteps = scheduler.timesteps
88
+ num_inference_steps = len(timesteps)
89
+ elif sigmas is not None:
90
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
91
+ if not accept_sigmas:
92
+ raise ValueError(
93
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
94
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
95
+ )
96
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
97
+ timesteps = scheduler.timesteps
98
+ num_inference_steps = len(timesteps)
99
+ else:
100
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
101
+ timesteps = scheduler.timesteps
102
+ return timesteps, num_inference_steps
103
+
104
+
105
+ class LTXTextInputStep(ModularPipelineBlocks):
106
+ model_name = "ltx"
107
+
108
+ @property
109
+ def description(self) -> str:
110
+ return (
111
+ "Input processing step that:\n"
112
+ " 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
113
+ " 2. Adjusts input tensor shapes based on `batch_size` and `num_videos_per_prompt`"
114
+ )
115
+
116
+ @property
117
+ def inputs(self) -> list[InputParam]:
118
+ return [
119
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
120
+ InputParam.template("prompt_embeds", required=True),
121
+ InputParam.template("prompt_embeds_mask", name="prompt_attention_mask"),
122
+ InputParam.template("negative_prompt_embeds"),
123
+ InputParam.template("negative_prompt_embeds_mask", name="negative_prompt_attention_mask"),
124
+ ]
125
+
126
+ @property
127
+ def intermediate_outputs(self) -> list[OutputParam]:
128
+ return [
129
+ OutputParam("batch_size", type_hint=int),
130
+ OutputParam("dtype", type_hint=torch.dtype),
131
+ ]
132
+
133
+ @torch.no_grad()
134
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
135
+ block_state = self.get_block_state(state)
136
+
137
+ block_state.batch_size = block_state.prompt_embeds.shape[0]
138
+ block_state.dtype = block_state.prompt_embeds.dtype
139
+ num_videos = block_state.num_videos_per_prompt
140
+
141
+ # Repeat prompt_embeds for num_videos_per_prompt
142
+ _, seq_len, _ = block_state.prompt_embeds.shape
143
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, num_videos, 1)
144
+ block_state.prompt_embeds = block_state.prompt_embeds.view(block_state.batch_size * num_videos, seq_len, -1)
145
+
146
+ if block_state.prompt_attention_mask is not None:
147
+ block_state.prompt_attention_mask = block_state.prompt_attention_mask.repeat(num_videos, 1)
148
+
149
+ if block_state.negative_prompt_embeds is not None:
150
+ _, seq_len, _ = block_state.negative_prompt_embeds.shape
151
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(1, num_videos, 1)
152
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
153
+ block_state.batch_size * num_videos, seq_len, -1
154
+ )
155
+
156
+ if block_state.negative_prompt_attention_mask is not None:
157
+ block_state.negative_prompt_attention_mask = block_state.negative_prompt_attention_mask.repeat(
158
+ num_videos, 1
159
+ )
160
+
161
+ self.set_block_state(state, block_state)
162
+ return components, state
163
+
164
+
165
+ class LTXSetTimestepsStep(ModularPipelineBlocks):
166
+ model_name = "ltx"
167
+
168
+ @property
169
+ def expected_components(self) -> list[ComponentSpec]:
170
+ return [
171
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
172
+ ]
173
+
174
+ @property
175
+ def description(self) -> str:
176
+ return "Step that sets the scheduler's timesteps for inference"
177
+
178
+ @property
179
+ def inputs(self) -> list[InputParam]:
180
+ return [
181
+ InputParam.template("num_inference_steps"),
182
+ InputParam.template("timesteps"),
183
+ InputParam.template("sigmas"),
184
+ InputParam.template("height", default=512),
185
+ InputParam.template("width", default=704),
186
+ InputParam("num_frames", type_hint=int, default=161),
187
+ InputParam("frame_rate", type_hint=int, default=25),
188
+ ]
189
+
190
+ @property
191
+ def intermediate_outputs(self) -> list[OutputParam]:
192
+ return [
193
+ OutputParam("timesteps", type_hint=torch.Tensor),
194
+ OutputParam("num_inference_steps", type_hint=int),
195
+ OutputParam("rope_interpolation_scale", type_hint=tuple),
196
+ ]
197
+
198
+ @torch.no_grad()
199
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
200
+ block_state = self.get_block_state(state)
201
+ device = components._execution_device
202
+
203
+ height = block_state.height
204
+ width = block_state.width
205
+ num_frames = block_state.num_frames
206
+ frame_rate = block_state.frame_rate
207
+
208
+ latent_num_frames = (num_frames - 1) // components.vae_temporal_compression_ratio + 1
209
+ latent_height = height // components.vae_spatial_compression_ratio
210
+ latent_width = width // components.vae_spatial_compression_ratio
211
+ video_sequence_length = latent_num_frames * latent_height * latent_width
212
+
213
+ custom_timesteps = block_state.timesteps
214
+ sigmas = block_state.sigmas
215
+
216
+ if custom_timesteps is not None:
217
+ # User provided custom timesteps, don't compute sigmas
218
+ block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
219
+ components.scheduler,
220
+ block_state.num_inference_steps,
221
+ device,
222
+ custom_timesteps,
223
+ )
224
+ else:
225
+ if sigmas is None:
226
+ sigmas = np.linspace(1.0, 1 / block_state.num_inference_steps, block_state.num_inference_steps)
227
+
228
+ mu = calculate_shift(
229
+ video_sequence_length,
230
+ components.scheduler.config.get("base_image_seq_len", 256),
231
+ components.scheduler.config.get("max_image_seq_len", 4096),
232
+ components.scheduler.config.get("base_shift", 0.5),
233
+ components.scheduler.config.get("max_shift", 1.15),
234
+ )
235
+
236
+ block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
237
+ components.scheduler,
238
+ block_state.num_inference_steps,
239
+ device,
240
+ sigmas=sigmas,
241
+ mu=mu,
242
+ )
243
+
244
+ block_state.rope_interpolation_scale = (
245
+ components.vae_temporal_compression_ratio / frame_rate,
246
+ components.vae_spatial_compression_ratio,
247
+ components.vae_spatial_compression_ratio,
248
+ )
249
+
250
+ self.set_block_state(state, block_state)
251
+ return components, state
252
+
253
+
254
+ class LTXPrepareLatentsStep(ModularPipelineBlocks):
255
+ model_name = "ltx"
256
+
257
+ @property
258
+ def description(self) -> str:
259
+ return "Prepare latents step that prepares the latents for the text-to-video generation process"
260
+
261
+ @property
262
+ def expected_components(self) -> list[ComponentSpec]:
263
+ return [
264
+ ComponentSpec(
265
+ "pachifier",
266
+ LTXVideoPachifier,
267
+ config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
268
+ default_creation_method="from_config",
269
+ ),
270
+ ]
271
+
272
+ @property
273
+ def inputs(self) -> list[InputParam]:
274
+ return [
275
+ InputParam.template("height", default=512),
276
+ InputParam.template("width", default=704),
277
+ InputParam("num_frames", type_hint=int, default=161),
278
+ InputParam.template("latents"),
279
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
280
+ InputParam.template("generator"),
281
+ InputParam.template("batch_size", required=True),
282
+ ]
283
+
284
+ @property
285
+ def intermediate_outputs(self) -> list[OutputParam]:
286
+ return [
287
+ OutputParam("latents", type_hint=torch.Tensor),
288
+ ]
289
+
290
+ @torch.no_grad()
291
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
292
+ block_state = self.get_block_state(state)
293
+ device = components._execution_device
294
+
295
+ batch_size = block_state.batch_size * block_state.num_videos_per_prompt
296
+ num_channels_latents = components.transformer.config.in_channels
297
+
298
+ if block_state.latents is not None:
299
+ block_state.latents = block_state.latents.to(device=device, dtype=torch.float32)
300
+ else:
301
+ height = block_state.height // components.vae_spatial_compression_ratio
302
+ width = block_state.width // components.vae_spatial_compression_ratio
303
+ num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
304
+
305
+ shape = (batch_size, num_channels_latents, num_frames, height, width)
306
+ block_state.latents = randn_tensor(
307
+ shape, generator=block_state.generator, device=device, dtype=torch.float32
308
+ )
309
+ block_state.latents = components.pachifier.pack_latents(block_state.latents)
310
+
311
+ self.set_block_state(state, block_state)
312
+ return components, state
313
+
314
+
315
+ class LTXImage2VideoPrepareLatentsStep(ModularPipelineBlocks):
316
+ model_name = "ltx"
317
+
318
+ @property
319
+ def description(self) -> str:
320
+ return (
321
+ "Prepare image-to-video latents: adds noise to pre-encoded image latents and creates a conditioning mask. "
322
+ "Expects pure noise `latents` from LTXPrepareLatentsStep."
323
+ )
324
+
325
+ @property
326
+ def expected_components(self) -> list[ComponentSpec]:
327
+ return [
328
+ ComponentSpec(
329
+ "pachifier",
330
+ LTXVideoPachifier,
331
+ config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
332
+ default_creation_method="from_config",
333
+ ),
334
+ ]
335
+
336
+ @property
337
+ def inputs(self) -> list[InputParam]:
338
+ return [
339
+ InputParam("image_latents", type_hint=torch.Tensor, required=True),
340
+ InputParam.template("latents", required=True),
341
+ InputParam.template("height", default=512),
342
+ InputParam.template("width", default=704),
343
+ InputParam("num_frames", type_hint=int, default=161),
344
+ InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
345
+ InputParam.template("batch_size", required=True),
346
+ ]
347
+
348
+ @property
349
+ def intermediate_outputs(self) -> list[OutputParam]:
350
+ return [
351
+ OutputParam("latents", type_hint=torch.Tensor),
352
+ OutputParam("conditioning_mask", type_hint=torch.Tensor),
353
+ ]
354
+
355
+ @torch.no_grad()
356
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
357
+ block_state = self.get_block_state(state)
358
+ device = components._execution_device
359
+
360
+ batch_size = block_state.batch_size * block_state.num_videos_per_prompt
361
+
362
+ height = block_state.height // components.vae_spatial_compression_ratio
363
+ width = block_state.width // components.vae_spatial_compression_ratio
364
+ num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
365
+
366
+ init_latents = block_state.image_latents.to(device=device, dtype=torch.float32)
367
+ if init_latents.shape[0] < batch_size:
368
+ init_latents = init_latents.repeat_interleave(batch_size // init_latents.shape[0], dim=0)
369
+ init_latents = init_latents.repeat(1, 1, num_frames, 1, 1)
370
+
371
+ conditioning_mask = torch.zeros(
372
+ init_latents.shape[0],
373
+ 1,
374
+ init_latents.shape[2],
375
+ init_latents.shape[3],
376
+ init_latents.shape[4],
377
+ device=device,
378
+ dtype=torch.float32,
379
+ )
380
+ conditioning_mask[:, :, 0] = 1.0
381
+
382
+ noise = components.pachifier.unpack_latents(block_state.latents, num_frames, height, width)
383
+ latents = init_latents * conditioning_mask + noise * (1 - conditioning_mask)
384
+
385
+ conditioning_mask = components.pachifier.pack_latents(conditioning_mask).squeeze(-1)
386
+ latents = components.pachifier.pack_latents(latents)
387
+
388
+ block_state.latents = latents
389
+ block_state.conditioning_mask = conditioning_mask
390
+
391
+ self.set_block_state(state, block_state)
392
+ return components, state
diffusers/modular_pipelines/ltx/decoders.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Any
16
+
17
+ import torch
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...models import AutoencoderKLLTXVideo
21
+ from ...utils import logging
22
+ from ...utils.torch_utils import randn_tensor
23
+ from ...video_processor import VideoProcessor
24
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
25
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
26
+ from .modular_pipeline import LTXVideoPachifier
27
+
28
+
29
+ logger = logging.get_logger(__name__)
30
+
31
+
32
+ def _denormalize_latents(
33
+ latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
34
+ ) -> torch.Tensor:
35
+ # Denormalize latents across the channel dimension [B, C, F, H, W]
36
+ latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
37
+ latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
38
+ latents = latents * latents_std / scaling_factor + latents_mean
39
+ return latents
40
+
41
+
42
+ class LTXVaeDecoderStep(ModularPipelineBlocks):
43
+ model_name = "ltx"
44
+
45
+ @property
46
+ def expected_components(self) -> list[ComponentSpec]:
47
+ return [
48
+ ComponentSpec("vae", AutoencoderKLLTXVideo),
49
+ ComponentSpec(
50
+ "video_processor",
51
+ VideoProcessor,
52
+ config=FrozenDict({"vae_scale_factor": 32}),
53
+ default_creation_method="from_config",
54
+ ),
55
+ ComponentSpec(
56
+ "pachifier",
57
+ LTXVideoPachifier,
58
+ config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
59
+ default_creation_method="from_config",
60
+ ),
61
+ ]
62
+
63
+ @property
64
+ def description(self) -> str:
65
+ return "Step that decodes the denoised latents into videos"
66
+
67
+ @property
68
+ def inputs(self) -> list[tuple[str, Any]]:
69
+ return [
70
+ InputParam.template("latents", required=True),
71
+ InputParam.template("output_type", default="np"),
72
+ InputParam.template("height", default=512),
73
+ InputParam.template("width", default=704),
74
+ InputParam("num_frames", type_hint=int, default=161),
75
+ InputParam("decode_timestep", default=0.0),
76
+ InputParam("decode_noise_scale", default=None),
77
+ InputParam.template("generator"),
78
+ InputParam.template("batch_size"),
79
+ InputParam.template("dtype", required=True),
80
+ ]
81
+
82
+ @property
83
+ def intermediate_outputs(self) -> list[OutputParam]:
84
+ return [OutputParam.template("videos")]
85
+
86
+ @torch.no_grad()
87
+ def __call__(self, components, state: PipelineState) -> PipelineState:
88
+ block_state = self.get_block_state(state)
89
+ vae = components.vae
90
+
91
+ latents = block_state.latents
92
+
93
+ height = block_state.height
94
+ width = block_state.width
95
+ num_frames = block_state.num_frames
96
+
97
+ latent_num_frames = (num_frames - 1) // components.vae_temporal_compression_ratio + 1
98
+ latent_height = height // components.vae_spatial_compression_ratio
99
+ latent_width = width // components.vae_spatial_compression_ratio
100
+
101
+ latents = components.pachifier.unpack_latents(latents, latent_num_frames, latent_height, latent_width)
102
+ latents = _denormalize_latents(latents, vae.latents_mean, vae.latents_std, vae.config.scaling_factor)
103
+ latents = latents.to(block_state.dtype)
104
+
105
+ if not vae.config.timestep_conditioning:
106
+ timestep = None
107
+ else:
108
+ device = latents.device
109
+ batch_size = block_state.batch_size
110
+ decode_timestep = block_state.decode_timestep
111
+ decode_noise_scale = block_state.decode_noise_scale
112
+
113
+ noise = randn_tensor(latents.shape, generator=block_state.generator, device=device, dtype=latents.dtype)
114
+ if not isinstance(decode_timestep, list):
115
+ decode_timestep = [decode_timestep] * batch_size
116
+ if decode_noise_scale is None:
117
+ decode_noise_scale = decode_timestep
118
+ elif not isinstance(decode_noise_scale, list):
119
+ decode_noise_scale = [decode_noise_scale] * batch_size
120
+
121
+ timestep = torch.tensor(decode_timestep, device=device, dtype=latents.dtype)
122
+ decode_noise_scale = torch.tensor(decode_noise_scale, device=device, dtype=latents.dtype)[
123
+ :, None, None, None, None
124
+ ]
125
+ latents = (1 - decode_noise_scale) * latents + decode_noise_scale * noise
126
+
127
+ latents = latents.to(vae.dtype)
128
+ video = vae.decode(latents, timestep, return_dict=False)[0]
129
+ block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type)
130
+
131
+ self.set_block_state(state, block_state)
132
+ return components, state
diffusers/modular_pipelines/ltx/denoise.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Any
16
+
17
+ import torch
18
+
19
+ from ...configuration_utils import FrozenDict
20
+ from ...guiders import ClassifierFreeGuidance
21
+ from ...models import LTXVideoTransformer3DModel
22
+ from ...schedulers import FlowMatchEulerDiscreteScheduler
23
+ from ..modular_pipeline import (
24
+ BlockState,
25
+ LoopSequentialPipelineBlocks,
26
+ ModularPipelineBlocks,
27
+ PipelineState,
28
+ )
29
+ from ..modular_pipeline_utils import ComponentSpec, InputParam
30
+ from .modular_pipeline import LTXModularPipeline, LTXVideoPachifier
31
+
32
+
33
+ class LTXLoopBeforeDenoiser(ModularPipelineBlocks):
34
+ model_name = "ltx"
35
+
36
+ @property
37
+ def description(self) -> str:
38
+ return (
39
+ "Step within the denoising loop that prepares the latent input for the denoiser. "
40
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
41
+ "object (e.g. `LTXDenoiseLoopWrapper`)"
42
+ )
43
+
44
+ @property
45
+ def inputs(self) -> list[InputParam]:
46
+ return [
47
+ InputParam.template("latents", required=True),
48
+ InputParam.template("dtype", required=True),
49
+ ]
50
+
51
+ @torch.no_grad()
52
+ def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
53
+ block_state.latent_model_input = block_state.latents.to(block_state.dtype)
54
+ return components, block_state
55
+
56
+
57
+ class LTXLoopDenoiser(ModularPipelineBlocks):
58
+ model_name = "ltx"
59
+
60
+ def __init__(
61
+ self,
62
+ guider_input_fields: dict[str, Any] | None = None,
63
+ ):
64
+ if guider_input_fields is None:
65
+ guider_input_fields = {
66
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
67
+ "encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
68
+ }
69
+ if not isinstance(guider_input_fields, dict):
70
+ raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
71
+ self._guider_input_fields = guider_input_fields
72
+ super().__init__()
73
+
74
+ @property
75
+ def expected_components(self) -> list[ComponentSpec]:
76
+ return [
77
+ ComponentSpec(
78
+ "guider",
79
+ ClassifierFreeGuidance,
80
+ config=FrozenDict({"guidance_scale": 3.0}),
81
+ default_creation_method="from_config",
82
+ ),
83
+ ComponentSpec("transformer", LTXVideoTransformer3DModel),
84
+ ]
85
+
86
+ @property
87
+ def description(self) -> str:
88
+ return (
89
+ "Step within the denoising loop that denoises the latents with guidance. "
90
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
91
+ "object (e.g. `LTXDenoiseLoopWrapper`)"
92
+ )
93
+
94
+ @property
95
+ def inputs(self) -> list[tuple[str, Any]]:
96
+ inputs = [
97
+ InputParam.template("attention_kwargs"),
98
+ InputParam.template("num_inference_steps", required=True),
99
+ InputParam("rope_interpolation_scale", type_hint=tuple),
100
+ InputParam.template("height"),
101
+ InputParam.template("width"),
102
+ InputParam("num_frames", type_hint=int),
103
+ ]
104
+ guider_input_names = []
105
+ for value in self._guider_input_fields.values():
106
+ if isinstance(value, tuple):
107
+ guider_input_names.extend(value)
108
+ else:
109
+ guider_input_names.append(value)
110
+
111
+ for name in guider_input_names:
112
+ inputs.append(InputParam(name=name, required=True, type_hint=torch.Tensor))
113
+ return inputs
114
+
115
+ @torch.no_grad()
116
+ def __call__(
117
+ self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
118
+ ) -> PipelineState:
119
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
120
+
121
+ latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
122
+ latent_height = block_state.height // components.vae_spatial_compression_ratio
123
+ latent_width = block_state.width // components.vae_spatial_compression_ratio
124
+
125
+ guider_state = components.guider.prepare_inputs_from_block_state(block_state, self._guider_input_fields)
126
+
127
+ for guider_state_batch in guider_state:
128
+ components.guider.prepare_models(components.transformer)
129
+ cond_kwargs = guider_state_batch.as_dict()
130
+ cond_kwargs = {
131
+ k: v.to(block_state.dtype) if isinstance(v, torch.Tensor) else v
132
+ for k, v in cond_kwargs.items()
133
+ if k in self._guider_input_fields.keys()
134
+ }
135
+
136
+ context_name = getattr(guider_state_batch, components.guider._identifier_key, None)
137
+ with components.transformer.cache_context(context_name):
138
+ guider_state_batch.noise_pred = components.transformer(
139
+ hidden_states=block_state.latent_model_input,
140
+ timestep=t.expand(block_state.latent_model_input.shape[0]).to(block_state.dtype),
141
+ num_frames=latent_num_frames,
142
+ height=latent_height,
143
+ width=latent_width,
144
+ rope_interpolation_scale=block_state.rope_interpolation_scale,
145
+ attention_kwargs=block_state.attention_kwargs,
146
+ return_dict=False,
147
+ **cond_kwargs,
148
+ )[0]
149
+ components.guider.cleanup_models(components.transformer)
150
+
151
+ block_state.noise_pred = components.guider(guider_state)[0]
152
+
153
+ return components, block_state
154
+
155
+
156
+ class LTXLoopAfterDenoiser(ModularPipelineBlocks):
157
+ model_name = "ltx"
158
+
159
+ @property
160
+ def expected_components(self) -> list[ComponentSpec]:
161
+ return [
162
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
163
+ ]
164
+
165
+ @property
166
+ def description(self) -> str:
167
+ return (
168
+ "Step within the denoising loop that updates the latents. "
169
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
170
+ "object (e.g. `LTXDenoiseLoopWrapper`)"
171
+ )
172
+
173
+ @torch.no_grad()
174
+ def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
175
+ latents_dtype = block_state.latents.dtype
176
+ block_state.latents = components.scheduler.step(
177
+ block_state.noise_pred,
178
+ t,
179
+ block_state.latents,
180
+ return_dict=False,
181
+ )[0]
182
+
183
+ if block_state.latents.dtype != latents_dtype:
184
+ block_state.latents = block_state.latents.to(latents_dtype)
185
+
186
+ return components, block_state
187
+
188
+
189
+ class LTXDenoiseLoopWrapper(LoopSequentialPipelineBlocks):
190
+ model_name = "ltx"
191
+
192
+ @property
193
+ def description(self) -> str:
194
+ return (
195
+ "Pipeline block that iteratively denoises the latents over `timesteps`. "
196
+ "The specific steps within each iteration can be customized with `sub_blocks` attributes"
197
+ )
198
+
199
+ @property
200
+ def loop_expected_components(self) -> list[ComponentSpec]:
201
+ return [
202
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
203
+ ComponentSpec("transformer", LTXVideoTransformer3DModel),
204
+ ]
205
+
206
+ @property
207
+ def loop_inputs(self) -> list[InputParam]:
208
+ return [
209
+ InputParam.template("timesteps", required=True),
210
+ InputParam.template("num_inference_steps", required=True),
211
+ ]
212
+
213
+ @torch.no_grad()
214
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
215
+ block_state = self.get_block_state(state)
216
+
217
+ block_state.num_warmup_steps = max(
218
+ len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
219
+ )
220
+
221
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
222
+ for i, t in enumerate(block_state.timesteps):
223
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
224
+ if i == len(block_state.timesteps) - 1 or (
225
+ (i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
226
+ ):
227
+ progress_bar.update()
228
+
229
+ self.set_block_state(state, block_state)
230
+ return components, state
231
+
232
+
233
+ class LTXDenoiseStep(LTXDenoiseLoopWrapper):
234
+ block_classes = [
235
+ LTXLoopBeforeDenoiser,
236
+ LTXLoopDenoiser(
237
+ guider_input_fields={
238
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
239
+ "encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
240
+ }
241
+ ),
242
+ LTXLoopAfterDenoiser,
243
+ ]
244
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
245
+
246
+ @property
247
+ def description(self) -> str:
248
+ return (
249
+ "Denoise step that iteratively denoises the latents.\n"
250
+ "Its loop logic is defined in `LTXDenoiseLoopWrapper.__call__` method.\n"
251
+ "At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
252
+ " - `LTXLoopBeforeDenoiser`\n"
253
+ " - `LTXLoopDenoiser`\n"
254
+ " - `LTXLoopAfterDenoiser`\n"
255
+ "This block supports text-to-video tasks."
256
+ )
257
+
258
+
259
+ class LTXImage2VideoLoopBeforeDenoiser(ModularPipelineBlocks):
260
+ model_name = "ltx"
261
+
262
+ @property
263
+ def description(self) -> str:
264
+ return (
265
+ "Step within the i2v denoising loop that prepares the latent input and modulates "
266
+ "the timestep with the conditioning mask."
267
+ )
268
+
269
+ @property
270
+ def inputs(self) -> list[InputParam]:
271
+ return [
272
+ InputParam.template("latents", required=True),
273
+ InputParam("conditioning_mask", required=True, type_hint=torch.Tensor),
274
+ InputParam.template("dtype", required=True),
275
+ ]
276
+
277
+ @torch.no_grad()
278
+ def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
279
+ block_state.latent_model_input = block_state.latents.to(block_state.dtype)
280
+ block_state.timestep_adjusted = t.expand(block_state.latent_model_input.shape[0]).unsqueeze(-1) * (
281
+ 1 - block_state.conditioning_mask
282
+ )
283
+ return components, block_state
284
+
285
+
286
+ class LTXImage2VideoLoopDenoiser(ModularPipelineBlocks):
287
+ model_name = "ltx"
288
+
289
+ def __init__(
290
+ self,
291
+ guider_input_fields: dict[str, Any] | None = None,
292
+ ):
293
+ if guider_input_fields is None:
294
+ guider_input_fields = {
295
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
296
+ "encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
297
+ }
298
+ if not isinstance(guider_input_fields, dict):
299
+ raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
300
+ self._guider_input_fields = guider_input_fields
301
+ super().__init__()
302
+
303
+ @property
304
+ def expected_components(self) -> list[ComponentSpec]:
305
+ return [
306
+ ComponentSpec(
307
+ "guider",
308
+ ClassifierFreeGuidance,
309
+ config=FrozenDict({"guidance_scale": 3.0}),
310
+ default_creation_method="from_config",
311
+ ),
312
+ ComponentSpec("transformer", LTXVideoTransformer3DModel),
313
+ ]
314
+
315
+ @property
316
+ def description(self) -> str:
317
+ return (
318
+ "Step within the i2v denoising loop that denoises the latents with guidance "
319
+ "using timestep modulated by the conditioning mask."
320
+ )
321
+
322
+ @property
323
+ def inputs(self) -> list[tuple[str, Any]]:
324
+ inputs = [
325
+ InputParam.template("attention_kwargs"),
326
+ InputParam.template("num_inference_steps", required=True),
327
+ InputParam("rope_interpolation_scale", type_hint=tuple),
328
+ InputParam.template("height"),
329
+ InputParam.template("width"),
330
+ InputParam("num_frames", type_hint=int),
331
+ ]
332
+ guider_input_names = []
333
+ for value in self._guider_input_fields.values():
334
+ if isinstance(value, tuple):
335
+ guider_input_names.extend(value)
336
+ else:
337
+ guider_input_names.append(value)
338
+ for name in guider_input_names:
339
+ inputs.append(InputParam(name=name, required=True, type_hint=torch.Tensor))
340
+ return inputs
341
+
342
+ @torch.no_grad()
343
+ def __call__(
344
+ self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
345
+ ) -> PipelineState:
346
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
347
+
348
+ latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
349
+ latent_height = block_state.height // components.vae_spatial_compression_ratio
350
+ latent_width = block_state.width // components.vae_spatial_compression_ratio
351
+
352
+ guider_state = components.guider.prepare_inputs_from_block_state(block_state, self._guider_input_fields)
353
+
354
+ for guider_state_batch in guider_state:
355
+ components.guider.prepare_models(components.transformer)
356
+ cond_kwargs = guider_state_batch.as_dict()
357
+ cond_kwargs = {
358
+ k: v.to(block_state.dtype) if isinstance(v, torch.Tensor) else v
359
+ for k, v in cond_kwargs.items()
360
+ if k in self._guider_input_fields.keys()
361
+ }
362
+
363
+ context_name = getattr(guider_state_batch, components.guider._identifier_key, None)
364
+ with components.transformer.cache_context(context_name):
365
+ guider_state_batch.noise_pred = components.transformer(
366
+ hidden_states=block_state.latent_model_input,
367
+ timestep=block_state.timestep_adjusted,
368
+ num_frames=latent_num_frames,
369
+ height=latent_height,
370
+ width=latent_width,
371
+ rope_interpolation_scale=block_state.rope_interpolation_scale,
372
+ attention_kwargs=block_state.attention_kwargs,
373
+ return_dict=False,
374
+ **cond_kwargs,
375
+ )[0]
376
+ components.guider.cleanup_models(components.transformer)
377
+
378
+ block_state.noise_pred = components.guider(guider_state)[0]
379
+
380
+ return components, block_state
381
+
382
+
383
+ class LTXImage2VideoLoopAfterDenoiser(ModularPipelineBlocks):
384
+ model_name = "ltx"
385
+
386
+ @property
387
+ def expected_components(self) -> list[ComponentSpec]:
388
+ return [
389
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
390
+ ComponentSpec(
391
+ "pachifier",
392
+ LTXVideoPachifier,
393
+ config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
394
+ default_creation_method="from_config",
395
+ ),
396
+ ]
397
+
398
+ @property
399
+ def description(self) -> str:
400
+ return (
401
+ "Step within the i2v denoising loop that updates the latents, "
402
+ "applying the scheduler step only to frames after the first (conditioned) frame."
403
+ )
404
+
405
+ @property
406
+ def inputs(self) -> list[InputParam]:
407
+ return [
408
+ InputParam.template("height"),
409
+ InputParam.template("width"),
410
+ InputParam("num_frames", type_hint=int),
411
+ ]
412
+
413
+ @torch.no_grad()
414
+ def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
415
+ latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
416
+ latent_height = block_state.height // components.vae_spatial_compression_ratio
417
+ latent_width = block_state.width // components.vae_spatial_compression_ratio
418
+
419
+ noise_pred = components.pachifier.unpack_latents(
420
+ block_state.noise_pred, latent_num_frames, latent_height, latent_width
421
+ )
422
+ latents = components.pachifier.unpack_latents(
423
+ block_state.latents, latent_num_frames, latent_height, latent_width
424
+ )
425
+
426
+ noise_pred = noise_pred[:, :, 1:]
427
+ noise_latents = latents[:, :, 1:]
428
+ pred_latents = components.scheduler.step(noise_pred, t, noise_latents, return_dict=False)[0]
429
+
430
+ latents = torch.cat([latents[:, :, :1], pred_latents], dim=2)
431
+ block_state.latents = components.pachifier.pack_latents(latents)
432
+
433
+ return components, block_state
434
+
435
+
436
+ class LTXImage2VideoDenoiseStep(LTXDenoiseLoopWrapper):
437
+ block_classes = [
438
+ LTXImage2VideoLoopBeforeDenoiser,
439
+ LTXImage2VideoLoopDenoiser(
440
+ guider_input_fields={
441
+ "encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
442
+ "encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
443
+ }
444
+ ),
445
+ LTXImage2VideoLoopAfterDenoiser,
446
+ ]
447
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
448
+
449
+ @property
450
+ def description(self) -> str:
451
+ return (
452
+ "Denoise step for image-to-video that iteratively denoises the latents.\n"
453
+ "The first frame is kept fixed via a conditioning mask.\n"
454
+ "At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
455
+ " - `LTXImage2VideoLoopBeforeDenoiser`\n"
456
+ " - `LTXImage2VideoLoopDenoiser`\n"
457
+ " - `LTXImage2VideoLoopAfterDenoiser`"
458
+ )
diffusers/modular_pipelines/ltx/encoders.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+ from transformers import T5EncoderModel, T5TokenizerFast
17
+
18
+ from ...configuration_utils import FrozenDict
19
+ from ...guiders import ClassifierFreeGuidance
20
+ from ...models import AutoencoderKLLTXVideo
21
+ from ...utils import logging
22
+ from ...video_processor import VideoProcessor
23
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
24
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
25
+ from .modular_pipeline import LTXModularPipeline
26
+
27
+
28
+ logger = logging.get_logger(__name__)
29
+
30
+
31
+ def _get_t5_prompt_embeds(
32
+ components,
33
+ prompt: str | list[str],
34
+ max_sequence_length: int,
35
+ device: torch.device,
36
+ dtype: torch.dtype,
37
+ ):
38
+ prompt = [prompt] if isinstance(prompt, str) else prompt
39
+
40
+ text_inputs = components.tokenizer(
41
+ prompt,
42
+ padding="max_length",
43
+ max_length=max_sequence_length,
44
+ truncation=True,
45
+ add_special_tokens=True,
46
+ return_tensors="pt",
47
+ )
48
+ text_input_ids = text_inputs.input_ids
49
+ prompt_attention_mask = text_inputs.attention_mask
50
+ prompt_attention_mask = prompt_attention_mask.bool().to(device)
51
+
52
+ prompt_embeds = components.text_encoder(text_input_ids.to(device))[0]
53
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
54
+
55
+ return prompt_embeds, prompt_attention_mask
56
+
57
+
58
+ class LTXTextEncoderStep(ModularPipelineBlocks):
59
+ model_name = "ltx"
60
+
61
+ @property
62
+ def description(self) -> str:
63
+ return "Text Encoder step that generates text embeddings to guide the video generation"
64
+
65
+ @property
66
+ def expected_components(self) -> list[ComponentSpec]:
67
+ return [
68
+ ComponentSpec("text_encoder", T5EncoderModel),
69
+ ComponentSpec("tokenizer", T5TokenizerFast),
70
+ ComponentSpec(
71
+ "guider",
72
+ ClassifierFreeGuidance,
73
+ config=FrozenDict({"guidance_scale": 3.0}),
74
+ default_creation_method="from_config",
75
+ ),
76
+ ]
77
+
78
+ @property
79
+ def inputs(self) -> list[InputParam]:
80
+ return [
81
+ InputParam.template("prompt"),
82
+ InputParam.template("negative_prompt"),
83
+ InputParam.template("max_sequence_length", default=128),
84
+ ]
85
+
86
+ @property
87
+ def intermediate_outputs(self) -> list[OutputParam]:
88
+ return [
89
+ OutputParam.template("prompt_embeds"),
90
+ OutputParam.template("prompt_embeds_mask", name="prompt_attention_mask"),
91
+ OutputParam.template("negative_prompt_embeds"),
92
+ OutputParam.template("negative_prompt_embeds_mask", name="negative_prompt_attention_mask"),
93
+ ]
94
+
95
+ @staticmethod
96
+ def check_inputs(block_state):
97
+ if block_state.prompt is not None and (
98
+ not isinstance(block_state.prompt, str) and not isinstance(block_state.prompt, list)
99
+ ):
100
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(block_state.prompt)}")
101
+
102
+ @staticmethod
103
+ def encode_prompt(
104
+ components,
105
+ prompt: str,
106
+ device: torch.device | None = None,
107
+ prepare_unconditional_embeds: bool = True,
108
+ negative_prompt: str | None = None,
109
+ max_sequence_length: int = 128,
110
+ ):
111
+ device = device or components._execution_device
112
+ dtype = components.text_encoder.dtype
113
+
114
+ if not isinstance(prompt, list):
115
+ prompt = [prompt]
116
+ batch_size = len(prompt)
117
+
118
+ prompt_embeds, prompt_attention_mask = _get_t5_prompt_embeds(
119
+ components=components,
120
+ prompt=prompt,
121
+ max_sequence_length=max_sequence_length,
122
+ device=device,
123
+ dtype=dtype,
124
+ )
125
+
126
+ negative_prompt_embeds = None
127
+ negative_prompt_attention_mask = None
128
+
129
+ if prepare_unconditional_embeds:
130
+ negative_prompt = negative_prompt or ""
131
+ negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
132
+
133
+ if batch_size != len(negative_prompt):
134
+ raise ValueError(
135
+ f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
136
+ f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
137
+ " the batch size of `prompt`."
138
+ )
139
+
140
+ negative_prompt_embeds, negative_prompt_attention_mask = _get_t5_prompt_embeds(
141
+ components=components,
142
+ prompt=negative_prompt,
143
+ max_sequence_length=max_sequence_length,
144
+ device=device,
145
+ dtype=dtype,
146
+ )
147
+
148
+ return prompt_embeds, prompt_attention_mask, negative_prompt_embeds, negative_prompt_attention_mask
149
+
150
+ @torch.no_grad()
151
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
152
+ block_state = self.get_block_state(state)
153
+ self.check_inputs(block_state)
154
+
155
+ block_state.device = components._execution_device
156
+
157
+ (
158
+ block_state.prompt_embeds,
159
+ block_state.prompt_attention_mask,
160
+ block_state.negative_prompt_embeds,
161
+ block_state.negative_prompt_attention_mask,
162
+ ) = self.encode_prompt(
163
+ components=components,
164
+ prompt=block_state.prompt,
165
+ device=block_state.device,
166
+ prepare_unconditional_embeds=components.requires_unconditional_embeds,
167
+ negative_prompt=block_state.negative_prompt,
168
+ max_sequence_length=block_state.max_sequence_length,
169
+ )
170
+
171
+ self.set_block_state(state, block_state)
172
+ return components, state
173
+
174
+
175
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
176
+ def retrieve_latents(
177
+ encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
178
+ ):
179
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
180
+ return encoder_output.latent_dist.sample(generator)
181
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
182
+ return encoder_output.latent_dist.mode()
183
+ elif hasattr(encoder_output, "latents"):
184
+ return encoder_output.latents
185
+ else:
186
+ raise AttributeError("Could not access latents of provided encoder_output")
187
+
188
+
189
+ def _normalize_latents(
190
+ latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
191
+ ) -> torch.Tensor:
192
+ # Normalize latents across the channel dimension [B, C, F, H, W]
193
+ latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
194
+ latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
195
+ latents = (latents - latents_mean) * scaling_factor / latents_std
196
+ return latents
197
+
198
+
199
+ class LTXVaeEncoderStep(ModularPipelineBlocks):
200
+ model_name = "ltx"
201
+
202
+ @property
203
+ def description(self) -> str:
204
+ return "VAE Encoder step that encodes an input image into latent space for image-to-video generation"
205
+
206
+ @property
207
+ def expected_components(self) -> list[ComponentSpec]:
208
+ return [
209
+ ComponentSpec("vae", AutoencoderKLLTXVideo),
210
+ ComponentSpec(
211
+ "video_processor",
212
+ VideoProcessor,
213
+ config=FrozenDict({"vae_scale_factor": 32}),
214
+ default_creation_method="from_config",
215
+ ),
216
+ ]
217
+
218
+ @property
219
+ def inputs(self) -> list[InputParam]:
220
+ return [
221
+ InputParam.template("image", required=True),
222
+ InputParam.template("height", default=512),
223
+ InputParam.template("width", default=704),
224
+ InputParam.template("generator"),
225
+ ]
226
+
227
+ @property
228
+ def intermediate_outputs(self) -> list[OutputParam]:
229
+ return [
230
+ OutputParam(
231
+ "image_latents",
232
+ type_hint=torch.Tensor,
233
+ description="Encoded image latents from the VAE encoder",
234
+ ),
235
+ ]
236
+
237
+ @torch.no_grad()
238
+ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
239
+ block_state = self.get_block_state(state)
240
+ device = components._execution_device
241
+
242
+ image = block_state.image
243
+ if not isinstance(image, torch.Tensor):
244
+ image = components.video_processor.preprocess(image, height=block_state.height, width=block_state.width)
245
+ image = image.to(device=device, dtype=torch.float32)
246
+
247
+ vae_dtype = components.vae.dtype
248
+
249
+ num_images = image.shape[0]
250
+ if isinstance(block_state.generator, list):
251
+ init_latents = [
252
+ retrieve_latents(
253
+ components.vae.encode(image[i].unsqueeze(0).unsqueeze(2).to(vae_dtype)),
254
+ block_state.generator[i],
255
+ )
256
+ for i in range(num_images)
257
+ ]
258
+ else:
259
+ init_latents = [
260
+ retrieve_latents(
261
+ components.vae.encode(img.unsqueeze(0).unsqueeze(2).to(vae_dtype)),
262
+ block_state.generator,
263
+ )
264
+ for img in image
265
+ ]
266
+
267
+ init_latents = torch.cat(init_latents, dim=0).to(torch.float32)
268
+ block_state.image_latents = _normalize_latents(
269
+ init_latents, components.vae.latents_mean, components.vae.latents_std
270
+ )
271
+
272
+ self.set_block_state(state, block_state)
273
+ return components, state
diffusers/modular_pipelines/ltx/modular_blocks_ltx.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from ...utils import logging
16
+ from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
17
+ from ..modular_pipeline_utils import OutputParam
18
+ from .before_denoise import (
19
+ LTXImage2VideoPrepareLatentsStep,
20
+ LTXPrepareLatentsStep,
21
+ LTXSetTimestepsStep,
22
+ LTXTextInputStep,
23
+ )
24
+ from .decoders import LTXVaeDecoderStep
25
+ from .denoise import LTXDenoiseStep, LTXImage2VideoDenoiseStep
26
+ from .encoders import LTXTextEncoderStep, LTXVaeEncoderStep
27
+
28
+
29
+ logger = logging.get_logger(__name__)
30
+
31
+
32
+ # auto_docstring
33
+ class LTXCoreDenoiseStep(SequentialPipelineBlocks):
34
+ """
35
+ Denoise block that takes encoded conditions and runs the denoising process.
36
+
37
+ Components:
38
+ scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
39
+ (`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
40
+
41
+ Inputs:
42
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
43
+ The number of images to generate per prompt.
44
+ prompt_embeds (`Tensor`):
45
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
46
+ prompt_attention_mask (`Tensor`):
47
+ mask for the text embeddings. Can be generated from text_encoder step.
48
+ negative_prompt_embeds (`Tensor`, *optional*):
49
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
50
+ negative_prompt_attention_mask (`Tensor`, *optional*):
51
+ mask for the negative text embeddings. Can be generated from text_encoder step.
52
+ num_inference_steps (`int`, *optional*, defaults to 50):
53
+ The number of denoising steps.
54
+ timesteps (`Tensor`, *optional*):
55
+ Timesteps for the denoising process.
56
+ sigmas (`list`, *optional*):
57
+ Custom sigmas for the denoising process.
58
+ height (`int`, *optional*, defaults to 512):
59
+ The height in pixels of the generated image.
60
+ width (`int`, *optional*, defaults to 704):
61
+ The width in pixels of the generated image.
62
+ num_frames (`int`, *optional*, defaults to 161):
63
+ TODO: Add description.
64
+ frame_rate (`int`, *optional*, defaults to 25):
65
+ TODO: Add description.
66
+ latents (`Tensor`, *optional*):
67
+ Pre-generated noisy latents for image generation.
68
+ generator (`Generator`, *optional*):
69
+ Torch generator for deterministic generation.
70
+ attention_kwargs (`dict`, *optional*):
71
+ Additional kwargs for attention processors.
72
+
73
+ Outputs:
74
+ latents (`Tensor`):
75
+ Denoised latents.
76
+ """
77
+
78
+ model_name = "ltx"
79
+ block_classes = [
80
+ LTXTextInputStep,
81
+ LTXSetTimestepsStep,
82
+ LTXPrepareLatentsStep,
83
+ LTXDenoiseStep,
84
+ ]
85
+ block_names = ["input", "set_timesteps", "prepare_latents", "denoise"]
86
+
87
+ @property
88
+ def description(self):
89
+ return "Denoise block that takes encoded conditions and runs the denoising process."
90
+
91
+ @property
92
+ def outputs(self):
93
+ return [OutputParam.template("latents")]
94
+
95
+
96
+ # auto_docstring
97
+ class LTXImage2VideoCoreDenoiseStep(SequentialPipelineBlocks):
98
+ """
99
+ Denoise block for image-to-video that takes encoded conditions and image latents, and runs the denoising process.
100
+
101
+ Components:
102
+ scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
103
+ (`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
104
+
105
+ Inputs:
106
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
107
+ The number of images to generate per prompt.
108
+ prompt_embeds (`Tensor`):
109
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
110
+ prompt_attention_mask (`Tensor`):
111
+ mask for the text embeddings. Can be generated from text_encoder step.
112
+ negative_prompt_embeds (`Tensor`, *optional*):
113
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
114
+ negative_prompt_attention_mask (`Tensor`, *optional*):
115
+ mask for the negative text embeddings. Can be generated from text_encoder step.
116
+ num_inference_steps (`int`, *optional*, defaults to 50):
117
+ The number of denoising steps.
118
+ timesteps (`Tensor`, *optional*):
119
+ Timesteps for the denoising process.
120
+ sigmas (`list`, *optional*):
121
+ Custom sigmas for the denoising process.
122
+ height (`int`, *optional*, defaults to 512):
123
+ The height in pixels of the generated image.
124
+ width (`int`, *optional*, defaults to 704):
125
+ The width in pixels of the generated image.
126
+ num_frames (`int`, *optional*, defaults to 161):
127
+ TODO: Add description.
128
+ frame_rate (`int`, *optional*, defaults to 25):
129
+ TODO: Add description.
130
+ latents (`Tensor`, *optional*):
131
+ Pre-generated noisy latents for image generation.
132
+ generator (`Generator`, *optional*):
133
+ Torch generator for deterministic generation.
134
+ image_latents (`Tensor`):
135
+ TODO: Add description.
136
+ attention_kwargs (`dict`, *optional*):
137
+ Additional kwargs for attention processors.
138
+
139
+ Outputs:
140
+ latents (`Tensor`):
141
+ Denoised latents.
142
+ """
143
+
144
+ model_name = "ltx"
145
+ block_classes = [
146
+ LTXTextInputStep,
147
+ LTXSetTimestepsStep,
148
+ LTXPrepareLatentsStep,
149
+ LTXImage2VideoPrepareLatentsStep,
150
+ LTXImage2VideoDenoiseStep,
151
+ ]
152
+ block_names = ["input", "set_timesteps", "prepare_latents", "prepare_i2v_latents", "denoise"]
153
+
154
+ @property
155
+ def description(self):
156
+ return "Denoise block for image-to-video that takes encoded conditions and image latents, and runs the denoising process."
157
+
158
+ @property
159
+ def outputs(self):
160
+ return [OutputParam.template("latents")]
161
+
162
+
163
+ # auto_docstring
164
+ class LTXBlocks(SequentialPipelineBlocks):
165
+ """
166
+ Modular pipeline blocks for LTX Video text-to-video.
167
+
168
+ Components:
169
+ text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) scheduler
170
+ (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) transformer
171
+ (`LTXVideoTransformer3DModel`) vae (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`)
172
+
173
+ Inputs:
174
+ prompt (`str`):
175
+ The prompt or prompts to guide image generation.
176
+ negative_prompt (`str`, *optional*):
177
+ The prompt or prompts not to guide the image generation.
178
+ max_sequence_length (`int`, *optional*, defaults to 128):
179
+ Maximum sequence length for prompt encoding.
180
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
181
+ The number of images to generate per prompt.
182
+ num_inference_steps (`int`, *optional*, defaults to 50):
183
+ The number of denoising steps.
184
+ timesteps (`Tensor`, *optional*):
185
+ Timesteps for the denoising process.
186
+ sigmas (`list`, *optional*):
187
+ Custom sigmas for the denoising process.
188
+ height (`int`, *optional*, defaults to 512):
189
+ The height in pixels of the generated image.
190
+ width (`int`, *optional*, defaults to 704):
191
+ The width in pixels of the generated image.
192
+ num_frames (`int`, *optional*, defaults to 161):
193
+ TODO: Add description.
194
+ frame_rate (`int`, *optional*, defaults to 25):
195
+ TODO: Add description.
196
+ latents (`Tensor`, *optional*):
197
+ Pre-generated noisy latents for image generation.
198
+ generator (`Generator`, *optional*):
199
+ Torch generator for deterministic generation.
200
+ attention_kwargs (`dict`, *optional*):
201
+ Additional kwargs for attention processors.
202
+ output_type (`str`, *optional*, defaults to np):
203
+ Output format: 'pil', 'np', 'pt'.
204
+ decode_timestep (`None`, *optional*, defaults to 0.0):
205
+ TODO: Add description.
206
+ decode_noise_scale (`None`, *optional*):
207
+ TODO: Add description.
208
+
209
+ Outputs:
210
+ videos (`list`):
211
+ The generated videos.
212
+ """
213
+
214
+ model_name = "ltx"
215
+ block_classes = [
216
+ LTXTextEncoderStep,
217
+ LTXCoreDenoiseStep,
218
+ LTXVaeDecoderStep,
219
+ ]
220
+ block_names = ["text_encoder", "denoise", "decode"]
221
+
222
+ @property
223
+ def description(self):
224
+ return "Modular pipeline blocks for LTX Video text-to-video."
225
+
226
+ @property
227
+ def outputs(self):
228
+ return [OutputParam.template("videos")]
229
+
230
+
231
+ # auto_docstring
232
+ class LTXAutoVaeEncoderStep(AutoPipelineBlocks):
233
+ """
234
+ VAE encoder step that encodes the image input into its latent representation.
235
+ This is an auto pipeline block that works for image-to-video tasks.
236
+ - `LTXVaeEncoderStep` is used when `image` is provided.
237
+ - If `image` is not provided, step will be skipped.
238
+
239
+ Components:
240
+ vae (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`)
241
+
242
+ Inputs:
243
+ image (`Image | list`, *optional*):
244
+ Reference image(s) for denoising. Can be a single image or list of images.
245
+ height (`int`, *optional*, defaults to 512):
246
+ The height in pixels of the generated image.
247
+ width (`int`, *optional*, defaults to 704):
248
+ The width in pixels of the generated image.
249
+ generator (`Generator`, *optional*):
250
+ Torch generator for deterministic generation.
251
+
252
+ Outputs:
253
+ image_latents (`Tensor`):
254
+ Encoded image latents from the VAE encoder
255
+ """
256
+
257
+ model_name = "ltx"
258
+ block_classes = [LTXVaeEncoderStep]
259
+ block_names = ["vae_encoder"]
260
+ block_trigger_inputs = ["image"]
261
+
262
+ @property
263
+ def description(self):
264
+ return (
265
+ "VAE encoder step that encodes the image input into its latent representation.\n"
266
+ "This is an auto pipeline block that works for image-to-video tasks.\n"
267
+ " - `LTXVaeEncoderStep` is used when `image` is provided.\n"
268
+ " - If `image` is not provided, step will be skipped."
269
+ )
270
+
271
+
272
+ # auto_docstring
273
+ class LTXAutoCoreDenoiseStep(AutoPipelineBlocks):
274
+ """
275
+ Auto denoise block that selects the appropriate denoise pipeline based on inputs.
276
+ - `LTXImage2VideoCoreDenoiseStep` is used when `image_latents` is provided.
277
+ - `LTXCoreDenoiseStep` is used otherwise (text-to-video).
278
+
279
+ Components:
280
+ scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
281
+ (`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
282
+
283
+ Inputs:
284
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
285
+ The number of images to generate per prompt.
286
+ prompt_embeds (`Tensor`):
287
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
288
+ prompt_attention_mask (`Tensor`):
289
+ mask for the text embeddings. Can be generated from text_encoder step.
290
+ negative_prompt_embeds (`Tensor`):
291
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
292
+ negative_prompt_attention_mask (`Tensor`):
293
+ mask for the negative text embeddings. Can be generated from text_encoder step.
294
+ num_inference_steps (`int`):
295
+ The number of denoising steps.
296
+ timesteps (`Tensor`):
297
+ Timesteps for the denoising process.
298
+ sigmas (`list`, *optional*):
299
+ Custom sigmas for the denoising process.
300
+ height (`int`, *optional*, defaults to 512):
301
+ The height in pixels of the generated image.
302
+ width (`int`, *optional*, defaults to 704):
303
+ The width in pixels of the generated image.
304
+ num_frames (`int`, *optional*, defaults to 161):
305
+ TODO: Add description.
306
+ frame_rate (`int`, *optional*, defaults to 25):
307
+ TODO: Add description.
308
+ latents (`Tensor`):
309
+ Pre-generated noisy latents for image generation.
310
+ generator (`Generator`, *optional*):
311
+ Torch generator for deterministic generation.
312
+ image_latents (`Tensor`, *optional*):
313
+ TODO: Add description.
314
+ attention_kwargs (`dict`, *optional*):
315
+ Additional kwargs for attention processors.
316
+
317
+ Outputs:
318
+ latents (`Tensor`):
319
+ Denoised latents.
320
+ """
321
+
322
+ model_name = "ltx"
323
+ block_classes = [LTXImage2VideoCoreDenoiseStep, LTXCoreDenoiseStep]
324
+ block_names = ["image2video", "text2video"]
325
+ block_trigger_inputs = ["image_latents", None]
326
+
327
+ @property
328
+ def description(self):
329
+ return (
330
+ "Auto denoise block that selects the appropriate denoise pipeline based on inputs.\n"
331
+ " - `LTXImage2VideoCoreDenoiseStep` is used when `image_latents` is provided.\n"
332
+ " - `LTXCoreDenoiseStep` is used otherwise (text-to-video)."
333
+ )
334
+
335
+
336
+ # auto_docstring
337
+ class LTXAutoBlocks(SequentialPipelineBlocks):
338
+ """
339
+ Auto blocks for LTX Video that support both text-to-video and image-to-video workflows.
340
+
341
+ Supported workflows:
342
+ - `text2video`: requires `prompt`
343
+ - `image2video`: requires `image`, `prompt`
344
+
345
+ Components:
346
+ text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) vae
347
+ (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`)
348
+ pachifier (`LTXVideoPachifier`) transformer (`LTXVideoTransformer3DModel`)
349
+
350
+ Inputs:
351
+ prompt (`str`):
352
+ The prompt or prompts to guide image generation.
353
+ negative_prompt (`str`, *optional*):
354
+ The prompt or prompts not to guide the image generation.
355
+ max_sequence_length (`int`, *optional*, defaults to 128):
356
+ Maximum sequence length for prompt encoding.
357
+ image (`Image | list`, *optional*):
358
+ Reference image(s) for denoising. Can be a single image or list of images.
359
+ height (`int`, *optional*, defaults to 512):
360
+ The height in pixels of the generated image.
361
+ width (`int`, *optional*, defaults to 704):
362
+ The width in pixels of the generated image.
363
+ generator (`Generator`, *optional*):
364
+ Torch generator for deterministic generation.
365
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
366
+ The number of images to generate per prompt.
367
+ num_inference_steps (`int`):
368
+ The number of denoising steps.
369
+ timesteps (`Tensor`):
370
+ Timesteps for the denoising process.
371
+ sigmas (`list`, *optional*):
372
+ Custom sigmas for the denoising process.
373
+ num_frames (`int`, *optional*, defaults to 161):
374
+ TODO: Add description.
375
+ frame_rate (`int`, *optional*, defaults to 25):
376
+ TODO: Add description.
377
+ latents (`Tensor`):
378
+ Pre-generated noisy latents for image generation.
379
+ image_latents (`Tensor`, *optional*):
380
+ TODO: Add description.
381
+ attention_kwargs (`dict`, *optional*):
382
+ Additional kwargs for attention processors.
383
+ output_type (`str`, *optional*, defaults to np):
384
+ Output format: 'pil', 'np', 'pt'.
385
+ decode_timestep (`None`, *optional*, defaults to 0.0):
386
+ TODO: Add description.
387
+ decode_noise_scale (`None`, *optional*):
388
+ TODO: Add description.
389
+
390
+ Outputs:
391
+ videos (`list`):
392
+ The generated videos.
393
+ """
394
+
395
+ model_name = "ltx"
396
+ block_classes = [
397
+ LTXTextEncoderStep,
398
+ LTXAutoVaeEncoderStep,
399
+ LTXAutoCoreDenoiseStep,
400
+ LTXVaeDecoderStep,
401
+ ]
402
+ block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
403
+ _workflow_map = {
404
+ "text2video": {"prompt": True},
405
+ "image2video": {"image": True, "prompt": True},
406
+ }
407
+
408
+ @property
409
+ def description(self):
410
+ return "Auto blocks for LTX Video that support both text-to-video and image-to-video workflows."
411
+
412
+ @property
413
+ def outputs(self):
414
+ return [OutputParam.template("videos")]
415
+
416
+
417
+ # auto_docstring
418
+ class LTXImage2VideoBlocks(SequentialPipelineBlocks):
419
+ """
420
+ Modular pipeline blocks for LTX Video image-to-video.
421
+
422
+ Components:
423
+ text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) vae
424
+ (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`)
425
+ pachifier (`LTXVideoPachifier`) transformer (`LTXVideoTransformer3DModel`)
426
+
427
+ Inputs:
428
+ prompt (`str`):
429
+ The prompt or prompts to guide image generation.
430
+ negative_prompt (`str`, *optional*):
431
+ The prompt or prompts not to guide the image generation.
432
+ max_sequence_length (`int`, *optional*, defaults to 128):
433
+ Maximum sequence length for prompt encoding.
434
+ image (`Image | list`, *optional*):
435
+ Reference image(s) for denoising. Can be a single image or list of images.
436
+ height (`int`, *optional*, defaults to 512):
437
+ The height in pixels of the generated image.
438
+ width (`int`, *optional*, defaults to 704):
439
+ The width in pixels of the generated image.
440
+ generator (`Generator`, *optional*):
441
+ Torch generator for deterministic generation.
442
+ num_videos_per_prompt (`int`, *optional*, defaults to 1):
443
+ The number of images to generate per prompt.
444
+ num_inference_steps (`int`, *optional*, defaults to 50):
445
+ The number of denoising steps.
446
+ timesteps (`Tensor`, *optional*):
447
+ Timesteps for the denoising process.
448
+ sigmas (`list`, *optional*):
449
+ Custom sigmas for the denoising process.
450
+ num_frames (`int`, *optional*, defaults to 161):
451
+ TODO: Add description.
452
+ frame_rate (`int`, *optional*, defaults to 25):
453
+ TODO: Add description.
454
+ latents (`Tensor`, *optional*):
455
+ Pre-generated noisy latents for image generation.
456
+ image_latents (`Tensor`):
457
+ TODO: Add description.
458
+ attention_kwargs (`dict`, *optional*):
459
+ Additional kwargs for attention processors.
460
+ output_type (`str`, *optional*, defaults to np):
461
+ Output format: 'pil', 'np', 'pt'.
462
+ decode_timestep (`None`, *optional*, defaults to 0.0):
463
+ TODO: Add description.
464
+ decode_noise_scale (`None`, *optional*):
465
+ TODO: Add description.
466
+
467
+ Outputs:
468
+ videos (`list`):
469
+ The generated videos.
470
+ """
471
+
472
+ model_name = "ltx"
473
+ block_classes = [
474
+ LTXTextEncoderStep,
475
+ LTXAutoVaeEncoderStep,
476
+ LTXImage2VideoCoreDenoiseStep,
477
+ LTXVaeDecoderStep,
478
+ ]
479
+ block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
480
+
481
+ @property
482
+ def description(self):
483
+ return "Modular pipeline blocks for LTX Video image-to-video."
484
+
485
+ @property
486
+ def outputs(self):
487
+ return [OutputParam.template("videos")]
diffusers/modular_pipelines/ltx/modular_pipeline.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ import torch
17
+
18
+ from ...configuration_utils import ConfigMixin, register_to_config
19
+ from ...loaders import LTXVideoLoraLoaderMixin
20
+ from ...utils import logging
21
+ from ..modular_pipeline import ModularPipeline
22
+
23
+
24
+ logger = logging.get_logger(__name__)
25
+
26
+
27
+ class LTXVideoPachifier(ConfigMixin):
28
+ """
29
+ A class to pack and unpack latents for LTX Video.
30
+ """
31
+
32
+ config_name = "config.json"
33
+
34
+ @register_to_config
35
+ def __init__(self, patch_size: int = 1, patch_size_t: int = 1):
36
+ super().__init__()
37
+
38
+ def pack_latents(self, latents: torch.Tensor) -> torch.Tensor:
39
+ batch_size, _, num_frames, height, width = latents.shape
40
+ patch_size = self.config.patch_size
41
+ patch_size_t = self.config.patch_size_t
42
+ post_patch_num_frames = num_frames // patch_size_t
43
+ post_patch_height = height // patch_size
44
+ post_patch_width = width // patch_size
45
+ latents = latents.reshape(
46
+ batch_size,
47
+ -1,
48
+ post_patch_num_frames,
49
+ patch_size_t,
50
+ post_patch_height,
51
+ patch_size,
52
+ post_patch_width,
53
+ patch_size,
54
+ )
55
+ latents = latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
56
+ return latents
57
+
58
+ def unpack_latents(self, latents: torch.Tensor, num_frames: int, height: int, width: int) -> torch.Tensor:
59
+ batch_size = latents.size(0)
60
+ patch_size = self.config.patch_size
61
+ patch_size_t = self.config.patch_size_t
62
+ latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size)
63
+ latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
64
+ return latents
65
+
66
+
67
+ class LTXModularPipeline(
68
+ ModularPipeline,
69
+ LTXVideoLoraLoaderMixin,
70
+ ):
71
+ """
72
+ A ModularPipeline for LTX Video.
73
+
74
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
75
+ """
76
+
77
+ default_blocks_name = "LTXAutoBlocks"
78
+
79
+ @property
80
+ def vae_spatial_compression_ratio(self):
81
+ if getattr(self, "vae", None) is not None:
82
+ return self.vae.spatial_compression_ratio
83
+ return 32
84
+
85
+ @property
86
+ def vae_temporal_compression_ratio(self):
87
+ if getattr(self, "vae", None) is not None:
88
+ return self.vae.temporal_compression_ratio
89
+ return 8
90
+
91
+ @property
92
+ def requires_unconditional_embeds(self):
93
+ if hasattr(self, "guider") and self.guider is not None:
94
+ return self.guider._enabled and self.guider.num_conditions > 1
95
+ return False
diffusers/modular_pipelines/mellon_node_utils.py ADDED
@@ -0,0 +1,1101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import json
3
+ import logging
4
+ import os
5
+
6
+ # Simple typed wrapper for parameter overrides
7
+ from dataclasses import asdict, dataclass
8
+ from typing import Any
9
+
10
+ from huggingface_hub import create_repo, hf_hub_download, upload_file
11
+ from huggingface_hub.utils import (
12
+ EntryNotFoundError,
13
+ HfHubHTTPError,
14
+ RepositoryNotFoundError,
15
+ RevisionNotFoundError,
16
+ )
17
+
18
+ from ..utils import HUGGINGFACE_CO_RESOLVE_ENDPOINT
19
+ from .modular_pipeline_utils import InputParam, OutputParam
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _name_to_label(name: str) -> str:
26
+ """Convert snake_case name to Title Case label."""
27
+ return name.replace("_", " ").title()
28
+
29
+
30
+ # Template definitions for standard diffuser pipeline parameters
31
+ MELLON_PARAM_TEMPLATES = {
32
+ # Image I/O
33
+ "image": {"label": "Image", "type": "image", "display": "input", "required_block_params": ["image"]},
34
+ "images": {"label": "Images", "type": "image", "display": "output", "required_block_params": ["images"]},
35
+ "control_image": {
36
+ "label": "Control Image",
37
+ "type": "image",
38
+ "display": "input",
39
+ "required_block_params": ["control_image"],
40
+ },
41
+ # Latents
42
+ "latents": {"label": "Latents", "type": "latents", "display": "input", "required_block_params": ["latents"]},
43
+ "image_latents": {
44
+ "label": "Image Latents",
45
+ "type": "latents",
46
+ "display": "input",
47
+ "required_block_params": ["image_latents"],
48
+ },
49
+ "first_frame_latents": {
50
+ "label": "First Frame Latents",
51
+ "type": "latents",
52
+ "display": "input",
53
+ "required_block_params": ["first_frame_latents"],
54
+ },
55
+ "latents_preview": {"label": "Latents Preview", "type": "latent", "display": "output"},
56
+ # Image Latents with Strength
57
+ "image_latents_with_strength": {
58
+ "name": "image_latents", # name is not same as template key
59
+ "label": "Image Latents",
60
+ "type": "latents",
61
+ "display": "input",
62
+ "onChange": {"false": ["height", "width"], "true": ["strength"]},
63
+ "required_block_params": ["image_latents", "strength"],
64
+ },
65
+ # Embeddings
66
+ "embeddings": {"label": "Text Embeddings", "type": "embeddings", "display": "output"},
67
+ "image_embeds": {
68
+ "label": "Image Embeddings",
69
+ "type": "image_embeds",
70
+ "display": "output",
71
+ "required_block_params": ["image_embeds"],
72
+ },
73
+ # Text inputs
74
+ "prompt": {
75
+ "label": "Prompt",
76
+ "type": "string",
77
+ "display": "textarea",
78
+ "default": "",
79
+ "required_block_params": ["prompt"],
80
+ },
81
+ "negative_prompt": {
82
+ "label": "Negative Prompt",
83
+ "type": "string",
84
+ "display": "textarea",
85
+ "default": "",
86
+ "required_block_params": ["negative_prompt"],
87
+ },
88
+ # Numeric params
89
+ "guidance_scale": {
90
+ "label": "Guidance Scale",
91
+ "type": "float",
92
+ "display": "slider",
93
+ "default": 5.0,
94
+ "min": 1.0,
95
+ "max": 30.0,
96
+ "step": 0.1,
97
+ },
98
+ "strength": {
99
+ "label": "Strength",
100
+ "type": "float",
101
+ "default": 0.5,
102
+ "min": 0.0,
103
+ "max": 1.0,
104
+ "step": 0.01,
105
+ "required_block_params": ["strength"],
106
+ },
107
+ "height": {
108
+ "label": "Height",
109
+ "type": "int",
110
+ "default": 1024,
111
+ "min": 64,
112
+ "step": 8,
113
+ "required_block_params": ["height"],
114
+ },
115
+ "width": {
116
+ "label": "Width",
117
+ "type": "int",
118
+ "default": 1024,
119
+ "min": 64,
120
+ "step": 8,
121
+ "required_block_params": ["width"],
122
+ },
123
+ "seed": {
124
+ "label": "Seed",
125
+ "type": "int",
126
+ "default": 0,
127
+ "min": 0,
128
+ "max": 4294967295,
129
+ "display": "random",
130
+ "required_block_params": ["generator"],
131
+ },
132
+ "num_inference_steps": {
133
+ "label": "Steps",
134
+ "type": "int",
135
+ "default": 25,
136
+ "min": 1,
137
+ "max": 100,
138
+ "display": "slider",
139
+ "required_block_params": ["num_inference_steps"],
140
+ },
141
+ "num_frames": {
142
+ "label": "Frames",
143
+ "type": "int",
144
+ "default": 81,
145
+ "min": 1,
146
+ "max": 480,
147
+ "display": "slider",
148
+ "required_block_params": ["num_frames"],
149
+ },
150
+ "layers": {
151
+ "label": "Layers",
152
+ "type": "int",
153
+ "default": 4,
154
+ "min": 1,
155
+ "max": 10,
156
+ "display": "slider",
157
+ "required_block_params": ["layers"],
158
+ },
159
+ "output_type": {
160
+ "label": "Output Type",
161
+ "type": "dropdown",
162
+ "default": "np",
163
+ "options": ["np", "pil", "pt"],
164
+ },
165
+ # ControlNet
166
+ "controlnet_conditioning_scale": {
167
+ "label": "Controlnet Conditioning Scale",
168
+ "type": "float",
169
+ "default": 0.5,
170
+ "min": 0.0,
171
+ "max": 1.0,
172
+ "step": 0.01,
173
+ "required_block_params": ["controlnet_conditioning_scale"],
174
+ },
175
+ "control_guidance_start": {
176
+ "label": "Control Guidance Start",
177
+ "type": "float",
178
+ "default": 0.0,
179
+ "min": 0.0,
180
+ "max": 1.0,
181
+ "step": 0.01,
182
+ "required_block_params": ["control_guidance_start"],
183
+ },
184
+ "control_guidance_end": {
185
+ "label": "Control Guidance End",
186
+ "type": "float",
187
+ "default": 1.0,
188
+ "min": 0.0,
189
+ "max": 1.0,
190
+ "step": 0.01,
191
+ "required_block_params": ["control_guidance_end"],
192
+ },
193
+ # Video
194
+ "videos": {"label": "Videos", "type": "video", "display": "output", "required_block_params": ["videos"]},
195
+ # Models
196
+ "vae": {"label": "VAE", "type": "diffusers_auto_model", "display": "input", "required_block_params": ["vae"]},
197
+ "image_encoder": {
198
+ "label": "Image Encoder",
199
+ "type": "diffusers_auto_model",
200
+ "display": "input",
201
+ "required_block_params": ["image_encoder"],
202
+ },
203
+ "unet": {"label": "Denoise Model", "type": "diffusers_auto_model", "display": "input"},
204
+ "scheduler": {"label": "Scheduler", "type": "diffusers_auto_model", "display": "input"},
205
+ "controlnet": {
206
+ "label": "ControlNet Model",
207
+ "type": "diffusers_auto_model",
208
+ "display": "input",
209
+ "required_block_params": ["controlnet"],
210
+ },
211
+ "text_encoders": {
212
+ "label": "Text Encoders",
213
+ "type": "diffusers_auto_models",
214
+ "display": "input",
215
+ "required_block_params": ["text_encoder"],
216
+ },
217
+ # Bundles/Custom
218
+ "controlnet_bundle": {
219
+ "label": "ControlNet",
220
+ "type": "custom_controlnet",
221
+ "display": "input",
222
+ "required_block_params": "controlnet_image",
223
+ },
224
+ "ip_adapter": {"label": "IP Adapter", "type": "custom_ip_adapter", "display": "input"},
225
+ "guider": {
226
+ "label": "Guider",
227
+ "type": "custom_guider",
228
+ "display": "input",
229
+ "onChange": {False: ["guidance_scale"], True: []},
230
+ },
231
+ "doc": {"label": "Doc", "type": "string", "display": "output"},
232
+ }
233
+
234
+
235
+ class MellonParamMeta(type):
236
+ """Metaclass that enables MellonParam.template_name(**overrides) syntax."""
237
+
238
+ def __getattr__(cls, name: str):
239
+ if name in MELLON_PARAM_TEMPLATES:
240
+
241
+ def factory(default=None, **overrides):
242
+ template = MELLON_PARAM_TEMPLATES[name]
243
+ # Use template's name if specified, otherwise use the key
244
+ params = {"name": template.get("name", name), **template, **overrides}
245
+ if default is not None:
246
+ params["default"] = default
247
+ return cls(**params)
248
+
249
+ return factory
250
+
251
+ raise AttributeError(f"type object 'MellonParam' has no attribute '{name}'")
252
+
253
+
254
+ @dataclass(frozen=True)
255
+ class MellonParam(metaclass=MellonParamMeta):
256
+ """
257
+ Parameter definition for Mellon nodes.
258
+
259
+ Usage:
260
+ ```python
261
+ # From template (standard diffuser params)
262
+ MellonParam.seed()
263
+ MellonParam.prompt(default="a cat")
264
+ MellonParam.latents(display="output")
265
+
266
+ # Generic inputs (for custom blocks)
267
+ MellonParam.Input.slider("my_scale", default=1.0, min=0.0, max=2.0)
268
+ MellonParam.Input.dropdown("mode", options=["fast", "slow"])
269
+
270
+ # Generic outputs
271
+ MellonParam.Output.image("result_images")
272
+
273
+ # Fully custom
274
+ MellonParam(name="custom", label="Custom", type="float", default=0.5)
275
+ ```
276
+ """
277
+
278
+ name: str
279
+ label: str
280
+ type: str
281
+ display: str | None = None
282
+ default: Any = None
283
+ min: float | None = None
284
+ max: float | None = None
285
+ step: float | None = None
286
+ options: Any = None
287
+ value: Any = None
288
+ fieldOptions: dict[str, Any] | None = None
289
+ onChange: Any = None
290
+ onSignal: Any = None
291
+ required_block_params: str | list[str] | None = None
292
+
293
+ def to_dict(self) -> dict[str, Any]:
294
+ """Convert to dict for Mellon schema, excluding None values and internal fields."""
295
+ data = asdict(self)
296
+ return {k: v for k, v in data.items() if v is not None and k not in ("name", "required_block_params")}
297
+
298
+ # =========================================================================
299
+ # Input: Generic input parameter factories (for custom blocks)
300
+ # =========================================================================
301
+ class Input:
302
+ """input UI elements for custom blocks."""
303
+
304
+ @classmethod
305
+ def image(cls, name: str) -> "MellonParam":
306
+ """image input."""
307
+ return MellonParam(name=name, label=_name_to_label(name), type="image", display="input")
308
+
309
+ @classmethod
310
+ def textbox(cls, name: str, default: str = "") -> "MellonParam":
311
+ """text input as textarea."""
312
+ return MellonParam(
313
+ name=name, label=_name_to_label(name), type="string", display="textarea", default=default
314
+ )
315
+
316
+ @classmethod
317
+ def dropdown(cls, name: str, options: list[str] = None, default: str = None) -> "MellonParam":
318
+ """dropdown selection."""
319
+ if options and not default:
320
+ default = options[0]
321
+ if not default:
322
+ default = ""
323
+ if not options:
324
+ options = [default]
325
+ return MellonParam(name=name, label=_name_to_label(name), type="string", options=options, value=default)
326
+
327
+ @classmethod
328
+ def slider(
329
+ cls, name: str, default: float = 0, min: float = None, max: float = None, step: float = None
330
+ ) -> "MellonParam":
331
+ """slider input."""
332
+ is_float = isinstance(default, float) or (step is not None and isinstance(step, float))
333
+ param_type = "float" if is_float else "int"
334
+ if min is None:
335
+ min = default
336
+ if max is None:
337
+ max = default
338
+ if step is None:
339
+ step = 0.01 if is_float else 1
340
+ return MellonParam(
341
+ name=name,
342
+ label=_name_to_label(name),
343
+ type=param_type,
344
+ display="slider",
345
+ default=default,
346
+ min=min,
347
+ max=max,
348
+ step=step,
349
+ )
350
+
351
+ @classmethod
352
+ def number(
353
+ cls, name: str, default: float = 0, min: float = None, max: float = None, step: float = None
354
+ ) -> "MellonParam":
355
+ """number input (no slider)."""
356
+ is_float = isinstance(default, float) or (step is not None and isinstance(step, float))
357
+ param_type = "float" if is_float else "int"
358
+ return MellonParam(
359
+ name=name, label=_name_to_label(name), type=param_type, default=default, min=min, max=max, step=step
360
+ )
361
+
362
+ @classmethod
363
+ def seed(cls, name: str = "seed", default: int = 0) -> "MellonParam":
364
+ """seed input with randomize button."""
365
+ return MellonParam(
366
+ name=name,
367
+ label=_name_to_label(name),
368
+ type="int",
369
+ display="random",
370
+ default=default,
371
+ min=0,
372
+ max=4294967295,
373
+ )
374
+
375
+ @classmethod
376
+ def checkbox(cls, name: str, default: bool = False) -> "MellonParam":
377
+ """boolean checkbox."""
378
+ return MellonParam(name=name, label=_name_to_label(name), type="boolean", value=default)
379
+
380
+ @classmethod
381
+ def custom_type(cls, name: str, type: str) -> "MellonParam":
382
+ """custom type input for node connections."""
383
+ return MellonParam(name=name, label=_name_to_label(name), type=type, display="input")
384
+
385
+ @classmethod
386
+ def model(cls, name: str) -> "MellonParam":
387
+ """model input for diffusers components."""
388
+ return MellonParam(name=name, label=_name_to_label(name), type="diffusers_auto_model", display="input")
389
+
390
+ # =========================================================================
391
+ # Output: Generic output parameter factories (for custom blocks)
392
+ # =========================================================================
393
+ class Output:
394
+ """output UI elements for custom blocks."""
395
+
396
+ @classmethod
397
+ def image(cls, name: str) -> "MellonParam":
398
+ """image output."""
399
+ return MellonParam(name=name, label=_name_to_label(name), type="image", display="output")
400
+
401
+ @classmethod
402
+ def video(cls, name: str) -> "MellonParam":
403
+ """video output."""
404
+ return MellonParam(name=name, label=_name_to_label(name), type="video", display="output")
405
+
406
+ @classmethod
407
+ def text(cls, name: str) -> "MellonParam":
408
+ """text output."""
409
+ return MellonParam(name=name, label=_name_to_label(name), type="string", display="output")
410
+
411
+ @classmethod
412
+ def custom_type(cls, name: str, type: str) -> "MellonParam":
413
+ """custom type output for node connections."""
414
+ return MellonParam(name=name, label=_name_to_label(name), type=type, display="output")
415
+
416
+ @classmethod
417
+ def model(cls, name: str) -> "MellonParam":
418
+ """model output for diffusers components."""
419
+ return MellonParam(name=name, label=_name_to_label(name), type="diffusers_auto_model", display="output")
420
+
421
+
422
+ def input_param_to_mellon_param(input_param: "InputParam") -> MellonParam:
423
+ """
424
+ Convert an InputParam to a MellonParam using metadata.
425
+
426
+ Args:
427
+ input_param: An InputParam with optional metadata containing either:
428
+ - {"mellon": "<type>"} for simple types (image, textbox, slider, etc.)
429
+ - {"mellon": MellonParam(...)} for full control over UI configuration
430
+
431
+ Returns:
432
+ MellonParam instance
433
+ """
434
+ name = input_param.name
435
+ metadata = input_param.metadata
436
+ mellon_value = metadata.get("mellon") if metadata else None
437
+ default = input_param.default
438
+
439
+ # If it's already a MellonParam, return it directly
440
+ if isinstance(mellon_value, MellonParam):
441
+ return mellon_value
442
+
443
+ mellon_type = mellon_value
444
+
445
+ if mellon_type == "image":
446
+ return MellonParam.Input.image(name)
447
+ elif mellon_type == "textbox":
448
+ return MellonParam.Input.textbox(name, default=default or "")
449
+ elif mellon_type == "dropdown":
450
+ return MellonParam.Input.dropdown(name, default=default or "")
451
+ elif mellon_type == "slider":
452
+ return MellonParam.Input.slider(name, default=default or 0)
453
+ elif mellon_type == "number":
454
+ return MellonParam.Input.number(name, default=default or 0)
455
+ elif mellon_type == "seed":
456
+ return MellonParam.Input.seed(name, default=default or 0)
457
+ elif mellon_type == "checkbox":
458
+ return MellonParam.Input.checkbox(name, default=default or False)
459
+ elif mellon_type == "model":
460
+ return MellonParam.Input.model(name)
461
+ else:
462
+ # None or unknown -> custom
463
+ return MellonParam.Input.custom_type(name, type="custom")
464
+
465
+
466
+ def output_param_to_mellon_param(output_param: "OutputParam") -> MellonParam:
467
+ """
468
+ Convert an OutputParam to a MellonParam using metadata.
469
+
470
+ Args:
471
+ output_param: An OutputParam with optional metadata={"mellon": "<type>"} where type is one of:
472
+ image, video, text, model. If metadata is None or unknown, maps to "custom".
473
+
474
+ Returns:
475
+ MellonParam instance
476
+ """
477
+ name = output_param.name
478
+ metadata = output_param.metadata
479
+ mellon_type = metadata.get("mellon") if metadata else None
480
+
481
+ if mellon_type == "image":
482
+ return MellonParam.Output.image(name)
483
+ elif mellon_type == "video":
484
+ return MellonParam.Output.video(name)
485
+ elif mellon_type == "text":
486
+ return MellonParam.Output.text(name)
487
+ elif mellon_type == "model":
488
+ return MellonParam.Output.model(name)
489
+ else:
490
+ # None or unknown -> custom
491
+ return MellonParam.Output.custom_type(name, type="custom")
492
+
493
+
494
+ DEFAULT_NODE_SPECS = {
495
+ "controlnet": None,
496
+ "denoise": {
497
+ "inputs": [
498
+ MellonParam.embeddings(display="input"),
499
+ MellonParam.width(),
500
+ MellonParam.height(),
501
+ MellonParam.seed(),
502
+ MellonParam.num_inference_steps(),
503
+ MellonParam.num_frames(),
504
+ MellonParam.guidance_scale(),
505
+ MellonParam.strength(),
506
+ MellonParam.image_latents_with_strength(),
507
+ MellonParam.image_latents(),
508
+ MellonParam.first_frame_latents(),
509
+ MellonParam.controlnet_bundle(display="input"),
510
+ ],
511
+ "model_inputs": [
512
+ MellonParam.unet(),
513
+ MellonParam.guider(),
514
+ MellonParam.scheduler(),
515
+ ],
516
+ "outputs": [
517
+ MellonParam.latents(display="output"),
518
+ MellonParam.latents_preview(),
519
+ MellonParam.doc(),
520
+ ],
521
+ "required_inputs": ["embeddings"],
522
+ "required_model_inputs": ["unet", "scheduler"],
523
+ "block_name": "denoise",
524
+ },
525
+ "vae_encoder": {
526
+ "inputs": [
527
+ MellonParam.image(),
528
+ ],
529
+ "model_inputs": [
530
+ MellonParam.vae(),
531
+ ],
532
+ "outputs": [
533
+ MellonParam.image_latents(display="output"),
534
+ MellonParam.doc(),
535
+ ],
536
+ "required_inputs": ["image"],
537
+ "required_model_inputs": ["vae"],
538
+ "block_name": "vae_encoder",
539
+ },
540
+ "text_encoder": {
541
+ "inputs": [
542
+ MellonParam.prompt(),
543
+ MellonParam.negative_prompt(),
544
+ ],
545
+ "model_inputs": [
546
+ MellonParam.text_encoders(),
547
+ ],
548
+ "outputs": [
549
+ MellonParam.embeddings(display="output"),
550
+ MellonParam.doc(),
551
+ ],
552
+ "required_inputs": ["prompt"],
553
+ "required_model_inputs": ["text_encoders"],
554
+ "block_name": "text_encoder",
555
+ },
556
+ "decoder": {
557
+ "inputs": [
558
+ MellonParam.latents(display="input"),
559
+ ],
560
+ "model_inputs": [
561
+ MellonParam.vae(),
562
+ ],
563
+ "outputs": [
564
+ MellonParam.images(),
565
+ MellonParam.videos(),
566
+ MellonParam.doc(),
567
+ ],
568
+ "required_inputs": ["latents"],
569
+ "required_model_inputs": ["vae"],
570
+ "block_name": "decode",
571
+ },
572
+ }
573
+
574
+
575
+ def mark_required(label: str, marker: str = " *") -> str:
576
+ """Add required marker to label if not already present."""
577
+ if label.endswith(marker):
578
+ return label
579
+ return f"{label}{marker}"
580
+
581
+
582
+ def node_spec_to_mellon_dict(node_spec: dict[str, Any], node_type: str) -> dict[str, Any]:
583
+ """
584
+ Convert a node spec dict into Mellon format.
585
+
586
+ A node spec is how we define a Mellon diffusers node in code. This function converts it into the `params` map
587
+ format that Mellon UI expects.
588
+
589
+ The `params` map is a dict where keys are parameter names and values are UI configuration:
590
+ ```python
591
+ {"seed": {"label": "Seed", "type": "int", "default": 0}}
592
+ ```
593
+
594
+ For Modular Mellon nodes, we need to distinguish:
595
+ - `inputs`: Pipeline inputs (e.g., seed, prompt, image)
596
+ - `model_inputs`: Model components (e.g., unet, vae, scheduler)
597
+ - `outputs`: Node outputs (e.g., latents, images)
598
+
599
+ The node spec also includes:
600
+ - `required_inputs` / `required_model_inputs`: Which params are required (marked with *)
601
+ - `block_name`: The modular pipeline block this node corresponds to on backend
602
+
603
+ We provide factory methods for common parameters (e.g., `MellonParam.seed()`, `MellonParam.unet()`) so you don't
604
+ have to manually specify all the UI configuration.
605
+
606
+ Args:
607
+ node_spec: Dict with `inputs`, `model_inputs`, `outputs` (lists of MellonParam),
608
+ plus `required_inputs`, `required_model_inputs`, `block_name`.
609
+ node_type: The node type string (e.g., "denoise", "controlnet")
610
+
611
+ Returns:
612
+ Dict with:
613
+ - `params`: Flat dict of all params in Mellon UI format
614
+ - `input_names`: List of input parameter names
615
+ - `model_input_names`: List of model input parameter names
616
+ - `output_names`: List of output parameter names
617
+ - `block_name`: The backend block name
618
+ - `node_type`: The node type
619
+
620
+ Example:
621
+ ```python
622
+ node_spec = {
623
+ "inputs": [MellonParam.seed(), MellonParam.prompt()],
624
+ "model_inputs": [MellonParam.unet()],
625
+ "outputs": [MellonParam.latents(display="output")],
626
+ "required_inputs": ["prompt"],
627
+ "required_model_inputs": ["unet"],
628
+ "block_name": "denoise",
629
+ }
630
+
631
+ result = node_spec_to_mellon_dict(node_spec, "denoise")
632
+ # Returns:
633
+ # {
634
+ # "params": {
635
+ # "seed": {"label": "Seed", "type": "int", "default": 0},
636
+ # "prompt": {"label": "Prompt *", "type": "string", "default": ""}, # * marks required
637
+ # "unet": {"label": "Denoise Model *", "type": "diffusers_auto_model", "display": "input"},
638
+ # "latents": {"label": "Latents", "type": "latents", "display": "output"},
639
+ # },
640
+ # "input_names": ["seed", "prompt"],
641
+ # "model_input_names": ["unet"],
642
+ # "output_names": ["latents"],
643
+ # "block_name": "denoise",
644
+ # "node_type": "denoise",
645
+ # }
646
+ ```
647
+ """
648
+ params = {}
649
+ input_names = []
650
+ model_input_names = []
651
+ output_names = []
652
+
653
+ required_inputs = node_spec.get("required_inputs", [])
654
+ required_model_inputs = node_spec.get("required_model_inputs", [])
655
+
656
+ # Process inputs
657
+ for p in node_spec.get("inputs", []):
658
+ param_dict = p.to_dict()
659
+ if p.name in required_inputs:
660
+ param_dict["label"] = mark_required(param_dict["label"])
661
+ params[p.name] = param_dict
662
+ input_names.append(p.name)
663
+
664
+ # Process model_inputs
665
+ for p in node_spec.get("model_inputs", []):
666
+ param_dict = p.to_dict()
667
+ if p.name in required_model_inputs:
668
+ param_dict["label"] = mark_required(param_dict["label"])
669
+ params[p.name] = param_dict
670
+ model_input_names.append(p.name)
671
+
672
+ # Process outputs: add a prefix to the output name if it already exists as an input
673
+ for p in node_spec.get("outputs", []):
674
+ if p.name in input_names:
675
+ # rename to out_<name>
676
+ output_name = f"out_{p.name}"
677
+ else:
678
+ output_name = p.name
679
+ params[output_name] = p.to_dict()
680
+ output_names.append(output_name)
681
+
682
+ return {
683
+ "params": params,
684
+ "input_names": input_names,
685
+ "model_input_names": model_input_names,
686
+ "output_names": output_names,
687
+ "block_name": node_spec.get("block_name"),
688
+ "node_type": node_type,
689
+ }
690
+
691
+
692
+ class MellonPipelineConfig:
693
+ """
694
+ Configuration for an entire Mellon pipeline containing multiple nodes.
695
+
696
+ Accepts node specs as dicts with inputs/model_inputs/outputs lists of MellonParam, converts them to Mellon-ready
697
+ format, and handles save/load to Hub.
698
+
699
+ Example:
700
+ ```python
701
+ config = MellonPipelineConfig(
702
+ node_specs={
703
+ "denoise": {
704
+ "inputs": [MellonParam.seed(), MellonParam.prompt()],
705
+ "model_inputs": [MellonParam.unet()],
706
+ "outputs": [MellonParam.latents(display="output")],
707
+ "required_inputs": ["prompt"],
708
+ "required_model_inputs": ["unet"],
709
+ "block_name": "denoise",
710
+ },
711
+ "decoder": {
712
+ "inputs": [MellonParam.latents(display="input")],
713
+ "outputs": [MellonParam.images()],
714
+ "block_name": "decoder",
715
+ },
716
+ },
717
+ label="My Pipeline",
718
+ default_repo="user/my-pipeline",
719
+ default_dtype="float16",
720
+ )
721
+
722
+ # Access Mellon format dict
723
+ denoise = config.node_params["denoise"]
724
+ input_names = denoise["input_names"]
725
+ params = denoise["params"]
726
+
727
+ # Save to Hub
728
+ config.save("./my_config", push_to_hub=True, repo_id="user/my-pipeline")
729
+
730
+ # Load from Hub
731
+ loaded = MellonPipelineConfig.load("user/my-pipeline")
732
+ ```
733
+ """
734
+
735
+ config_name = "mellon_pipeline_config.json"
736
+
737
+ def __init__(
738
+ self,
739
+ node_specs: dict[str, dict[str, Any] | None],
740
+ label: str = "",
741
+ default_repo: str = "",
742
+ default_dtype: str = "",
743
+ ):
744
+ """
745
+ Args:
746
+ node_specs: Dict mapping node_type to node spec or None.
747
+ Node spec has: inputs, model_inputs, outputs, required_inputs, required_model_inputs,
748
+ block_name (all optional)
749
+ label: Human-readable label for the pipeline
750
+ default_repo: Default HuggingFace repo for this pipeline
751
+ default_dtype: Default dtype (e.g., "float16", "bfloat16")
752
+ """
753
+ # Convert all node specs to Mellon format immediately
754
+ self.node_specs = node_specs
755
+
756
+ self.label = label
757
+ self.default_repo = default_repo
758
+ self.default_dtype = default_dtype
759
+
760
+ @property
761
+ def node_params(self) -> dict[str, Any]:
762
+ """Lazily compute node_params from node_specs."""
763
+ if self.node_specs is None:
764
+ return self._node_params
765
+
766
+ params = {}
767
+ for node_type, spec in self.node_specs.items():
768
+ if spec is None:
769
+ params[node_type] = None
770
+ else:
771
+ params[node_type] = node_spec_to_mellon_dict(spec, node_type)
772
+ return params
773
+
774
+ def __repr__(self) -> str:
775
+ lines = [
776
+ f"MellonPipelineConfig(label={self.label!r}, default_repo={self.default_repo!r}, default_dtype={self.default_dtype!r})"
777
+ ]
778
+ for node_type, spec in self.node_specs.items():
779
+ if spec is None:
780
+ lines.append(f" {node_type}: None")
781
+ else:
782
+ inputs = [p.name for p in spec.get("inputs", [])]
783
+ model_inputs = [p.name for p in spec.get("model_inputs", [])]
784
+ outputs = [p.name for p in spec.get("outputs", [])]
785
+ lines.append(f" {node_type}:")
786
+ lines.append(f" inputs: {inputs}")
787
+ lines.append(f" model_inputs: {model_inputs}")
788
+ lines.append(f" outputs: {outputs}")
789
+ return "\n".join(lines)
790
+
791
+ def to_dict(self) -> dict[str, Any]:
792
+ """Convert to a JSON-serializable dictionary."""
793
+ return {
794
+ "label": self.label,
795
+ "default_repo": self.default_repo,
796
+ "default_dtype": self.default_dtype,
797
+ "node_params": self.node_params,
798
+ }
799
+
800
+ @classmethod
801
+ def from_dict(cls, data: dict[str, Any]) -> "MellonPipelineConfig":
802
+ """
803
+ Create from a dictionary (loaded from JSON).
804
+
805
+ Note: The mellon_params are already in Mellon format when loading from JSON.
806
+ """
807
+ instance = cls.__new__(cls)
808
+ instance.node_specs = None
809
+ instance._node_params = data.get("node_params", {})
810
+ instance.label = data.get("label", "")
811
+ instance.default_repo = data.get("default_repo", "")
812
+ instance.default_dtype = data.get("default_dtype", "")
813
+ return instance
814
+
815
+ def to_json_string(self) -> str:
816
+ """Serialize to JSON string."""
817
+ return json.dumps(self.to_dict(), indent=2, sort_keys=False) + "\n"
818
+
819
+ def to_json_file(self, json_file_path: str | os.PathLike):
820
+ """Save to a JSON file."""
821
+ with open(json_file_path, "w", encoding="utf-8") as writer:
822
+ writer.write(self.to_json_string())
823
+
824
+ @classmethod
825
+ def from_json_file(cls, json_file_path: str | os.PathLike) -> "MellonPipelineConfig":
826
+ """Load from a JSON file."""
827
+ with open(json_file_path, "r", encoding="utf-8") as reader:
828
+ data = json.load(reader)
829
+ return cls.from_dict(data)
830
+
831
+ def save(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
832
+ """Save the mellon pipeline config to a directory."""
833
+ if os.path.isfile(save_directory):
834
+ raise AssertionError(f"Provided path ({save_directory}) should be a directory, not a file")
835
+
836
+ os.makedirs(save_directory, exist_ok=True)
837
+ output_path = os.path.join(save_directory, self.config_name)
838
+ self.to_json_file(output_path)
839
+ logger.info(f"Pipeline config saved to {output_path}")
840
+
841
+ if push_to_hub:
842
+ commit_message = kwargs.pop("commit_message", None)
843
+ private = kwargs.pop("private", None)
844
+ create_pr = kwargs.pop("create_pr", False)
845
+ token = kwargs.pop("token", None)
846
+ repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1])
847
+ repo_id = create_repo(repo_id, exist_ok=True, private=private, token=token).repo_id
848
+
849
+ upload_file(
850
+ path_or_fileobj=output_path,
851
+ path_in_repo=self.config_name,
852
+ repo_id=repo_id,
853
+ token=token,
854
+ commit_message=commit_message or "Upload MellonPipelineConfig",
855
+ create_pr=create_pr,
856
+ )
857
+ logger.info(f"Pipeline config pushed to hub: {repo_id}")
858
+
859
+ @classmethod
860
+ def load(
861
+ cls,
862
+ pretrained_model_name_or_path: str | os.PathLike,
863
+ **kwargs,
864
+ ) -> "MellonPipelineConfig":
865
+ """Load a pipeline config from a local path or Hugging Face Hub."""
866
+ cache_dir = kwargs.pop("cache_dir", None)
867
+ local_dir = kwargs.pop("local_dir", None)
868
+ local_dir_use_symlinks = kwargs.pop("local_dir_use_symlinks", "auto")
869
+ force_download = kwargs.pop("force_download", False)
870
+ proxies = kwargs.pop("proxies", None)
871
+ token = kwargs.pop("token", None)
872
+ local_files_only = kwargs.pop("local_files_only", False)
873
+ revision = kwargs.pop("revision", None)
874
+ subfolder = kwargs.pop("subfolder", None)
875
+
876
+ pretrained_model_name_or_path = str(pretrained_model_name_or_path)
877
+
878
+ if os.path.isfile(pretrained_model_name_or_path):
879
+ config_file = pretrained_model_name_or_path
880
+ elif os.path.isdir(pretrained_model_name_or_path):
881
+ config_file = os.path.join(pretrained_model_name_or_path, cls.config_name)
882
+ if not os.path.isfile(config_file):
883
+ raise EnvironmentError(f"No file named {cls.config_name} found in {pretrained_model_name_or_path}")
884
+ else:
885
+ try:
886
+ config_file = hf_hub_download(
887
+ pretrained_model_name_or_path,
888
+ filename=cls.config_name,
889
+ cache_dir=cache_dir,
890
+ force_download=force_download,
891
+ proxies=proxies,
892
+ local_files_only=local_files_only,
893
+ token=token,
894
+ revision=revision,
895
+ subfolder=subfolder,
896
+ local_dir=local_dir,
897
+ local_dir_use_symlinks=local_dir_use_symlinks,
898
+ )
899
+ except RepositoryNotFoundError:
900
+ raise EnvironmentError(
901
+ f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier"
902
+ " listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a"
903
+ " token having permission to this repo with `token` or log in with `hf auth login`."
904
+ )
905
+ except RevisionNotFoundError:
906
+ raise EnvironmentError(
907
+ f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for"
908
+ " this model name. Check the model page at"
909
+ f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions."
910
+ )
911
+ except EntryNotFoundError:
912
+ raise EnvironmentError(
913
+ f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
914
+ )
915
+ except HfHubHTTPError as err:
916
+ raise EnvironmentError(
917
+ "There was a specific connection error when trying to load"
918
+ f" {pretrained_model_name_or_path}:\n{err}"
919
+ )
920
+ except ValueError:
921
+ raise EnvironmentError(
922
+ f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it"
923
+ f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a"
924
+ f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to"
925
+ " run the library in offline mode at"
926
+ " 'https://huggingface.co/docs/diffusers/installation#offline-mode'."
927
+ )
928
+ except EnvironmentError:
929
+ raise EnvironmentError(
930
+ f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from "
931
+ "'https://huggingface.co/models', make sure you don't have a local directory with the same name. "
932
+ f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory "
933
+ f"containing a {cls.config_name} file"
934
+ )
935
+
936
+ try:
937
+ return cls.from_json_file(config_file)
938
+ except (json.JSONDecodeError, UnicodeDecodeError):
939
+ raise EnvironmentError(f"The config file at '{config_file}' is not a valid JSON file.")
940
+
941
+ @classmethod
942
+ def from_blocks(
943
+ cls,
944
+ blocks,
945
+ template: dict[str, dict[str, Any]] | None = None,
946
+ label: str = "",
947
+ default_repo: str = "",
948
+ default_dtype: str = "bfloat16",
949
+ ) -> "MellonPipelineConfig":
950
+ """
951
+ Create MellonPipelineConfig by matching template against actual pipeline blocks.
952
+ """
953
+ if template is None:
954
+ template = DEFAULT_NODE_SPECS
955
+
956
+ sub_block_map = dict(blocks.sub_blocks)
957
+
958
+ def filter_spec_for_block(template_spec: dict[str, Any], block) -> dict[str, Any] | None:
959
+ """Filter template spec params based on what the block actually supports."""
960
+ block_input_names = set(block.input_names)
961
+ block_output_names = set(block.intermediate_output_names)
962
+ block_component_names = set(block.component_names)
963
+
964
+ filtered_inputs = [
965
+ p
966
+ for p in template_spec.get("inputs", [])
967
+ if p.required_block_params is None
968
+ or all(name in block_input_names for name in p.required_block_params)
969
+ ]
970
+ filtered_model_inputs = [
971
+ p
972
+ for p in template_spec.get("model_inputs", [])
973
+ if p.required_block_params is None
974
+ or all(name in block_component_names for name in p.required_block_params)
975
+ ]
976
+ filtered_outputs = [
977
+ p
978
+ for p in template_spec.get("outputs", [])
979
+ if p.required_block_params is None
980
+ or all(name in block_output_names for name in p.required_block_params)
981
+ ]
982
+
983
+ filtered_input_names = {p.name for p in filtered_inputs}
984
+ filtered_model_input_names = {p.name for p in filtered_model_inputs}
985
+
986
+ filtered_required_inputs = [
987
+ r for r in template_spec.get("required_inputs", []) if r in filtered_input_names
988
+ ]
989
+ filtered_required_model_inputs = [
990
+ r for r in template_spec.get("required_model_inputs", []) if r in filtered_model_input_names
991
+ ]
992
+
993
+ return {
994
+ "inputs": filtered_inputs,
995
+ "model_inputs": filtered_model_inputs,
996
+ "outputs": filtered_outputs,
997
+ "required_inputs": filtered_required_inputs,
998
+ "required_model_inputs": filtered_required_model_inputs,
999
+ "block_name": template_spec.get("block_name"),
1000
+ }
1001
+
1002
+ # Build node specs
1003
+ node_specs = {}
1004
+ for node_type, template_spec in template.items():
1005
+ if template_spec is None:
1006
+ node_specs[node_type] = None
1007
+ continue
1008
+
1009
+ block_name = template_spec.get("block_name")
1010
+ if block_name is None or block_name not in sub_block_map:
1011
+ node_specs[node_type] = None
1012
+ continue
1013
+
1014
+ node_specs[node_type] = filter_spec_for_block(template_spec, sub_block_map[block_name])
1015
+
1016
+ return cls(
1017
+ node_specs=node_specs,
1018
+ label=label or getattr(blocks, "model_name", ""),
1019
+ default_repo=default_repo,
1020
+ default_dtype=default_dtype,
1021
+ )
1022
+
1023
+ @classmethod
1024
+ def from_custom_block(
1025
+ cls,
1026
+ block,
1027
+ node_label: str = None,
1028
+ input_types: dict[str, Any] | None = None,
1029
+ output_types: dict[str, Any] | None = None,
1030
+ ) -> "MellonPipelineConfig":
1031
+ """
1032
+ Create a MellonPipelineConfig from a custom block.
1033
+
1034
+ Args:
1035
+ block: A block instance with `inputs`, `outputs`, and `expected_components`/`component_names` properties.
1036
+ Each InputParam/OutputParam should have metadata={"mellon": "<type>"} where type is one of: image,
1037
+ video, text, checkbox, number, slider, dropdown, model. If metadata is None, maps to "custom".
1038
+ node_label: The display label for the node. Defaults to block class name with spaces.
1039
+ input_types:
1040
+ Optional dict mapping input param names to mellon types. Overrides the block's metadata if provided.
1041
+ Example: {"prompt": "textbox", "image": "image"}
1042
+ output_types:
1043
+ Optional dict mapping output param names to mellon types. Overrides the block's metadata if provided.
1044
+ Example: {"prompt": "text", "images": "image"}
1045
+
1046
+ Returns:
1047
+ MellonPipelineConfig instance
1048
+ """
1049
+ if node_label is None:
1050
+ class_name = block.__class__.__name__
1051
+ node_label = "".join([" " + c if c.isupper() else c for c in class_name]).strip()
1052
+
1053
+ if input_types is None:
1054
+ input_types = {}
1055
+ if output_types is None:
1056
+ output_types = {}
1057
+
1058
+ inputs = []
1059
+ model_inputs = []
1060
+ outputs = []
1061
+
1062
+ # Process block inputs
1063
+ for input_param in block.inputs:
1064
+ if input_param.name is None:
1065
+ continue
1066
+ if input_param.name in input_types:
1067
+ input_param = copy.copy(input_param)
1068
+ input_param.metadata = {"mellon": input_types[input_param.name]}
1069
+ print(f" processing input: {input_param.name}, metadata: {input_param.metadata}")
1070
+ inputs.append(input_param_to_mellon_param(input_param))
1071
+
1072
+ # Process block outputs
1073
+ for output_param in block.outputs:
1074
+ if output_param.name is None:
1075
+ continue
1076
+ if output_param.name in output_types:
1077
+ output_param = copy.copy(output_param)
1078
+ output_param.metadata = {"mellon": output_types[output_param.name]}
1079
+ outputs.append(output_param_to_mellon_param(output_param))
1080
+
1081
+ # Process expected components (all map to model inputs)
1082
+ component_names = block.component_names
1083
+ for component_name in component_names:
1084
+ model_inputs.append(MellonParam.Input.model(component_name))
1085
+
1086
+ # Always add doc output
1087
+ outputs.append(MellonParam.doc())
1088
+
1089
+ node_spec = {
1090
+ "inputs": inputs,
1091
+ "model_inputs": model_inputs,
1092
+ "outputs": outputs,
1093
+ "required_inputs": [],
1094
+ "required_model_inputs": [],
1095
+ "block_name": "custom",
1096
+ }
1097
+
1098
+ return cls(
1099
+ node_specs={"custom": node_spec},
1100
+ label=node_label,
1101
+ )
diffusers/modular_pipelines/minimax_h3/__init__.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ...utils import (
4
+ DIFFUSERS_SLOW_IMPORT,
5
+ OptionalDependencyNotAvailable,
6
+ _LazyModule,
7
+ get_objects_from_module,
8
+ is_torch_available,
9
+ is_transformers_available,
10
+ )
11
+
12
+
13
+ _dummy_objects = {}
14
+ _import_structure = {}
15
+
16
+ try:
17
+ if not (is_transformers_available() and is_torch_available()):
18
+ raise OptionalDependencyNotAvailable()
19
+ except OptionalDependencyNotAvailable:
20
+ from ...utils import dummy_torch_and_transformers_objects # noqa F403
21
+
22
+ _dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
23
+ else:
24
+ _import_structure["modular_blocks_minimax_h3"] = ["MiniMaxH3Blocks", "MiniMaxH3Ref2VABlocks"]
25
+ _import_structure["modular_pipeline"] = ["MiniMaxH3ModularPipeline", "MiniMaxH3Ref2VAModularPipeline"]
26
+ _import_structure["packing_ref2va"] = ["MiniMaxH3Reference"]
27
+
28
+ if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
29
+ try:
30
+ if not (is_transformers_available() and is_torch_available()):
31
+ raise OptionalDependencyNotAvailable()
32
+ except OptionalDependencyNotAvailable:
33
+ from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
34
+ else:
35
+ from .modular_blocks_minimax_h3 import MiniMaxH3Blocks, MiniMaxH3Ref2VABlocks
36
+ from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
37
+ from .packing_ref2va import MiniMaxH3Reference
38
+ else:
39
+ import sys
40
+
41
+ sys.modules[__name__] = _LazyModule(
42
+ __name__,
43
+ globals()["__file__"],
44
+ _import_structure,
45
+ module_spec=__spec__,
46
+ )
47
+
48
+ for name, value in _dummy_objects.items():
49
+ setattr(sys.modules[__name__], name, value)
diffusers/modular_pipelines/minimax_h3/before_denoise.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The MiniMax and HuggingFace Teams. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import torch
16
+
17
+ from ...schedulers import MiniMaxH3Scheduler
18
+ from ...utils import logging
19
+ from ...utils.torch_utils import randn_tensor
20
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
21
+ from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
22
+ from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
23
+ from .packing import (
24
+ MINIMAX_H3_AUDIO_CHANNELS,
25
+ MINIMAX_H3_KEYFRAME_NOISE_AUG,
26
+ MiniMaxH3PackedSequence,
27
+ build_packed_sequence,
28
+ build_row_timesteps,
29
+ patchify_video_latents,
30
+ )
31
+ from .packing_ref2va import MiniMaxH3PreparedReference, build_ref2va_packed_sequence
32
+
33
+
34
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
35
+
36
+
37
+ def _layout_inputs() -> list[InputParam]:
38
+ r"""What both packed layouts are built from, beyond the conditioning of the task itself."""
39
+ return [
40
+ InputParam(
41
+ name="text_token_tags",
42
+ type_hint=torch.Tensor,
43
+ required=True,
44
+ description="The per-row modality tag of every row of `prompt_embeds`.",
45
+ ),
46
+ InputParam(
47
+ name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames."
48
+ ),
49
+ InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."),
50
+ InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."),
51
+ InputParam(
52
+ name="num_audio_latents",
53
+ type_hint=int,
54
+ required=True,
55
+ description="Number of audio latents per channel.",
56
+ ),
57
+ ]
58
+
59
+
60
+ def _layout_outputs() -> list[OutputParam]:
61
+ r"""The row layout of the packed sequence, shared by the two tasks."""
62
+ return [
63
+ OutputParam(
64
+ "layout",
65
+ type_hint=MiniMaxH3PackedSequence,
66
+ description="The structural description of the packed sequence.",
67
+ ),
68
+ OutputParam(
69
+ "position_ids",
70
+ type_hint=torch.Tensor,
71
+ description="The `(t, h, w)` rotary coordinate of every row, in float64.",
72
+ ),
73
+ OutputParam("token_tags", type_hint=torch.Tensor, description="The modality tag of every row."),
74
+ OutputParam(
75
+ "video_indices",
76
+ type_hint=torch.Tensor,
77
+ description="Sequence positions of the video rows, conditioning rows first.",
78
+ ),
79
+ OutputParam(
80
+ "audio_indices",
81
+ type_hint=torch.Tensor,
82
+ description="Sequence positions of the audio rows, reference rows first.",
83
+ ),
84
+ OutputParam("text_indices", type_hint=torch.Tensor, description="Sequence positions of the text rows."),
85
+ OutputParam(
86
+ "num_condition_video_rows",
87
+ type_hint=int,
88
+ description="How many leading video rows are conditioning rows rather than generated rows.",
89
+ ),
90
+ OutputParam(
91
+ "num_condition_audio_rows",
92
+ type_hint=int,
93
+ description="How many leading audio rows are reference rows rather than generated rows.",
94
+ ),
95
+ ]
96
+
97
+
98
+ def _set_layout_state(block_state, layout: MiniMaxH3PackedSequence, device: torch.device) -> None:
99
+ block_state.layout = layout
100
+ block_state.position_ids = layout.position_ids.to(device)
101
+ block_state.token_tags = layout.token_tags.to(device)
102
+ block_state.video_indices = layout.video_indices.to(device)
103
+ block_state.audio_indices = layout.audio_indices.to(device)
104
+ block_state.text_indices = layout.text_indices.to(device)
105
+ block_state.num_condition_video_rows = layout.num_condition_video_rows
106
+ block_state.num_condition_audio_rows = layout.num_condition_audio_rows
107
+
108
+
109
+ class MiniMaxH3PrepareLayoutStep(ModularPipelineBlocks):
110
+ model_name = "minimax-h3"
111
+
112
+ @property
113
+ def description(self) -> str:
114
+ return (
115
+ "Builds the packed layout of a `t2va` / `fl2va` request — `[text | keyframe conditions | target audio | "
116
+ "target video]` — and its fp64 rotary grid. MiniMax-H3 runs full self-attention over this one sequence, "
117
+ "so the layout is what every later block addresses rows through."
118
+ )
119
+
120
+ @property
121
+ def inputs(self) -> list[InputParam]:
122
+ return [
123
+ *_layout_inputs(),
124
+ InputParam(
125
+ name="keyframe_anchors",
126
+ type_hint=tuple,
127
+ default=(),
128
+ description="Which end of the video every keyframe is anchored to, in packed order.",
129
+ ),
130
+ ]
131
+
132
+ @property
133
+ def intermediate_outputs(self) -> list[OutputParam]:
134
+ return _layout_outputs()
135
+
136
+ @torch.no_grad()
137
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
138
+ block_state = self.get_block_state(state)
139
+
140
+ layout = build_packed_sequence(
141
+ block_state.text_token_tags,
142
+ block_state.num_latent_frames,
143
+ block_state.latent_height,
144
+ block_state.latent_width,
145
+ block_state.num_audio_latents,
146
+ components.patch_size,
147
+ block_state.keyframe_anchors,
148
+ )
149
+ _set_layout_state(block_state, layout, components._execution_device)
150
+
151
+ self.set_block_state(state, block_state)
152
+ return components, state
153
+
154
+
155
+ class MiniMaxH3Ref2VAPrepareLayoutStep(ModularPipelineBlocks):
156
+ model_name = "minimax-h3-ref2va"
157
+
158
+ @property
159
+ def description(self) -> str:
160
+ return (
161
+ "Builds the packed layout of a `ref2va` request — `[text | reference blocks | target audio | target "
162
+ "video]` — and its fp64 rotary grid. The reference order advances the shared audio/video rotary clock, so "
163
+ "it is part of the layout rather than a detail of the presentation."
164
+ )
165
+
166
+ @property
167
+ def inputs(self) -> list[InputParam]:
168
+ return [
169
+ *_layout_inputs(),
170
+ InputParam(
171
+ name="prepared_references",
172
+ type_hint=list[MiniMaxH3PreparedReference],
173
+ required=True,
174
+ description="The prepared references, in packed order, with their latent geometry filled in.",
175
+ ),
176
+ ]
177
+
178
+ @property
179
+ def intermediate_outputs(self) -> list[OutputParam]:
180
+ return _layout_outputs()
181
+
182
+ @torch.no_grad()
183
+ def __call__(self, components: MiniMaxH3Ref2VAModularPipeline, state: PipelineState) -> PipelineState:
184
+ block_state = self.get_block_state(state)
185
+
186
+ layout = build_ref2va_packed_sequence(
187
+ block_state.text_token_tags,
188
+ block_state.prepared_references,
189
+ block_state.num_latent_frames,
190
+ block_state.latent_height,
191
+ block_state.latent_width,
192
+ block_state.num_audio_latents,
193
+ components.patch_size,
194
+ )
195
+ _set_layout_state(block_state, layout, components._execution_device)
196
+
197
+ self.set_block_state(state, block_state)
198
+ return components, state
199
+
200
+
201
+ class MiniMaxH3PrepareLatentsStep(ModularPipelineBlocks):
202
+ model_name = "minimax-h3"
203
+
204
+ @property
205
+ def description(self) -> str:
206
+ return (
207
+ "Draws the initial noise of the generated rows and prepends the conditioning rows. MiniMax-H3 draws the "
208
+ "video noise as a latent tensor and patchifies it afterwards, then the audio noise directly in row "
209
+ "layout — both off the request's generator, after the conditioning noise of the encoder step."
210
+ )
211
+
212
+ @property
213
+ def inputs(self) -> list[InputParam]:
214
+ return [
215
+ InputParam(
216
+ name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames."
217
+ ),
218
+ InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."),
219
+ InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."),
220
+ InputParam(
221
+ name="num_audio_latents",
222
+ type_hint=int,
223
+ required=True,
224
+ description="Number of audio latents per channel.",
225
+ ),
226
+ InputParam.template(
227
+ "generator",
228
+ description=(
229
+ "The generator of the request. The video noise is drawn from it first, then the audio noise."
230
+ ),
231
+ ),
232
+ InputParam(
233
+ name="latents",
234
+ type_hint=torch.Tensor,
235
+ description=(
236
+ "Pre-generated video noise of shape `(1, 24, num_latent_frames, latent_height, latent_width)`, "
237
+ "used instead of the draw."
238
+ ),
239
+ ),
240
+ InputParam(
241
+ name="audio_latents",
242
+ type_hint=torch.Tensor,
243
+ description="Pre-generated audio noise of shape `(2, 32, num_audio_latents)`.",
244
+ ),
245
+ InputParam(
246
+ name="condition_latents",
247
+ type_hint=torch.Tensor,
248
+ description="The video conditioning rows to prepend, or None for a request that has none.",
249
+ ),
250
+ InputParam(
251
+ name="audio_condition_latents",
252
+ type_hint=torch.Tensor,
253
+ description="The audio conditioning rows to prepend, or None for a request that has none.",
254
+ ),
255
+ ]
256
+
257
+ @property
258
+ def intermediate_outputs(self) -> list[OutputParam]:
259
+ return [
260
+ OutputParam(
261
+ "latents",
262
+ type_hint=torch.Tensor,
263
+ description="The video rows of the packed sequence, conditioning rows first.",
264
+ ),
265
+ OutputParam(
266
+ "audio_latents",
267
+ type_hint=torch.Tensor,
268
+ description="The channel-major audio rows of the packed sequence, reference rows first.",
269
+ ),
270
+ ]
271
+
272
+ @staticmethod
273
+ def prepare_latents(
274
+ components,
275
+ num_latent_frames: int,
276
+ latent_height: int,
277
+ latent_width: int,
278
+ num_audio_latents: int,
279
+ device: torch.device,
280
+ generator: torch.Generator | list[torch.Generator] | None = None,
281
+ latents: torch.Tensor | None = None,
282
+ audio_latents: torch.Tensor | None = None,
283
+ ) -> tuple[torch.Tensor, torch.Tensor]:
284
+ r"""
285
+ Draw the initial noise of both modalities and pack it into transformer rows.
286
+
287
+ A request draws every stream from the one generator it is given, and the order is part of what that generator
288
+ reproduces: the conditioning noise of the keyframes or references first (one draw per condition, in
289
+ [`~modular_pipelines.minimax_h3.packing.keyframe_condition_noise`]), then the video noise here, as a latent tensor
290
+ that is patchified afterwards, then the audio noise, directly in row layout. Passing `latents` or
291
+ `audio_latents` skips its draw and shifts the ones after it.
292
+
293
+ Args:
294
+ num_latent_frames (`int`): Number of video latent frames.
295
+ latent_height (`int`): Latent height.
296
+ latent_width (`int`): Latent width.
297
+ num_audio_latents (`int`): Number of audio latents per channel.
298
+ device (`torch.device`): The device the rows are drawn on.
299
+ generator (`torch.Generator`, *optional*): The generator of the request.
300
+ latents (`torch.Tensor`, *optional*):
301
+ Pre-generated video noise of shape `(1, latent_channels, num_latent_frames, latent_height,
302
+ latent_width)`, used instead of the draw.
303
+ audio_latents (`torch.Tensor`, *optional*):
304
+ Pre-generated audio noise of shape `(2, audio_latent_channels, num_audio_latents)`.
305
+
306
+ Returns:
307
+ `tuple[torch.Tensor, torch.Tensor]`: the video rows and the channel-major audio rows.
308
+ """
309
+ if latents is None:
310
+ latents = randn_tensor(
311
+ (1, components.vae_latent_channels, num_latent_frames, latent_height, latent_width),
312
+ generator=generator,
313
+ device=device,
314
+ dtype=torch.float32,
315
+ )
316
+ video_rows = patchify_video_latents(latents.to(torch.float32), components.patch_size)
317
+
318
+ if audio_latents is None:
319
+ audio_rows = randn_tensor(
320
+ (num_audio_latents * MINIMAX_H3_AUDIO_CHANNELS, components.audio_latent_channels),
321
+ generator=generator,
322
+ device=device,
323
+ dtype=torch.float32,
324
+ )
325
+ else:
326
+ audio_rows = audio_latents.to(torch.float32).permute(0, 2, 1).reshape(-1, components.audio_latent_channels)
327
+ return video_rows.to(device), audio_rows.to(device)
328
+
329
+ @torch.no_grad()
330
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
331
+ block_state = self.get_block_state(state)
332
+
333
+ latents, audio_latents = self.prepare_latents(
334
+ components,
335
+ block_state.num_latent_frames,
336
+ block_state.latent_height,
337
+ block_state.latent_width,
338
+ block_state.num_audio_latents,
339
+ components._execution_device,
340
+ block_state.generator,
341
+ block_state.latents,
342
+ block_state.audio_latents,
343
+ )
344
+ if block_state.condition_latents is not None:
345
+ latents = torch.cat([block_state.condition_latents, latents])
346
+ if block_state.audio_condition_latents is not None:
347
+ audio_latents = torch.cat([block_state.audio_condition_latents, audio_latents])
348
+ block_state.latents, block_state.audio_latents = latents, audio_latents
349
+
350
+ self.set_block_state(state, block_state)
351
+ return components, state
352
+
353
+
354
+ class MiniMaxH3SetTimestepsStep(ModularPipelineBlocks):
355
+ model_name = "minimax-h3"
356
+
357
+ @property
358
+ def description(self) -> str:
359
+ return (
360
+ "Initializes the two schedules — `shift = 12.0` for video, `shift = 3.0` for audio — and stages the "
361
+ "row-to-timestep plan of every step. One forward serves every modality and every noise level at once: "
362
+ "the generated rows step down their own schedule while the conditioning rows stay pinned at their "
363
+ "noise-augmentation level, and that assignment is static per step."
364
+ )
365
+
366
+ @property
367
+ def expected_components(self) -> list[ComponentSpec]:
368
+ return [
369
+ ComponentSpec("scheduler", MiniMaxH3Scheduler),
370
+ ComponentSpec("audio_scheduler", MiniMaxH3Scheduler),
371
+ ]
372
+
373
+ @property
374
+ def inputs(self) -> list[InputParam]:
375
+ return [
376
+ InputParam.template("num_inference_steps", required=True),
377
+ InputParam(
378
+ name="layout",
379
+ type_hint=MiniMaxH3PackedSequence,
380
+ required=True,
381
+ description="The structural description of the packed sequence.",
382
+ ),
383
+ ]
384
+
385
+ @property
386
+ def intermediate_outputs(self) -> list[OutputParam]:
387
+ return [
388
+ OutputParam("timesteps", type_hint=torch.Tensor, description="Timesteps of the video schedule."),
389
+ OutputParam("audio_timesteps", type_hint=torch.Tensor, description="Timesteps of the audio schedule."),
390
+ OutputParam(
391
+ "row_timestep_plan",
392
+ type_hint=list,
393
+ description=(
394
+ "One `(timestep, timestep_indices)` pair per step: the distinct timesteps of the sequence and the "
395
+ "index of every row into them."
396
+ ),
397
+ ),
398
+ ]
399
+
400
+ @torch.no_grad()
401
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
402
+ block_state = self.get_block_state(state)
403
+ device = components._execution_device
404
+
405
+ components.scheduler.set_timesteps(block_state.num_inference_steps, device=device)
406
+ components.audio_scheduler.set_timesteps(block_state.num_inference_steps, device=device)
407
+ block_state.timesteps = components.scheduler.timesteps
408
+ block_state.audio_timesteps = components.audio_scheduler.timesteps
409
+
410
+ block_state.row_timestep_plan = [
411
+ tuple(
412
+ tensor.to(device)
413
+ for tensor in build_row_timesteps(
414
+ block_state.layout,
415
+ float(timestep),
416
+ float(audio_timestep),
417
+ max(float(timestep), MINIMAX_H3_KEYFRAME_NOISE_AUG),
418
+ 1.0,
419
+ )
420
+ )
421
+ for timestep, audio_timestep in zip(block_state.timesteps, block_state.audio_timesteps)
422
+ ]
423
+
424
+ self.set_block_state(state, block_state)
425
+ return components, state
diffusers/modular_pipelines/minimax_h3/before_encoder.py ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The MiniMax and HuggingFace Teams. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import PIL
16
+ import torch
17
+ from PIL import Image, ImageOps
18
+
19
+ from ...utils import logging
20
+ from ..modular_pipeline import ModularPipelineBlocks, PipelineState
21
+ from ..modular_pipeline_utils import InputParam, OutputParam
22
+ from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
23
+ from .packing import (
24
+ MINIMAX_H3_CANVAS_MULTIPLE,
25
+ MINIMAX_H3_FPS,
26
+ MINIMAX_H3_MAX_DURATION,
27
+ MINIMAX_H3_MIN_DURATION,
28
+ align_num_frames,
29
+ audio_latent_num_frames,
30
+ prepare_keyframe_image,
31
+ resolve_canvas_size,
32
+ video_latent_num_frames,
33
+ )
34
+ from .packing_ref2va import (
35
+ MINIMAX_H3_MAX_REFERENCE_AUDIOS,
36
+ MINIMAX_H3_MAX_REFERENCE_IMAGES,
37
+ MINIMAX_H3_MAX_REFERENCE_VIDEOS,
38
+ MINIMAX_H3_MAX_REFERENCES,
39
+ MiniMaxH3PreparedReference,
40
+ MiniMaxH3Reference,
41
+ prepare_reference_frames,
42
+ prepare_reference_image,
43
+ prepare_reference_waveform,
44
+ reference_kind,
45
+ reference_media_to_uint8,
46
+ resample_reference_frames,
47
+ resolve_reference_image_size,
48
+ )
49
+
50
+
51
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
52
+
53
+
54
+ def _latent_geometry(components, height: int, width: int, num_frames: int) -> tuple[int, int, int, int]:
55
+ r"""The latent geometry the packed layout, the noise draws and the decoders all key off."""
56
+ ratio = components.vae_spatial_compression_ratio
57
+ return video_latent_num_frames(num_frames), height // ratio, width // ratio, audio_latent_num_frames(num_frames)
58
+
59
+
60
+ def _latent_geometry_outputs() -> list[OutputParam]:
61
+ r"""The declaration of what [`_latent_geometry`] resolves, shared by the two setup blocks."""
62
+ return [
63
+ OutputParam("num_latent_frames", type_hint=int, description="Number of generated video latent frames."),
64
+ OutputParam("latent_height", type_hint=int, description="Height of the generated video latents."),
65
+ OutputParam("latent_width", type_hint=int, description="Width of the generated video latents."),
66
+ OutputParam("num_audio_latents", type_hint=int, description="Number of generated audio latents per channel."),
67
+ ]
68
+
69
+
70
+ class MiniMaxH3SetupStep(ModularPipelineBlocks):
71
+ model_name = "minimax-h3"
72
+
73
+ @property
74
+ def description(self) -> str:
75
+ return (
76
+ "Resolves the plan shared by the `t2va` and `fl2va` tasks: the canvas (MiniMax-H3's own 768-short-edge "
77
+ "geometry for the aspect ratio of the first keyframe, or 16:9 without keyframes), the `17 * n + 5` frame "
78
+ "count the video VAE can decode, the latent geometry every later block keys off, and the keyframes put "
79
+ "onto that canvas."
80
+ )
81
+
82
+ @staticmethod
83
+ def _check_inputs(block_state) -> None:
84
+ if (block_state.height is None) != (block_state.width is None):
85
+ raise ValueError("`height` and `width` have to be passed together, or neither of them.")
86
+ if block_state.height is not None and (
87
+ block_state.height % MINIMAX_H3_CANVAS_MULTIPLE or block_state.width % MINIMAX_H3_CANVAS_MULTIPLE
88
+ ):
89
+ raise ValueError(
90
+ f"`height` and `width` must be multiples of {MINIMAX_H3_CANVAS_MULTIPLE}, got "
91
+ f"{block_state.height}x{block_state.width}."
92
+ )
93
+ # The duration the request generates is the one of the *aligned* frame count, so that is what the ceiling has
94
+ # to hold for: 346 frames would otherwise pass the check and then be rounded up to 362, i.e. 15.083 seconds.
95
+ aligned_num_frames = align_num_frames(block_state.num_frames)
96
+ duration = aligned_num_frames / MINIMAX_H3_FPS
97
+ if not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
98
+ raise ValueError(
99
+ f"MiniMax-H3 generates between {MINIMAX_H3_MIN_DURATION} and {MINIMAX_H3_MAX_DURATION} seconds at "
100
+ f"{MINIMAX_H3_FPS} fps, so `num_frames`, rounded up to the next `17 * n + 5` the video VAE can "
101
+ f"encode, must be between {int(MINIMAX_H3_MIN_DURATION * MINIMAX_H3_FPS)} and "
102
+ f"{int(MINIMAX_H3_MAX_DURATION * MINIMAX_H3_FPS)}, got {block_state.num_frames} (rounded up to "
103
+ f"{aligned_num_frames})."
104
+ )
105
+
106
+ @property
107
+ def inputs(self) -> list[InputParam]:
108
+ return [
109
+ InputParam(
110
+ name="image",
111
+ type_hint=PIL.Image.Image,
112
+ description=(
113
+ "Keyframe the video starts from. It is *stretched* onto the target canvas, which by default is "
114
+ "derived from its own aspect ratio."
115
+ ),
116
+ ),
117
+ InputParam(
118
+ name="last_image",
119
+ type_hint=PIL.Image.Image,
120
+ description=(
121
+ "Keyframe the video ends on. Can be passed on its own to generate *up to* a frame. Combined with "
122
+ "`image` it is the follower of the two and is cover-cropped onto the canvas."
123
+ ),
124
+ ),
125
+ InputParam.template("height", description="Height of the generated video in pixels, a multiple of 32."),
126
+ InputParam.template("width", description="Width of the generated video in pixels, a multiple of 32."),
127
+ InputParam(
128
+ name="num_frames",
129
+ type_hint=int,
130
+ default=124,
131
+ description=(
132
+ "Number of frames to generate, at the fixed 24 fps. Snapped up to the next `17 * n + 5` the video "
133
+ "VAE can decode; the resulting duration must stay between 5 and 15 seconds."
134
+ ),
135
+ ),
136
+ ]
137
+
138
+ @property
139
+ def intermediate_outputs(self) -> list[OutputParam]:
140
+ return [
141
+ OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
142
+ OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
143
+ OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5."),
144
+ *_latent_geometry_outputs(),
145
+ OutputParam(
146
+ "keyframes",
147
+ type_hint=list,
148
+ description="The keyframes put onto the target canvas, in packed order (empty for `t2va`).",
149
+ ),
150
+ OutputParam(
151
+ "keyframe_anchors",
152
+ type_hint=tuple,
153
+ description="Which end of the video every keyframe is anchored to, in packed order.",
154
+ ),
155
+ ]
156
+
157
+ @torch.no_grad()
158
+ def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
159
+ block_state = self.get_block_state(state)
160
+ self._check_inputs(block_state)
161
+
162
+ keyframes = [
163
+ ImageOps.exif_transpose(keyframe).convert("RGB")
164
+ for keyframe in (block_state.image, block_state.last_image)
165
+ if keyframe is not None
166
+ ]
167
+ block_state.keyframe_anchors = tuple(
168
+ anchor
169
+ for anchor, keyframe in (("first", block_state.image), ("last", block_state.last_image))
170
+ if keyframe is not None
171
+ )
172
+ if block_state.height is None:
173
+ block_state.height, block_state.width = resolve_canvas_size(*(keyframes[0].size if keyframes else (16, 9)))
174
+
175
+ aligned_num_frames = align_num_frames(block_state.num_frames)
176
+ if aligned_num_frames != block_state.num_frames:
177
+ logger.warning(
178
+ f"`num_frames` has to be of the form 17 * n + 5 for the video VAE; rounding {block_state.num_frames} "
179
+ f"up to {aligned_num_frames}."
180
+ )
181
+ block_state.num_frames = aligned_num_frames
182
+
183
+ (
184
+ block_state.num_latent_frames,
185
+ block_state.latent_height,
186
+ block_state.latent_width,
187
+ block_state.num_audio_latents,
188
+ ) = _latent_geometry(components, block_state.height, block_state.width, block_state.num_frames)
189
+
190
+ block_state.keyframes = [
191
+ prepare_keyframe_image(keyframe, block_state.height, block_state.width, stretch=index == 0)
192
+ for index, keyframe in enumerate(keyframes)
193
+ ]
194
+ self.set_block_state(state, block_state)
195
+ return components, state
196
+
197
+
198
+ class MiniMaxH3Ref2VASetupStep(ModularPipelineBlocks):
199
+ model_name = "minimax-h3-ref2va"
200
+
201
+ @property
202
+ def description(self) -> str:
203
+ return (
204
+ "Resolves the `ref2va` plan: the canvas (MiniMax-H3's own 16:9 unless asked otherwise — references never "
205
+ "bind the generated geometry), the references prepared at their own resolutions, the frame count they "
206
+ "imply when it was left open, and the latent geometry every later block keys off."
207
+ )
208
+
209
+ @staticmethod
210
+ def _check_inputs(components, block_state) -> None:
211
+ if (block_state.height is None) != (block_state.width is None):
212
+ raise ValueError("`height` and `width` have to be passed together, or neither of them.")
213
+ if block_state.height is not None and (
214
+ block_state.height % MINIMAX_H3_CANVAS_MULTIPLE or block_state.width % MINIMAX_H3_CANVAS_MULTIPLE
215
+ ):
216
+ raise ValueError(
217
+ f"`height` and `width` must be multiples of {MINIMAX_H3_CANVAS_MULTIPLE}, got "
218
+ f"{block_state.height}x{block_state.width}."
219
+ )
220
+ # The duration the request generates is the one of the *aligned* frame count, so that is what the ceiling has
221
+ # to hold for: 346 frames would otherwise pass the check and then be rounded up to 362, i.e. 15.083 seconds.
222
+ aligned_num_frames = None if block_state.num_frames is None else align_num_frames(block_state.num_frames)
223
+ duration = None if aligned_num_frames is None else aligned_num_frames / MINIMAX_H3_FPS
224
+ if duration is not None and not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
225
+ raise ValueError(
226
+ f"MiniMax-H3 generates between {MINIMAX_H3_MIN_DURATION} and {MINIMAX_H3_MAX_DURATION} seconds at "
227
+ f"{MINIMAX_H3_FPS} fps, so `num_frames`, rounded up to the next `17 * n + 5` the video VAE can "
228
+ f"encode, must be between {int(MINIMAX_H3_MIN_DURATION * MINIMAX_H3_FPS)} and "
229
+ f"{int(MINIMAX_H3_MAX_DURATION * MINIMAX_H3_FPS)}, got {block_state.num_frames} (rounded up to "
230
+ f"{aligned_num_frames})."
231
+ )
232
+
233
+ if not block_state.references:
234
+ raise ValueError(
235
+ "`ref2va` needs at least one reference; use `MiniMaxH3ModularPipeline` for text-only requests."
236
+ )
237
+ kinds = [reference_kind(index, entry) for index, entry in enumerate(block_state.references)]
238
+ for kind, limit in (
239
+ ("image", MINIMAX_H3_MAX_REFERENCE_IMAGES),
240
+ ("video", MINIMAX_H3_MAX_REFERENCE_VIDEOS),
241
+ ("audio", MINIMAX_H3_MAX_REFERENCE_AUDIOS),
242
+ ):
243
+ if kinds.count(kind) > limit:
244
+ raise ValueError(f"MiniMax-H3 accepts at most {limit} {kind} references, got {kinds.count(kind)}.")
245
+ if len(kinds) > MINIMAX_H3_MAX_REFERENCES:
246
+ raise ValueError(
247
+ f"MiniMax-H3 accepts at most {MINIMAX_H3_MAX_REFERENCES} references in total, got {len(kinds)}."
248
+ )
249
+ if set(kinds) == {"audio"}:
250
+ raise ValueError(
251
+ "An audio reference has to be paired with at least one image or video reference and cannot be used "
252
+ "on its own."
253
+ )
254
+
255
+ @property
256
+ def inputs(self) -> list[InputParam]:
257
+ return [
258
+ InputParam(
259
+ name="references",
260
+ type_hint=list[MiniMaxH3Reference],
261
+ required=True,
262
+ description=(
263
+ "The references to condition on, **in the order the model should read them**: the order labels "
264
+ "them in the prompt presentation and lays them out on the shared rotary clock, so a different "
265
+ "order is a different request. Every [`MiniMaxH3Reference`] carries exactly one medium, a path or "
266
+ "in-memory media — `image` (at most 9), `video` at its own `fps` (at most 3, whose `audio` "
267
+ "soundtrack is conditioned on as well), or `audio` at its own `sample_rate` (at most 3) — for at "
268
+ "most 12 references in total, and audio references cannot be the only ones. A path is decoded "
269
+ "when the reference is built, so these blocks only ever see pixels and samples."
270
+ ),
271
+ ),
272
+ InputParam.template("height", description="Height of the generated video in pixels, a multiple of 32."),
273
+ InputParam.template("width", description="Width of the generated video in pixels, a multiple of 32."),
274
+ InputParam(
275
+ name="num_frames",
276
+ type_hint=int,
277
+ description=(
278
+ "Number of frames to generate, at the fixed 24 fps. Snapped up to the next `17 * n + 5` the video "
279
+ "VAE can decode. May be left out, but only when exactly one reference carries audio, in which "
280
+ "case the duration is that soundtrack's."
281
+ ),
282
+ ),
283
+ ]
284
+
285
+ @property
286
+ def intermediate_outputs(self) -> list[OutputParam]:
287
+ return [
288
+ OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
289
+ OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
290
+ OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5."),
291
+ *_latent_geometry_outputs(),
292
+ OutputParam(
293
+ "prepared_references",
294
+ type_hint=list[MiniMaxH3PreparedReference],
295
+ description="The references prepared at their own resolutions, in packed order.",
296
+ ),
297
+ ]
298
+
299
+ @staticmethod
300
+ def prepare_references(
301
+ components, references: list[MiniMaxH3Reference], num_frames: int | None
302
+ ) -> tuple[list[MiniMaxH3PreparedReference], int]:
303
+ r"""
304
+ Resolve the references and, if it was left open, the duration they imply.
305
+
306
+ Every reference is prepared at its own resolution: an image is resized to a 2048 pixel short edge, a video is
307
+ resampled onto MiniMax-H3's own 24 fps, rescaled onto the 768 pixel canvas of *its own* aspect ratio and
308
+ truncated to the generated frame count, and a soundtrack is put on the audio VAE's sample rate and truncated to
309
+ the generated duration. None of this touches the target canvas.
310
+
311
+ A reference that left its `fps` or its `sample_rate` out is taken to already be at MiniMax-H3's own rate, and
312
+ its frames or its samples then flow through untouched.
313
+
314
+ A video reference goes through the two passes the reference implementation's `ffmpeg` decode applied, in the
315
+ same order: the constant frame rate resample of `resample_reference_frames` and the LANCZOS rescale of
316
+ `prepare_reference_frames`. Frames handed over at 24 fps and already at the canvas their own aspect ratio
317
+ resolves to therefore reach the VAE untouched, which is the parity-exact route.
318
+
319
+ Args:
320
+ references (`list[MiniMaxH3Reference]`):
321
+ The `references` input of a [`MiniMaxH3Ref2VABlocks`] request.
322
+ num_frames (`int`, *optional*):
323
+ The requested frame count, or `None` to derive it from the single audio-bearing reference.
324
+
325
+ Returns:
326
+ `tuple[list[MiniMaxH3PreparedReference], int]`: the prepared references, in packed order, and the frame
327
+ count.
328
+ """
329
+ resolved = [
330
+ MiniMaxH3PreparedReference(kind=reference_kind(index, entry), has_audio=entry.has_audio)
331
+ for index, entry in enumerate(references)
332
+ ]
333
+
334
+ # The duration may be left open, but then exactly one reference may carry audio, or the request is ambiguous.
335
+ if num_frames is None:
336
+ audio_bearing = [index for index, reference in enumerate(resolved) if reference.has_audio]
337
+ if len(audio_bearing) != 1:
338
+ raise ValueError(
339
+ "`num_frames` may only be left to the references when exactly one of them carries audio, got "
340
+ f"{len(audio_bearing)}."
341
+ )
342
+ index = audio_bearing[0]
343
+ sample_rate = references[index].sample_rate or components.audio_sampling_rate
344
+ duration = references[index].audio.shape[-1] / sample_rate
345
+ if not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
346
+ raise ValueError(
347
+ f"`references[{index}]` is {duration:g} seconds long, outside the "
348
+ f"{MINIMAX_H3_MIN_DURATION} to {MINIMAX_H3_MAX_DURATION} seconds MiniMax-H3 generates."
349
+ )
350
+ num_frames = align_num_frames(round(duration * MINIMAX_H3_FPS))
351
+ # The duration the request generates is the one of the *aligned* frame count, so that is what the
352
+ # ceiling has to hold for: a 14.99 second soundtrack rounds up to 362 frames, i.e. 15.083 seconds.
353
+ if num_frames / MINIMAX_H3_FPS > MINIMAX_H3_MAX_DURATION:
354
+ raise ValueError(
355
+ f"`references[{index}]` is {duration:g} seconds long, which rounds up to {num_frames} frames "
356
+ f"(`17 * n + 5`), i.e. {num_frames / MINIMAX_H3_FPS:g} seconds — past the "
357
+ f"{MINIMAX_H3_MAX_DURATION} seconds MiniMax-H3 generates. Pass `num_frames` to generate a "
358
+ "shorter video from this soundtrack."
359
+ )
360
+ num_frames = align_num_frames(num_frames)
361
+
362
+ for reference, entry in zip(resolved, references):
363
+ if reference.kind == "image":
364
+ image = entry.image
365
+ if not isinstance(image, Image.Image):
366
+ image = Image.fromarray(reference_media_to_uint8(image))
367
+ image = ImageOps.exif_transpose(image).convert("RGB")
368
+ height, width = resolve_reference_image_size(*image.size)
369
+ reference.image = prepare_reference_image(image, height, width)
370
+ elif reference.kind == "video":
371
+ frames = resample_reference_frames(reference_media_to_uint8(entry.video), float(entry.fps))
372
+ reference.frames = prepare_reference_frames(frames, num_frames)
373
+ if reference.has_audio:
374
+ reference.waveform = prepare_reference_waveform(
375
+ entry.audio,
376
+ entry.sample_rate or components.audio_sampling_rate,
377
+ components.audio_sampling_rate,
378
+ max_duration=num_frames / MINIMAX_H3_FPS,
379
+ )
380
+ return resolved, num_frames
381
+
382
+ @torch.no_grad()
383
+ def __call__(self, components: MiniMaxH3Ref2VAModularPipeline, state: PipelineState) -> PipelineState:
384
+ block_state = self.get_block_state(state)
385
+ self._check_inputs(components, block_state)
386
+
387
+ if block_state.height is None:
388
+ block_state.height, block_state.width = resolve_canvas_size(16, 9)
389
+
390
+ requested_num_frames = block_state.num_frames
391
+ block_state.prepared_references, block_state.num_frames = self.prepare_references(
392
+ components, block_state.references, block_state.num_frames
393
+ )
394
+ if requested_num_frames is not None and requested_num_frames != block_state.num_frames:
395
+ logger.warning(
396
+ f"`num_frames` has to be of the form 17 * n + 5 for the video VAE; rounding {requested_num_frames} up "
397
+ f"to {block_state.num_frames}."
398
+ )
399
+
400
+ (
401
+ block_state.num_latent_frames,
402
+ block_state.latent_height,
403
+ block_state.latent_width,
404
+ block_state.num_audio_latents,
405
+ ) = _latent_geometry(components, block_state.height, block_state.width, block_state.num_frames)
406
+
407
+ self.set_block_state(state, block_state)
408
+ return components, state