linoyts HF Staff commited on
Commit
4684d79
·
verified ·
1 Parent(s): 7e1087d

Krea 2 edit modular blocks (remote code)

Browse files
Files changed (12) hide show
  1. README.md +62 -0
  2. before_denoise.py +635 -0
  3. block.py +12 -0
  4. config.json +5 -0
  5. decoders.py +265 -0
  6. denoise.py +407 -0
  7. encoders.py +844 -0
  8. inputs.py +406 -0
  9. modular_blocks_krea2.py +879 -0
  10. modular_blocks_krea2_edit.py +97 -0
  11. modular_pipeline.py +135 -0
  12. transformer_krea2.py +566 -0
README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: diffusers
3
+ tags:
4
+ - modular-diffusers
5
+ - krea
6
+ - image-to-image
7
+ - image-editing
8
+ base_model: krea/Krea-2-Turbo
9
+ ---
10
+
11
+ # Krea 2 reference-image edit — Modular Diffusers blocks
12
+
13
+ Custom [Modular Diffusers](https://huggingface.co/docs/diffusers/main/en/modular_diffusers/overview)
14
+ blocks that reproduce the [`ostris/Krea2OstrisEdit`](https://huggingface.co/ostris/Krea2OstrisEdit)
15
+ reference-image ("edit") workflow for **Krea 2**, loadable as remote code on top of stock `diffusers`.
16
+
17
+ ```python
18
+ import torch
19
+ from transformers import Qwen3VLProcessor
20
+ from diffusers import ClassifierFreeGuidance
21
+ from diffusers.modular_pipelines import ModularPipelineBlocks
22
+
23
+ blocks = ModularPipelineBlocks.from_pretrained("diffusers-modular/krea2-edit", trust_remote_code=True)
24
+ pipe = blocks.init_pipeline("krea/Krea-2-Turbo") # weights from the base repo
25
+ pipe.load_components(torch_dtype=torch.bfloat16)
26
+ pipe.update_components(processor=Qwen3VLProcessor.from_pretrained("Qwen/Qwen3-VL-4B-Instruct"))
27
+ pipe.update_components(guider=ClassifierFreeGuidance(guidance_scale=0.0, use_original_formulation=True))
28
+ pipe.to("cuda")
29
+
30
+ from PIL import Image
31
+ image = pipe(
32
+ prompt="a white yeti with horns reading a book",
33
+ image=Image.open("reference.png"), # one or more reference images
34
+ num_inference_steps=8, mu=1.15, output="images",
35
+ )[0]
36
+ ```
37
+
38
+ ## What it does
39
+
40
+ Reference images condition generation two ways (matching how the Ostris AI-Toolkit edit LoRAs train):
41
+
42
+ 1. **Qwen3-VL prompt embedding** — a coarse view of each reference is embedded into the text
43
+ conditioning through the vision tower.
44
+ 2. **Clean VAE latents at flow time t=0** — each reference is VAE-encoded and appended to the
45
+ transformer sequence as clean tokens on its own rotary frame axis, so the noisy image tokens
46
+ attend to it at every block.
47
+
48
+ The bundled `Krea2Transformer2DModel` adds a small, backward-compatible `ref_seq_len` argument to the
49
+ Krea 2 transformer forward (t=0 modulation of the reference span; those tokens are excluded from the
50
+ predicted velocity). With `ref_seq_len=0` it is numerically identical to plain Krea 2 text-to-image.
51
+
52
+ ## Files
53
+
54
+ - `block.py` — entry point (`Krea2EditBlocks`), referenced by `config.json`'s `auto_map`.
55
+ - `transformer_krea2.py` — the Krea 2 transformer (with the `ref_seq_len` edit path).
56
+ - `modular_blocks_krea2*.py`, `encoders.py`, `before_denoise.py`, `denoise.py`, `decoders.py`,
57
+ `inputs.py`, `modular_pipeline.py` — the modular blocks.
58
+
59
+ Part of the [`diffusers-modular`](https://huggingface.co/diffusers-modular) effort to replicate
60
+ popular ComfyUI / community workflows with Modular Diffusers.
61
+
62
+ > Requires access to the gated `krea/Krea-2-Turbo` weights.
before_denoise.py ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import inspect
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
21
+ from diffusers.utils.torch_utils import randn_tensor
22
+ from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
23
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
24
+ from .modular_pipeline import Krea2ModularPipeline, Krea2Pachifier
25
+
26
+
27
+ # Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift
28
+ def calculate_shift(
29
+ image_seq_len,
30
+ base_seq_len: int = 256,
31
+ max_seq_len: int = 4096,
32
+ base_shift: float = 0.5,
33
+ max_shift: float = 1.15,
34
+ ):
35
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
36
+ b = base_shift - m * base_seq_len
37
+ mu = image_seq_len * m + b
38
+ return mu
39
+
40
+
41
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
42
+ def retrieve_timesteps(
43
+ scheduler,
44
+ num_inference_steps: int | None = None,
45
+ device: str | torch.device | None = None,
46
+ timesteps: list[int] | None = None,
47
+ sigmas: list[float] | None = None,
48
+ **kwargs,
49
+ ):
50
+ r"""
51
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
52
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
53
+
54
+ Args:
55
+ scheduler (`SchedulerMixin`):
56
+ The scheduler to get timesteps from.
57
+ num_inference_steps (`int`):
58
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
59
+ must be `None`.
60
+ device (`str` or `torch.device`, *optional*):
61
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
62
+ timesteps (`list[int]`, *optional*):
63
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
64
+ `num_inference_steps` and `sigmas` must be `None`.
65
+ sigmas (`list[float]`, *optional*):
66
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
67
+ `num_inference_steps` and `timesteps` must be `None`.
68
+
69
+ Returns:
70
+ `tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
71
+ second element is the number of inference steps.
72
+ """
73
+ if timesteps is not None and sigmas is not None:
74
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
75
+ if timesteps is not None:
76
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
77
+ if not accepts_timesteps:
78
+ raise ValueError(
79
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
80
+ f" timestep schedules. Please check whether you are using the correct scheduler."
81
+ )
82
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
83
+ timesteps = scheduler.timesteps
84
+ num_inference_steps = len(timesteps)
85
+ elif sigmas is not None:
86
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
87
+ if not accept_sigmas:
88
+ raise ValueError(
89
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
90
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
91
+ )
92
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
93
+ timesteps = scheduler.timesteps
94
+ num_inference_steps = len(timesteps)
95
+ else:
96
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
97
+ timesteps = scheduler.timesteps
98
+ return timesteps, num_inference_steps
99
+
100
+
101
+ # Copied from diffusers.modular_pipelines.qwenimage.before_denoise.get_timesteps
102
+ def get_timesteps(scheduler, num_inference_steps, strength):
103
+ # get the original timestep using init_timestep
104
+ init_timestep = min(num_inference_steps * strength, num_inference_steps)
105
+
106
+ t_start = int(max(num_inference_steps - init_timestep, 0))
107
+ timesteps = scheduler.timesteps[t_start * scheduler.order :]
108
+ if hasattr(scheduler, "set_begin_index"):
109
+ scheduler.set_begin_index(t_start * scheduler.order)
110
+
111
+ return timesteps, num_inference_steps - t_start
112
+
113
+
114
+ # ====================
115
+ # 1. PREPARE LATENTS
116
+ # ====================
117
+
118
+
119
+ class Krea2PrepareLatentsStep(ModularPipelineBlocks):
120
+ model_name = "krea2"
121
+
122
+ @property
123
+ def description(self) -> str:
124
+ return "Prepare initial random noise for the generation process"
125
+
126
+ @property
127
+ def expected_components(self) -> list[ComponentSpec]:
128
+ return [
129
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
130
+ ]
131
+
132
+ @property
133
+ def inputs(self) -> list[InputParam]:
134
+ return [
135
+ InputParam.template("latents"),
136
+ InputParam.template("height"),
137
+ InputParam.template("width"),
138
+ InputParam.template("num_images_per_prompt"),
139
+ InputParam.template("generator"),
140
+ InputParam.template("batch_size"),
141
+ InputParam.template("dtype"),
142
+ ]
143
+
144
+ @property
145
+ def intermediate_outputs(self) -> list[OutputParam]:
146
+ return [
147
+ OutputParam(name="height", type_hint=int, description="if not set, updated to default value"),
148
+ OutputParam(name="width", type_hint=int, description="if not set, updated to default value"),
149
+ OutputParam(
150
+ name="latents",
151
+ type_hint=torch.Tensor,
152
+ description="The initial latents to use for the denoising process",
153
+ ),
154
+ ]
155
+
156
+ @staticmethod
157
+ def check_inputs(height, width, vae_scale_factor):
158
+ if height is not None and height % (vae_scale_factor * 2) != 0:
159
+ raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")
160
+
161
+ if width is not None and width % (vae_scale_factor * 2) != 0:
162
+ raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")
163
+
164
+ @torch.no_grad()
165
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
166
+ block_state = self.get_block_state(state)
167
+
168
+ self.check_inputs(
169
+ height=block_state.height,
170
+ width=block_state.width,
171
+ vae_scale_factor=components.vae_scale_factor,
172
+ )
173
+
174
+ device = components._execution_device
175
+ batch_size = block_state.batch_size * block_state.num_images_per_prompt
176
+
177
+ # we can update the height and width here since it's used to generate the initial
178
+ block_state.height = block_state.height or components.default_height
179
+ block_state.width = block_state.width or components.default_width
180
+
181
+ # VAE applies 8x compression on images but we must also account for packing which requires
182
+ # latent height and width to be divisible by 2.
183
+ latent_height = 2 * (int(block_state.height) // (components.vae_scale_factor * 2))
184
+ latent_width = 2 * (int(block_state.width) // (components.vae_scale_factor * 2))
185
+
186
+ shape = (batch_size, components.num_channels_latents, 1, latent_height, latent_width)
187
+ if isinstance(block_state.generator, list) and len(block_state.generator) != batch_size:
188
+ raise ValueError(
189
+ f"You have passed a list of generators of length {len(block_state.generator)}, but requested an effective batch"
190
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
191
+ )
192
+ if block_state.latents is None:
193
+ block_state.latents = randn_tensor(
194
+ shape, generator=block_state.generator, device=device, dtype=block_state.dtype
195
+ )
196
+ block_state.latents = components.pachifier.pack_latents(block_state.latents)
197
+
198
+ self.set_block_state(state, block_state)
199
+ return components, state
200
+
201
+
202
+ class Krea2PrepareLatentsWithStrengthStep(ModularPipelineBlocks):
203
+ model_name = "krea2"
204
+
205
+ @property
206
+ def description(self) -> str:
207
+ return "Step that adds noise to image latents for image-to-image/inpainting. Should be run after set_timesteps, prepare_latents. Both noise and image latents should already be patchified."
208
+
209
+ @property
210
+ def expected_components(self) -> list[ComponentSpec]:
211
+ return [
212
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
213
+ ]
214
+
215
+ @property
216
+ def inputs(self) -> list[InputParam]:
217
+ return [
218
+ InputParam(
219
+ name="latents",
220
+ required=True,
221
+ type_hint=torch.Tensor,
222
+ description="The initial random noised, can be generated in prepare latent step.",
223
+ ),
224
+ InputParam.template("image_latents", note="Can be generated from vae encoder and updated in input step."),
225
+ InputParam(
226
+ name="timesteps",
227
+ required=True,
228
+ type_hint=torch.Tensor,
229
+ description="The timesteps to use for the denoising process. Can be generated in set_timesteps step.",
230
+ ),
231
+ ]
232
+
233
+ @property
234
+ def intermediate_outputs(self) -> list[OutputParam]:
235
+ return [
236
+ OutputParam(
237
+ name="initial_noise",
238
+ type_hint=torch.Tensor,
239
+ description="The initial random noised used for inpainting denoising.",
240
+ ),
241
+ OutputParam(
242
+ name="latents",
243
+ type_hint=torch.Tensor,
244
+ description="The scaled noisy latents to use for inpainting/image-to-image denoising.",
245
+ ),
246
+ ]
247
+
248
+ @staticmethod
249
+ def check_inputs(image_latents, latents):
250
+ if image_latents.shape[0] != latents.shape[0]:
251
+ raise ValueError(
252
+ f"`image_latents` must have have same batch size as `latents`, but got {image_latents.shape[0]} and {latents.shape[0]}"
253
+ )
254
+
255
+ if image_latents.ndim != 3:
256
+ raise ValueError(f"`image_latents` must have 3 dimensions (patchified), but got {image_latents.ndim}")
257
+
258
+ @torch.no_grad()
259
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
260
+ block_state = self.get_block_state(state)
261
+
262
+ self.check_inputs(
263
+ image_latents=block_state.image_latents,
264
+ latents=block_state.latents,
265
+ )
266
+
267
+ # prepare latent timestep
268
+ latent_timestep = block_state.timesteps[:1].repeat(block_state.latents.shape[0])
269
+
270
+ # make copy of initial_noise
271
+ block_state.initial_noise = block_state.latents
272
+
273
+ # scale noise
274
+ block_state.latents = components.scheduler.scale_noise(
275
+ block_state.image_latents, latent_timestep, block_state.latents
276
+ )
277
+
278
+ self.set_block_state(state, block_state)
279
+
280
+ return components, state
281
+
282
+
283
+ class Krea2CreateMaskLatentsStep(ModularPipelineBlocks):
284
+ model_name = "krea2"
285
+
286
+ @property
287
+ def description(self) -> str:
288
+ return "Step that creates mask latents from preprocessed mask_image by interpolating to latent space."
289
+
290
+ @property
291
+ def expected_components(self) -> list[ComponentSpec]:
292
+ return [
293
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
294
+ ]
295
+
296
+ @property
297
+ def inputs(self) -> list[InputParam]:
298
+ return [
299
+ InputParam(
300
+ name="processed_mask_image",
301
+ required=True,
302
+ type_hint=torch.Tensor,
303
+ description="The processed mask to use for the inpainting process.",
304
+ ),
305
+ InputParam.template("height", required=True),
306
+ InputParam.template("width", required=True),
307
+ InputParam.template("dtype"),
308
+ ]
309
+
310
+ @property
311
+ def intermediate_outputs(self) -> list[OutputParam]:
312
+ return [
313
+ OutputParam(
314
+ name="mask", type_hint=torch.Tensor, description="The mask to use for the inpainting process."
315
+ ),
316
+ ]
317
+
318
+ @torch.no_grad()
319
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
320
+ block_state = self.get_block_state(state)
321
+
322
+ device = components._execution_device
323
+
324
+ # VAE applies 8x compression on images but we must also account for packing which requires
325
+ # latent height and width to be divisible by 2.
326
+
327
+ height_latents = 2 * (int(block_state.height) // (components.vae_scale_factor * 2))
328
+ width_latents = 2 * (int(block_state.width) // (components.vae_scale_factor * 2))
329
+
330
+ block_state.mask = torch.nn.functional.interpolate(
331
+ block_state.processed_mask_image,
332
+ size=(height_latents, width_latents),
333
+ )
334
+
335
+ block_state.mask = block_state.mask.unsqueeze(2)
336
+ block_state.mask = block_state.mask.repeat(1, components.num_channels_latents, 1, 1, 1)
337
+ block_state.mask = block_state.mask.to(device=device, dtype=block_state.dtype)
338
+
339
+ block_state.mask = components.pachifier.pack_latents(block_state.mask)
340
+
341
+ self.set_block_state(state, block_state)
342
+
343
+ return components, state
344
+
345
+
346
+ # ====================
347
+ # 2. SET TIMESTEPS
348
+ # ====================
349
+
350
+
351
+ class Krea2SetTimestepsStep(ModularPipelineBlocks):
352
+ model_name = "krea2"
353
+
354
+ @property
355
+ def description(self) -> str:
356
+ return "Step that sets the scheduler's timesteps for text-to-image generation. Should be run after prepare latents step."
357
+
358
+ @property
359
+ def expected_components(self) -> list[ComponentSpec]:
360
+ return [
361
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
362
+ ]
363
+
364
+ @property
365
+ def inputs(self) -> list[InputParam]:
366
+ return [
367
+ InputParam.template("num_inference_steps", default=28),
368
+ InputParam.template("sigmas"),
369
+ InputParam(
370
+ name="mu",
371
+ type_hint=float,
372
+ description="Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if not provided, computed from the image sequence length (base checkpoint behavior).",
373
+ ),
374
+ InputParam(
375
+ name="latents",
376
+ required=True,
377
+ type_hint=torch.Tensor,
378
+ description="The initial random noised latents for the denoising process. Can be generated in prepare latents step.",
379
+ ),
380
+ ]
381
+
382
+ @property
383
+ def intermediate_outputs(self) -> list[OutputParam]:
384
+ return [
385
+ OutputParam(
386
+ name="timesteps", type_hint=torch.Tensor, description="The timesteps to use for the denoising process"
387
+ ),
388
+ ]
389
+
390
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
391
+ block_state = self.get_block_state(state)
392
+
393
+ device = components._execution_device
394
+ sigmas = (
395
+ np.linspace(1.0, 1 / block_state.num_inference_steps, block_state.num_inference_steps)
396
+ if block_state.sigmas is None
397
+ else block_state.sigmas
398
+ )
399
+
400
+ mu = block_state.mu
401
+ if mu is None:
402
+ mu = calculate_shift(
403
+ image_seq_len=block_state.latents.shape[1],
404
+ base_seq_len=components.scheduler.config.get("base_image_seq_len", 256),
405
+ max_seq_len=components.scheduler.config.get("max_image_seq_len", 6400),
406
+ base_shift=components.scheduler.config.get("base_shift", 0.5),
407
+ max_shift=components.scheduler.config.get("max_shift", 1.15),
408
+ )
409
+ block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
410
+ scheduler=components.scheduler,
411
+ num_inference_steps=block_state.num_inference_steps,
412
+ device=device,
413
+ sigmas=sigmas,
414
+ mu=mu,
415
+ )
416
+
417
+ components.scheduler.set_begin_index(0)
418
+
419
+ self.set_block_state(state, block_state)
420
+
421
+ return components, state
422
+
423
+
424
+ class Krea2SetTimestepsWithStrengthStep(ModularPipelineBlocks):
425
+ model_name = "krea2"
426
+
427
+ @property
428
+ def description(self) -> str:
429
+ return "Step that sets the scheduler's timesteps for image-to-image generation, and inpainting. Should be run after prepare latents step."
430
+
431
+ @property
432
+ def expected_components(self) -> list[ComponentSpec]:
433
+ return [
434
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
435
+ ]
436
+
437
+ @property
438
+ def inputs(self) -> list[InputParam]:
439
+ return [
440
+ InputParam.template("num_inference_steps", default=28),
441
+ InputParam.template("sigmas"),
442
+ InputParam(
443
+ name="mu",
444
+ type_hint=float,
445
+ description="Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if not provided, computed from the image sequence length (base checkpoint behavior).",
446
+ ),
447
+ InputParam(
448
+ "latents",
449
+ required=True,
450
+ type_hint=torch.Tensor,
451
+ description="The latents to use for the denoising process. Can be generated in prepare latents step.",
452
+ ),
453
+ InputParam.template("strength"),
454
+ ]
455
+
456
+ @property
457
+ def intermediate_outputs(self) -> list[OutputParam]:
458
+ return [
459
+ OutputParam(
460
+ name="timesteps",
461
+ type_hint=torch.Tensor,
462
+ description="The timesteps to use for the denoising process.",
463
+ ),
464
+ OutputParam(
465
+ name="num_inference_steps",
466
+ type_hint=int,
467
+ description="The number of denoising steps to perform at inference time. Updated based on strength.",
468
+ ),
469
+ ]
470
+
471
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
472
+ block_state = self.get_block_state(state)
473
+
474
+ device = components._execution_device
475
+ sigmas = (
476
+ np.linspace(1.0, 1 / block_state.num_inference_steps, block_state.num_inference_steps)
477
+ if block_state.sigmas is None
478
+ else block_state.sigmas
479
+ )
480
+
481
+ mu = block_state.mu
482
+ if mu is None:
483
+ mu = calculate_shift(
484
+ image_seq_len=block_state.latents.shape[1],
485
+ base_seq_len=components.scheduler.config.get("base_image_seq_len", 256),
486
+ max_seq_len=components.scheduler.config.get("max_image_seq_len", 6400),
487
+ base_shift=components.scheduler.config.get("base_shift", 0.5),
488
+ max_shift=components.scheduler.config.get("max_shift", 1.15),
489
+ )
490
+ block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
491
+ scheduler=components.scheduler,
492
+ num_inference_steps=block_state.num_inference_steps,
493
+ device=device,
494
+ sigmas=sigmas,
495
+ mu=mu,
496
+ )
497
+
498
+ block_state.timesteps, block_state.num_inference_steps = get_timesteps(
499
+ scheduler=components.scheduler,
500
+ num_inference_steps=block_state.num_inference_steps,
501
+ strength=block_state.strength,
502
+ )
503
+
504
+ self.set_block_state(state, block_state)
505
+
506
+ return components, state
507
+
508
+
509
+ # ====================
510
+ # 3. OTHER INPUTS FOR DENOISER
511
+ # ====================
512
+
513
+ ## RoPE inputs for denoiser
514
+
515
+
516
+ class Krea2RoPEInputsStep(ModularPipelineBlocks):
517
+ model_name = "krea2"
518
+
519
+ @property
520
+ def description(self) -> str:
521
+ return (
522
+ "Step that prepares the rotary position ids for the denoising process. Text tokens sit at the origin, "
523
+ "image tokens carry their `(0, h, w)` latent-grid coordinates. Should be placed after prepare_latents step."
524
+ )
525
+
526
+ @property
527
+ def expected_components(self) -> list[ComponentSpec]:
528
+ return [
529
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
530
+ ]
531
+
532
+ @property
533
+ def inputs(self) -> list[InputParam]:
534
+ return [
535
+ InputParam.template("height", required=True),
536
+ InputParam.template("width", required=True),
537
+ InputParam.template("prompt_embeds_mask"),
538
+ ]
539
+
540
+ @property
541
+ def intermediate_outputs(self) -> list[OutputParam]:
542
+ return [
543
+ OutputParam(
544
+ name="position_ids",
545
+ kwargs_type="denoiser_input_fields",
546
+ type_hint=torch.Tensor,
547
+ description="The rotary coordinates of shape (text_seq_len + grid_height * grid_width, 3) for the combined text-image sequence.",
548
+ ),
549
+ ]
550
+
551
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
552
+ block_state = self.get_block_state(state)
553
+
554
+ device = components._execution_device
555
+ patch_size = components.pachifier.config.patch_size
556
+
557
+ text_seq_len = block_state.prompt_embeds_mask.shape[1]
558
+ grid_height = block_state.height // (components.vae_scale_factor * patch_size)
559
+ grid_width = block_state.width // (components.vae_scale_factor * patch_size)
560
+
561
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
562
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
563
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
564
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
565
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
566
+ block_state.position_ids = torch.cat([text_ids, image_ids], dim=0)
567
+
568
+ self.set_block_state(state, block_state)
569
+
570
+ return components, state
571
+
572
+
573
+ class Krea2EditRoPEInputsStep(ModularPipelineBlocks):
574
+ model_name = "krea2"
575
+
576
+ @property
577
+ def description(self) -> str:
578
+ return (
579
+ "Step that prepares the rotary position ids for the edit task: the `[text, image]` coordinates followed "
580
+ "by the reference tokens' coordinates (each reference on its own frame axis). Should be placed after "
581
+ "prepare_latents and the reference latents step."
582
+ )
583
+
584
+ @property
585
+ def expected_components(self) -> list[ComponentSpec]:
586
+ return [
587
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
588
+ ]
589
+
590
+ @property
591
+ def inputs(self) -> list[InputParam]:
592
+ return [
593
+ InputParam.template("height", required=True),
594
+ InputParam.template("width", required=True),
595
+ InputParam.template("prompt_embeds_mask"),
596
+ InputParam(
597
+ "reference_position_ids",
598
+ required=True,
599
+ type_hint=torch.Tensor,
600
+ description="Rotary coordinates for the reference tokens. Can be generated in the reference latents step.",
601
+ ),
602
+ ]
603
+
604
+ @property
605
+ def intermediate_outputs(self) -> list[OutputParam]:
606
+ return [
607
+ OutputParam(
608
+ name="position_ids",
609
+ kwargs_type="denoiser_input_fields",
610
+ type_hint=torch.Tensor,
611
+ description="The rotary coordinates of shape (text_seq_len + grid_height * grid_width + ref_seq_len, 3) "
612
+ "for the combined text-image-reference sequence.",
613
+ ),
614
+ ]
615
+
616
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
617
+ block_state = self.get_block_state(state)
618
+
619
+ device = components._execution_device
620
+ patch_size = components.pachifier.config.patch_size
621
+
622
+ text_seq_len = block_state.prompt_embeds_mask.shape[1]
623
+ grid_height = block_state.height // (components.vae_scale_factor * patch_size)
624
+ grid_width = block_state.width // (components.vae_scale_factor * patch_size)
625
+
626
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
627
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
628
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
629
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
630
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
631
+ block_state.position_ids = torch.cat([text_ids, image_ids, block_state.reference_position_ids], dim=0)
632
+
633
+ self.set_block_state(state, block_state)
634
+
635
+ return components, state
block.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Entry point for the Krea 2 reference-image edit modular blocks (loaded via `trust_remote_code`).
2
+
3
+ Importing this module also registers `Krea2Transformer2DModel` into the `diffusers` namespace so that
4
+ `ModularPipeline` component loading can resolve the transformer referenced as
5
+ `["diffusers", "Krea2Transformer2DModel"]` in the base `krea/Krea-2-Turbo` `model_index.json`.
6
+ """
7
+
8
+ from .transformer_krea2 import Krea2Transformer2DModel # noqa: F401 (registers into diffusers namespace)
9
+ from .modular_blocks_krea2_edit import Krea2EditBlocks
10
+
11
+
12
+ __all__ = ["Krea2EditBlocks", "Krea2Transformer2DModel"]
config.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "ModularPipelineBlocks": "block.Krea2EditBlocks"
4
+ }
5
+ }
decoders.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 typing import Any
17
+
18
+ import torch
19
+
20
+ from diffusers.configuration_utils import FrozenDict
21
+ from diffusers.image_processor import InpaintProcessor, VaeImageProcessor
22
+ from diffusers.models import AutoencoderKLQwenImage
23
+ from diffusers.utils import logging
24
+ from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
25
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
26
+ from .modular_pipeline import Krea2ModularPipeline, Krea2Pachifier
27
+
28
+
29
+ logger = logging.get_logger(__name__)
30
+
31
+
32
+ # after denoising loop (unpack latents)
33
+
34
+
35
+ class Krea2AfterDenoiseStep(ModularPipelineBlocks):
36
+ model_name = "krea2"
37
+
38
+ @property
39
+ def description(self) -> str:
40
+ return "Step that unpack the latents from 3D tensor (batch_size, sequence_length, channels) into 5D tensor (batch_size, channels, 1, height, width)"
41
+
42
+ @property
43
+ def expected_components(self) -> list[ComponentSpec]:
44
+ components = [
45
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
46
+ ]
47
+
48
+ return components
49
+
50
+ @property
51
+ def inputs(self) -> list[InputParam]:
52
+ return [
53
+ InputParam.template("height", required=True),
54
+ InputParam.template("width", required=True),
55
+ InputParam(
56
+ name="latents",
57
+ required=True,
58
+ type_hint=torch.Tensor,
59
+ description="The latents to decode, can be generated in the denoise step.",
60
+ ),
61
+ ]
62
+
63
+ @property
64
+ def intermediate_outputs(self) -> list[OutputParam]:
65
+ return [
66
+ OutputParam(
67
+ name="latents", type_hint=torch.Tensor, description="The denoised latents unpacked to B, C, 1, H, W"
68
+ ),
69
+ ]
70
+
71
+ @torch.no_grad()
72
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
73
+ block_state = self.get_block_state(state)
74
+
75
+ vae_scale_factor = components.vae_scale_factor
76
+ block_state.latents = components.pachifier.unpack_latents(
77
+ block_state.latents, block_state.height, block_state.width, vae_scale_factor=vae_scale_factor
78
+ )
79
+
80
+ self.set_block_state(state, block_state)
81
+ return components, state
82
+
83
+
84
+ # decode step
85
+
86
+
87
+ class Krea2DecoderStep(ModularPipelineBlocks):
88
+ model_name = "krea2"
89
+
90
+ @property
91
+ def description(self) -> str:
92
+ return "Step that decodes the latents to images"
93
+
94
+ @property
95
+ def expected_components(self) -> list[ComponentSpec]:
96
+ components = [
97
+ ComponentSpec("vae", AutoencoderKLQwenImage),
98
+ ]
99
+
100
+ return components
101
+
102
+ @property
103
+ def inputs(self) -> list[InputParam]:
104
+ return [
105
+ InputParam(
106
+ name="latents",
107
+ required=True,
108
+ type_hint=torch.Tensor,
109
+ description="The denoised latents to decode, can be generated in the denoise step and unpacked in the after denoise step.",
110
+ ),
111
+ ]
112
+
113
+ @property
114
+ def intermediate_outputs(self) -> list[OutputParam]:
115
+ return [OutputParam.template("images", note="tensor output of the vae decoder.")]
116
+
117
+ @torch.no_grad()
118
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
119
+ block_state = self.get_block_state(state)
120
+
121
+ if block_state.latents.ndim == 4:
122
+ block_state.latents = block_state.latents.unsqueeze(dim=1)
123
+ elif block_state.latents.ndim != 5:
124
+ raise ValueError(
125
+ f"expect latents to be a 4D or 5D tensor but got: {block_state.latents.shape}. Please make sure the latents are unpacked before decode step."
126
+ )
127
+ block_state.latents = block_state.latents.to(components.vae.dtype)
128
+
129
+ latents_mean = (
130
+ torch.tensor(components.vae.config.latents_mean)
131
+ .view(1, components.vae.config.z_dim, 1, 1, 1)
132
+ .to(block_state.latents.device, block_state.latents.dtype)
133
+ )
134
+ latents_std = 1.0 / torch.tensor(components.vae.config.latents_std).view(
135
+ 1, components.vae.config.z_dim, 1, 1, 1
136
+ ).to(block_state.latents.device, block_state.latents.dtype)
137
+ block_state.latents = block_state.latents / latents_std + latents_mean
138
+ block_state.images = components.vae.decode(block_state.latents, return_dict=False)[0][:, :, 0]
139
+
140
+ self.set_block_state(state, block_state)
141
+ return components, state
142
+
143
+
144
+ # postprocess the decoded images
145
+
146
+
147
+ class Krea2ProcessImagesOutputStep(ModularPipelineBlocks):
148
+ model_name = "krea2"
149
+
150
+ @property
151
+ def description(self) -> str:
152
+ return "postprocess the generated image"
153
+
154
+ @property
155
+ def expected_components(self) -> list[ComponentSpec]:
156
+ return [
157
+ ComponentSpec(
158
+ "image_processor",
159
+ VaeImageProcessor,
160
+ config=FrozenDict({"vae_scale_factor": 16}),
161
+ default_creation_method="from_config",
162
+ ),
163
+ ]
164
+
165
+ @property
166
+ def inputs(self) -> list[InputParam]:
167
+ return [
168
+ InputParam(
169
+ name="images",
170
+ required=True,
171
+ type_hint=torch.Tensor,
172
+ description="the generated image tensor from decoders step",
173
+ ),
174
+ InputParam.template("output_type"),
175
+ ]
176
+
177
+ @property
178
+ def intermediate_outputs(self) -> list[OutputParam]:
179
+ return [OutputParam.template("images")]
180
+
181
+ @staticmethod
182
+ def check_inputs(output_type):
183
+ if output_type not in ["pil", "np", "pt"]:
184
+ raise ValueError(f"Invalid output_type: {output_type}")
185
+
186
+ @torch.no_grad()
187
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
188
+ block_state = self.get_block_state(state)
189
+
190
+ self.check_inputs(block_state.output_type)
191
+
192
+ block_state.images = components.image_processor.postprocess(
193
+ image=block_state.images,
194
+ output_type=block_state.output_type,
195
+ )
196
+
197
+ self.set_block_state(state, block_state)
198
+ return components, state
199
+
200
+
201
+ class Krea2InpaintProcessImagesOutputStep(ModularPipelineBlocks):
202
+ model_name = "krea2"
203
+
204
+ @property
205
+ def description(self) -> str:
206
+ return "postprocess the generated image, optionally apply the mask overlay to the original image."
207
+
208
+ @property
209
+ def expected_components(self) -> list[ComponentSpec]:
210
+ return [
211
+ ComponentSpec(
212
+ "image_mask_processor",
213
+ InpaintProcessor,
214
+ config=FrozenDict({"vae_scale_factor": 16}),
215
+ default_creation_method="from_config",
216
+ ),
217
+ ]
218
+
219
+ @property
220
+ def inputs(self) -> list[InputParam]:
221
+ return [
222
+ InputParam(
223
+ name="images",
224
+ required=True,
225
+ type_hint=torch.Tensor,
226
+ description="the generated image tensor from decoders step",
227
+ ),
228
+ InputParam.template("output_type"),
229
+ InputParam(
230
+ name="mask_overlay_kwargs",
231
+ type_hint=dict[str, Any],
232
+ description="The kwargs for the postprocess step to apply the mask overlay. generated in Krea2InpaintProcessImagesInputStep.",
233
+ ),
234
+ ]
235
+
236
+ @property
237
+ def intermediate_outputs(self) -> list[OutputParam]:
238
+ return [OutputParam.template("images")]
239
+
240
+ @staticmethod
241
+ def check_inputs(output_type, mask_overlay_kwargs):
242
+ if output_type not in ["pil", "np", "pt"]:
243
+ raise ValueError(f"Invalid output_type: {output_type}")
244
+
245
+ if mask_overlay_kwargs and output_type != "pil":
246
+ raise ValueError("only support output_type 'pil' for mask overlay")
247
+
248
+ @torch.no_grad()
249
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
250
+ block_state = self.get_block_state(state)
251
+
252
+ self.check_inputs(block_state.output_type, block_state.mask_overlay_kwargs)
253
+
254
+ if block_state.mask_overlay_kwargs is None:
255
+ mask_overlay_kwargs = {}
256
+ else:
257
+ mask_overlay_kwargs = block_state.mask_overlay_kwargs
258
+
259
+ block_state.images = components.image_mask_processor.postprocess(
260
+ image=block_state.images,
261
+ **mask_overlay_kwargs,
262
+ )
263
+
264
+ self.set_block_state(state, block_state)
265
+ return components, state
denoise.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import inspect
16
+
17
+ import torch
18
+
19
+ from diffusers.configuration_utils import FrozenDict
20
+ from diffusers.guiders import ClassifierFreeGuidance
21
+ from .transformer_krea2 import Krea2Transformer2DModel
22
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
23
+ from diffusers.utils import logging
24
+ from diffusers.modular_pipelines.modular_pipeline import BlockState, LoopSequentialPipelineBlocks, ModularPipelineBlocks, PipelineState
25
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
26
+ from .modular_pipeline import Krea2ModularPipeline
27
+
28
+
29
+ logger = logging.get_logger(__name__)
30
+
31
+ # ====================
32
+ # 1. LOOP STEPS (run at each denoising step)
33
+ # ====================
34
+
35
+
36
+ # loop step:before denoiser
37
+ class Krea2LoopBeforeDenoiser(ModularPipelineBlocks):
38
+ model_name = "krea2"
39
+
40
+ @property
41
+ def description(self) -> str:
42
+ return (
43
+ "step within the denoising loop that prepares the latent input for the denoiser. "
44
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
45
+ "object (e.g. `Krea2DenoiseLoopWrapper`)"
46
+ )
47
+
48
+ @property
49
+ def inputs(self) -> list[InputParam]:
50
+ return [
51
+ InputParam(
52
+ name="latents",
53
+ required=True,
54
+ type_hint=torch.Tensor,
55
+ description="The initial latents to use for the denoising process. Can be generated in prepare_latent step.",
56
+ ),
57
+ ]
58
+
59
+ @torch.no_grad()
60
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
61
+ # one timestep
62
+ block_state.timestep = t.expand(block_state.latents.shape[0]).to(block_state.latents.dtype)
63
+ block_state.latent_model_input = block_state.latents
64
+ return components, block_state
65
+
66
+
67
+ # loop step:before denoiser (edit) -- appends the clean reference tokens to the denoiser input each step
68
+ class Krea2EditLoopBeforeDenoiser(ModularPipelineBlocks):
69
+ model_name = "krea2"
70
+
71
+ @property
72
+ def description(self) -> str:
73
+ return (
74
+ "step within the denoising loop that prepares the latent input for the edit denoiser: it appends the "
75
+ "packed clean reference tokens after the noisy image tokens. This block should be used to compose the "
76
+ "`sub_blocks` attribute of a `LoopSequentialPipelineBlocks` object (e.g. `Krea2EditDenoiseStep`)."
77
+ )
78
+
79
+ @property
80
+ def inputs(self) -> list[InputParam]:
81
+ return [
82
+ InputParam(
83
+ name="latents",
84
+ required=True,
85
+ type_hint=torch.Tensor,
86
+ description="The initial latents to use for the denoising process. Can be generated in prepare_latent step.",
87
+ ),
88
+ InputParam(
89
+ name="reference_latents",
90
+ required=True,
91
+ type_hint=torch.Tensor,
92
+ description="Packed clean reference tokens to append to the denoiser sequence. Can be generated in the reference latents step.",
93
+ ),
94
+ ]
95
+
96
+ @torch.no_grad()
97
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
98
+ block_state.timestep = t.expand(block_state.latents.shape[0]).to(block_state.latents.dtype)
99
+ # Reference tokens are shared across the batch; expand and append them after the noisy image tokens.
100
+ reference_latents = block_state.reference_latents.expand(block_state.latents.shape[0], -1, -1)
101
+ block_state.latent_model_input = torch.cat(
102
+ [block_state.latents, reference_latents.to(block_state.latents.dtype)], dim=1
103
+ )
104
+ return components, block_state
105
+
106
+
107
+ # loop step:denoiser
108
+ class Krea2LoopDenoiser(ModularPipelineBlocks):
109
+ model_name = "krea2"
110
+
111
+ @property
112
+ def description(self) -> str:
113
+ return (
114
+ "step within the denoising loop that denoise the latent input for the denoiser. "
115
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
116
+ "object (e.g. `Krea2DenoiseLoopWrapper`)"
117
+ )
118
+
119
+ @property
120
+ def expected_components(self) -> list[ComponentSpec]:
121
+ return [
122
+ ComponentSpec(
123
+ "guider",
124
+ ClassifierFreeGuidance,
125
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
126
+ default_creation_method="from_config",
127
+ ),
128
+ ComponentSpec("transformer", Krea2Transformer2DModel),
129
+ ]
130
+
131
+ @property
132
+ def inputs(self) -> list[InputParam]:
133
+ return [
134
+ InputParam.template("denoiser_input_fields"),
135
+ InputParam(
136
+ "position_ids",
137
+ required=True,
138
+ type_hint=torch.Tensor,
139
+ description="The rotary coordinates for the combined text-image sequence. Can be generated in prepare_rope_inputs step.",
140
+ ),
141
+ ]
142
+
143
+ @torch.no_grad()
144
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
145
+ guider_inputs = {
146
+ "encoder_hidden_states": (
147
+ getattr(block_state, "prompt_embeds", None),
148
+ getattr(block_state, "negative_prompt_embeds", None),
149
+ ),
150
+ "encoder_attention_mask": (
151
+ getattr(block_state, "prompt_embeds_mask", None),
152
+ getattr(block_state, "negative_prompt_embeds_mask", None),
153
+ ),
154
+ }
155
+
156
+ transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
157
+ additional_cond_kwargs = {}
158
+ for field_name, field_value in block_state.denoiser_input_fields.items():
159
+ if field_name in transformer_args and field_name not in guider_inputs:
160
+ additional_cond_kwargs[field_name] = field_value
161
+ block_state.additional_cond_kwargs.update(additional_cond_kwargs)
162
+
163
+ components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
164
+ guider_state = components.guider.prepare_inputs(guider_inputs)
165
+
166
+ for guider_state_batch in guider_state:
167
+ components.guider.prepare_models(components.transformer)
168
+ cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
169
+
170
+ guider_state_batch.noise_pred = components.transformer(
171
+ hidden_states=block_state.latent_model_input,
172
+ timestep=block_state.timestep / 1000,
173
+ return_dict=False,
174
+ **cond_kwargs,
175
+ **block_state.additional_cond_kwargs,
176
+ )[0]
177
+
178
+ components.guider.cleanup_models(components.transformer)
179
+
180
+ guider_output = components.guider(guider_state)
181
+ block_state.noise_pred = guider_output.pred
182
+
183
+ return components, block_state
184
+
185
+
186
+ # loop step:after denoiser
187
+ class Krea2LoopAfterDenoiser(ModularPipelineBlocks):
188
+ model_name = "krea2"
189
+
190
+ @property
191
+ def description(self) -> str:
192
+ return (
193
+ "step within the denoising loop that updates the latents. "
194
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
195
+ "object (e.g. `Krea2DenoiseLoopWrapper`)"
196
+ )
197
+
198
+ @property
199
+ def expected_components(self) -> list[ComponentSpec]:
200
+ return [
201
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
202
+ ]
203
+
204
+ @property
205
+ def intermediate_outputs(self) -> list[OutputParam]:
206
+ return [
207
+ OutputParam.template("latents"),
208
+ ]
209
+
210
+ @torch.no_grad()
211
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
212
+ latents_dtype = block_state.latents.dtype
213
+ block_state.latents = components.scheduler.step(
214
+ block_state.noise_pred,
215
+ t,
216
+ block_state.latents,
217
+ return_dict=False,
218
+ )[0]
219
+
220
+ if block_state.latents.dtype != latents_dtype:
221
+ if torch.backends.mps.is_available():
222
+ # some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
223
+ block_state.latents = block_state.latents.to(latents_dtype)
224
+
225
+ return components, block_state
226
+
227
+
228
+ class Krea2LoopAfterDenoiserInpaint(ModularPipelineBlocks):
229
+ model_name = "krea2"
230
+
231
+ @property
232
+ def description(self) -> str:
233
+ return (
234
+ "step within the denoising loop that updates the latents using mask and image_latents for inpainting. "
235
+ "This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
236
+ "object (e.g. `Krea2DenoiseLoopWrapper`)"
237
+ )
238
+
239
+ @property
240
+ def inputs(self) -> list[InputParam]:
241
+ return [
242
+ InputParam(
243
+ "mask",
244
+ required=True,
245
+ type_hint=torch.Tensor,
246
+ description="The mask to use for the inpainting process. Can be generated in inpaint prepare latents step.",
247
+ ),
248
+ InputParam.template("image_latents"),
249
+ InputParam(
250
+ "initial_noise",
251
+ required=True,
252
+ type_hint=torch.Tensor,
253
+ description="The initial noise to use for the inpainting process. Can be generated in inpaint prepare latents step.",
254
+ ),
255
+ ]
256
+
257
+ @property
258
+ def intermediate_outputs(self) -> list[OutputParam]:
259
+ return [
260
+ OutputParam.template("latents"),
261
+ ]
262
+
263
+ @torch.no_grad()
264
+ def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
265
+ block_state.init_latents_proper = block_state.image_latents
266
+ if i < len(block_state.timesteps) - 1:
267
+ block_state.noise_timestep = block_state.timesteps[i + 1]
268
+ block_state.init_latents_proper = components.scheduler.scale_noise(
269
+ block_state.init_latents_proper, torch.tensor([block_state.noise_timestep]), block_state.initial_noise
270
+ )
271
+
272
+ block_state.latents = (
273
+ 1 - block_state.mask
274
+ ) * block_state.init_latents_proper + block_state.mask * block_state.latents
275
+
276
+ return components, block_state
277
+
278
+
279
+ # ====================
280
+ # 2. DENOISE LOOP WRAPPER: define the denoising loop logic
281
+ # ====================
282
+ class Krea2DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
283
+ model_name = "krea2"
284
+
285
+ @property
286
+ def description(self) -> str:
287
+ return (
288
+ "Pipeline block that iteratively denoise the latents over `timesteps`. "
289
+ "The specific steps with each iteration can be customized with `sub_blocks` attributes"
290
+ )
291
+
292
+ @property
293
+ def loop_expected_components(self) -> list[ComponentSpec]:
294
+ return [
295
+ ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
296
+ ]
297
+
298
+ @property
299
+ def loop_inputs(self) -> list[InputParam]:
300
+ return [
301
+ InputParam(
302
+ name="timesteps",
303
+ required=True,
304
+ type_hint=torch.Tensor,
305
+ description="The timesteps to use for the denoising process. Can be generated in set_timesteps step.",
306
+ ),
307
+ InputParam.template("num_inference_steps", required=True),
308
+ ]
309
+
310
+ @torch.no_grad()
311
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
312
+ block_state = self.get_block_state(state)
313
+
314
+ block_state.num_warmup_steps = max(
315
+ len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
316
+ )
317
+
318
+ block_state.additional_cond_kwargs = {}
319
+
320
+ with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
321
+ for i, t in enumerate(block_state.timesteps):
322
+ components, block_state = self.loop_step(components, block_state, i=i, t=t)
323
+ if i == len(block_state.timesteps) - 1 or (
324
+ (i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
325
+ ):
326
+ progress_bar.update()
327
+
328
+ self.set_block_state(state, block_state)
329
+
330
+ return components, state
331
+
332
+
333
+ # ====================
334
+ # 3. DENOISE STEPS: compose the denoising loop with loop wrapper + loop steps
335
+ # ====================
336
+
337
+
338
+ # Krea 2 (text2image, image2image)
339
+ class Krea2DenoiseStep(Krea2DenoiseLoopWrapper):
340
+ model_name = "krea2"
341
+
342
+ block_classes = [
343
+ Krea2LoopBeforeDenoiser,
344
+ Krea2LoopDenoiser,
345
+ Krea2LoopAfterDenoiser,
346
+ ]
347
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
348
+
349
+ @property
350
+ def description(self) -> str:
351
+ return (
352
+ "Denoise step that iteratively denoise the latents.\n"
353
+ "Its loop logic is defined in `Krea2DenoiseLoopWrapper.__call__` method\n"
354
+ "At each iteration, it runs blocks defined in `sub_blocks` sequencially:\n"
355
+ " - `Krea2LoopBeforeDenoiser`\n"
356
+ " - `Krea2LoopDenoiser`\n"
357
+ " - `Krea2LoopAfterDenoiser`\n"
358
+ "This block supports text2image and image2image tasks for Krea 2."
359
+ )
360
+
361
+
362
+ # Krea 2 (inpainting)
363
+ class Krea2InpaintDenoiseStep(Krea2DenoiseLoopWrapper):
364
+ model_name = "krea2"
365
+ block_classes = [
366
+ Krea2LoopBeforeDenoiser,
367
+ Krea2LoopDenoiser,
368
+ Krea2LoopAfterDenoiser,
369
+ Krea2LoopAfterDenoiserInpaint,
370
+ ]
371
+ block_names = ["before_denoiser", "denoiser", "after_denoiser", "after_denoiser_inpaint"]
372
+
373
+ @property
374
+ def description(self) -> str:
375
+ return (
376
+ "Denoise step that iteratively denoise the latents. \n"
377
+ "Its loop logic is defined in `Krea2DenoiseLoopWrapper.__call__` method \n"
378
+ "At each iteration, it runs blocks defined in `sub_blocks` sequencially:\n"
379
+ " - `Krea2LoopBeforeDenoiser`\n"
380
+ " - `Krea2LoopDenoiser`\n"
381
+ " - `Krea2LoopAfterDenoiser`\n"
382
+ " - `Krea2LoopAfterDenoiserInpaint`\n"
383
+ "This block supports inpainting tasks for Krea 2."
384
+ )
385
+
386
+
387
+ # Krea 2 (reference-image edit)
388
+ class Krea2EditDenoiseStep(Krea2DenoiseLoopWrapper):
389
+ model_name = "krea2"
390
+ block_classes = [
391
+ Krea2EditLoopBeforeDenoiser,
392
+ Krea2LoopDenoiser,
393
+ Krea2LoopAfterDenoiser,
394
+ ]
395
+ block_names = ["before_denoiser", "denoiser", "after_denoiser"]
396
+
397
+ @property
398
+ def description(self) -> str:
399
+ return (
400
+ "Denoise step that iteratively denoise the latents for the reference-image edit task.\n"
401
+ "Its loop logic is defined in `Krea2DenoiseLoopWrapper.__call__` method\n"
402
+ "At each iteration, it runs blocks defined in `sub_blocks` sequencially:\n"
403
+ " - `Krea2EditLoopBeforeDenoiser` (appends the clean reference tokens)\n"
404
+ " - `Krea2LoopDenoiser`\n"
405
+ " - `Krea2LoopAfterDenoiser`\n"
406
+ "This block supports reference-image (edit) generation for Krea 2."
407
+ )
encoders.py ADDED
@@ -0,0 +1,844 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Text and VAE encoder blocks for Krea 2 pipelines.
17
+ """
18
+
19
+ import math
20
+
21
+ import numpy as np
22
+ import PIL.Image
23
+ import torch
24
+ import torch.nn.functional as F
25
+ from transformers import Qwen2Tokenizer, Qwen3VLModel, Qwen3VLProcessor
26
+
27
+ from diffusers.configuration_utils import FrozenDict
28
+ from diffusers.guiders import ClassifierFreeGuidance
29
+ from diffusers.image_processor import InpaintProcessor, VaeImageProcessor
30
+ from diffusers.models import AutoencoderKLQwenImage
31
+ from diffusers.utils import logging
32
+ from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
33
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
34
+ from .modular_pipeline import Krea2ModularPipeline, Krea2Pachifier
35
+
36
+
37
+ logger = logging.get_logger(__name__)
38
+
39
+ # Text conditioning uses the Qwen-Image chat template, tokenized as a fixed-length block: the prompt is padded to a
40
+ # fixed length first and the assistant suffix is appended after the padding (matching how the model was sampled at
41
+ # training time). The first `KREA2_PROMPT_TEMPLATE_START_IDX` (system prefix) tokens are dropped from the encoder
42
+ # outputs.
43
+ KREA2_PROMPT_TEMPLATE_PREFIX = (
44
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
45
+ "spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
46
+ )
47
+ KREA2_PROMPT_TEMPLATE_SUFFIX = "<|im_end|>\n<|im_start|>assistant\n"
48
+ KREA2_PROMPT_TEMPLATE_START_IDX = 34
49
+ KREA2_PROMPT_TEMPLATE_NUM_SUFFIX_TOKENS = 5
50
+
51
+ # Indices into the text encoder's `hidden_states` tuple (0 is the embedding output) whose states are stacked per token
52
+ # and fed to the transformer's text fusion stage. These are the Krea 2 (Qwen3-VL-4B) taps; must have
53
+ # `transformer.config.num_text_layers` entries.
54
+ KREA2_TEXT_ENCODER_SELECT_LAYERS = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
55
+
56
+
57
+ def get_krea2_prompt_embeds(
58
+ text_encoder,
59
+ tokenizer,
60
+ prompt: str | list[str],
61
+ text_encoder_select_layers: tuple[int, ...] = KREA2_TEXT_ENCODER_SELECT_LAYERS,
62
+ prompt_template_prefix: str = KREA2_PROMPT_TEMPLATE_PREFIX,
63
+ prompt_template_suffix: str = KREA2_PROMPT_TEMPLATE_SUFFIX,
64
+ prompt_template_start_idx: int = KREA2_PROMPT_TEMPLATE_START_IDX,
65
+ prompt_template_num_suffix_tokens: int = KREA2_PROMPT_TEMPLATE_NUM_SUFFIX_TOKENS,
66
+ max_sequence_length: int = 512,
67
+ device: torch.device | None = None,
68
+ ):
69
+ """Tokenize `prompt` into the fixed-length Krea 2 layout and tap the selected encoder hidden states.
70
+
71
+ Returns a `(prompt_embeds, prompt_embeds_mask)` tuple of shapes `(batch_size, text_seq_len, num_text_layers,
72
+ text_hidden_dim)` and `(batch_size, text_seq_len)` (bool).
73
+ """
74
+ prompt = [prompt] if isinstance(prompt, str) else prompt
75
+ prefix_idx = prompt_template_start_idx
76
+ text = [prompt_template_prefix + e for e in prompt]
77
+ text_tokens = tokenizer(
78
+ text,
79
+ truncation=True,
80
+ padding="max_length",
81
+ max_length=max_sequence_length + prefix_idx - prompt_template_num_suffix_tokens,
82
+ return_tensors="pt",
83
+ ).to(device)
84
+ suffix_tokens = tokenizer([prompt_template_suffix] * len(text), return_tensors="pt").to(device)
85
+
86
+ input_ids = torch.cat([text_tokens.input_ids, suffix_tokens.input_ids], dim=1)
87
+ attention_mask = torch.cat([text_tokens.attention_mask, suffix_tokens.attention_mask], dim=1).bool()
88
+
89
+ # Krea 2 pads in the middle of the template (`[prefix | prompt | PAD | suffix]`), so the suffix tokens sit
90
+ # downstream of the padding. The text features must use positions that count only real tokens (padding does
91
+ # not consume a position) to match how the model was trained; otherwise the suffix gets a shifted mRoPE phase.
92
+ # `Qwen3VLModel`'s default raw-index positions would place the suffix at ~max_length instead. Build the
93
+ # cumulative-valid-token positions explicitly and broadcast across the 3 mRoPE axes (T/H/W are equal for text).
94
+ position_ids = (attention_mask.long().cumsum(dim=-1) - 1).clamp(min=0)
95
+ position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
96
+
97
+ outputs = text_encoder(
98
+ input_ids=input_ids,
99
+ attention_mask=attention_mask,
100
+ position_ids=position_ids,
101
+ output_hidden_states=True,
102
+ )
103
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in text_encoder_select_layers], dim=2)
104
+
105
+ prompt_embeds = hidden_states[:, prefix_idx:]
106
+ prompt_embeds_mask = attention_mask[:, prefix_idx:]
107
+ return prompt_embeds, prompt_embeds_mask
108
+
109
+
110
+ # Reference images ride in the user message ahead of the prompt through named vision placeholders; the Qwen3-VL
111
+ # processor expands each `<|image_pad|>` into the image's token grid so the text conditioning "sees" the references.
112
+ KREA2_EDIT_IMAGE_PLACEHOLDER = "Picture {}: <|vision_start|><|image_pad|><|vision_end|>"
113
+
114
+
115
+ def to_chw_tensor(image) -> torch.Tensor:
116
+ """Convert a PIL image / numpy array / CHW tensor to a float CHW tensor in [0, 1]."""
117
+ if isinstance(image, torch.Tensor):
118
+ t = image.squeeze(0) if image.ndim == 4 else image
119
+ t = t.float()
120
+ if t.min() < 0: # assume [-1, 1]
121
+ t = (t + 1.0) / 2.0
122
+ return t.clamp(0, 1)
123
+ if isinstance(image, np.ndarray):
124
+ image = PIL.Image.fromarray(image)
125
+ image = image.convert("RGB")
126
+ arr = np.asarray(image).astype(np.float32) / 255.0
127
+ return torch.from_numpy(arr).permute(2, 0, 1)
128
+
129
+
130
+ def prep_vl_images(images: list[torch.Tensor], max_pixels: int) -> list[torch.Tensor]:
131
+ """Resize reference images for the Qwen3-VL pass: aspect-preserving downscale (never upscaled) to fit
132
+ `max_pixels` total area. The MLLM only needs a coarse view of the references; high-res detail flows through the
133
+ VAE reference latents."""
134
+ prepped = []
135
+ for img in images:
136
+ h, w = img.shape[1], img.shape[2]
137
+ scale = min(1.0, math.sqrt(max_pixels / (h * w)))
138
+ nh, nw = max(round(h * scale), 28), max(round(w * scale), 28)
139
+ if (nh, nw) != (h, w):
140
+ img = F.interpolate(img.unsqueeze(0).float(), size=(nh, nw), mode="bicubic", antialias=True).squeeze(0)
141
+ img = img.clamp(0, 1)
142
+ prepped.append(img.float())
143
+ return prepped
144
+
145
+
146
+ def get_krea2_edit_prompt_embeds(
147
+ text_encoder,
148
+ tokenizer,
149
+ processor,
150
+ prompt: str | list[str],
151
+ images: list[torch.Tensor] | None = None,
152
+ text_encoder_select_layers: tuple[int, ...] = KREA2_TEXT_ENCODER_SELECT_LAYERS,
153
+ prompt_template_prefix: str = KREA2_PROMPT_TEMPLATE_PREFIX,
154
+ prompt_template_suffix: str = KREA2_PROMPT_TEMPLATE_SUFFIX,
155
+ prompt_template_start_idx: int = KREA2_PROMPT_TEMPLATE_START_IDX,
156
+ max_sequence_length: int = 512,
157
+ device: torch.device | None = None,
158
+ ):
159
+ """Encode prompts for the edit task, embedding reference images (a coarse VL view of each) into the text
160
+ conditioning through the Qwen3-VL vision tower.
161
+
162
+ Unlike `get_krea2_prompt_embeds`, prompts are tokenized at their natural (unpadded) length: the processor
163
+ expands each `<|image_pad|>` placeholder into a run of vision tokens that must stay intact, so no truncation or
164
+ fixed padding is applied before encoding. All prompts share the same `images`. Returns a
165
+ `(prompt_embeds, prompt_embeds_mask)` tuple of shapes `(batch_size, text_seq_len, num_text_layers,
166
+ text_hidden_dim)` and `(batch_size, text_seq_len)` (bool), right-padded across the batch.
167
+ """
168
+ prompt = [prompt] if isinstance(prompt, str) else prompt
169
+ prefix_idx = prompt_template_start_idx
170
+
171
+ # The suffix is tokenized separately so it lands after the (image +) prompt tokens.
172
+ suffix_inputs = tokenizer([prompt_template_suffix], return_tensors="pt").to(device)
173
+ suffix_ids = suffix_inputs["input_ids"]
174
+ suffix_mask = suffix_inputs["attention_mask"].bool()
175
+
176
+ image_prompt = ""
177
+ if images:
178
+ image_prompt = "".join(KREA2_EDIT_IMAGE_PLACEHOLDER.format(i + 1) for i in range(len(images)))
179
+
180
+ features = []
181
+ for p in prompt:
182
+ text = prompt_template_prefix + image_prompt + p
183
+ extra_inputs = {}
184
+ if images:
185
+ # No truncation: the expanded image-pad runs must stay intact.
186
+ inputs = processor(text=[text], images=list(images), return_tensors="pt", do_rescale=False).to(device)
187
+ for k, v in inputs.items():
188
+ if k in ("input_ids", "attention_mask"):
189
+ continue
190
+ if isinstance(v, torch.Tensor) and v.is_floating_point():
191
+ v = v.to(text_encoder.dtype)
192
+ extra_inputs[k] = v
193
+ else:
194
+ inputs = tokenizer(
195
+ [text], truncation=True, max_length=max_sequence_length + prefix_idx, return_tensors="pt"
196
+ ).to(device)
197
+
198
+ input_ids = torch.cat([inputs["input_ids"], suffix_ids], dim=1)
199
+ attention_mask = torch.cat([inputs["attention_mask"].bool(), suffix_mask], dim=1)
200
+
201
+ # mm_token_type_ids (used for M-RoPE) must cover the appended suffix tokens too; they are plain text -> type 0.
202
+ if "mm_token_type_ids" in extra_inputs:
203
+ tt = extra_inputs["mm_token_type_ids"]
204
+ extra_inputs["mm_token_type_ids"] = torch.cat([tt, torch.zeros_like(suffix_ids, dtype=tt.dtype)], dim=1)
205
+
206
+ outputs = text_encoder(
207
+ input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True, **extra_inputs
208
+ )
209
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in text_encoder_select_layers], dim=2)
210
+ # Drop the system-prefix tokens; what remains is (image +) prompt + suffix.
211
+ features.append(hidden_states[0, prefix_idx:])
212
+
213
+ max_len = max(f.shape[0] for f in features)
214
+ prompt_embeds = features[0].new_zeros(len(features), max_len, *features[0].shape[1:])
215
+ prompt_embeds_mask = torch.zeros(len(features), max_len, dtype=torch.bool, device=device)
216
+ for i, f in enumerate(features):
217
+ prompt_embeds[i, : f.shape[0]] = f
218
+ prompt_embeds_mask[i, : f.shape[0]] = True
219
+
220
+ return prompt_embeds, prompt_embeds_mask
221
+
222
+
223
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
224
+ def retrieve_latents(
225
+ encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
226
+ ):
227
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
228
+ return encoder_output.latent_dist.sample(generator)
229
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
230
+ return encoder_output.latent_dist.mode()
231
+ elif hasattr(encoder_output, "latents"):
232
+ return encoder_output.latents
233
+ else:
234
+ raise AttributeError("Could not access latents of provided encoder_output")
235
+
236
+
237
+ # Modified from diffusers.modular_pipelines.qwenimage.encoders.encode_vae_image
238
+ def encode_vae_image(
239
+ image: torch.Tensor,
240
+ vae: AutoencoderKLQwenImage,
241
+ generator: torch.Generator,
242
+ device: torch.device,
243
+ dtype: torch.dtype,
244
+ latent_channels: int = 16,
245
+ sample_mode: str = "argmax",
246
+ ):
247
+ if not isinstance(image, torch.Tensor):
248
+ raise ValueError(f"Expected image to be a tensor, got {type(image)}.")
249
+
250
+ # preprocessed image should be a 4D tensor: batch_size, num_channels, height, width
251
+ if image.dim() == 4:
252
+ image = image.unsqueeze(2)
253
+ elif image.dim() != 5:
254
+ raise ValueError(f"Expected image dims 4 or 5, got {image.dim()}.")
255
+
256
+ image = image.to(device=device, dtype=dtype)
257
+
258
+ if isinstance(generator, list):
259
+ image_latents = [
260
+ retrieve_latents(vae.encode(image[i : i + 1]), generator=generator[i], sample_mode=sample_mode)
261
+ for i in range(image.shape[0])
262
+ ]
263
+ image_latents = torch.cat(image_latents, dim=0)
264
+ else:
265
+ image_latents = retrieve_latents(vae.encode(image), generator=generator, sample_mode=sample_mode)
266
+ latents_mean = (
267
+ torch.tensor(vae.config.latents_mean)
268
+ .view(1, latent_channels, 1, 1, 1)
269
+ .to(image_latents.device, image_latents.dtype)
270
+ )
271
+ latents_std = (
272
+ torch.tensor(vae.config.latents_std)
273
+ .view(1, latent_channels, 1, 1, 1)
274
+ .to(image_latents.device, image_latents.dtype)
275
+ )
276
+ image_latents = (image_latents - latents_mean) / latents_std
277
+
278
+ return image_latents
279
+
280
+
281
+ def encode_reference_latents(
282
+ images: list[torch.Tensor],
283
+ vae: AutoencoderKLQwenImage,
284
+ max_pixels: int,
285
+ generator: torch.Generator | None,
286
+ device: torch.device,
287
+ vae_scale_factor: int,
288
+ patch_size: int,
289
+ latent_channels: int = 16,
290
+ ) -> list[torch.Tensor]:
291
+ """Encode `[0, 1]` CHW reference images to normalized VAE latents, one `(C, h, w)` tensor per image. Each image
292
+ is downscaled (aspect-preserving, never upscaled) to fit within `max_pixels`, then snapped so the latent grid is
293
+ patchifiable. References keep their own aspect ratio, independent of the generated output size."""
294
+ snap = vae_scale_factor * patch_size
295
+ vae_dtype = vae.dtype
296
+
297
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, latent_channels, 1, 1, 1)
298
+ latents_std = torch.tensor(vae.config.latents_std).view(1, latent_channels, 1, 1, 1)
299
+
300
+ ref_latents = []
301
+ for img in images:
302
+ img = img.unsqueeze(0).to(device, dtype=vae_dtype)
303
+ h, w = img.shape[2], img.shape[3]
304
+ if h * w > max_pixels:
305
+ ratio = h / w
306
+ new_h, new_w = math.sqrt(max_pixels * ratio), math.sqrt(max_pixels / ratio)
307
+ else:
308
+ new_h, new_w = float(h), float(w)
309
+ new_h = max(snap, int(round(new_h / snap)) * snap)
310
+ new_w = max(snap, int(round(new_w / snap)) * snap)
311
+ if (new_h, new_w) != (h, w):
312
+ img = F.interpolate(img.float(), size=(new_h, new_w), mode="bilinear").to(vae_dtype)
313
+
314
+ img = (img * 2.0 - 1.0).unsqueeze(2) # [0, 1] -> [-1, 1], add frame dim
315
+ latent = retrieve_latents(vae.encode(img), generator=generator, sample_mode="sample")
316
+ latent = (latent - latents_mean.to(latent.device, latent.dtype)) / latents_std.to(latent.device, latent.dtype)
317
+ ref_latents.append(latent[:, :, 0][0]) # drop frame + batch dims -> (C, h, w)
318
+ return ref_latents
319
+
320
+
321
+ def pack_reference_latents(
322
+ ref_latents: list[torch.Tensor],
323
+ pachifier: Krea2Pachifier,
324
+ device: torch.device,
325
+ dtype: torch.dtype,
326
+ ) -> tuple[torch.Tensor, torch.Tensor]:
327
+ """Patchify reference latents into `(1, ref_seq_len, C * p * p)` tokens and build their `(ref_seq_len, 3)` rotary
328
+ coordinates. The i-th reference sits on frame axis `i + 1` with its own y/x grid starting at 0 (the Kontext-style
329
+ "index" placement that marks each reference as a distinct image rather than more of the canvas)."""
330
+ p = pachifier.config.patch_size
331
+ tokens, position_ids = [], []
332
+ for i, ref in enumerate(ref_latents):
333
+ ref = ref.unsqueeze(0).to(device, dtype)
334
+ tokens.append(pachifier.pack_latents(ref))
335
+ _, _, h, w = ref.shape
336
+ ids = torch.zeros(h // p, w // p, 3, device=device)
337
+ ids[..., 0] = i + 1
338
+ ids[..., 1] = torch.arange(h // p, device=device)[:, None]
339
+ ids[..., 2] = torch.arange(w // p, device=device)[None, :]
340
+ position_ids.append(ids.reshape(-1, 3))
341
+ return torch.cat(tokens, dim=1), torch.cat(position_ids, dim=0)
342
+
343
+
344
+ # ====================
345
+ # 1. TEXT ENCODER
346
+ # ====================
347
+
348
+
349
+ class Krea2TextEncoderStep(ModularPipelineBlocks):
350
+ model_name = "krea2"
351
+
352
+ def __init__(self, text_encoder_select_layers: tuple[int, ...] | None = None):
353
+ """Text encoder step for Krea 2.
354
+
355
+ Args:
356
+ text_encoder_select_layers (`tuple[int, ...]`, *optional*):
357
+ Indices into the text encoder's `hidden_states` tuple (0 is the embedding output) whose states are
358
+ stacked per token as the transformer's text conditioning. Must have
359
+ `transformer.config.num_text_layers` entries. Defaults to the Krea 2 (Qwen3-VL-4B) taps.
360
+ """
361
+ if text_encoder_select_layers is None:
362
+ text_encoder_select_layers = KREA2_TEXT_ENCODER_SELECT_LAYERS
363
+ self.text_encoder_select_layers = tuple(text_encoder_select_layers)
364
+ super().__init__()
365
+
366
+ @property
367
+ def description(self) -> str:
368
+ return "Text Encoder step that generates text embeddings to guide the image generation."
369
+
370
+ @property
371
+ def expected_components(self) -> list[ComponentSpec]:
372
+ return [
373
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The text encoder to use"),
374
+ ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer to use"),
375
+ ComponentSpec(
376
+ "guider",
377
+ ClassifierFreeGuidance,
378
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
379
+ default_creation_method="from_config",
380
+ ),
381
+ ]
382
+
383
+ @property
384
+ def inputs(self) -> list[InputParam]:
385
+ return [
386
+ InputParam.template("prompt"),
387
+ InputParam.template("negative_prompt"),
388
+ InputParam.template("max_sequence_length"),
389
+ ]
390
+
391
+ @property
392
+ def intermediate_outputs(self) -> list[OutputParam]:
393
+ return [
394
+ OutputParam.template("prompt_embeds"),
395
+ OutputParam.template("prompt_embeds_mask"),
396
+ OutputParam.template("negative_prompt_embeds"),
397
+ OutputParam.template("negative_prompt_embeds_mask"),
398
+ ]
399
+
400
+ @staticmethod
401
+ def check_inputs(prompt, negative_prompt, max_sequence_length):
402
+ if not isinstance(prompt, str) and not isinstance(prompt, list):
403
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
404
+
405
+ if (
406
+ negative_prompt is not None
407
+ and not isinstance(negative_prompt, str)
408
+ and not isinstance(negative_prompt, list)
409
+ ):
410
+ raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
411
+
412
+ if max_sequence_length is not None and max_sequence_length <= 0:
413
+ raise ValueError(f"`max_sequence_length` must be a positive integer but is {max_sequence_length}")
414
+
415
+ @torch.no_grad()
416
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
417
+ block_state = self.get_block_state(state)
418
+
419
+ device = components._execution_device
420
+ self.check_inputs(block_state.prompt, block_state.negative_prompt, block_state.max_sequence_length)
421
+
422
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = get_krea2_prompt_embeds(
423
+ components.text_encoder,
424
+ components.tokenizer,
425
+ prompt=block_state.prompt,
426
+ text_encoder_select_layers=self.text_encoder_select_layers,
427
+ max_sequence_length=block_state.max_sequence_length,
428
+ device=device,
429
+ )
430
+
431
+ block_state.negative_prompt_embeds = None
432
+ block_state.negative_prompt_embeds_mask = None
433
+ if components.requires_unconditional_embeds:
434
+ negative_prompt = block_state.negative_prompt or ""
435
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = get_krea2_prompt_embeds(
436
+ components.text_encoder,
437
+ components.tokenizer,
438
+ prompt=negative_prompt,
439
+ text_encoder_select_layers=self.text_encoder_select_layers,
440
+ max_sequence_length=block_state.max_sequence_length,
441
+ device=device,
442
+ )
443
+
444
+ self.set_block_state(state, block_state)
445
+ return components, state
446
+
447
+
448
+ class Krea2EditTextEncoderStep(ModularPipelineBlocks):
449
+ model_name = "krea2"
450
+
451
+ def __init__(self, text_encoder_select_layers: tuple[int, ...] | None = None):
452
+ """Text encoder step for the Krea 2 edit task: encodes the prompt while embedding a coarse view of the
453
+ reference image(s) into the conditioning through the Qwen3-VL vision tower.
454
+
455
+ Args:
456
+ text_encoder_select_layers (`tuple[int, ...]`, *optional*):
457
+ Indices into the text encoder's `hidden_states` tuple whose states are stacked per token as the
458
+ transformer's text conditioning. Defaults to the Krea 2 (Qwen3-VL-4B) taps.
459
+ """
460
+ if text_encoder_select_layers is None:
461
+ text_encoder_select_layers = KREA2_TEXT_ENCODER_SELECT_LAYERS
462
+ self.text_encoder_select_layers = tuple(text_encoder_select_layers)
463
+ super().__init__()
464
+
465
+ @property
466
+ def description(self) -> str:
467
+ return (
468
+ "Text encoder step for the edit task. Embeds reference image(s) into the text conditioning via the "
469
+ "Qwen3-VL vision tower, matching how the Ostris AI-Toolkit edit LoRAs are trained."
470
+ )
471
+
472
+ @property
473
+ def expected_components(self) -> list[ComponentSpec]:
474
+ return [
475
+ ComponentSpec("text_encoder", Qwen3VLModel, description="The text encoder to use"),
476
+ ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer to use"),
477
+ ComponentSpec("processor", Qwen3VLProcessor, description="The Qwen3-VL processor for reference images"),
478
+ ComponentSpec(
479
+ "guider",
480
+ ClassifierFreeGuidance,
481
+ config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
482
+ default_creation_method="from_config",
483
+ ),
484
+ ]
485
+
486
+ @property
487
+ def inputs(self) -> list[InputParam]:
488
+ return [
489
+ InputParam.template("prompt"),
490
+ InputParam.template("negative_prompt"),
491
+ InputParam.template("image", required=True, note="The reference image(s) for the edit."),
492
+ InputParam.template("max_sequence_length"),
493
+ InputParam(
494
+ "vl_image_max_pixels",
495
+ type_hint=int,
496
+ default=384 * 384,
497
+ description="Pixel budget for the coarse Qwen3-VL view of each reference image.",
498
+ ),
499
+ ]
500
+
501
+ @property
502
+ def intermediate_outputs(self) -> list[OutputParam]:
503
+ return [
504
+ OutputParam.template("prompt_embeds"),
505
+ OutputParam.template("prompt_embeds_mask"),
506
+ OutputParam.template("negative_prompt_embeds"),
507
+ OutputParam.template("negative_prompt_embeds_mask"),
508
+ ]
509
+
510
+ @staticmethod
511
+ def check_inputs(prompt, negative_prompt, max_sequence_length):
512
+ if not isinstance(prompt, str) and not isinstance(prompt, list):
513
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
514
+ if (
515
+ negative_prompt is not None
516
+ and not isinstance(negative_prompt, str)
517
+ and not isinstance(negative_prompt, list)
518
+ ):
519
+ raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
520
+ if max_sequence_length is not None and max_sequence_length <= 0:
521
+ raise ValueError(f"`max_sequence_length` must be a positive integer but is {max_sequence_length}")
522
+
523
+ @torch.no_grad()
524
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
525
+ block_state = self.get_block_state(state)
526
+
527
+ device = components._execution_device
528
+ self.check_inputs(block_state.prompt, block_state.negative_prompt, block_state.max_sequence_length)
529
+
530
+ image_list = block_state.image if isinstance(block_state.image, (list, tuple)) else [block_state.image]
531
+ ref_images = [to_chw_tensor(img).to(device) for img in image_list]
532
+ vl_images = prep_vl_images(ref_images, block_state.vl_image_max_pixels)
533
+
534
+ block_state.prompt_embeds, block_state.prompt_embeds_mask = get_krea2_edit_prompt_embeds(
535
+ components.text_encoder,
536
+ components.tokenizer,
537
+ components.processor,
538
+ prompt=block_state.prompt,
539
+ images=vl_images,
540
+ text_encoder_select_layers=self.text_encoder_select_layers,
541
+ max_sequence_length=block_state.max_sequence_length,
542
+ device=device,
543
+ )
544
+
545
+ block_state.negative_prompt_embeds = None
546
+ block_state.negative_prompt_embeds_mask = None
547
+ if components.requires_unconditional_embeds:
548
+ negative_prompt = block_state.negative_prompt or ""
549
+ block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = get_krea2_edit_prompt_embeds(
550
+ components.text_encoder,
551
+ components.tokenizer,
552
+ components.processor,
553
+ prompt=negative_prompt,
554
+ images=vl_images,
555
+ text_encoder_select_layers=self.text_encoder_select_layers,
556
+ max_sequence_length=block_state.max_sequence_length,
557
+ device=device,
558
+ )
559
+
560
+ self.set_block_state(state, block_state)
561
+ return components, state
562
+
563
+
564
+ # ====================
565
+ # 2. IMAGE PREPROCESS
566
+ # ====================
567
+
568
+
569
+ class Krea2InpaintProcessImagesInputStep(ModularPipelineBlocks):
570
+ model_name = "krea2"
571
+
572
+ @property
573
+ def description(self) -> str:
574
+ return "Image Preprocess step for inpainting task. This processes the image and mask inputs together. Images will be resized to the given height and width."
575
+
576
+ @property
577
+ def expected_components(self) -> list[ComponentSpec]:
578
+ return [
579
+ ComponentSpec(
580
+ "image_mask_processor",
581
+ InpaintProcessor,
582
+ config=FrozenDict({"vae_scale_factor": 16}),
583
+ default_creation_method="from_config",
584
+ ),
585
+ ]
586
+
587
+ @property
588
+ def inputs(self) -> list[InputParam]:
589
+ return [
590
+ InputParam.template("mask_image"),
591
+ InputParam.template("image"),
592
+ InputParam.template("height"),
593
+ InputParam.template("width"),
594
+ InputParam.template("padding_mask_crop"),
595
+ ]
596
+
597
+ @property
598
+ def intermediate_outputs(self) -> list[OutputParam]:
599
+ return [
600
+ OutputParam(
601
+ name="processed_image",
602
+ type_hint=torch.Tensor,
603
+ description="The processed image",
604
+ ),
605
+ OutputParam(
606
+ name="processed_mask_image",
607
+ type_hint=torch.Tensor,
608
+ description="The processed mask image",
609
+ ),
610
+ OutputParam(
611
+ name="mask_overlay_kwargs",
612
+ type_hint=dict,
613
+ description="The kwargs for the postprocess step to apply the mask overlay",
614
+ ),
615
+ ]
616
+
617
+ @staticmethod
618
+ def check_inputs(height, width, vae_scale_factor):
619
+ if height is not None and height % (vae_scale_factor * 2) != 0:
620
+ raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")
621
+
622
+ if width is not None and width % (vae_scale_factor * 2) != 0:
623
+ raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")
624
+
625
+ @torch.no_grad()
626
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
627
+ block_state = self.get_block_state(state)
628
+
629
+ self.check_inputs(
630
+ height=block_state.height, width=block_state.width, vae_scale_factor=components.vae_scale_factor
631
+ )
632
+ height = block_state.height or components.default_height
633
+ width = block_state.width or components.default_width
634
+
635
+ block_state.processed_image, block_state.processed_mask_image, block_state.mask_overlay_kwargs = (
636
+ components.image_mask_processor.preprocess(
637
+ image=block_state.image,
638
+ mask=block_state.mask_image,
639
+ height=height,
640
+ width=width,
641
+ padding_mask_crop=block_state.padding_mask_crop,
642
+ )
643
+ )
644
+
645
+ self.set_block_state(state, block_state)
646
+ return components, state
647
+
648
+
649
+ class Krea2ProcessImagesInputStep(ModularPipelineBlocks):
650
+ model_name = "krea2"
651
+
652
+ @property
653
+ def description(self) -> str:
654
+ return "Image Preprocess step. will resize the image to the given height and width."
655
+
656
+ @property
657
+ def expected_components(self) -> list[ComponentSpec]:
658
+ return [
659
+ ComponentSpec(
660
+ "image_processor",
661
+ VaeImageProcessor,
662
+ config=FrozenDict({"vae_scale_factor": 16}),
663
+ default_creation_method="from_config",
664
+ ),
665
+ ]
666
+
667
+ @property
668
+ def inputs(self) -> list[InputParam]:
669
+ return [
670
+ InputParam.template("image"),
671
+ InputParam.template("height"),
672
+ InputParam.template("width"),
673
+ ]
674
+
675
+ @property
676
+ def intermediate_outputs(self) -> list[OutputParam]:
677
+ return [
678
+ OutputParam(
679
+ name="processed_image",
680
+ type_hint=torch.Tensor,
681
+ description="The processed image",
682
+ )
683
+ ]
684
+
685
+ @staticmethod
686
+ def check_inputs(height, width, vae_scale_factor):
687
+ if height is not None and height % (vae_scale_factor * 2) != 0:
688
+ raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")
689
+
690
+ if width is not None and width % (vae_scale_factor * 2) != 0:
691
+ raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")
692
+
693
+ @torch.no_grad()
694
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState):
695
+ block_state = self.get_block_state(state)
696
+
697
+ self.check_inputs(
698
+ height=block_state.height, width=block_state.width, vae_scale_factor=components.vae_scale_factor
699
+ )
700
+ height = block_state.height or components.default_height
701
+ width = block_state.width or components.default_width
702
+
703
+ block_state.processed_image = components.image_processor.preprocess(
704
+ image=block_state.image,
705
+ height=height,
706
+ width=width,
707
+ )
708
+
709
+ self.set_block_state(state, block_state)
710
+ return components, state
711
+
712
+
713
+ # ====================
714
+ # 3. VAE ENCODER
715
+ # ====================
716
+
717
+
718
+ class Krea2VaeEncoderStep(ModularPipelineBlocks):
719
+ model_name = "krea2"
720
+
721
+ @property
722
+ def description(self) -> str:
723
+ return "VAE Encoder step that converts processed_image into latent representations image_latents."
724
+
725
+ @property
726
+ def expected_components(self) -> list[ComponentSpec]:
727
+ return [ComponentSpec("vae", AutoencoderKLQwenImage)]
728
+
729
+ @property
730
+ def inputs(self) -> list[InputParam]:
731
+ return [
732
+ InputParam(
733
+ name="processed_image", required=True, type_hint=torch.Tensor, description="The image tensor to encode"
734
+ ),
735
+ InputParam.template("generator"),
736
+ ]
737
+
738
+ @property
739
+ def intermediate_outputs(self) -> list[OutputParam]:
740
+ return [OutputParam.template("image_latents")]
741
+
742
+ @torch.no_grad()
743
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
744
+ block_state = self.get_block_state(state)
745
+
746
+ device = components._execution_device
747
+ dtype = components.vae.dtype
748
+
749
+ block_state.image_latents = encode_vae_image(
750
+ image=block_state.processed_image,
751
+ vae=components.vae,
752
+ generator=block_state.generator,
753
+ device=device,
754
+ dtype=dtype,
755
+ latent_channels=components.num_channels_latents,
756
+ )
757
+
758
+ self.set_block_state(state, block_state)
759
+
760
+ return components, state
761
+
762
+
763
+ class Krea2EditReferenceLatentsStep(ModularPipelineBlocks):
764
+ model_name = "krea2"
765
+
766
+ @property
767
+ def description(self) -> str:
768
+ return (
769
+ "Reference (edit) VAE encoder step. Encodes reference image(s) to clean, normalized VAE latents and packs "
770
+ "them into transformer tokens with their frame-axis rotary coordinates. These tokens are appended to the "
771
+ "sequence at flow time t=0 to condition the generation on the references."
772
+ )
773
+
774
+ @property
775
+ def expected_components(self) -> list[ComponentSpec]:
776
+ return [
777
+ ComponentSpec("vae", AutoencoderKLQwenImage),
778
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
779
+ ]
780
+
781
+ @property
782
+ def inputs(self) -> list[InputParam]:
783
+ return [
784
+ InputParam.template("image", required=True, note="The reference image(s) for the edit."),
785
+ InputParam.template("generator"),
786
+ InputParam(
787
+ "reference_max_pixels",
788
+ type_hint=int,
789
+ default=1024 * 1024,
790
+ description="Pixel budget each reference image is downscaled to fit before VAE encoding.",
791
+ ),
792
+ ]
793
+
794
+ @property
795
+ def intermediate_outputs(self) -> list[OutputParam]:
796
+ return [
797
+ OutputParam(
798
+ name="reference_latents",
799
+ type_hint=torch.Tensor,
800
+ description="Packed clean reference tokens of shape (1, ref_seq_len, C * p * p), appended to the "
801
+ "denoiser sequence at t=0.",
802
+ ),
803
+ OutputParam(
804
+ name="reference_position_ids",
805
+ type_hint=torch.Tensor,
806
+ description="Rotary coordinates (ref_seq_len, 3) for the reference tokens; the i-th reference sits on "
807
+ "frame axis i + 1.",
808
+ ),
809
+ OutputParam(
810
+ name="ref_seq_len",
811
+ kwargs_type="denoiser_input_fields",
812
+ type_hint=int,
813
+ description="Number of reference tokens appended to the denoiser sequence.",
814
+ ),
815
+ ]
816
+
817
+ @torch.no_grad()
818
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
819
+ block_state = self.get_block_state(state)
820
+
821
+ device = components._execution_device
822
+ patch_size = components.pachifier.config.patch_size
823
+
824
+ image_list = block_state.image if isinstance(block_state.image, (list, tuple)) else [block_state.image]
825
+ ref_images = [to_chw_tensor(img) for img in image_list]
826
+
827
+ ref_latents = encode_reference_latents(
828
+ images=ref_images,
829
+ vae=components.vae,
830
+ max_pixels=block_state.reference_max_pixels,
831
+ generator=block_state.generator,
832
+ device=device,
833
+ vae_scale_factor=components.vae_scale_factor,
834
+ patch_size=patch_size,
835
+ latent_channels=components.num_channels_latents,
836
+ )
837
+ block_state.reference_latents, block_state.reference_position_ids = pack_reference_latents(
838
+ ref_latents, components.pachifier, device, components.vae.dtype
839
+ )
840
+ block_state.ref_seq_len = block_state.reference_latents.shape[1]
841
+
842
+ self.set_block_state(state, block_state)
843
+
844
+ return components, state
inputs.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState
19
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
20
+ from .modular_pipeline import Krea2ModularPipeline, Krea2Pachifier
21
+
22
+
23
+ # Copied from diffusers.modular_pipelines.qwenimage.inputs.repeat_tensor_to_batch_size
24
+ def repeat_tensor_to_batch_size(
25
+ input_name: str,
26
+ input_tensor: torch.Tensor,
27
+ batch_size: int,
28
+ num_images_per_prompt: int = 1,
29
+ ) -> torch.Tensor:
30
+ """Repeat tensor elements to match the final batch size.
31
+
32
+ This function expands a tensor's batch dimension to match the final batch size (batch_size * num_images_per_prompt)
33
+ by repeating each element along dimension 0.
34
+
35
+ The input tensor must have batch size 1 or batch_size. The function will:
36
+ - If batch size is 1: repeat each element (batch_size * num_images_per_prompt) times
37
+ - If batch size equals batch_size: repeat each element num_images_per_prompt times
38
+
39
+ Args:
40
+ input_name (str): Name of the input tensor (used for error messages)
41
+ input_tensor (torch.Tensor): The tensor to repeat. Must have batch size 1 or batch_size.
42
+ batch_size (int): The base batch size (number of prompts)
43
+ num_images_per_prompt (int, optional): Number of images to generate per prompt. Defaults to 1.
44
+
45
+ Returns:
46
+ torch.Tensor: The repeated tensor with final batch size (batch_size * num_images_per_prompt)
47
+
48
+ Raises:
49
+ ValueError: If input_tensor is not a torch.Tensor or has invalid batch size
50
+
51
+ Examples:
52
+ tensor = torch.tensor([[1, 2, 3]]) # shape: [1, 3] repeated = repeat_tensor_to_batch_size("image", tensor,
53
+ batch_size=2, num_images_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) - shape:
54
+ [4, 3]
55
+
56
+ tensor = torch.tensor([[1, 2, 3], [4, 5, 6]]) # shape: [2, 3] repeated = repeat_tensor_to_batch_size("image",
57
+ tensor, batch_size=2, num_images_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [4, 5, 6], [4, 5, 6]])
58
+ - shape: [4, 3]
59
+ """
60
+ # make sure input is a tensor
61
+ if not isinstance(input_tensor, torch.Tensor):
62
+ raise ValueError(f"`{input_name}` must be a tensor")
63
+
64
+ # make sure input tensor e.g. image_latents has batch size 1 or batch_size same as prompts
65
+ if input_tensor.shape[0] == 1:
66
+ repeat_by = batch_size * num_images_per_prompt
67
+ elif input_tensor.shape[0] == batch_size:
68
+ repeat_by = num_images_per_prompt
69
+ else:
70
+ raise ValueError(
71
+ f"`{input_name}` must have have batch size 1 or {batch_size}, but got {input_tensor.shape[0]}"
72
+ )
73
+
74
+ # expand the tensor to match the batch_size * num_images_per_prompt
75
+ input_tensor = input_tensor.repeat_interleave(repeat_by, dim=0)
76
+
77
+ return input_tensor
78
+
79
+
80
+ # Copied from diffusers.modular_pipelines.qwenimage.inputs.calculate_dimension_from_latents
81
+ def calculate_dimension_from_latents(latents: torch.Tensor, vae_scale_factor: int) -> tuple[int, int]:
82
+ """Calculate image dimensions from latent tensor dimensions.
83
+
84
+ This function converts latent space dimensions to image space dimensions by multiplying the latent height and width
85
+ by the VAE scale factor.
86
+
87
+ Args:
88
+ latents (torch.Tensor): The latent tensor. Must have 4 or 5 dimensions.
89
+ Expected shapes: [batch, channels, height, width] or [batch, channels, frames, height, width]
90
+ vae_scale_factor (int): The scale factor used by the VAE to compress images.
91
+ Typically 8 for most VAEs (image is 8x larger than latents in each dimension)
92
+
93
+ Returns:
94
+ tuple[int, int]: The calculated image dimensions as (height, width)
95
+
96
+ Raises:
97
+ ValueError: If latents tensor doesn't have 4 or 5 dimensions
98
+
99
+ """
100
+ # make sure the latents are not packed
101
+ if latents.ndim != 4 and latents.ndim != 5:
102
+ raise ValueError(f"unpacked latents must have 4 or 5 dimensions, but got {latents.ndim}")
103
+
104
+ latent_height, latent_width = latents.shape[-2:]
105
+
106
+ height = latent_height * vae_scale_factor
107
+ width = latent_width * vae_scale_factor
108
+
109
+ return height, width
110
+
111
+
112
+ class Krea2TextInputsStep(ModularPipelineBlocks):
113
+ model_name = "krea2"
114
+
115
+ @property
116
+ def description(self) -> str:
117
+ summary_section = (
118
+ "Text input processing step that standardizes text embeddings for the pipeline.\n"
119
+ "This step:\n"
120
+ " 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
121
+ " 2. Ensures all text embeddings have consistent batch sizes (batch_size * num_images_per_prompt)"
122
+ )
123
+
124
+ # Placement guidance
125
+ placement_section = "\n\nThis block should be placed after all encoder steps to process the text embeddings before they are used in subsequent pipeline steps."
126
+
127
+ return summary_section + placement_section
128
+
129
+ @property
130
+ def inputs(self) -> list[InputParam]:
131
+ return [
132
+ InputParam.template("num_images_per_prompt"),
133
+ InputParam.template("prompt_embeds"),
134
+ InputParam.template("prompt_embeds_mask"),
135
+ InputParam.template("negative_prompt_embeds"),
136
+ InputParam.template("negative_prompt_embeds_mask"),
137
+ ]
138
+
139
+ @property
140
+ def intermediate_outputs(self) -> list[OutputParam]:
141
+ return [
142
+ OutputParam(name="batch_size", type_hint=int, description="The batch size of the prompt embeddings"),
143
+ OutputParam(name="dtype", type_hint=torch.dtype, description="The data type of the prompt embeddings"),
144
+ OutputParam.template("prompt_embeds", note="batch-expanded"),
145
+ OutputParam.template("prompt_embeds_mask", note="batch-expanded"),
146
+ OutputParam.template("negative_prompt_embeds", note="batch-expanded"),
147
+ OutputParam.template("negative_prompt_embeds_mask", note="batch-expanded"),
148
+ ]
149
+
150
+ @staticmethod
151
+ def check_inputs(
152
+ prompt_embeds,
153
+ prompt_embeds_mask,
154
+ negative_prompt_embeds,
155
+ negative_prompt_embeds_mask,
156
+ ):
157
+ if prompt_embeds.ndim != 4:
158
+ raise ValueError(
159
+ f"`prompt_embeds` must have 4 dimensions (batch_size, text_seq_len, num_text_layers, text_hidden_dim), but got {prompt_embeds.ndim}"
160
+ )
161
+
162
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
163
+ raise ValueError("`negative_prompt_embeds_mask` is required when `negative_prompt_embeds` is not None")
164
+
165
+ if negative_prompt_embeds is None and negative_prompt_embeds_mask is not None:
166
+ raise ValueError("cannot pass `negative_prompt_embeds_mask` without `negative_prompt_embeds`")
167
+
168
+ if prompt_embeds_mask.shape[0] != prompt_embeds.shape[0]:
169
+ raise ValueError("`prompt_embeds_mask` must have the same batch size as `prompt_embeds`")
170
+
171
+ elif negative_prompt_embeds is not None and negative_prompt_embeds.shape[0] != prompt_embeds.shape[0]:
172
+ raise ValueError("`negative_prompt_embeds` must have the same batch size as `prompt_embeds`")
173
+
174
+ elif (
175
+ negative_prompt_embeds_mask is not None and negative_prompt_embeds_mask.shape[0] != prompt_embeds.shape[0]
176
+ ):
177
+ raise ValueError("`negative_prompt_embeds_mask` must have the same batch size as `prompt_embeds`")
178
+
179
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
180
+ block_state = self.get_block_state(state)
181
+
182
+ self.check_inputs(
183
+ prompt_embeds=block_state.prompt_embeds,
184
+ prompt_embeds_mask=block_state.prompt_embeds_mask,
185
+ negative_prompt_embeds=block_state.negative_prompt_embeds,
186
+ negative_prompt_embeds_mask=block_state.negative_prompt_embeds_mask,
187
+ )
188
+
189
+ block_state.batch_size = block_state.prompt_embeds.shape[0]
190
+ block_state.dtype = block_state.prompt_embeds.dtype
191
+
192
+ # Krea 2 prompt embeddings are 4D: (batch_size, text_seq_len, num_text_layers, text_hidden_dim)
193
+ _, seq_len, num_text_layers, dim = block_state.prompt_embeds.shape
194
+
195
+ block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_images_per_prompt, 1, 1)
196
+ block_state.prompt_embeds = block_state.prompt_embeds.view(
197
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len, num_text_layers, dim
198
+ )
199
+
200
+ block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.repeat(1, block_state.num_images_per_prompt)
201
+ block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.view(
202
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len
203
+ )
204
+
205
+ if block_state.negative_prompt_embeds is not None:
206
+ _, seq_len, num_text_layers, dim = block_state.negative_prompt_embeds.shape
207
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(
208
+ 1, block_state.num_images_per_prompt, 1, 1
209
+ )
210
+ block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
211
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len, num_text_layers, dim
212
+ )
213
+
214
+ block_state.negative_prompt_embeds_mask = block_state.negative_prompt_embeds_mask.repeat(
215
+ 1, block_state.num_images_per_prompt
216
+ )
217
+ block_state.negative_prompt_embeds_mask = block_state.negative_prompt_embeds_mask.view(
218
+ block_state.batch_size * block_state.num_images_per_prompt, seq_len
219
+ )
220
+
221
+ self.set_block_state(state, block_state)
222
+
223
+ return components, state
224
+
225
+
226
+ class Krea2AdditionalInputsStep(ModularPipelineBlocks):
227
+ model_name = "krea2"
228
+
229
+ def __init__(
230
+ self,
231
+ image_latent_inputs: list[InputParam] | None = None,
232
+ additional_batch_inputs: list[InputParam] | None = None,
233
+ ):
234
+ """Input processing step for additional (non-text) inputs.
235
+
236
+ For image latent inputs: updates height/width if None, patchifies, and expands batch size. For additional batch
237
+ inputs: expands batch dimensions to match the final batch size.
238
+
239
+ Args:
240
+ image_latent_inputs (list[InputParam], optional): Image latent inputs to process. Defaults to
241
+ `[InputParam.template("image_latents")]`.
242
+ additional_batch_inputs (list[InputParam], optional): Additional tensor inputs that only need batch
243
+ expansion. Defaults to `[]`.
244
+ """
245
+ # by default, process `image_latents`
246
+ if image_latent_inputs is None:
247
+ image_latent_inputs = [InputParam.template("image_latents")]
248
+ if additional_batch_inputs is None:
249
+ additional_batch_inputs = []
250
+
251
+ if not isinstance(image_latent_inputs, list):
252
+ raise ValueError(f"image_latent_inputs must be a list, but got {type(image_latent_inputs)}")
253
+ else:
254
+ for input_param in image_latent_inputs:
255
+ if not isinstance(input_param, InputParam):
256
+ raise ValueError(f"image_latent_inputs must be a list of InputParam, but got {type(input_param)}")
257
+
258
+ if not isinstance(additional_batch_inputs, list):
259
+ raise ValueError(f"additional_batch_inputs must be a list, but got {type(additional_batch_inputs)}")
260
+ else:
261
+ for input_param in additional_batch_inputs:
262
+ if not isinstance(input_param, InputParam):
263
+ raise ValueError(
264
+ f"additional_batch_inputs must be a list of InputParam, but got {type(input_param)}"
265
+ )
266
+
267
+ self._image_latent_inputs = image_latent_inputs
268
+ self._additional_batch_inputs = additional_batch_inputs
269
+ super().__init__()
270
+
271
+ @property
272
+ def description(self) -> str:
273
+ summary_section = (
274
+ "Input processing step that:\n"
275
+ " 1. For image latent inputs: Updates height/width if None, patchifies, and expands batch size\n"
276
+ " 2. For additional batch inputs: Expands batch dimensions to match final batch size"
277
+ )
278
+
279
+ inputs_info = ""
280
+ if self._image_latent_inputs or self._additional_batch_inputs:
281
+ inputs_info = "\n\nConfigured inputs:"
282
+ if self._image_latent_inputs:
283
+ inputs_info += f"\n - Image latent inputs: {[p.name for p in self._image_latent_inputs]}"
284
+ if self._additional_batch_inputs:
285
+ inputs_info += f"\n - Additional batch inputs: {[p.name for p in self._additional_batch_inputs]}"
286
+
287
+ placement_section = "\n\nThis block should be placed after the encoder steps and the text input step."
288
+
289
+ return summary_section + inputs_info + placement_section
290
+
291
+ @property
292
+ def expected_components(self) -> list[ComponentSpec]:
293
+ return [
294
+ ComponentSpec("pachifier", Krea2Pachifier, default_creation_method="from_config"),
295
+ ]
296
+
297
+ @property
298
+ def inputs(self) -> list[InputParam]:
299
+ inputs = [
300
+ InputParam.template("num_images_per_prompt"),
301
+ InputParam.template("batch_size"),
302
+ InputParam.template("height"),
303
+ InputParam.template("width"),
304
+ ]
305
+ # default is `image_latents`
306
+ inputs += self._image_latent_inputs + self._additional_batch_inputs
307
+
308
+ return inputs
309
+
310
+ @property
311
+ def intermediate_outputs(self) -> list[OutputParam]:
312
+ outputs = [
313
+ OutputParam(
314
+ name="image_height",
315
+ type_hint=int,
316
+ description="The image height calculated from the image latents dimension",
317
+ ),
318
+ OutputParam(
319
+ name="image_width",
320
+ type_hint=int,
321
+ description="The image width calculated from the image latents dimension",
322
+ ),
323
+ ]
324
+
325
+ # `height`/`width` are not new outputs, but they will be updated if any image latent inputs are provided
326
+ if len(self._image_latent_inputs) > 0:
327
+ outputs.append(
328
+ OutputParam(name="height", type_hint=int, description="if not provided, updated to image height")
329
+ )
330
+ outputs.append(
331
+ OutputParam(name="width", type_hint=int, description="if not provided, updated to image width")
332
+ )
333
+
334
+ # image latent inputs are modified in place (patchified and batch-expanded)
335
+ for input_param in self._image_latent_inputs:
336
+ outputs.append(
337
+ OutputParam(
338
+ name=input_param.name,
339
+ type_hint=input_param.type_hint,
340
+ description=input_param.description + " (patchified and batch-expanded)",
341
+ )
342
+ )
343
+
344
+ # additional batch inputs (batch-expanded only)
345
+ for input_param in self._additional_batch_inputs:
346
+ outputs.append(
347
+ OutputParam(
348
+ name=input_param.name,
349
+ type_hint=input_param.type_hint,
350
+ description=input_param.description + " (batch-expanded)",
351
+ )
352
+ )
353
+
354
+ return outputs
355
+
356
+ def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
357
+ block_state = self.get_block_state(state)
358
+
359
+ # Process image latent inputs
360
+ for input_param in self._image_latent_inputs:
361
+ image_latent_input_name = input_param.name
362
+ image_latent_tensor = getattr(block_state, image_latent_input_name)
363
+ if image_latent_tensor is None:
364
+ continue
365
+
366
+ # 1. Calculate height/width from latents and update if not provided
367
+ height, width = calculate_dimension_from_latents(image_latent_tensor, components.vae_scale_factor)
368
+ block_state.height = block_state.height or height
369
+ block_state.width = block_state.width or width
370
+
371
+ if not hasattr(block_state, "image_height"):
372
+ block_state.image_height = height
373
+ if not hasattr(block_state, "image_width"):
374
+ block_state.image_width = width
375
+
376
+ # 2. Patchify
377
+ image_latent_tensor = components.pachifier.pack_latents(image_latent_tensor)
378
+
379
+ # 3. Expand batch size
380
+ image_latent_tensor = repeat_tensor_to_batch_size(
381
+ input_name=image_latent_input_name,
382
+ input_tensor=image_latent_tensor,
383
+ num_images_per_prompt=block_state.num_images_per_prompt,
384
+ batch_size=block_state.batch_size,
385
+ )
386
+
387
+ setattr(block_state, image_latent_input_name, image_latent_tensor)
388
+
389
+ # Process additional batch inputs (only batch expansion)
390
+ for input_param in self._additional_batch_inputs:
391
+ input_name = input_param.name
392
+ input_tensor = getattr(block_state, input_name)
393
+ if input_tensor is None:
394
+ continue
395
+
396
+ input_tensor = repeat_tensor_to_batch_size(
397
+ input_name=input_name,
398
+ input_tensor=input_tensor,
399
+ num_images_per_prompt=block_state.num_images_per_prompt,
400
+ batch_size=block_state.batch_size,
401
+ )
402
+
403
+ setattr(block_state, input_name, input_tensor)
404
+
405
+ self.set_block_state(state, block_state)
406
+ return components, state
modular_blocks_krea2.py ADDED
@@ -0,0 +1,879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import torch
16
+
17
+ from diffusers.utils import logging
18
+ from diffusers.modular_pipelines.modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
19
+ from diffusers.modular_pipelines.modular_pipeline_utils import InputParam, InsertableDict, OutputParam
20
+ from .before_denoise import (
21
+ Krea2CreateMaskLatentsStep,
22
+ Krea2PrepareLatentsStep,
23
+ Krea2PrepareLatentsWithStrengthStep,
24
+ Krea2RoPEInputsStep,
25
+ Krea2SetTimestepsStep,
26
+ Krea2SetTimestepsWithStrengthStep,
27
+ )
28
+ from .decoders import (
29
+ Krea2AfterDenoiseStep,
30
+ Krea2DecoderStep,
31
+ Krea2InpaintProcessImagesOutputStep,
32
+ Krea2ProcessImagesOutputStep,
33
+ )
34
+ from .denoise import (
35
+ Krea2DenoiseStep,
36
+ Krea2InpaintDenoiseStep,
37
+ )
38
+ from .encoders import (
39
+ Krea2InpaintProcessImagesInputStep,
40
+ Krea2ProcessImagesInputStep,
41
+ Krea2TextEncoderStep,
42
+ Krea2VaeEncoderStep,
43
+ )
44
+ from .inputs import (
45
+ Krea2AdditionalInputsStep,
46
+ Krea2TextInputsStep,
47
+ )
48
+
49
+
50
+ logger = logging.get_logger(__name__)
51
+
52
+
53
+ # ====================
54
+ # 1. TEXT ENCODER
55
+ # ====================
56
+
57
+
58
+ # auto_docstring
59
+ class Krea2AutoTextEncoderStep(AutoPipelineBlocks):
60
+ """
61
+ Text encoder step that encodes the text prompt into a text embedding. This is an auto pipeline block.
62
+ - `Krea2TextEncoderStep` (text_encoder) is used when `prompt` is provided.
63
+ - if `prompt` is not provided, step will be skipped.
64
+
65
+ Components:
66
+ text_encoder (`Qwen3VLModel`): The text encoder to use tokenizer (`Qwen2Tokenizer`): The tokenizer to use
67
+ guider (`ClassifierFreeGuidance`)
68
+
69
+ Inputs:
70
+ prompt (`str`, *optional*):
71
+ The prompt or prompts to guide image generation.
72
+ negative_prompt (`str`, *optional*):
73
+ The prompt or prompts not to guide the image generation.
74
+ max_sequence_length (`int`, *optional*, defaults to 512):
75
+ Maximum sequence length for prompt encoding.
76
+
77
+ Outputs:
78
+ prompt_embeds (`Tensor`):
79
+ The prompt embeddings.
80
+ prompt_embeds_mask (`Tensor`):
81
+ The encoder attention mask.
82
+ negative_prompt_embeds (`Tensor`):
83
+ The negative prompt embeddings.
84
+ negative_prompt_embeds_mask (`Tensor`):
85
+ The negative prompt embeddings mask.
86
+ """
87
+
88
+ model_name = "krea2"
89
+ block_classes = [Krea2TextEncoderStep()]
90
+ block_names = ["text_encoder"]
91
+ block_trigger_inputs = ["prompt"]
92
+
93
+ @property
94
+ def description(self) -> str:
95
+ return (
96
+ "Text encoder step that encodes the text prompt into a text embedding. This is an auto pipeline block.\n"
97
+ " - `Krea2TextEncoderStep` (text_encoder) is used when `prompt` is provided.\n"
98
+ " - if `prompt` is not provided, step will be skipped."
99
+ )
100
+
101
+
102
+ # ====================
103
+ # 2. VAE ENCODER
104
+ # ====================
105
+
106
+
107
+ # auto_docstring
108
+ class Krea2InpaintVaeEncoderStep(SequentialPipelineBlocks):
109
+ """
110
+ This step is used for processing image and mask inputs for inpainting tasks. It:
111
+ - Resizes the image to the target size, based on `height` and `width`.
112
+ - Processes and updates `image` and `mask_image`.
113
+ - Creates `image_latents`.
114
+
115
+ Components:
116
+ image_mask_processor (`InpaintProcessor`) vae (`AutoencoderKLQwenImage`)
117
+
118
+ Inputs:
119
+ mask_image (`Image`):
120
+ Mask image for inpainting.
121
+ image (`Image | list`):
122
+ Reference image(s) for denoising. Can be a single image or list of images.
123
+ height (`int`, *optional*):
124
+ The height in pixels of the generated image.
125
+ width (`int`, *optional*):
126
+ The width in pixels of the generated image.
127
+ padding_mask_crop (`int`, *optional*):
128
+ Padding for mask cropping in inpainting.
129
+ generator (`Generator`, *optional*):
130
+ Torch generator for deterministic generation.
131
+
132
+ Outputs:
133
+ processed_image (`Tensor`):
134
+ The processed image
135
+ processed_mask_image (`Tensor`):
136
+ The processed mask image
137
+ mask_overlay_kwargs (`dict`):
138
+ The kwargs for the postprocess step to apply the mask overlay
139
+ image_latents (`Tensor`):
140
+ The latent representation of the input image.
141
+ """
142
+
143
+ model_name = "krea2"
144
+ block_classes = [Krea2InpaintProcessImagesInputStep(), Krea2VaeEncoderStep()]
145
+ block_names = ["preprocess", "encode"]
146
+
147
+ @property
148
+ def description(self) -> str:
149
+ return (
150
+ "This step is used for processing image and mask inputs for inpainting tasks. It:\n"
151
+ " - Resizes the image to the target size, based on `height` and `width`.\n"
152
+ " - Processes and updates `image` and `mask_image`.\n"
153
+ " - Creates `image_latents`."
154
+ )
155
+
156
+
157
+ # auto_docstring
158
+ class Krea2Img2ImgVaeEncoderStep(SequentialPipelineBlocks):
159
+ """
160
+ Vae encoder step that preprocess and encode the image inputs into their latent representations.
161
+
162
+ Components:
163
+ image_processor (`VaeImageProcessor`) vae (`AutoencoderKLQwenImage`)
164
+
165
+ Inputs:
166
+ image (`Image | list`):
167
+ Reference image(s) for denoising. Can be a single image or list of images.
168
+ height (`int`, *optional*):
169
+ The height in pixels of the generated image.
170
+ width (`int`, *optional*):
171
+ The width in pixels of the generated image.
172
+ generator (`Generator`, *optional*):
173
+ Torch generator for deterministic generation.
174
+
175
+ Outputs:
176
+ processed_image (`Tensor`):
177
+ The processed image
178
+ image_latents (`Tensor`):
179
+ The latent representation of the input image.
180
+ """
181
+
182
+ model_name = "krea2"
183
+
184
+ block_classes = [Krea2ProcessImagesInputStep(), Krea2VaeEncoderStep()]
185
+ block_names = ["preprocess", "encode"]
186
+
187
+ @property
188
+ def description(self) -> str:
189
+ return "Vae encoder step that preprocess and encode the image inputs into their latent representations."
190
+
191
+
192
+ class Krea2AutoVaeEncoderStep(AutoPipelineBlocks):
193
+ model_name = "krea2"
194
+ block_classes = [Krea2InpaintVaeEncoderStep, Krea2Img2ImgVaeEncoderStep]
195
+ block_names = ["inpaint", "img2img"]
196
+ block_trigger_inputs = ["mask_image", "image"]
197
+
198
+ @property
199
+ def description(self):
200
+ return (
201
+ "Vae encoder step that encode the image inputs into their latent representations.\n"
202
+ + "This is an auto pipeline block.\n"
203
+ + " - `Krea2InpaintVaeEncoderStep` (inpaint) is used when `mask_image` is provided.\n"
204
+ + " - `Krea2Img2ImgVaeEncoderStep` (img2img) is used when `image` is provided.\n"
205
+ + " - if `mask_image` or `image` is not provided, step will be skipped."
206
+ )
207
+
208
+
209
+ # ====================
210
+ # 3. DENOISE (input -> prepare_latents -> set_timesteps -> prepare_rope_inputs -> denoise -> after_denoise)
211
+ # ====================
212
+
213
+
214
+ # assemble input steps
215
+ # auto_docstring
216
+ class Krea2Img2ImgInputStep(SequentialPipelineBlocks):
217
+ """
218
+ Input step that prepares the inputs for the img2img denoising step. It:
219
+ - make sure the text embeddings have consistent batch size as well as the additional inputs (`image_latents`).
220
+ - update height/width based `image_latents`, patchify `image_latents`.
221
+
222
+ Components:
223
+ pachifier (`Krea2Pachifier`)
224
+
225
+ Inputs:
226
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
227
+ The number of images to generate per prompt.
228
+ prompt_embeds (`Tensor`):
229
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
230
+ prompt_embeds_mask (`Tensor`):
231
+ mask for the text embeddings. Can be generated from text_encoder step.
232
+ negative_prompt_embeds (`Tensor`, *optional*):
233
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
234
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
235
+ mask for the negative text embeddings. Can be generated from text_encoder step.
236
+ height (`int`, *optional*):
237
+ The height in pixels of the generated image.
238
+ width (`int`, *optional*):
239
+ The width in pixels of the generated image.
240
+ image_latents (`Tensor`):
241
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
242
+
243
+ Outputs:
244
+ batch_size (`int`):
245
+ The batch size of the prompt embeddings
246
+ dtype (`dtype`):
247
+ The data type of the prompt embeddings
248
+ prompt_embeds (`Tensor`):
249
+ The prompt embeddings. (batch-expanded)
250
+ prompt_embeds_mask (`Tensor`):
251
+ The encoder attention mask. (batch-expanded)
252
+ negative_prompt_embeds (`Tensor`):
253
+ The negative prompt embeddings. (batch-expanded)
254
+ negative_prompt_embeds_mask (`Tensor`):
255
+ The negative prompt embeddings mask. (batch-expanded)
256
+ image_height (`int`):
257
+ The image height calculated from the image latents dimension
258
+ image_width (`int`):
259
+ The image width calculated from the image latents dimension
260
+ height (`int`):
261
+ if not provided, updated to image height
262
+ width (`int`):
263
+ if not provided, updated to image width
264
+ image_latents (`Tensor`):
265
+ image latents used to guide the image generation. Can be generated from vae_encoder step. (patchified and
266
+ batch-expanded)
267
+ """
268
+
269
+ model_name = "krea2"
270
+ block_classes = [Krea2TextInputsStep(), Krea2AdditionalInputsStep()]
271
+ block_names = ["text_inputs", "additional_inputs"]
272
+
273
+ @property
274
+ def description(self):
275
+ return (
276
+ "Input step that prepares the inputs for the img2img denoising step. It:\n"
277
+ " - make sure the text embeddings have consistent batch size as well as the additional inputs (`image_latents`).\n"
278
+ " - update height/width based `image_latents`, patchify `image_latents`."
279
+ )
280
+
281
+
282
+ # auto_docstring
283
+ class Krea2InpaintInputStep(SequentialPipelineBlocks):
284
+ """
285
+ Input step that prepares the inputs for the inpainting denoising step. It:
286
+ - make sure the text embeddings have consistent batch size as well as the additional inputs (`image_latents` and
287
+ `processed_mask_image`).
288
+ - update height/width based `image_latents`, patchify `image_latents`.
289
+
290
+ Components:
291
+ pachifier (`Krea2Pachifier`)
292
+
293
+ Inputs:
294
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
295
+ The number of images to generate per prompt.
296
+ prompt_embeds (`Tensor`):
297
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
298
+ prompt_embeds_mask (`Tensor`):
299
+ mask for the text embeddings. Can be generated from text_encoder step.
300
+ negative_prompt_embeds (`Tensor`, *optional*):
301
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
302
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
303
+ mask for the negative text embeddings. Can be generated from text_encoder step.
304
+ height (`int`, *optional*):
305
+ The height in pixels of the generated image.
306
+ width (`int`, *optional*):
307
+ The width in pixels of the generated image.
308
+ image_latents (`Tensor`, *optional*):
309
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
310
+ processed_mask_image (`Tensor`, *optional*):
311
+ The processed mask image
312
+
313
+ Outputs:
314
+ batch_size (`int`):
315
+ The batch size of the prompt embeddings
316
+ dtype (`dtype`):
317
+ The data type of the prompt embeddings
318
+ prompt_embeds (`Tensor`):
319
+ The prompt embeddings. (batch-expanded)
320
+ prompt_embeds_mask (`Tensor`):
321
+ The encoder attention mask. (batch-expanded)
322
+ negative_prompt_embeds (`Tensor`):
323
+ The negative prompt embeddings. (batch-expanded)
324
+ negative_prompt_embeds_mask (`Tensor`):
325
+ The negative prompt embeddings mask. (batch-expanded)
326
+ image_height (`int`):
327
+ The image height calculated from the image latents dimension
328
+ image_width (`int`):
329
+ The image width calculated from the image latents dimension
330
+ height (`int`):
331
+ if not provided, updated to image height
332
+ width (`int`):
333
+ if not provided, updated to image width
334
+ image_latents (`Tensor`):
335
+ image latents used to guide the image generation. Can be generated from vae_encoder step. (patchified and
336
+ batch-expanded)
337
+ processed_mask_image (`Tensor`):
338
+ The processed mask image (batch-expanded)
339
+ """
340
+
341
+ model_name = "krea2"
342
+ block_classes = [
343
+ Krea2TextInputsStep(),
344
+ Krea2AdditionalInputsStep(
345
+ additional_batch_inputs=[
346
+ InputParam(name="processed_mask_image", type_hint=torch.Tensor, description="The processed mask image")
347
+ ]
348
+ ),
349
+ ]
350
+ block_names = ["text_inputs", "additional_inputs"]
351
+
352
+ @property
353
+ def description(self):
354
+ return (
355
+ "Input step that prepares the inputs for the inpainting denoising step. It:\n"
356
+ " - make sure the text embeddings have consistent batch size as well as the additional inputs (`image_latents` and `processed_mask_image`).\n"
357
+ " - update height/width based `image_latents`, patchify `image_latents`."
358
+ )
359
+
360
+
361
+ # assemble prepare latents steps
362
+ # auto_docstring
363
+ class Krea2InpaintPrepareLatentsStep(SequentialPipelineBlocks):
364
+ """
365
+ This step prepares the latents/image_latents and mask inputs for the inpainting denoising step. It:
366
+ - Add noise to the image latents to create the latents input for the denoiser.
367
+ - Create the patchified latents `mask` based on the processed mask image.
368
+
369
+ Components:
370
+ scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`Krea2Pachifier`)
371
+
372
+ Inputs:
373
+ latents (`Tensor`):
374
+ The initial random noised, can be generated in prepare latent step.
375
+ image_latents (`Tensor`):
376
+ image latents used to guide the image generation. Can be generated from vae_encoder step. (Can be
377
+ generated from vae encoder and updated in input step.)
378
+ timesteps (`Tensor`):
379
+ The timesteps to use for the denoising process. Can be generated in set_timesteps step.
380
+ processed_mask_image (`Tensor`):
381
+ The processed mask to use for the inpainting process.
382
+ height (`int`):
383
+ The height in pixels of the generated image.
384
+ width (`int`):
385
+ The width in pixels of the generated image.
386
+ dtype (`dtype`, *optional*, defaults to torch.float32):
387
+ The dtype of the model inputs, can be generated in input step.
388
+
389
+ Outputs:
390
+ initial_noise (`Tensor`):
391
+ The initial random noised used for inpainting denoising.
392
+ latents (`Tensor`):
393
+ The scaled noisy latents to use for inpainting/image-to-image denoising.
394
+ mask (`Tensor`):
395
+ The mask to use for the inpainting process.
396
+ """
397
+
398
+ model_name = "krea2"
399
+ block_classes = [Krea2PrepareLatentsWithStrengthStep(), Krea2CreateMaskLatentsStep()]
400
+ block_names = ["add_noise_to_latents", "create_mask_latents"]
401
+
402
+ @property
403
+ def description(self) -> str:
404
+ return (
405
+ "This step prepares the latents/image_latents and mask inputs for the inpainting denoising step. It:\n"
406
+ " - Add noise to the image latents to create the latents input for the denoiser.\n"
407
+ " - Create the patchified latents `mask` based on the processed mask image.\n"
408
+ )
409
+
410
+
411
+ # assemble denoising steps
412
+
413
+
414
+ # Krea 2 (text2image)
415
+ # auto_docstring
416
+ class Krea2CoreDenoiseStep(SequentialPipelineBlocks):
417
+ """
418
+ step that denoise noise into image for text2image task. It includes the denoise loop, as well as prepare the inputs
419
+ (timesteps, latents, rope inputs etc.).
420
+
421
+ Components:
422
+ pachifier (`Krea2Pachifier`) scheduler (`FlowMatchEulerDiscreteScheduler`) guider (`ClassifierFreeGuidance`)
423
+ transformer (`Krea2Transformer2DModel`)
424
+
425
+ Inputs:
426
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
427
+ The number of images to generate per prompt.
428
+ prompt_embeds (`Tensor`):
429
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
430
+ prompt_embeds_mask (`Tensor`):
431
+ mask for the text embeddings. Can be generated from text_encoder step.
432
+ negative_prompt_embeds (`Tensor`, *optional*):
433
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
434
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
435
+ mask for the negative text embeddings. Can be generated from text_encoder step.
436
+ latents (`Tensor`, *optional*):
437
+ Pre-generated noisy latents for image generation.
438
+ height (`int`, *optional*):
439
+ The height in pixels of the generated image.
440
+ width (`int`, *optional*):
441
+ The width in pixels of the generated image.
442
+ generator (`Generator`, *optional*):
443
+ Torch generator for deterministic generation.
444
+ num_inference_steps (`int`, *optional*, defaults to 28):
445
+ The number of denoising steps.
446
+ sigmas (`list`, *optional*):
447
+ Custom sigmas for the denoising process.
448
+ mu (`float`, *optional*):
449
+ Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if
450
+ not provided, computed from the image sequence length (base checkpoint behavior).
451
+ **denoiser_input_fields (`None`, *optional*):
452
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
453
+
454
+ Outputs:
455
+ latents (`Tensor`):
456
+ Denoised latents.
457
+ """
458
+
459
+ model_name = "krea2"
460
+ block_classes = [
461
+ Krea2TextInputsStep(),
462
+ Krea2PrepareLatentsStep(),
463
+ Krea2SetTimestepsStep(),
464
+ Krea2RoPEInputsStep(),
465
+ Krea2DenoiseStep(),
466
+ Krea2AfterDenoiseStep(),
467
+ ]
468
+ block_names = [
469
+ "input",
470
+ "prepare_latents",
471
+ "set_timesteps",
472
+ "prepare_rope_inputs",
473
+ "denoise",
474
+ "after_denoise",
475
+ ]
476
+
477
+ @property
478
+ def description(self):
479
+ return "step that denoise noise into image for text2image task. It includes the denoise loop, as well as prepare the inputs (timesteps, latents, rope inputs etc.)."
480
+
481
+ @property
482
+ def outputs(self):
483
+ return [
484
+ OutputParam.template("latents"),
485
+ ]
486
+
487
+
488
+ # Krea 2 (inpainting)
489
+ # auto_docstring
490
+ class Krea2InpaintCoreDenoiseStep(SequentialPipelineBlocks):
491
+ """
492
+ step that denoise noise into image for inpaint task. It includes the denoise loop, as well as prepare the inputs
493
+ (timesteps, latents, rope inputs etc.).
494
+
495
+ Components:
496
+ pachifier (`Krea2Pachifier`) scheduler (`FlowMatchEulerDiscreteScheduler`) guider (`ClassifierFreeGuidance`)
497
+ transformer (`Krea2Transformer2DModel`)
498
+
499
+ Inputs:
500
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
501
+ The number of images to generate per prompt.
502
+ prompt_embeds (`Tensor`):
503
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
504
+ prompt_embeds_mask (`Tensor`):
505
+ mask for the text embeddings. Can be generated from text_encoder step.
506
+ negative_prompt_embeds (`Tensor`, *optional*):
507
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
508
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
509
+ mask for the negative text embeddings. Can be generated from text_encoder step.
510
+ height (`int`, *optional*):
511
+ The height in pixels of the generated image.
512
+ width (`int`, *optional*):
513
+ The width in pixels of the generated image.
514
+ image_latents (`Tensor`, *optional*):
515
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
516
+ processed_mask_image (`Tensor`, *optional*):
517
+ The processed mask image
518
+ latents (`Tensor`, *optional*):
519
+ Pre-generated noisy latents for image generation.
520
+ generator (`Generator`, *optional*):
521
+ Torch generator for deterministic generation.
522
+ num_inference_steps (`int`, *optional*, defaults to 28):
523
+ The number of denoising steps.
524
+ sigmas (`list`, *optional*):
525
+ Custom sigmas for the denoising process.
526
+ mu (`float`, *optional*):
527
+ Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if
528
+ not provided, computed from the image sequence length (base checkpoint behavior).
529
+ strength (`float`, *optional*, defaults to 0.9):
530
+ Strength for img2img/inpainting.
531
+ **denoiser_input_fields (`None`, *optional*):
532
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
533
+
534
+ Outputs:
535
+ latents (`Tensor`):
536
+ Denoised latents.
537
+ """
538
+
539
+ model_name = "krea2"
540
+ block_classes = [
541
+ Krea2InpaintInputStep(),
542
+ Krea2PrepareLatentsStep(),
543
+ Krea2SetTimestepsWithStrengthStep(),
544
+ Krea2InpaintPrepareLatentsStep(),
545
+ Krea2RoPEInputsStep(),
546
+ Krea2InpaintDenoiseStep(),
547
+ Krea2AfterDenoiseStep(),
548
+ ]
549
+ block_names = [
550
+ "input",
551
+ "prepare_latents",
552
+ "set_timesteps",
553
+ "prepare_inpaint_latents",
554
+ "prepare_rope_inputs",
555
+ "denoise",
556
+ "after_denoise",
557
+ ]
558
+
559
+ @property
560
+ def description(self):
561
+ return "step that denoise noise into image for inpaint task. It includes the denoise loop, as well as prepare the inputs (timesteps, latents, rope inputs etc.)."
562
+
563
+ @property
564
+ def outputs(self):
565
+ return [
566
+ OutputParam.template("latents"),
567
+ ]
568
+
569
+
570
+ # Krea 2 (image2image)
571
+ # auto_docstring
572
+ class Krea2Img2ImgCoreDenoiseStep(SequentialPipelineBlocks):
573
+ """
574
+ step that denoise noise into image for img2img task. It includes the denoise loop, as well as prepare the inputs
575
+ (timesteps, latents, rope inputs etc.).
576
+
577
+ Components:
578
+ pachifier (`Krea2Pachifier`) scheduler (`FlowMatchEulerDiscreteScheduler`) guider (`ClassifierFreeGuidance`)
579
+ transformer (`Krea2Transformer2DModel`)
580
+
581
+ Inputs:
582
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
583
+ The number of images to generate per prompt.
584
+ prompt_embeds (`Tensor`):
585
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
586
+ prompt_embeds_mask (`Tensor`):
587
+ mask for the text embeddings. Can be generated from text_encoder step.
588
+ negative_prompt_embeds (`Tensor`, *optional*):
589
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
590
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
591
+ mask for the negative text embeddings. Can be generated from text_encoder step.
592
+ height (`int`, *optional*):
593
+ The height in pixels of the generated image.
594
+ width (`int`, *optional*):
595
+ The width in pixels of the generated image.
596
+ image_latents (`Tensor`):
597
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
598
+ latents (`Tensor`, *optional*):
599
+ Pre-generated noisy latents for image generation.
600
+ generator (`Generator`, *optional*):
601
+ Torch generator for deterministic generation.
602
+ num_inference_steps (`int`, *optional*, defaults to 28):
603
+ The number of denoising steps.
604
+ sigmas (`list`, *optional*):
605
+ Custom sigmas for the denoising process.
606
+ mu (`float`, *optional*):
607
+ Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if
608
+ not provided, computed from the image sequence length (base checkpoint behavior).
609
+ strength (`float`, *optional*, defaults to 0.9):
610
+ Strength for img2img/inpainting.
611
+ **denoiser_input_fields (`None`, *optional*):
612
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
613
+
614
+ Outputs:
615
+ latents (`Tensor`):
616
+ Denoised latents.
617
+ """
618
+
619
+ model_name = "krea2"
620
+ block_classes = [
621
+ Krea2Img2ImgInputStep(),
622
+ Krea2PrepareLatentsStep(),
623
+ Krea2SetTimestepsWithStrengthStep(),
624
+ Krea2PrepareLatentsWithStrengthStep(),
625
+ Krea2RoPEInputsStep(),
626
+ Krea2DenoiseStep(),
627
+ Krea2AfterDenoiseStep(),
628
+ ]
629
+ block_names = [
630
+ "input",
631
+ "prepare_latents",
632
+ "set_timesteps",
633
+ "prepare_img2img_latents",
634
+ "prepare_rope_inputs",
635
+ "denoise",
636
+ "after_denoise",
637
+ ]
638
+
639
+ @property
640
+ def description(self):
641
+ return "step that denoise noise into image for img2img task. It includes the denoise loop, as well as prepare the inputs (timesteps, latents, rope inputs etc.)."
642
+
643
+ @property
644
+ def outputs(self):
645
+ return [
646
+ OutputParam.template("latents"),
647
+ ]
648
+
649
+
650
+ # Auto denoise step for Krea 2
651
+ class Krea2AutoCoreDenoiseStep(AutoPipelineBlocks):
652
+ model_name = "krea2"
653
+ block_classes = [
654
+ Krea2InpaintCoreDenoiseStep,
655
+ Krea2Img2ImgCoreDenoiseStep,
656
+ Krea2CoreDenoiseStep,
657
+ ]
658
+ block_names = [
659
+ "inpaint",
660
+ "img2img",
661
+ "text2image",
662
+ ]
663
+ block_trigger_inputs = ["processed_mask_image", "image_latents", None]
664
+
665
+ @property
666
+ def description(self):
667
+ return (
668
+ "Core step that performs the denoising process. \n"
669
+ + " - `Krea2InpaintCoreDenoiseStep` (inpaint) is used when `processed_mask_image` is provided.\n"
670
+ + " - `Krea2Img2ImgCoreDenoiseStep` (img2img) is used when `image_latents` is provided.\n"
671
+ + " - `Krea2CoreDenoiseStep` (text2image) is used otherwise.\n"
672
+ + "This step support text-to-image, image-to-image, and inpainting tasks for Krea 2:\n"
673
+ + " - for image-to-image generation, you need to provide `image_latents`\n"
674
+ + " - for inpainting, you need to provide `processed_mask_image` and `image_latents`\n"
675
+ + " - for text-to-image generation, all you need to provide is prompt embeddings"
676
+ )
677
+
678
+ @property
679
+ def outputs(self):
680
+ return [
681
+ OutputParam.template("latents"),
682
+ ]
683
+
684
+
685
+ # ====================
686
+ # 4. DECODE
687
+ # ====================
688
+
689
+
690
+ # standard decode step works for most tasks except for inpaint
691
+ # auto_docstring
692
+ class Krea2DecodeStep(SequentialPipelineBlocks):
693
+ """
694
+ Decode step that decodes the latents to images and postprocess the generated image.
695
+
696
+ Components:
697
+ vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
698
+
699
+ Inputs:
700
+ latents (`Tensor`):
701
+ The denoised latents to decode, can be generated in the denoise step and unpacked in the after denoise
702
+ step.
703
+ output_type (`str`, *optional*, defaults to pil):
704
+ Output format: 'pil', 'np', 'pt'.
705
+
706
+ Outputs:
707
+ images (`list`):
708
+ Generated images. (tensor output of the vae decoder.)
709
+ """
710
+
711
+ model_name = "krea2"
712
+ block_classes = [Krea2DecoderStep(), Krea2ProcessImagesOutputStep()]
713
+ block_names = ["decode", "postprocess"]
714
+
715
+ @property
716
+ def description(self):
717
+ return "Decode step that decodes the latents to images and postprocess the generated image."
718
+
719
+
720
+ # Inpaint decode step
721
+ # auto_docstring
722
+ class Krea2InpaintDecodeStep(SequentialPipelineBlocks):
723
+ """
724
+ Decode step that decodes the latents to images and postprocess the generated image, optionally apply the mask
725
+ overlay to the original image.
726
+
727
+ Components:
728
+ vae (`AutoencoderKLQwenImage`) image_mask_processor (`InpaintProcessor`)
729
+
730
+ Inputs:
731
+ latents (`Tensor`):
732
+ The denoised latents to decode, can be generated in the denoise step and unpacked in the after denoise
733
+ step.
734
+ output_type (`str`, *optional*, defaults to pil):
735
+ Output format: 'pil', 'np', 'pt'.
736
+ mask_overlay_kwargs (`dict`, *optional*):
737
+ The kwargs for the postprocess step to apply the mask overlay. generated in
738
+ Krea2InpaintProcessImagesInputStep.
739
+
740
+ Outputs:
741
+ images (`list`):
742
+ Generated images. (tensor output of the vae decoder.)
743
+ """
744
+
745
+ model_name = "krea2"
746
+ block_classes = [Krea2DecoderStep(), Krea2InpaintProcessImagesOutputStep()]
747
+ block_names = ["decode", "postprocess"]
748
+
749
+ @property
750
+ def description(self):
751
+ return "Decode step that decodes the latents to images and postprocess the generated image, optionally apply the mask overlay to the original image."
752
+
753
+
754
+ # Auto decode step for Krea 2
755
+ class Krea2AutoDecodeStep(AutoPipelineBlocks):
756
+ model_name = "krea2"
757
+ block_classes = [Krea2InpaintDecodeStep, Krea2DecodeStep]
758
+ block_names = ["inpaint_decode", "decode"]
759
+ block_trigger_inputs = ["mask", None]
760
+
761
+ @property
762
+ def description(self):
763
+ return (
764
+ "Decode step that decode the latents into images. \n"
765
+ " This is an auto pipeline block that works for inpaint/text2image/img2img tasks.\n"
766
+ + " - `Krea2InpaintDecodeStep` (inpaint_decode) is used when `mask` is provided.\n"
767
+ + " - `Krea2DecodeStep` (decode) is used when `mask` is not provided.\n"
768
+ )
769
+
770
+
771
+ # ====================
772
+ # 5. AUTO BLOCKS & PRESETS
773
+ # ====================
774
+ AUTO_BLOCKS = InsertableDict(
775
+ [
776
+ ("text_encoder", Krea2AutoTextEncoderStep()),
777
+ ("vae_encoder", Krea2AutoVaeEncoderStep()),
778
+ ("denoise", Krea2AutoCoreDenoiseStep()),
779
+ ("decode", Krea2AutoDecodeStep()),
780
+ ]
781
+ )
782
+
783
+
784
+ # auto_docstring
785
+ class Krea2AutoBlocks(SequentialPipelineBlocks):
786
+ """
787
+ Auto Modular pipeline for text-to-image, image-to-image, and inpainting tasks using Krea 2.
788
+
789
+ Supported workflows:
790
+ - `text2image`: requires `prompt`
791
+ - `image2image`: requires `prompt`, `image`
792
+ - `inpainting`: requires `prompt`, `mask_image`, `image`
793
+
794
+ Components:
795
+ text_encoder (`Qwen3VLModel`): The text encoder to use tokenizer (`Qwen2Tokenizer`): The tokenizer to use
796
+ guider (`ClassifierFreeGuidance`) image_mask_processor (`InpaintProcessor`) vae (`AutoencoderKLQwenImage`)
797
+ image_processor (`VaeImageProcessor`) pachifier (`Krea2Pachifier`) scheduler
798
+ (`FlowMatchEulerDiscreteScheduler`) transformer (`Krea2Transformer2DModel`)
799
+
800
+ Inputs:
801
+ prompt (`str`, *optional*):
802
+ The prompt or prompts to guide image generation.
803
+ negative_prompt (`str`, *optional*):
804
+ The prompt or prompts not to guide the image generation.
805
+ max_sequence_length (`int`, *optional*, defaults to 512):
806
+ Maximum sequence length for prompt encoding.
807
+ mask_image (`Image`, *optional*):
808
+ Mask image for inpainting.
809
+ image (`Image | list`, *optional*):
810
+ Reference image(s) for denoising. Can be a single image or list of images.
811
+ height (`int`, *optional*):
812
+ The height in pixels of the generated image.
813
+ width (`int`, *optional*):
814
+ The width in pixels of the generated image.
815
+ padding_mask_crop (`int`, *optional*):
816
+ Padding for mask cropping in inpainting.
817
+ generator (`Generator`, *optional*):
818
+ Torch generator for deterministic generation.
819
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
820
+ The number of images to generate per prompt.
821
+ prompt_embeds (`Tensor`):
822
+ text embeddings used to guide the image generation. Can be generated from text_encoder step.
823
+ prompt_embeds_mask (`Tensor`):
824
+ mask for the text embeddings. Can be generated from text_encoder step.
825
+ negative_prompt_embeds (`Tensor`, *optional*):
826
+ negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
827
+ negative_prompt_embeds_mask (`Tensor`, *optional*):
828
+ mask for the negative text embeddings. Can be generated from text_encoder step.
829
+ image_latents (`Tensor`, *optional*):
830
+ image latents used to guide the image generation. Can be generated from vae_encoder step.
831
+ processed_mask_image (`Tensor`, *optional*):
832
+ The processed mask image
833
+ latents (`Tensor`):
834
+ Pre-generated noisy latents for image generation.
835
+ num_inference_steps (`int`):
836
+ The number of denoising steps.
837
+ sigmas (`list`, *optional*):
838
+ Custom sigmas for the denoising process.
839
+ mu (`float`, *optional*):
840
+ Fixed timestep shift for the scheduler. Pass `1.15` for the few-step distilled (TDM/turbo) checkpoint; if
841
+ not provided, computed from the image sequence length (base checkpoint behavior).
842
+ strength (`float`, *optional*, defaults to 0.9):
843
+ Strength for img2img/inpainting.
844
+ **denoiser_input_fields (`None`, *optional*):
845
+ conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
846
+ output_type (`str`, *optional*, defaults to pil):
847
+ Output format: 'pil', 'np', 'pt'.
848
+ mask_overlay_kwargs (`dict`, *optional*):
849
+ The kwargs for the postprocess step to apply the mask overlay. generated in
850
+ Krea2InpaintProcessImagesInputStep.
851
+
852
+ Outputs:
853
+ images (`list`):
854
+ Generated images.
855
+ """
856
+
857
+ model_name = "krea2"
858
+
859
+ block_classes = AUTO_BLOCKS.values()
860
+ block_names = AUTO_BLOCKS.keys()
861
+
862
+ # Workflow map defines the trigger conditions for each workflow.
863
+ # How to define:
864
+ # - Only include required inputs and trigger inputs (inputs that determine which blocks run)
865
+ # - currently, only supports `True` means the workflow triggers when the input is not None
866
+
867
+ _workflow_map = {
868
+ "text2image": {"prompt": True},
869
+ "image2image": {"prompt": True, "image": True},
870
+ "inpainting": {"prompt": True, "mask_image": True, "image": True},
871
+ }
872
+
873
+ @property
874
+ def description(self):
875
+ return "Auto Modular pipeline for text-to-image, image-to-image, and inpainting tasks using Krea 2."
876
+
877
+ @property
878
+ def outputs(self):
879
+ return [OutputParam.template("images")]
modular_blocks_krea2_edit.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 diffusers.utils import logging
16
+ from diffusers.modular_pipelines.modular_pipeline import SequentialPipelineBlocks
17
+ from diffusers.modular_pipelines.modular_pipeline_utils import InsertableDict, OutputParam
18
+ from .before_denoise import (
19
+ Krea2EditRoPEInputsStep,
20
+ Krea2PrepareLatentsStep,
21
+ Krea2SetTimestepsStep,
22
+ )
23
+ from .decoders import Krea2AfterDenoiseStep
24
+ from .denoise import Krea2EditDenoiseStep
25
+ from .encoders import (
26
+ Krea2EditReferenceLatentsStep,
27
+ Krea2EditTextEncoderStep,
28
+ )
29
+ from .inputs import Krea2TextInputsStep
30
+ from .modular_blocks_krea2 import Krea2DecodeStep
31
+
32
+
33
+ logger = logging.get_logger(__name__)
34
+
35
+
36
+ # Krea 2 (reference-image edit)
37
+ class Krea2EditCoreDenoiseStep(SequentialPipelineBlocks):
38
+ model_name = "krea2"
39
+ block_classes = [
40
+ Krea2TextInputsStep(),
41
+ Krea2PrepareLatentsStep(),
42
+ Krea2SetTimestepsStep(),
43
+ Krea2EditRoPEInputsStep(),
44
+ Krea2EditDenoiseStep(),
45
+ Krea2AfterDenoiseStep(),
46
+ ]
47
+ block_names = [
48
+ "input",
49
+ "prepare_latents",
50
+ "set_timesteps",
51
+ "prepare_rope_inputs",
52
+ "denoise",
53
+ "after_denoise",
54
+ ]
55
+
56
+ @property
57
+ def description(self):
58
+ return (
59
+ "step that denoises noise into an image for the reference-image edit task. It includes the denoise loop "
60
+ "(with the clean reference tokens appended each step), as well as preparing the inputs (timesteps, "
61
+ "latents, rope inputs etc.)."
62
+ )
63
+
64
+ @property
65
+ def outputs(self):
66
+ return [
67
+ OutputParam.template("latents"),
68
+ ]
69
+
70
+
71
+ # Preset: reference-image edit. `image` carries the reference image(s); without it, use the text2image preset.
72
+ EDIT_BLOCKS = InsertableDict(
73
+ [
74
+ ("text_encoder", Krea2EditTextEncoderStep()),
75
+ ("reference_latents", Krea2EditReferenceLatentsStep()),
76
+ ("denoise", Krea2EditCoreDenoiseStep()),
77
+ ("decode", Krea2DecodeStep()),
78
+ ]
79
+ )
80
+
81
+
82
+ class Krea2EditBlocks(SequentialPipelineBlocks):
83
+ """
84
+ Modular pipeline for the Krea 2 reference-image edit task.
85
+
86
+ Pass one or more reference images as `image`; they condition generation both through the Qwen3-VL text encoder
87
+ and as clean VAE latents appended to the transformer sequence at flow time t=0 (matching the Ostris AI-Toolkit
88
+ edit LoRAs). Load an edit/style LoRA into the transformer to control the effect.
89
+ """
90
+
91
+ model_name = "krea2"
92
+ block_classes = EDIT_BLOCKS.values()
93
+ block_names = EDIT_BLOCKS.keys()
94
+
95
+ @property
96
+ def description(self):
97
+ return "Auto Modular pipeline for the reference-image edit task using Krea 2. Requires `prompt` and `image`."
modular_pipeline.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 diffusers.configuration_utils import ConfigMixin, register_to_config
17
+ from diffusers.modular_pipelines.modular_pipeline import ModularPipeline
18
+
19
+
20
+ # Copied from diffusers.modular_pipelines.qwenimage.modular_pipeline.QwenImagePachifier with QwenImage->Krea2
21
+ class Krea2Pachifier(ConfigMixin):
22
+ """
23
+ A class to pack and unpack latents for Krea2.
24
+ """
25
+
26
+ config_name = "config.json"
27
+
28
+ @register_to_config
29
+ def __init__(self, patch_size: int = 2):
30
+ super().__init__()
31
+
32
+ def pack_latents(self, latents):
33
+ if latents.ndim != 4 and latents.ndim != 5:
34
+ raise ValueError(f"Latents must have 4 or 5 dimensions, but got {latents.ndim}")
35
+
36
+ if latents.ndim == 4:
37
+ latents = latents.unsqueeze(2)
38
+
39
+ batch_size, num_channels_latents, num_latent_frames, latent_height, latent_width = latents.shape
40
+ patch_size = self.config.patch_size
41
+
42
+ if latent_height % patch_size != 0 or latent_width % patch_size != 0:
43
+ raise ValueError(
44
+ f"Latent height and width must be divisible by {patch_size}, but got {latent_height} and {latent_width}"
45
+ )
46
+
47
+ latents = latents.view(
48
+ batch_size,
49
+ num_channels_latents,
50
+ latent_height // patch_size,
51
+ patch_size,
52
+ latent_width // patch_size,
53
+ patch_size,
54
+ )
55
+ latents = latents.permute(
56
+ 0, 2, 4, 1, 3, 5
57
+ ) # Batch_size, num_patches_height, num_patches_width, num_channels_latents, patch_size, patch_size
58
+ latents = latents.reshape(
59
+ batch_size,
60
+ (latent_height // patch_size) * (latent_width // patch_size),
61
+ num_channels_latents * patch_size * patch_size,
62
+ )
63
+
64
+ return latents
65
+
66
+ def unpack_latents(self, latents, height, width, vae_scale_factor=8):
67
+ if latents.ndim != 3:
68
+ raise ValueError(f"Latents must have 3 dimensions, but got {latents.ndim}")
69
+
70
+ batch_size, num_patches, channels = latents.shape
71
+ patch_size = self.config.patch_size
72
+
73
+ # VAE applies 8x compression on images but we must also account for packing which requires
74
+ # latent height and width to be divisible by 2.
75
+ height = patch_size * (int(height) // (vae_scale_factor * patch_size))
76
+ width = patch_size * (int(width) // (vae_scale_factor * patch_size))
77
+
78
+ latents = latents.view(
79
+ batch_size,
80
+ height // patch_size,
81
+ width // patch_size,
82
+ channels // (patch_size * patch_size),
83
+ patch_size,
84
+ patch_size,
85
+ )
86
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
87
+
88
+ latents = latents.reshape(batch_size, channels // (patch_size * patch_size), 1, height, width)
89
+
90
+ return latents
91
+
92
+
93
+ class Krea2ModularPipeline(ModularPipeline):
94
+ """
95
+ A ModularPipeline for Krea 2.
96
+
97
+ > [!WARNING] > This is an experimental feature and is likely to change in the future.
98
+ """
99
+
100
+ default_blocks_name = "Krea2AutoBlocks"
101
+
102
+ @property
103
+ def default_height(self):
104
+ return self.default_sample_size * self.vae_scale_factor
105
+
106
+ @property
107
+ def default_width(self):
108
+ return self.default_sample_size * self.vae_scale_factor
109
+
110
+ @property
111
+ def default_sample_size(self):
112
+ return 128
113
+
114
+ @property
115
+ def vae_scale_factor(self):
116
+ vae_scale_factor = 8
117
+ if hasattr(self, "vae") and self.vae is not None:
118
+ vae_scale_factor = 2 ** len(self.vae.temperal_downsample)
119
+ return vae_scale_factor
120
+
121
+ @property
122
+ def num_channels_latents(self):
123
+ num_channels_latents = 16
124
+ if hasattr(self, "transformer") and self.transformer is not None:
125
+ num_channels_latents = self.transformer.config.in_channels // 4
126
+ return num_channels_latents
127
+
128
+ @property
129
+ def requires_unconditional_embeds(self):
130
+ requires_unconditional_embeds = False
131
+
132
+ if hasattr(self, "guider") and self.guider is not None:
133
+ requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
134
+
135
+ return requires_unconditional_embeds
transformer_krea2.py ADDED
@@ -0,0 +1,566 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import inspect
16
+ import math
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+
22
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
23
+ from diffusers.loaders import PeftAdapterMixin
24
+ from diffusers.utils import logging
25
+ from diffusers.utils.torch_utils import maybe_adjust_dtype_for_device
26
+ from diffusers.models.attention import AttentionMixin, AttentionModuleMixin
27
+ from diffusers.models.attention_dispatch import dispatch_attention_fn
28
+ from diffusers.models.embeddings import apply_rotary_emb, get_1d_rotary_pos_embed
29
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
30
+ from diffusers.models.modeling_utils import ModelMixin
31
+
32
+
33
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
34
+
35
+
36
+ class Krea2RMSNorm(nn.Module):
37
+ """RMSNorm with a zero-centered scale: the effective multiplier is `1 + weight`, matching the Krea 2 checkpoint
38
+ format. The activations are upcast so the normalization runs in float32; the scale weight is kept in float32 by
39
+ the model's `_keep_in_fp32_modules`."""
40
+
41
+ def __init__(self, dim: int, eps: float = 1e-5) -> None:
42
+ super().__init__()
43
+ self.dim = dim
44
+ self.eps = eps
45
+ self.weight = nn.Parameter(torch.zeros(dim))
46
+
47
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
48
+ dtype = hidden_states.dtype
49
+ hidden_states = F.rms_norm(hidden_states.float(), (self.dim,), weight=self.weight + 1.0, eps=self.eps)
50
+ return hidden_states.to(dtype)
51
+
52
+
53
+ class Krea2AttnProcessor:
54
+ _attention_backend = None
55
+ _parallel_config = None
56
+
57
+ def __call__(
58
+ self,
59
+ attn: "Krea2Attention",
60
+ hidden_states: torch.Tensor,
61
+ attention_mask: torch.Tensor | None = None,
62
+ image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None,
63
+ ) -> torch.Tensor:
64
+ query = attn.to_q(hidden_states).unflatten(-1, (attn.num_heads, attn.head_dim))
65
+ key = attn.to_k(hidden_states).unflatten(-1, (attn.num_kv_heads, attn.head_dim))
66
+ value = attn.to_v(hidden_states).unflatten(-1, (attn.num_kv_heads, attn.head_dim))
67
+ gate = attn.to_gate(hidden_states)
68
+
69
+ query = attn.norm_q(query)
70
+ key = attn.norm_k(key)
71
+
72
+ if image_rotary_emb is not None:
73
+ query = apply_rotary_emb(query, image_rotary_emb, sequence_dim=1)
74
+ key = apply_rotary_emb(key, image_rotary_emb, sequence_dim=1)
75
+
76
+ hidden_states = dispatch_attention_fn(
77
+ query,
78
+ key,
79
+ value,
80
+ attn_mask=attention_mask,
81
+ enable_gqa=attn.num_heads != attn.num_kv_heads,
82
+ backend=self._attention_backend,
83
+ parallel_config=self._parallel_config,
84
+ )
85
+ hidden_states = hidden_states.flatten(2, 3)
86
+ hidden_states = hidden_states * torch.sigmoid(gate)
87
+ return attn.to_out[0](hidden_states)
88
+
89
+
90
+ class Krea2Attention(nn.Module, AttentionModuleMixin):
91
+ """Self-attention with grouped-query projections, q/k RMSNorm, rotary embeddings and a sigmoid output gate."""
92
+
93
+ _default_processor_cls = Krea2AttnProcessor
94
+ _available_processors = [Krea2AttnProcessor]
95
+
96
+ def __init__(
97
+ self, hidden_size: int, num_heads: int, num_kv_heads: int | None = None, eps: float = 1e-5, processor=None
98
+ ) -> None:
99
+ super().__init__()
100
+ if hidden_size % num_heads != 0:
101
+ raise ValueError(f"hidden_size={hidden_size} must be divisible by num_heads={num_heads}")
102
+ self.hidden_size = hidden_size
103
+ self.num_heads = num_heads
104
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
105
+ self.head_dim = hidden_size // num_heads
106
+ self.use_bias = False
107
+
108
+ self.to_q = nn.Linear(hidden_size, self.head_dim * self.num_heads, bias=False)
109
+ self.to_k = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
110
+ self.to_v = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
111
+ self.to_gate = nn.Linear(hidden_size, hidden_size, bias=False)
112
+ self.norm_q = Krea2RMSNorm(self.head_dim, eps=eps)
113
+ self.norm_k = Krea2RMSNorm(self.head_dim, eps=eps)
114
+ self.to_out = nn.ModuleList([nn.Linear(hidden_size, hidden_size, bias=False), nn.Dropout(0.0)])
115
+
116
+ if processor is None:
117
+ processor = self._default_processor_cls()
118
+ self.set_processor(processor)
119
+
120
+ def forward(
121
+ self,
122
+ hidden_states: torch.Tensor,
123
+ attention_mask: torch.Tensor | None = None,
124
+ image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None,
125
+ **kwargs,
126
+ ) -> torch.Tensor:
127
+ attn_parameters = set(inspect.signature(self.processor.__call__).parameters.keys())
128
+ unused_kwargs = [k for k in kwargs if k not in attn_parameters]
129
+ if len(unused_kwargs) > 0:
130
+ logger.warning(
131
+ f"attention_kwargs {unused_kwargs} are not expected by {self.processor.__class__.__name__} and will be ignored."
132
+ )
133
+ kwargs = {k: w for k, w in kwargs.items() if k in attn_parameters}
134
+ return self.processor(self, hidden_states, attention_mask, image_rotary_emb, **kwargs)
135
+
136
+
137
+ class Krea2SwiGLU(nn.Module):
138
+ """SwiGLU feed-forward network."""
139
+
140
+ def __init__(self, dim: int, hidden_dim: int) -> None:
141
+ super().__init__()
142
+ self.gate = nn.Linear(dim, hidden_dim, bias=False)
143
+ self.up = nn.Linear(dim, hidden_dim, bias=False)
144
+ self.down = nn.Linear(hidden_dim, dim, bias=False)
145
+
146
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
147
+ return self.down(F.silu(self.gate(hidden_states)) * self.up(hidden_states))
148
+
149
+
150
+ class Krea2TextFusionBlock(nn.Module):
151
+ """Pre-norm transformer block (no rotary embeddings, no time modulation) used by the text fusion stage."""
152
+
153
+ def __init__(self, dim: int, num_heads: int, num_kv_heads: int, intermediate_size: int, eps: float) -> None:
154
+ super().__init__()
155
+ self.norm1 = Krea2RMSNorm(dim, eps=eps)
156
+ self.norm2 = Krea2RMSNorm(dim, eps=eps)
157
+ self.attn = Krea2Attention(dim, num_heads, num_kv_heads, eps=eps)
158
+ self.ff = Krea2SwiGLU(dim, intermediate_size)
159
+
160
+ def forward(self, hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
161
+ hidden_states = hidden_states + self.attn(self.norm1(hidden_states), attention_mask=attention_mask)
162
+ hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
163
+ return hidden_states
164
+
165
+
166
+ class Krea2TextFusion(nn.Module):
167
+ """Fuses the stack of tapped text-encoder hidden states into a single sequence of text features.
168
+
169
+ Two `layerwise_blocks` attend across the `num_text_layers` axis independently for every token, a linear
170
+ `projector` collapses that axis, and two `refiner_blocks` attend across the token sequence.
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ num_text_layers: int,
176
+ dim: int,
177
+ num_heads: int,
178
+ num_kv_heads: int,
179
+ intermediate_size: int,
180
+ num_layerwise_blocks: int,
181
+ num_refiner_blocks: int,
182
+ eps: float,
183
+ ) -> None:
184
+ super().__init__()
185
+ self.layerwise_blocks = nn.ModuleList(
186
+ [
187
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
188
+ for _ in range(num_layerwise_blocks)
189
+ ]
190
+ )
191
+ self.projector = nn.Linear(num_text_layers, 1, bias=False)
192
+ self.refiner_blocks = nn.ModuleList(
193
+ [
194
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
195
+ for _ in range(num_refiner_blocks)
196
+ ]
197
+ )
198
+
199
+ def forward(self, encoder_hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None) -> torch.Tensor:
200
+ batch_size, seq_len, num_text_layers, dim = encoder_hidden_states.shape
201
+
202
+ hidden_states = encoder_hidden_states.reshape(batch_size * seq_len, num_text_layers, dim)
203
+ for block in self.layerwise_blocks:
204
+ hidden_states = block(hidden_states.contiguous())
205
+
206
+ hidden_states = hidden_states.reshape(batch_size, seq_len, num_text_layers, dim).permute(0, 1, 3, 2)
207
+ hidden_states = self.projector(hidden_states).squeeze(-1)
208
+
209
+ for block in self.refiner_blocks:
210
+ hidden_states = block(hidden_states, attention_mask=attention_mask)
211
+
212
+ return hidden_states
213
+
214
+
215
+ class Krea2TransformerBlock(nn.Module):
216
+ def __init__(
217
+ self, hidden_size: int, intermediate_size: int, num_heads: int, num_kv_heads: int, norm_eps: float
218
+ ) -> None:
219
+ super().__init__()
220
+ self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
221
+ self.norm1 = Krea2RMSNorm(hidden_size, eps=norm_eps)
222
+ self.norm2 = Krea2RMSNorm(hidden_size, eps=norm_eps)
223
+ self.attn = Krea2Attention(hidden_size, num_heads, num_kv_heads, eps=norm_eps)
224
+ self.ff = Krea2SwiGLU(hidden_size, intermediate_size)
225
+
226
+ def forward(
227
+ self,
228
+ hidden_states: torch.Tensor,
229
+ temb: torch.Tensor | tuple[torch.Tensor, torch.Tensor, int],
230
+ image_rotary_emb: tuple[torch.Tensor, torch.Tensor],
231
+ attention_mask: torch.Tensor | None = None,
232
+ ) -> torch.Tensor:
233
+ # temb: (B, 1, 6 * hidden_size), shared across all blocks; each block only learns an additive table.
234
+ # For reference-image ("edit") conditioning, temb is instead a tuple (temb, ref_temb, split): the leading
235
+ # `split` tokens (text + noisy image) are modulated with the real timestep while the trailing tokens (clean
236
+ # reference tokens) use the t=0 embedding `ref_temb`.
237
+ if isinstance(temb, tuple):
238
+ temb, ref_temb, split = temb
239
+ m = (temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
240
+ r = (ref_temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
241
+
242
+ def modulate(h, scale_idx, shift_idx):
243
+ return torch.cat(
244
+ (
245
+ (1.0 + m[scale_idx]) * h[:, :split] + m[shift_idx],
246
+ (1.0 + r[scale_idx]) * h[:, split:] + r[shift_idx],
247
+ ),
248
+ dim=1,
249
+ )
250
+
251
+ def gate(h, gate_idx):
252
+ return torch.cat((m[gate_idx] * h[:, :split], r[gate_idx] * h[:, split:]), dim=1)
253
+
254
+ attn_out = self.attn(
255
+ modulate(self.norm1(hidden_states), 0, 1),
256
+ attention_mask=attention_mask,
257
+ image_rotary_emb=image_rotary_emb,
258
+ )
259
+ hidden_states = hidden_states + gate(attn_out, 2)
260
+ ff_out = self.ff(modulate(self.norm2(hidden_states), 3, 4))
261
+ hidden_states = hidden_states + gate(ff_out, 5)
262
+ return hidden_states
263
+
264
+ modulation = temb.unflatten(-1, (6, -1)) + self.scale_shift_table
265
+ prescale, preshift, pregate, postscale, postshift, postgate = modulation.unbind(-2)
266
+
267
+ attn_out = self.attn(
268
+ (1.0 + prescale) * self.norm1(hidden_states) + preshift,
269
+ attention_mask=attention_mask,
270
+ image_rotary_emb=image_rotary_emb,
271
+ )
272
+ hidden_states = hidden_states + pregate * attn_out
273
+ ff_out = self.ff((1.0 + postscale) * self.norm2(hidden_states) + postshift)
274
+ hidden_states = hidden_states + postgate * ff_out
275
+ return hidden_states
276
+
277
+
278
+ class Krea2TimestepEmbedding(nn.Module):
279
+ """Sinusoidal flow-time embedding (cos-first, input scaled by 1000) followed by a two-layer MLP.
280
+
281
+ Keeps the sequence dimension at size 1 so the per-block modulations broadcast over tokens.
282
+ """
283
+
284
+ def __init__(self, embed_dim: int, hidden_size: int) -> None:
285
+ super().__init__()
286
+ self.embed_dim = embed_dim
287
+ self.linear_1 = nn.Linear(embed_dim, hidden_size, bias=True)
288
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
289
+
290
+ def forward(self, timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
291
+ half = self.embed_dim // 2
292
+ freqs = torch.exp(-math.log(1e4) * torch.arange(half, dtype=torch.float32, device=timestep.device) / half)
293
+ args = (timestep.float() * 1e3)[:, None, None] * freqs
294
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(dtype)
295
+ return self.linear_2(F.gelu(self.linear_1(emb), approximate="tanh"))
296
+
297
+
298
+ class Krea2TextProjection(nn.Module):
299
+ """Projects the fused text features into the transformer width."""
300
+
301
+ def __init__(self, text_dim: int, hidden_size: int, eps: float) -> None:
302
+ super().__init__()
303
+ self.norm = Krea2RMSNorm(text_dim, eps=eps)
304
+ self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
305
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
306
+
307
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
308
+ hidden_states = self.linear_1(self.norm(hidden_states))
309
+ return self.linear_2(F.gelu(hidden_states, approximate="tanh"))
310
+
311
+
312
+ class Krea2FinalLayer(nn.Module):
313
+ """Final adaptive RMSNorm and output projection. Kept as one module (and in `_no_split_modules`) so the learned
314
+ modulation table, norm and projection stay co-located under device-mapped inference."""
315
+
316
+ def __init__(self, hidden_size: int, out_channels: int, eps: float) -> None:
317
+ super().__init__()
318
+ self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
319
+ self.norm = Krea2RMSNorm(hidden_size, eps=eps)
320
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
321
+
322
+ def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor:
323
+ modulation = temb + self.scale_shift_table
324
+ scale, shift = modulation.chunk(2, dim=1)
325
+ hidden_states = (1.0 + scale) * self.norm(hidden_states) + shift
326
+ return self.linear(hidden_states)
327
+
328
+
329
+ # Copied from diffusers.models.transformers.transformer_flux.FluxPosEmbed with FluxPosEmbed->Krea2RotaryPosEmbed
330
+ class Krea2RotaryPosEmbed(nn.Module):
331
+ # modified from https://github.com/black-forest-labs/flux/blob/c00d7c60b085fce8058b9df845e036090873f2ce/src/flux/modules/layers.py#L11
332
+ def __init__(self, theta: int, axes_dim: list[int]):
333
+ super().__init__()
334
+ self.theta = theta
335
+ self.axes_dim = axes_dim
336
+
337
+ def forward(self, ids: torch.Tensor) -> torch.Tensor:
338
+ n_axes = ids.shape[-1]
339
+ cos_out = []
340
+ sin_out = []
341
+ pos = ids.float()
342
+ freqs_dtype = maybe_adjust_dtype_for_device(torch.float64, ids.device)
343
+ for i in range(n_axes):
344
+ cos, sin = get_1d_rotary_pos_embed(
345
+ self.axes_dim[i],
346
+ pos[:, i],
347
+ theta=self.theta,
348
+ repeat_interleave_real=True,
349
+ use_real=True,
350
+ freqs_dtype=freqs_dtype,
351
+ )
352
+ cos_out.append(cos)
353
+ sin_out.append(sin)
354
+ freqs_cos = torch.cat(cos_out, dim=-1).to(ids.device)
355
+ freqs_sin = torch.cat(sin_out, dim=-1).to(ids.device)
356
+ return freqs_cos, freqs_sin
357
+
358
+
359
+ class Krea2Transformer2DModel(ModelMixin, ConfigMixin, AttentionMixin, PeftAdapterMixin):
360
+ r"""
361
+ The single-stream MMDiT flow-matching backbone used by the Krea 2 pipeline.
362
+
363
+ Text conditioning enters as a stack of hidden states tapped from several layers of a multimodal text encoder. A
364
+ small text-fusion transformer collapses the layer axis and refines the token sequence; the result is concatenated
365
+ with the patchified image latents into a single `[text, image]` sequence processed by the transformer blocks. The
366
+ timestep conditions every block through one shared modulation vector plus per-block learned tables.
367
+
368
+ Args:
369
+ in_channels (`int`, defaults to 64):
370
+ Latent channel count after patchification (`vae_channels * patch_size ** 2`).
371
+ num_layers (`int`, defaults to 28):
372
+ Number of transformer blocks.
373
+ attention_head_dim (`int`, defaults to 128):
374
+ Dimension of each attention head; the total hidden size is `attention_head_dim * num_attention_heads`.
375
+ num_attention_heads (`int`, defaults to 48):
376
+ Number of query heads.
377
+ num_key_value_heads (`int`, defaults to 12):
378
+ Number of key/value heads for grouped-query attention.
379
+ intermediate_size (`int`, defaults to 16384):
380
+ Feed-forward hidden size of the SwiGLU MLP inside each block.
381
+ timestep_embed_dim (`int`, defaults to 256):
382
+ Width of the sinusoidal timestep embedding before its MLP.
383
+ text_hidden_dim (`int`, defaults to 2560):
384
+ Hidden size of the text encoder whose hidden states are consumed.
385
+ num_text_layers (`int`, defaults to 12):
386
+ Number of tapped text-encoder hidden states stacked per token.
387
+ text_num_attention_heads (`int`, defaults to 20):
388
+ Number of query heads in the text fusion blocks.
389
+ text_num_key_value_heads (`int`, defaults to 20):
390
+ Number of key/value heads in the text fusion blocks.
391
+ text_intermediate_size (`int`, defaults to 6912):
392
+ Feed-forward hidden size of the SwiGLU MLP inside the text fusion blocks.
393
+ num_layerwise_text_blocks (`int`, defaults to 2):
394
+ Number of text fusion blocks applied across the tapped-layer axis (per token).
395
+ num_refiner_text_blocks (`int`, defaults to 2):
396
+ Number of text fusion blocks applied across the token sequence.
397
+ axes_dims_rope (`tuple[int, int, int]`, defaults to `(32, 48, 48)`):
398
+ Head-dim split across the (t, h, w) rotary position axes.
399
+ rope_theta (`float`, defaults to 1000.0):
400
+ Base used by the rotary position embedding.
401
+ norm_eps (`float`, defaults to 1e-5):
402
+ Epsilon used by all RMSNorm modules.
403
+ """
404
+
405
+ _supports_gradient_checkpointing = True
406
+ _no_split_modules = ["Krea2TransformerBlock", "Krea2TextFusionBlock", "Krea2FinalLayer"]
407
+ _repeated_blocks = ["Krea2TransformerBlock"]
408
+ _keep_in_fp32_modules = ["norm", "norm1", "norm2", "norm_q", "norm_k"]
409
+ _skip_layerwise_casting_patterns = ["time_embed", "norm"]
410
+
411
+ @register_to_config
412
+ def __init__(
413
+ self,
414
+ in_channels: int = 64,
415
+ num_layers: int = 28,
416
+ attention_head_dim: int = 128,
417
+ num_attention_heads: int = 48,
418
+ num_key_value_heads: int = 12,
419
+ intermediate_size: int = 16384,
420
+ timestep_embed_dim: int = 256,
421
+ text_hidden_dim: int = 2560,
422
+ num_text_layers: int = 12,
423
+ text_num_attention_heads: int = 20,
424
+ text_num_key_value_heads: int = 20,
425
+ text_intermediate_size: int = 6912,
426
+ num_layerwise_text_blocks: int = 2,
427
+ num_refiner_text_blocks: int = 2,
428
+ axes_dims_rope: tuple[int, int, int] = (32, 48, 48),
429
+ rope_theta: float = 1000.0,
430
+ norm_eps: float = 1e-5,
431
+ ) -> None:
432
+ super().__init__()
433
+
434
+ hidden_size = attention_head_dim * num_attention_heads
435
+ if sum(axes_dims_rope) != attention_head_dim:
436
+ raise ValueError(
437
+ f"sum(axes_dims_rope)={sum(axes_dims_rope)} must equal attention_head_dim={attention_head_dim}"
438
+ )
439
+
440
+ self.in_channels = in_channels
441
+ self.out_channels = in_channels
442
+ self.hidden_size = hidden_size
443
+ self.gradient_checkpointing = False
444
+
445
+ self.img_in = nn.Linear(in_channels, hidden_size, bias=True)
446
+ self.time_embed = Krea2TimestepEmbedding(timestep_embed_dim, hidden_size)
447
+ self.time_mod_proj = nn.Linear(hidden_size, 6 * hidden_size, bias=True)
448
+ self.text_fusion = Krea2TextFusion(
449
+ num_text_layers=num_text_layers,
450
+ dim=text_hidden_dim,
451
+ num_heads=text_num_attention_heads,
452
+ num_kv_heads=text_num_key_value_heads,
453
+ intermediate_size=text_intermediate_size,
454
+ num_layerwise_blocks=num_layerwise_text_blocks,
455
+ num_refiner_blocks=num_refiner_text_blocks,
456
+ eps=norm_eps,
457
+ )
458
+ self.txt_in = Krea2TextProjection(text_hidden_dim, hidden_size, eps=norm_eps)
459
+ self.rotary_emb = Krea2RotaryPosEmbed(theta=rope_theta, axes_dim=list(axes_dims_rope))
460
+
461
+ self.transformer_blocks = nn.ModuleList(
462
+ [
463
+ Krea2TransformerBlock(
464
+ hidden_size=hidden_size,
465
+ intermediate_size=intermediate_size,
466
+ num_heads=num_attention_heads,
467
+ num_kv_heads=num_key_value_heads,
468
+ norm_eps=norm_eps,
469
+ )
470
+ for _ in range(num_layers)
471
+ ]
472
+ )
473
+
474
+ self.final_layer = Krea2FinalLayer(hidden_size, out_channels=in_channels, eps=norm_eps)
475
+
476
+ def forward(
477
+ self,
478
+ hidden_states: torch.Tensor,
479
+ encoder_hidden_states: torch.Tensor,
480
+ timestep: torch.Tensor,
481
+ position_ids: torch.Tensor,
482
+ encoder_attention_mask: torch.Tensor | None = None,
483
+ ref_seq_len: int = 0,
484
+ return_dict: bool = True,
485
+ ) -> Transformer2DModelOutput | tuple[torch.Tensor]:
486
+ r"""
487
+ Predict the flow-matching velocity for the (noisy) image tokens.
488
+
489
+ Args:
490
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_seq_len + ref_seq_len, in_channels)`):
491
+ Packed (patchified) noisy image latents, with any packed clean reference latents appended at the end.
492
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_seq_len, num_text_layers, text_hidden_dim)`):
493
+ Stack of tapped text-encoder hidden states per token.
494
+ timestep (`torch.Tensor` of shape `(batch_size,)`):
495
+ Flow-matching time in `[0, 1]` (1 is pure noise, 0 is clean data).
496
+ position_ids (`torch.Tensor` of shape `(text_seq_len + image_seq_len + ref_seq_len, 3)`):
497
+ `(t, h, w)` rotary coordinates for the combined sequence. Text rows are all-zero; image rows hold the
498
+ latent-grid coordinates; the i-th reference image sits on frame axis `i + 1` with its own grid.
499
+ encoder_attention_mask (`torch.Tensor` of shape `(batch_size, text_seq_len)`, *optional*):
500
+ Boolean mask marking valid text tokens. Pass `None` when every text token is valid.
501
+ ref_seq_len (`int`, *optional*, defaults to 0):
502
+ Number of trailing reference (edit) tokens in `hidden_states`. They are modulated with the t=0
503
+ embedding and excluded from the returned velocity.
504
+ return_dict (`bool`, *optional*, defaults to `True`):
505
+ Whether to return a [`~models.modeling_outputs.Transformer2DModelOutput`] instead of a plain tuple.
506
+
507
+ Returns:
508
+ [`~models.modeling_outputs.Transformer2DModelOutput`] or a `tuple` whose first element is the velocity
509
+ tensor of shape `(batch_size, image_seq_len, in_channels)`.
510
+ """
511
+ if position_ids.ndim != 2 or position_ids.shape[-1] != 3:
512
+ raise ValueError(f"`position_ids` must have shape (sequence_length, 3), got {tuple(position_ids.shape)}.")
513
+
514
+ batch_size, image_seq_len, _ = hidden_states.shape # image_seq_len includes any trailing reference tokens
515
+ text_seq_len = encoder_hidden_states.shape[1]
516
+
517
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
518
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
519
+
520
+ # Clean reference tokens are conditioned at flow time t=0; the text + noisy image tokens keep the real
521
+ # timestep. The blocks then receive a (temb, ref_temb, split) tuple that modulates the two spans separately.
522
+ block_temb = temb_mod
523
+ if ref_seq_len > 0:
524
+ temb_zero = self.time_embed(torch.zeros_like(timestep), dtype=hidden_states.dtype)
525
+ ref_temb_mod = self.time_mod_proj(F.gelu(temb_zero, approximate="tanh"))
526
+ block_temb = (temb_mod, ref_temb_mod, text_seq_len + image_seq_len - ref_seq_len)
527
+
528
+ text_attention_mask = None
529
+ attention_mask = None
530
+ if encoder_attention_mask is not None:
531
+ # Key-padding masks of shape (B, 1, 1, L): padded text tokens are excluded as attention keys everywhere;
532
+ # their own (garbage) lanes are never read back and are dropped at the output slice.
533
+ text_attention_mask = encoder_attention_mask[:, None, None, :]
534
+ image_mask = encoder_attention_mask.new_ones((batch_size, image_seq_len))
535
+ attention_mask = torch.cat([encoder_attention_mask, image_mask], dim=1)[:, None, None, :]
536
+
537
+ encoder_hidden_states = self.text_fusion(encoder_hidden_states, attention_mask=text_attention_mask)
538
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
539
+
540
+ hidden_states = self.img_in(hidden_states)
541
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
542
+
543
+ image_rotary_emb = self.rotary_emb(position_ids)
544
+
545
+ for block in self.transformer_blocks:
546
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
547
+ hidden_states = self._gradient_checkpointing_func(
548
+ block, hidden_states, block_temb, image_rotary_emb, attention_mask
549
+ )
550
+ else:
551
+ hidden_states = block(hidden_states, block_temb, image_rotary_emb, attention_mask)
552
+
553
+ # Keep only the noisy image tokens: drop the leading text tokens and any trailing reference (edit) tokens.
554
+ hidden_states = hidden_states[:, text_seq_len : text_seq_len + image_seq_len - ref_seq_len]
555
+ output = self.final_layer(hidden_states, temb)
556
+
557
+ if not return_dict:
558
+ return (output,)
559
+ return Transformer2DModelOutput(sample=output)
560
+
561
+
562
+ # Register into the diffusers namespace so `ModularPipeline` component loading can resolve the
563
+ # transformer referenced as ["diffusers", "Krea2Transformer2DModel"] in the base repo's model_index.json.
564
+ import diffusers as _diffusers # noqa: E402
565
+
566
+ _diffusers.Krea2Transformer2DModel = Krea2Transformer2DModel