Spaces:
Running on Zero
Running on Zero
Sync the split MiniMax-H3 Spaces (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- diffusers/modular_pipelines/flux2/decoders.py +185 -0
- diffusers/modular_pipelines/flux2/denoise.py +501 -0
- diffusers/modular_pipelines/flux2/encoders.py +608 -0
- diffusers/modular_pipelines/flux2/inputs.py +242 -0
- diffusers/modular_pipelines/flux2/modular_blocks_flux2.py +356 -0
- diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein.py +399 -0
- diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein_base.py +413 -0
- diffusers/modular_pipelines/flux2/modular_pipeline.py +99 -0
- diffusers/modular_pipelines/helios/__init__.py +59 -0
- diffusers/modular_pipelines/helios/before_denoise.py +836 -0
- diffusers/modular_pipelines/helios/decoders.py +112 -0
- diffusers/modular_pipelines/helios/denoise.py +1069 -0
- diffusers/modular_pipelines/helios/encoders.py +392 -0
- diffusers/modular_pipelines/helios/modular_blocks_helios.py +542 -0
- diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid.py +520 -0
- diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid_distilled.py +530 -0
- diffusers/modular_pipelines/helios/modular_pipeline.py +87 -0
- diffusers/modular_pipelines/hunyuan_video1_5/__init__.py +49 -0
- diffusers/modular_pipelines/hunyuan_video1_5/before_denoise.py +324 -0
- diffusers/modular_pipelines/hunyuan_video1_5/decoders.py +70 -0
- diffusers/modular_pipelines/hunyuan_video1_5/denoise.py +401 -0
- diffusers/modular_pipelines/hunyuan_video1_5/encoders.py +441 -0
- diffusers/modular_pipelines/hunyuan_video1_5/modular_blocks_hunyuan_video1_5.py +535 -0
- diffusers/modular_pipelines/hunyuan_video1_5/modular_pipeline.py +90 -0
- diffusers/modular_pipelines/ideogram4/__init__.py +47 -0
- diffusers/modular_pipelines/ideogram4/before_denoise.py +558 -0
- diffusers/modular_pipelines/ideogram4/decoders.py +112 -0
- diffusers/modular_pipelines/ideogram4/denoise.py +363 -0
- diffusers/modular_pipelines/ideogram4/encoders.py +327 -0
- diffusers/modular_pipelines/ideogram4/modular_blocks_ideogram4.py +185 -0
- diffusers/modular_pipelines/ideogram4/modular_pipeline.py +46 -0
- diffusers/modular_pipelines/krea2/__init__.py +49 -0
- diffusers/modular_pipelines/krea2/before_denoise.py +590 -0
- diffusers/modular_pipelines/krea2/decoders.py +121 -0
- diffusers/modular_pipelines/krea2/denoise.py +369 -0
- diffusers/modular_pipelines/krea2/encoders.py +276 -0
- diffusers/modular_pipelines/krea2/modular_blocks_krea2.py +170 -0
- diffusers/modular_pipelines/krea2/modular_blocks_krea2_turbo.py +164 -0
- diffusers/modular_pipelines/krea2/modular_pipeline.py +67 -0
- diffusers/modular_pipelines/ltx/__init__.py +47 -0
- diffusers/modular_pipelines/ltx/before_denoise.py +392 -0
- diffusers/modular_pipelines/ltx/decoders.py +132 -0
- diffusers/modular_pipelines/ltx/denoise.py +458 -0
- diffusers/modular_pipelines/ltx/encoders.py +273 -0
- diffusers/modular_pipelines/ltx/modular_blocks_ltx.py +487 -0
- diffusers/modular_pipelines/ltx/modular_pipeline.py +95 -0
- diffusers/modular_pipelines/mellon_node_utils.py +1101 -0
- diffusers/modular_pipelines/minimax_h3/__init__.py +49 -0
- diffusers/modular_pipelines/minimax_h3/before_denoise.py +425 -0
- diffusers/modular_pipelines/minimax_h3/before_encoder.py +408 -0
diffusers/modular_pipelines/flux2/decoders.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from typing import Any, Union
|
| 18 |
+
|
| 19 |
+
import numpy as np
|
| 20 |
+
import PIL
|
| 21 |
+
import torch
|
| 22 |
+
|
| 23 |
+
from ...configuration_utils import FrozenDict
|
| 24 |
+
from ...models import AutoencoderKLFlux2
|
| 25 |
+
from ...pipelines.flux2.image_processor import Flux2ImageProcessor
|
| 26 |
+
from ...utils import logging
|
| 27 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 28 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Flux2UnpackLatentsStep(ModularPipelineBlocks):
|
| 35 |
+
model_name = "flux2"
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def description(self) -> str:
|
| 39 |
+
return "Step that unpacks the latents from the denoising step"
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 43 |
+
return [
|
| 44 |
+
InputParam(
|
| 45 |
+
"latents",
|
| 46 |
+
required=True,
|
| 47 |
+
type_hint=torch.Tensor,
|
| 48 |
+
description="The denoised latents from the denoising step",
|
| 49 |
+
),
|
| 50 |
+
InputParam(
|
| 51 |
+
"latent_ids",
|
| 52 |
+
required=True,
|
| 53 |
+
type_hint=torch.Tensor,
|
| 54 |
+
description="Position IDs for the latents, used for unpacking",
|
| 55 |
+
),
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def intermediate_outputs(self) -> list[str]:
|
| 60 |
+
return [
|
| 61 |
+
OutputParam(
|
| 62 |
+
"latents",
|
| 63 |
+
type_hint=torch.Tensor,
|
| 64 |
+
description="The denoise latents from denoising step, unpacked with position IDs.",
|
| 65 |
+
)
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
def _unpack_latents_with_ids(x: torch.Tensor, x_ids: torch.Tensor) -> torch.Tensor:
|
| 70 |
+
"""
|
| 71 |
+
Unpack latents using position IDs to scatter tokens into place.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
x: Packed latents tensor of shape (B, seq_len, C)
|
| 75 |
+
x_ids: Position IDs tensor of shape (B, seq_len, 4) with (T, H, W, L) coordinates
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Unpacked latents tensor of shape (B, C, H, W)
|
| 79 |
+
"""
|
| 80 |
+
x_list = []
|
| 81 |
+
for data, pos in zip(x, x_ids):
|
| 82 |
+
_, ch = data.shape # noqa: F841
|
| 83 |
+
h_ids = pos[:, 1].to(torch.int64)
|
| 84 |
+
w_ids = pos[:, 2].to(torch.int64)
|
| 85 |
+
|
| 86 |
+
h = torch.max(h_ids) + 1
|
| 87 |
+
w = torch.max(w_ids) + 1
|
| 88 |
+
|
| 89 |
+
flat_ids = h_ids * w + w_ids
|
| 90 |
+
|
| 91 |
+
out = torch.zeros((h * w, ch), device=data.device, dtype=data.dtype)
|
| 92 |
+
out.scatter_(0, flat_ids.unsqueeze(1).expand(-1, ch), data)
|
| 93 |
+
|
| 94 |
+
out = out.view(h, w, ch).permute(2, 0, 1)
|
| 95 |
+
x_list.append(out)
|
| 96 |
+
|
| 97 |
+
return torch.stack(x_list, dim=0)
|
| 98 |
+
|
| 99 |
+
@torch.no_grad()
|
| 100 |
+
def __call__(self, components, state: PipelineState) -> PipelineState:
|
| 101 |
+
block_state = self.get_block_state(state)
|
| 102 |
+
|
| 103 |
+
latents = block_state.latents
|
| 104 |
+
latent_ids = block_state.latent_ids
|
| 105 |
+
|
| 106 |
+
latents = self._unpack_latents_with_ids(latents, latent_ids)
|
| 107 |
+
|
| 108 |
+
block_state.latents = latents
|
| 109 |
+
|
| 110 |
+
self.set_block_state(state, block_state)
|
| 111 |
+
return components, state
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class Flux2DecodeStep(ModularPipelineBlocks):
|
| 115 |
+
model_name = "flux2"
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 119 |
+
return [
|
| 120 |
+
ComponentSpec("vae", AutoencoderKLFlux2),
|
| 121 |
+
ComponentSpec(
|
| 122 |
+
"image_processor",
|
| 123 |
+
Flux2ImageProcessor,
|
| 124 |
+
config=FrozenDict({"vae_scale_factor": 16, "vae_latent_channels": 32}),
|
| 125 |
+
default_creation_method="from_config",
|
| 126 |
+
),
|
| 127 |
+
]
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def description(self) -> str:
|
| 131 |
+
return "Step that decodes the denoised latents into images using Flux2 VAE with batch norm denormalization"
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 135 |
+
return [
|
| 136 |
+
InputParam("output_type", default="pil"),
|
| 137 |
+
InputParam(
|
| 138 |
+
"latents",
|
| 139 |
+
required=True,
|
| 140 |
+
type_hint=torch.Tensor,
|
| 141 |
+
description="The denoised latents from the denoising step",
|
| 142 |
+
),
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def intermediate_outputs(self) -> list[str]:
|
| 147 |
+
return [
|
| 148 |
+
OutputParam(
|
| 149 |
+
"images",
|
| 150 |
+
type_hint=Union[list[PIL.Image.Image], torch.Tensor, np.ndarray],
|
| 151 |
+
description="The generated images, can be a list of PIL.Image.Image, torch.Tensor or a numpy array",
|
| 152 |
+
)
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
@staticmethod
|
| 156 |
+
def _unpatchify_latents(latents):
|
| 157 |
+
"""Convert patchified latents back to regular format."""
|
| 158 |
+
batch_size, num_channels_latents, height, width = latents.shape
|
| 159 |
+
latents = latents.reshape(batch_size, num_channels_latents // (2 * 2), 2, 2, height, width)
|
| 160 |
+
latents = latents.permute(0, 1, 4, 2, 5, 3)
|
| 161 |
+
latents = latents.reshape(batch_size, num_channels_latents // (2 * 2), height * 2, width * 2)
|
| 162 |
+
return latents
|
| 163 |
+
|
| 164 |
+
@torch.no_grad()
|
| 165 |
+
def __call__(self, components, state: PipelineState) -> PipelineState:
|
| 166 |
+
block_state = self.get_block_state(state)
|
| 167 |
+
vae = components.vae
|
| 168 |
+
|
| 169 |
+
latents = block_state.latents
|
| 170 |
+
|
| 171 |
+
latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(latents.device, latents.dtype)
|
| 172 |
+
latents_bn_std = torch.sqrt(vae.bn.running_var.view(1, -1, 1, 1) + vae.config.batch_norm_eps).to(
|
| 173 |
+
latents.device, latents.dtype
|
| 174 |
+
)
|
| 175 |
+
latents = latents * latents_bn_std + latents_bn_mean
|
| 176 |
+
|
| 177 |
+
latents = self._unpatchify_latents(latents)
|
| 178 |
+
|
| 179 |
+
block_state.images = vae.decode(latents, return_dict=False)[0]
|
| 180 |
+
block_state.images = components.image_processor.postprocess(
|
| 181 |
+
block_state.images, output_type=block_state.output_type
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
self.set_block_state(state, block_state)
|
| 185 |
+
return components, state
|
diffusers/modular_pipelines/flux2/denoise.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...guiders import ClassifierFreeGuidance
|
| 21 |
+
from ...models import Flux2Transformer2DModel
|
| 22 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 23 |
+
from ...utils import is_torch_xla_available, logging
|
| 24 |
+
from ..modular_pipeline import (
|
| 25 |
+
BlockState,
|
| 26 |
+
LoopSequentialPipelineBlocks,
|
| 27 |
+
ModularPipelineBlocks,
|
| 28 |
+
PipelineState,
|
| 29 |
+
)
|
| 30 |
+
from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
|
| 31 |
+
from .modular_pipeline import Flux2KleinModularPipeline, Flux2ModularPipeline
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if is_torch_xla_available():
|
| 35 |
+
import torch_xla.core.xla_model as xm
|
| 36 |
+
|
| 37 |
+
XLA_AVAILABLE = True
|
| 38 |
+
else:
|
| 39 |
+
XLA_AVAILABLE = False
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class Flux2LoopDenoiser(ModularPipelineBlocks):
|
| 46 |
+
model_name = "flux2"
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 50 |
+
return [ComponentSpec("transformer", Flux2Transformer2DModel)]
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def description(self) -> str:
|
| 54 |
+
return (
|
| 55 |
+
"Step within the denoising loop that denoises the latents for Flux2. "
|
| 56 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 57 |
+
"object (e.g. `Flux2DenoiseLoopWrapper`)"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 62 |
+
return [
|
| 63 |
+
InputParam("joint_attention_kwargs"),
|
| 64 |
+
InputParam(
|
| 65 |
+
"latents",
|
| 66 |
+
required=True,
|
| 67 |
+
type_hint=torch.Tensor,
|
| 68 |
+
description="The latents to denoise. Shape: (B, seq_len, C)",
|
| 69 |
+
),
|
| 70 |
+
InputParam(
|
| 71 |
+
"image_latents",
|
| 72 |
+
type_hint=torch.Tensor,
|
| 73 |
+
description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
|
| 74 |
+
),
|
| 75 |
+
InputParam(
|
| 76 |
+
"image_latent_ids",
|
| 77 |
+
type_hint=torch.Tensor,
|
| 78 |
+
description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
|
| 79 |
+
),
|
| 80 |
+
InputParam(
|
| 81 |
+
"guidance",
|
| 82 |
+
required=True,
|
| 83 |
+
type_hint=torch.Tensor,
|
| 84 |
+
description="Guidance scale as a tensor",
|
| 85 |
+
),
|
| 86 |
+
InputParam(
|
| 87 |
+
"prompt_embeds",
|
| 88 |
+
required=True,
|
| 89 |
+
type_hint=torch.Tensor,
|
| 90 |
+
description="Text embeddings from Mistral3",
|
| 91 |
+
),
|
| 92 |
+
InputParam(
|
| 93 |
+
"txt_ids",
|
| 94 |
+
required=True,
|
| 95 |
+
type_hint=torch.Tensor,
|
| 96 |
+
description="4D position IDs for text tokens (T, H, W, L)",
|
| 97 |
+
),
|
| 98 |
+
InputParam(
|
| 99 |
+
"latent_ids",
|
| 100 |
+
required=True,
|
| 101 |
+
type_hint=torch.Tensor,
|
| 102 |
+
description="4D position IDs for latent tokens (T, H, W, L)",
|
| 103 |
+
),
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
@torch.no_grad()
|
| 107 |
+
def __call__(
|
| 108 |
+
self, components: Flux2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 109 |
+
) -> PipelineState:
|
| 110 |
+
latents = block_state.latents
|
| 111 |
+
latent_model_input = latents.to(components.transformer.dtype)
|
| 112 |
+
img_ids = block_state.latent_ids
|
| 113 |
+
|
| 114 |
+
image_latents = getattr(block_state, "image_latents", None)
|
| 115 |
+
if image_latents is not None:
|
| 116 |
+
latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
|
| 117 |
+
image_latent_ids = block_state.image_latent_ids
|
| 118 |
+
img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
|
| 119 |
+
|
| 120 |
+
timestep = t.expand(latents.shape[0]).to(latents.dtype)
|
| 121 |
+
|
| 122 |
+
noise_pred = components.transformer(
|
| 123 |
+
hidden_states=latent_model_input,
|
| 124 |
+
timestep=timestep / 1000,
|
| 125 |
+
guidance=block_state.guidance,
|
| 126 |
+
encoder_hidden_states=block_state.prompt_embeds,
|
| 127 |
+
txt_ids=block_state.txt_ids,
|
| 128 |
+
img_ids=img_ids,
|
| 129 |
+
joint_attention_kwargs=block_state.joint_attention_kwargs,
|
| 130 |
+
return_dict=False,
|
| 131 |
+
)[0]
|
| 132 |
+
|
| 133 |
+
noise_pred = noise_pred[:, : latents.size(1)]
|
| 134 |
+
block_state.noise_pred = noise_pred
|
| 135 |
+
|
| 136 |
+
return components, block_state
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# same as Flux2LoopDenoiser but guidance=None
|
| 140 |
+
class Flux2KleinLoopDenoiser(ModularPipelineBlocks):
|
| 141 |
+
model_name = "flux2-klein"
|
| 142 |
+
|
| 143 |
+
@property
|
| 144 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 145 |
+
return [ComponentSpec("transformer", Flux2Transformer2DModel)]
|
| 146 |
+
|
| 147 |
+
@property
|
| 148 |
+
def description(self) -> str:
|
| 149 |
+
return (
|
| 150 |
+
"Step within the denoising loop that denoises the latents for Flux2. "
|
| 151 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 152 |
+
"object (e.g. `Flux2DenoiseLoopWrapper`)"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 157 |
+
return [
|
| 158 |
+
InputParam("joint_attention_kwargs"),
|
| 159 |
+
InputParam(
|
| 160 |
+
"latents",
|
| 161 |
+
required=True,
|
| 162 |
+
type_hint=torch.Tensor,
|
| 163 |
+
description="The latents to denoise. Shape: (B, seq_len, C)",
|
| 164 |
+
),
|
| 165 |
+
InputParam(
|
| 166 |
+
"image_latents",
|
| 167 |
+
type_hint=torch.Tensor,
|
| 168 |
+
description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
|
| 169 |
+
),
|
| 170 |
+
InputParam(
|
| 171 |
+
"image_latent_ids",
|
| 172 |
+
type_hint=torch.Tensor,
|
| 173 |
+
description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
|
| 174 |
+
),
|
| 175 |
+
InputParam(
|
| 176 |
+
"prompt_embeds",
|
| 177 |
+
required=True,
|
| 178 |
+
type_hint=torch.Tensor,
|
| 179 |
+
description="Text embeddings from Qwen3",
|
| 180 |
+
),
|
| 181 |
+
InputParam(
|
| 182 |
+
"txt_ids",
|
| 183 |
+
required=True,
|
| 184 |
+
type_hint=torch.Tensor,
|
| 185 |
+
description="4D position IDs for text tokens (T, H, W, L)",
|
| 186 |
+
),
|
| 187 |
+
InputParam(
|
| 188 |
+
"latent_ids",
|
| 189 |
+
required=True,
|
| 190 |
+
type_hint=torch.Tensor,
|
| 191 |
+
description="4D position IDs for latent tokens (T, H, W, L)",
|
| 192 |
+
),
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
@torch.no_grad()
|
| 196 |
+
def __call__(
|
| 197 |
+
self, components: Flux2KleinModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 198 |
+
) -> PipelineState:
|
| 199 |
+
latents = block_state.latents
|
| 200 |
+
latent_model_input = latents.to(components.transformer.dtype)
|
| 201 |
+
img_ids = block_state.latent_ids
|
| 202 |
+
|
| 203 |
+
image_latents = getattr(block_state, "image_latents", None)
|
| 204 |
+
if image_latents is not None:
|
| 205 |
+
latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
|
| 206 |
+
image_latent_ids = block_state.image_latent_ids
|
| 207 |
+
img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
|
| 208 |
+
|
| 209 |
+
timestep = t.expand(latents.shape[0]).to(latents.dtype)
|
| 210 |
+
|
| 211 |
+
noise_pred = components.transformer(
|
| 212 |
+
hidden_states=latent_model_input,
|
| 213 |
+
timestep=timestep / 1000,
|
| 214 |
+
guidance=None,
|
| 215 |
+
encoder_hidden_states=block_state.prompt_embeds,
|
| 216 |
+
txt_ids=block_state.txt_ids,
|
| 217 |
+
img_ids=img_ids,
|
| 218 |
+
joint_attention_kwargs=block_state.joint_attention_kwargs,
|
| 219 |
+
return_dict=False,
|
| 220 |
+
)[0]
|
| 221 |
+
|
| 222 |
+
noise_pred = noise_pred[:, : latents.size(1)]
|
| 223 |
+
block_state.noise_pred = noise_pred
|
| 224 |
+
|
| 225 |
+
return components, block_state
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# support CFG for Flux2-Klein base model
|
| 229 |
+
class Flux2KleinBaseLoopDenoiser(ModularPipelineBlocks):
|
| 230 |
+
model_name = "flux2-klein"
|
| 231 |
+
|
| 232 |
+
@property
|
| 233 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 234 |
+
return [
|
| 235 |
+
ComponentSpec("transformer", Flux2Transformer2DModel),
|
| 236 |
+
ComponentSpec(
|
| 237 |
+
"guider",
|
| 238 |
+
ClassifierFreeGuidance,
|
| 239 |
+
config=FrozenDict({"guidance_scale": 4.0}),
|
| 240 |
+
default_creation_method="from_config",
|
| 241 |
+
),
|
| 242 |
+
]
|
| 243 |
+
|
| 244 |
+
@property
|
| 245 |
+
def expected_configs(self) -> list[ConfigSpec]:
|
| 246 |
+
return [
|
| 247 |
+
ConfigSpec(name="is_distilled", default=False),
|
| 248 |
+
]
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def description(self) -> str:
|
| 252 |
+
return (
|
| 253 |
+
"Step within the denoising loop that denoises the latents for Flux2. "
|
| 254 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 255 |
+
"object (e.g. `Flux2DenoiseLoopWrapper`)"
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
@property
|
| 259 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 260 |
+
return [
|
| 261 |
+
InputParam("joint_attention_kwargs"),
|
| 262 |
+
InputParam(
|
| 263 |
+
"latents",
|
| 264 |
+
required=True,
|
| 265 |
+
type_hint=torch.Tensor,
|
| 266 |
+
description="The latents to denoise. Shape: (B, seq_len, C)",
|
| 267 |
+
),
|
| 268 |
+
InputParam(
|
| 269 |
+
"image_latents",
|
| 270 |
+
type_hint=torch.Tensor,
|
| 271 |
+
description="Packed image latents for conditioning. Shape: (B, img_seq_len, C)",
|
| 272 |
+
),
|
| 273 |
+
InputParam(
|
| 274 |
+
"image_latent_ids",
|
| 275 |
+
type_hint=torch.Tensor,
|
| 276 |
+
description="Position IDs for image latents. Shape: (B, img_seq_len, 4)",
|
| 277 |
+
),
|
| 278 |
+
InputParam(
|
| 279 |
+
"prompt_embeds",
|
| 280 |
+
required=True,
|
| 281 |
+
type_hint=torch.Tensor,
|
| 282 |
+
description="Text embeddings from Qwen3",
|
| 283 |
+
),
|
| 284 |
+
InputParam(
|
| 285 |
+
"negative_prompt_embeds",
|
| 286 |
+
required=False,
|
| 287 |
+
type_hint=torch.Tensor,
|
| 288 |
+
description="Negative text embeddings from Qwen3",
|
| 289 |
+
),
|
| 290 |
+
InputParam(
|
| 291 |
+
"txt_ids",
|
| 292 |
+
required=True,
|
| 293 |
+
type_hint=torch.Tensor,
|
| 294 |
+
description="4D position IDs for text tokens (T, H, W, L)",
|
| 295 |
+
),
|
| 296 |
+
InputParam(
|
| 297 |
+
"negative_txt_ids",
|
| 298 |
+
required=False,
|
| 299 |
+
type_hint=torch.Tensor,
|
| 300 |
+
description="4D position IDs for negative text tokens (T, H, W, L)",
|
| 301 |
+
),
|
| 302 |
+
InputParam(
|
| 303 |
+
"latent_ids",
|
| 304 |
+
required=True,
|
| 305 |
+
type_hint=torch.Tensor,
|
| 306 |
+
description="4D position IDs for latent tokens (T, H, W, L)",
|
| 307 |
+
),
|
| 308 |
+
]
|
| 309 |
+
|
| 310 |
+
@torch.no_grad()
|
| 311 |
+
def __call__(
|
| 312 |
+
self, components: Flux2KleinModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 313 |
+
) -> PipelineState:
|
| 314 |
+
latents = block_state.latents
|
| 315 |
+
latent_model_input = latents.to(components.transformer.dtype)
|
| 316 |
+
img_ids = block_state.latent_ids
|
| 317 |
+
|
| 318 |
+
image_latents = getattr(block_state, "image_latents", None)
|
| 319 |
+
if image_latents is not None:
|
| 320 |
+
latent_model_input = torch.cat([latents, image_latents], dim=1).to(components.transformer.dtype)
|
| 321 |
+
image_latent_ids = block_state.image_latent_ids
|
| 322 |
+
img_ids = torch.cat([img_ids, image_latent_ids], dim=1)
|
| 323 |
+
|
| 324 |
+
timestep = t.expand(latents.shape[0]).to(latents.dtype)
|
| 325 |
+
|
| 326 |
+
guider_inputs = {
|
| 327 |
+
"encoder_hidden_states": (
|
| 328 |
+
getattr(block_state, "prompt_embeds", None),
|
| 329 |
+
getattr(block_state, "negative_prompt_embeds", None),
|
| 330 |
+
),
|
| 331 |
+
"txt_ids": (
|
| 332 |
+
getattr(block_state, "txt_ids", None),
|
| 333 |
+
getattr(block_state, "negative_txt_ids", None),
|
| 334 |
+
),
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 338 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 339 |
+
|
| 340 |
+
for guider_state_batch in guider_state:
|
| 341 |
+
components.guider.prepare_models(components.transformer)
|
| 342 |
+
cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
|
| 343 |
+
|
| 344 |
+
noise_pred = components.transformer(
|
| 345 |
+
hidden_states=latent_model_input,
|
| 346 |
+
timestep=timestep / 1000,
|
| 347 |
+
guidance=None,
|
| 348 |
+
img_ids=img_ids,
|
| 349 |
+
joint_attention_kwargs=block_state.joint_attention_kwargs,
|
| 350 |
+
return_dict=False,
|
| 351 |
+
**cond_kwargs,
|
| 352 |
+
)[0]
|
| 353 |
+
guider_state_batch.noise_pred = noise_pred[:, : latents.size(1)]
|
| 354 |
+
components.guider.cleanup_models(components.transformer)
|
| 355 |
+
|
| 356 |
+
# perform guidance
|
| 357 |
+
block_state.noise_pred = components.guider(guider_state)[0]
|
| 358 |
+
|
| 359 |
+
return components, block_state
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class Flux2LoopAfterDenoiser(ModularPipelineBlocks):
|
| 363 |
+
model_name = "flux2"
|
| 364 |
+
|
| 365 |
+
@property
|
| 366 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 367 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 368 |
+
|
| 369 |
+
@property
|
| 370 |
+
def description(self) -> str:
|
| 371 |
+
return (
|
| 372 |
+
"Step within the denoising loop that updates the latents after denoising. "
|
| 373 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 374 |
+
"object (e.g. `Flux2DenoiseLoopWrapper`)"
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
@property
|
| 378 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 379 |
+
return [OutputParam("latents", type_hint=torch.Tensor, description="The denoised latents")]
|
| 380 |
+
|
| 381 |
+
@torch.no_grad()
|
| 382 |
+
def __call__(self, components: Flux2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 383 |
+
latents_dtype = block_state.latents.dtype
|
| 384 |
+
block_state.latents = components.scheduler.step(
|
| 385 |
+
block_state.noise_pred,
|
| 386 |
+
t,
|
| 387 |
+
block_state.latents,
|
| 388 |
+
return_dict=False,
|
| 389 |
+
)[0]
|
| 390 |
+
|
| 391 |
+
if block_state.latents.dtype != latents_dtype:
|
| 392 |
+
if torch.backends.mps.is_available():
|
| 393 |
+
block_state.latents = block_state.latents.to(latents_dtype)
|
| 394 |
+
|
| 395 |
+
return components, block_state
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
class Flux2DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
|
| 399 |
+
model_name = "flux2"
|
| 400 |
+
|
| 401 |
+
@property
|
| 402 |
+
def description(self) -> str:
|
| 403 |
+
return (
|
| 404 |
+
"Pipeline block that iteratively denoises the latents over `timesteps`. "
|
| 405 |
+
"The specific steps within each iteration can be customized with `sub_blocks` attribute"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
@property
|
| 409 |
+
def loop_expected_components(self) -> list[ComponentSpec]:
|
| 410 |
+
return [
|
| 411 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 412 |
+
ComponentSpec("transformer", Flux2Transformer2DModel),
|
| 413 |
+
]
|
| 414 |
+
|
| 415 |
+
@property
|
| 416 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 417 |
+
return [
|
| 418 |
+
InputParam(
|
| 419 |
+
"timesteps",
|
| 420 |
+
required=True,
|
| 421 |
+
type_hint=torch.Tensor,
|
| 422 |
+
description="The timesteps to use for the denoising process.",
|
| 423 |
+
),
|
| 424 |
+
InputParam(
|
| 425 |
+
"num_inference_steps",
|
| 426 |
+
required=True,
|
| 427 |
+
type_hint=int,
|
| 428 |
+
description="The number of inference steps to use for the denoising process.",
|
| 429 |
+
),
|
| 430 |
+
]
|
| 431 |
+
|
| 432 |
+
@torch.no_grad()
|
| 433 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 434 |
+
block_state = self.get_block_state(state)
|
| 435 |
+
|
| 436 |
+
block_state.num_warmup_steps = max(
|
| 437 |
+
len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
|
| 441 |
+
for i, t in enumerate(block_state.timesteps):
|
| 442 |
+
components, block_state = self.loop_step(components, block_state, i=i, t=t)
|
| 443 |
+
|
| 444 |
+
if i == len(block_state.timesteps) - 1 or (
|
| 445 |
+
(i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 446 |
+
):
|
| 447 |
+
progress_bar.update()
|
| 448 |
+
|
| 449 |
+
if XLA_AVAILABLE:
|
| 450 |
+
xm.mark_step()
|
| 451 |
+
|
| 452 |
+
self.set_block_state(state, block_state)
|
| 453 |
+
return components, state
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
class Flux2DenoiseStep(Flux2DenoiseLoopWrapper):
|
| 457 |
+
block_classes = [Flux2LoopDenoiser, Flux2LoopAfterDenoiser]
|
| 458 |
+
block_names = ["denoiser", "after_denoiser"]
|
| 459 |
+
|
| 460 |
+
@property
|
| 461 |
+
def description(self) -> str:
|
| 462 |
+
return (
|
| 463 |
+
"Denoise step that iteratively denoises the latents for Flux2. \n"
|
| 464 |
+
"Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
|
| 465 |
+
"At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
|
| 466 |
+
" - `Flux2LoopDenoiser`\n"
|
| 467 |
+
" - `Flux2LoopAfterDenoiser`\n"
|
| 468 |
+
"This block supports both text-to-image and image-conditioned generation."
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
class Flux2KleinDenoiseStep(Flux2DenoiseLoopWrapper):
|
| 473 |
+
block_classes = [Flux2KleinLoopDenoiser, Flux2LoopAfterDenoiser]
|
| 474 |
+
block_names = ["denoiser", "after_denoiser"]
|
| 475 |
+
|
| 476 |
+
@property
|
| 477 |
+
def description(self) -> str:
|
| 478 |
+
return (
|
| 479 |
+
"Denoise step that iteratively denoises the latents for Flux2. \n"
|
| 480 |
+
"Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
|
| 481 |
+
"At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
|
| 482 |
+
" - `Flux2KleinLoopDenoiser`\n"
|
| 483 |
+
" - `Flux2LoopAfterDenoiser`\n"
|
| 484 |
+
"This block supports both text-to-image and image-conditioned generation."
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
class Flux2KleinBaseDenoiseStep(Flux2DenoiseLoopWrapper):
|
| 489 |
+
block_classes = [Flux2KleinBaseLoopDenoiser, Flux2LoopAfterDenoiser]
|
| 490 |
+
block_names = ["denoiser", "after_denoiser"]
|
| 491 |
+
|
| 492 |
+
@property
|
| 493 |
+
def description(self) -> str:
|
| 494 |
+
return (
|
| 495 |
+
"Denoise step that iteratively denoises the latents for Flux2. \n"
|
| 496 |
+
"Its loop logic is defined in `Flux2DenoiseLoopWrapper.__call__` method \n"
|
| 497 |
+
"At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
|
| 498 |
+
" - `Flux2KleinBaseLoopDenoiser`\n"
|
| 499 |
+
" - `Flux2LoopAfterDenoiser`\n"
|
| 500 |
+
"This block supports both text-to-image and image-conditioned generation."
|
| 501 |
+
)
|
diffusers/modular_pipelines/flux2/encoders.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from transformers import AutoProcessor, Mistral3ForConditionalGeneration, Qwen2TokenizerFast, Qwen3ForCausalLM
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...guiders import ClassifierFreeGuidance
|
| 21 |
+
from ...models import AutoencoderKLFlux2
|
| 22 |
+
from ...utils import logging
|
| 23 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 24 |
+
from ..modular_pipeline_utils import ComponentSpec, ConfigSpec, InputParam, OutputParam
|
| 25 |
+
from .modular_pipeline import Flux2KleinModularPipeline, Flux2ModularPipeline
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def format_text_input(prompts: list[str], system_message: str = None):
|
| 32 |
+
"""Format prompts for Mistral3 chat template."""
|
| 33 |
+
cleaned_txt = [prompt.replace("[IMG]", "") for prompt in prompts]
|
| 34 |
+
|
| 35 |
+
return [
|
| 36 |
+
[
|
| 37 |
+
{
|
| 38 |
+
"role": "system",
|
| 39 |
+
"content": [{"type": "text", "text": system_message}],
|
| 40 |
+
},
|
| 41 |
+
{"role": "user", "content": [{"type": "text", "text": prompt}]},
|
| 42 |
+
]
|
| 43 |
+
for prompt in cleaned_txt
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
|
| 48 |
+
def retrieve_latents(
|
| 49 |
+
encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
|
| 50 |
+
):
|
| 51 |
+
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
|
| 52 |
+
return encoder_output.latent_dist.sample(generator)
|
| 53 |
+
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
|
| 54 |
+
return encoder_output.latent_dist.mode()
|
| 55 |
+
elif hasattr(encoder_output, "latents"):
|
| 56 |
+
return encoder_output.latents
|
| 57 |
+
else:
|
| 58 |
+
raise AttributeError("Could not access latents of provided encoder_output")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class Flux2TextEncoderStep(ModularPipelineBlocks):
|
| 62 |
+
model_name = "flux2"
|
| 63 |
+
|
| 64 |
+
# fmt: off
|
| 65 |
+
DEFAULT_SYSTEM_MESSAGE = "You are an AI that reasons about image descriptions. You give structured responses focusing on object relationships, object attribution and actions without speculation."
|
| 66 |
+
# fmt: on
|
| 67 |
+
|
| 68 |
+
@property
|
| 69 |
+
def description(self) -> str:
|
| 70 |
+
return "Text Encoder step that generates text embeddings using Mistral3 to guide the image generation"
|
| 71 |
+
|
| 72 |
+
@property
|
| 73 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 74 |
+
return [
|
| 75 |
+
ComponentSpec("text_encoder", Mistral3ForConditionalGeneration),
|
| 76 |
+
ComponentSpec("tokenizer", AutoProcessor),
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def inputs(self) -> list[InputParam]:
|
| 81 |
+
return [
|
| 82 |
+
InputParam("prompt"),
|
| 83 |
+
InputParam("max_sequence_length", type_hint=int, default=512, required=False),
|
| 84 |
+
InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(10, 20, 30), required=False),
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 89 |
+
return [
|
| 90 |
+
OutputParam(
|
| 91 |
+
"prompt_embeds",
|
| 92 |
+
kwargs_type="denoiser_input_fields",
|
| 93 |
+
type_hint=torch.Tensor,
|
| 94 |
+
description="Text embeddings from Mistral3 used to guide the image generation",
|
| 95 |
+
),
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
def check_inputs(block_state):
|
| 100 |
+
prompt = block_state.prompt
|
| 101 |
+
if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
|
| 102 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
| 103 |
+
|
| 104 |
+
@staticmethod
|
| 105 |
+
def _get_mistral_3_prompt_embeds(
|
| 106 |
+
text_encoder: Mistral3ForConditionalGeneration,
|
| 107 |
+
tokenizer: AutoProcessor,
|
| 108 |
+
prompt: str | list[str],
|
| 109 |
+
dtype: torch.dtype | None = None,
|
| 110 |
+
device: torch.device | None = None,
|
| 111 |
+
max_sequence_length: int = 512,
|
| 112 |
+
# fmt: off
|
| 113 |
+
system_message: str = "You are an AI that reasons about image descriptions. You give structured responses focusing on object relationships, object attribution and actions without speculation.",
|
| 114 |
+
# fmt: on
|
| 115 |
+
hidden_states_layers: tuple[int] = (10, 20, 30),
|
| 116 |
+
):
|
| 117 |
+
dtype = text_encoder.dtype if dtype is None else dtype
|
| 118 |
+
device = text_encoder.device if device is None else device
|
| 119 |
+
|
| 120 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 121 |
+
|
| 122 |
+
messages_batch = format_text_input(prompts=prompt, system_message=system_message)
|
| 123 |
+
|
| 124 |
+
inputs = tokenizer.apply_chat_template(
|
| 125 |
+
messages_batch,
|
| 126 |
+
add_generation_prompt=False,
|
| 127 |
+
tokenize=True,
|
| 128 |
+
return_dict=True,
|
| 129 |
+
return_tensors="pt",
|
| 130 |
+
padding="max_length",
|
| 131 |
+
truncation=True,
|
| 132 |
+
max_length=max_sequence_length,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
input_ids = inputs["input_ids"].to(device)
|
| 136 |
+
attention_mask = inputs["attention_mask"].to(device)
|
| 137 |
+
|
| 138 |
+
output = text_encoder(
|
| 139 |
+
input_ids=input_ids,
|
| 140 |
+
attention_mask=attention_mask,
|
| 141 |
+
output_hidden_states=True,
|
| 142 |
+
use_cache=False,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
|
| 146 |
+
out = out.to(dtype=dtype, device=device)
|
| 147 |
+
|
| 148 |
+
batch_size, num_channels, seq_len, hidden_dim = out.shape
|
| 149 |
+
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
|
| 150 |
+
|
| 151 |
+
return prompt_embeds
|
| 152 |
+
|
| 153 |
+
@torch.no_grad()
|
| 154 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 155 |
+
block_state = self.get_block_state(state)
|
| 156 |
+
self.check_inputs(block_state)
|
| 157 |
+
|
| 158 |
+
block_state.device = components._execution_device
|
| 159 |
+
|
| 160 |
+
prompt = block_state.prompt
|
| 161 |
+
if prompt is None:
|
| 162 |
+
prompt = ""
|
| 163 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 164 |
+
|
| 165 |
+
block_state.prompt_embeds = self._get_mistral_3_prompt_embeds(
|
| 166 |
+
text_encoder=components.text_encoder,
|
| 167 |
+
tokenizer=components.tokenizer,
|
| 168 |
+
prompt=prompt,
|
| 169 |
+
device=block_state.device,
|
| 170 |
+
max_sequence_length=block_state.max_sequence_length,
|
| 171 |
+
system_message=self.DEFAULT_SYSTEM_MESSAGE,
|
| 172 |
+
hidden_states_layers=block_state.text_encoder_out_layers,
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
self.set_block_state(state, block_state)
|
| 176 |
+
return components, state
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class Flux2RemoteTextEncoderStep(ModularPipelineBlocks):
|
| 180 |
+
model_name = "flux2"
|
| 181 |
+
|
| 182 |
+
REMOTE_URL = "https://remote-text-encoder-flux-2.huggingface.co/predict"
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def description(self) -> str:
|
| 186 |
+
return "Text Encoder step that generates text embeddings using a remote API endpoint"
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 190 |
+
return []
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def inputs(self) -> list[InputParam]:
|
| 194 |
+
return [
|
| 195 |
+
InputParam("prompt"),
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 200 |
+
return [
|
| 201 |
+
OutputParam(
|
| 202 |
+
"prompt_embeds",
|
| 203 |
+
kwargs_type="denoiser_input_fields",
|
| 204 |
+
type_hint=torch.Tensor,
|
| 205 |
+
description="Text embeddings from remote API used to guide the image generation",
|
| 206 |
+
),
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
@staticmethod
|
| 210 |
+
def check_inputs(block_state):
|
| 211 |
+
prompt = block_state.prompt
|
| 212 |
+
if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
|
| 213 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(block_state.prompt)}")
|
| 214 |
+
|
| 215 |
+
@torch.no_grad()
|
| 216 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 217 |
+
import io
|
| 218 |
+
|
| 219 |
+
import requests
|
| 220 |
+
from huggingface_hub import get_token
|
| 221 |
+
|
| 222 |
+
block_state = self.get_block_state(state)
|
| 223 |
+
self.check_inputs(block_state)
|
| 224 |
+
|
| 225 |
+
block_state.device = components._execution_device
|
| 226 |
+
|
| 227 |
+
prompt = block_state.prompt
|
| 228 |
+
if prompt is None:
|
| 229 |
+
prompt = ""
|
| 230 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 231 |
+
|
| 232 |
+
response = requests.post(
|
| 233 |
+
self.REMOTE_URL,
|
| 234 |
+
json={"prompt": prompt},
|
| 235 |
+
headers={
|
| 236 |
+
"Authorization": f"Bearer {get_token()}",
|
| 237 |
+
"Content-Type": "application/json",
|
| 238 |
+
},
|
| 239 |
+
)
|
| 240 |
+
response.raise_for_status()
|
| 241 |
+
|
| 242 |
+
block_state.prompt_embeds = torch.load(io.BytesIO(response.content), weights_only=True)
|
| 243 |
+
block_state.prompt_embeds = block_state.prompt_embeds.to(block_state.device)
|
| 244 |
+
|
| 245 |
+
self.set_block_state(state, block_state)
|
| 246 |
+
return components, state
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class Flux2KleinTextEncoderStep(ModularPipelineBlocks):
|
| 250 |
+
model_name = "flux2-klein"
|
| 251 |
+
|
| 252 |
+
@property
|
| 253 |
+
def description(self) -> str:
|
| 254 |
+
return "Text Encoder step that generates text embeddings using Qwen3 to guide the image generation"
|
| 255 |
+
|
| 256 |
+
@property
|
| 257 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 258 |
+
return [
|
| 259 |
+
ComponentSpec("text_encoder", Qwen3ForCausalLM),
|
| 260 |
+
ComponentSpec("tokenizer", Qwen2TokenizerFast),
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
@property
|
| 264 |
+
def expected_configs(self) -> list[ConfigSpec]:
|
| 265 |
+
return [
|
| 266 |
+
ConfigSpec(name="is_distilled", default=True),
|
| 267 |
+
]
|
| 268 |
+
|
| 269 |
+
@property
|
| 270 |
+
def inputs(self) -> list[InputParam]:
|
| 271 |
+
return [
|
| 272 |
+
InputParam("prompt"),
|
| 273 |
+
InputParam("max_sequence_length", type_hint=int, default=512, required=False),
|
| 274 |
+
InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(9, 18, 27), required=False),
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
@property
|
| 278 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 279 |
+
return [
|
| 280 |
+
OutputParam(
|
| 281 |
+
"prompt_embeds",
|
| 282 |
+
kwargs_type="denoiser_input_fields",
|
| 283 |
+
type_hint=torch.Tensor,
|
| 284 |
+
description="Text embeddings from qwen3 used to guide the image generation",
|
| 285 |
+
),
|
| 286 |
+
]
|
| 287 |
+
|
| 288 |
+
@staticmethod
|
| 289 |
+
def check_inputs(block_state):
|
| 290 |
+
prompt = block_state.prompt
|
| 291 |
+
|
| 292 |
+
if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
|
| 293 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
| 294 |
+
|
| 295 |
+
@staticmethod
|
| 296 |
+
# Copied from diffusers.pipelines.flux2.pipeline_flux2_klein.Flux2KleinPipeline._get_qwen3_prompt_embeds
|
| 297 |
+
def _get_qwen3_prompt_embeds(
|
| 298 |
+
text_encoder: Qwen3ForCausalLM,
|
| 299 |
+
tokenizer: Qwen2TokenizerFast,
|
| 300 |
+
prompt: str | list[str],
|
| 301 |
+
dtype: torch.dtype | None = None,
|
| 302 |
+
device: torch.device | None = None,
|
| 303 |
+
max_sequence_length: int = 512,
|
| 304 |
+
hidden_states_layers: list[int] = (9, 18, 27),
|
| 305 |
+
):
|
| 306 |
+
dtype = text_encoder.dtype if dtype is None else dtype
|
| 307 |
+
device = text_encoder.device if device is None else device
|
| 308 |
+
|
| 309 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 310 |
+
|
| 311 |
+
all_input_ids = []
|
| 312 |
+
all_attention_masks = []
|
| 313 |
+
|
| 314 |
+
for single_prompt in prompt:
|
| 315 |
+
messages = [{"role": "user", "content": single_prompt}]
|
| 316 |
+
text = tokenizer.apply_chat_template(
|
| 317 |
+
messages,
|
| 318 |
+
tokenize=False,
|
| 319 |
+
add_generation_prompt=True,
|
| 320 |
+
enable_thinking=False,
|
| 321 |
+
)
|
| 322 |
+
inputs = tokenizer(
|
| 323 |
+
text,
|
| 324 |
+
return_tensors="pt",
|
| 325 |
+
padding="max_length",
|
| 326 |
+
truncation=True,
|
| 327 |
+
max_length=max_sequence_length,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
all_input_ids.append(inputs["input_ids"])
|
| 331 |
+
all_attention_masks.append(inputs["attention_mask"])
|
| 332 |
+
|
| 333 |
+
input_ids = torch.cat(all_input_ids, dim=0).to(device)
|
| 334 |
+
attention_mask = torch.cat(all_attention_masks, dim=0).to(device)
|
| 335 |
+
|
| 336 |
+
# Forward pass through the model
|
| 337 |
+
output = text_encoder(
|
| 338 |
+
input_ids=input_ids,
|
| 339 |
+
attention_mask=attention_mask,
|
| 340 |
+
output_hidden_states=True,
|
| 341 |
+
use_cache=False,
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
# Only use outputs from intermediate layers and stack them
|
| 345 |
+
out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
|
| 346 |
+
out = out.to(dtype=dtype, device=device)
|
| 347 |
+
|
| 348 |
+
batch_size, num_channels, seq_len, hidden_dim = out.shape
|
| 349 |
+
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
|
| 350 |
+
|
| 351 |
+
return prompt_embeds
|
| 352 |
+
|
| 353 |
+
@torch.no_grad()
|
| 354 |
+
def __call__(self, components: Flux2KleinModularPipeline, state: PipelineState) -> PipelineState:
|
| 355 |
+
block_state = self.get_block_state(state)
|
| 356 |
+
self.check_inputs(block_state)
|
| 357 |
+
|
| 358 |
+
device = components._execution_device
|
| 359 |
+
|
| 360 |
+
prompt = block_state.prompt
|
| 361 |
+
if prompt is None:
|
| 362 |
+
prompt = ""
|
| 363 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 364 |
+
|
| 365 |
+
block_state.prompt_embeds = self._get_qwen3_prompt_embeds(
|
| 366 |
+
text_encoder=components.text_encoder,
|
| 367 |
+
tokenizer=components.tokenizer,
|
| 368 |
+
prompt=prompt,
|
| 369 |
+
device=device,
|
| 370 |
+
max_sequence_length=block_state.max_sequence_length,
|
| 371 |
+
hidden_states_layers=block_state.text_encoder_out_layers,
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
self.set_block_state(state, block_state)
|
| 375 |
+
return components, state
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
class Flux2KleinBaseTextEncoderStep(ModularPipelineBlocks):
|
| 379 |
+
model_name = "flux2-klein"
|
| 380 |
+
|
| 381 |
+
@property
|
| 382 |
+
def description(self) -> str:
|
| 383 |
+
return "Text Encoder step that generates text embeddings using Qwen3 to guide the image generation"
|
| 384 |
+
|
| 385 |
+
@property
|
| 386 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 387 |
+
return [
|
| 388 |
+
ComponentSpec("text_encoder", Qwen3ForCausalLM),
|
| 389 |
+
ComponentSpec("tokenizer", Qwen2TokenizerFast),
|
| 390 |
+
ComponentSpec(
|
| 391 |
+
"guider",
|
| 392 |
+
ClassifierFreeGuidance,
|
| 393 |
+
config=FrozenDict({"guidance_scale": 4.0}),
|
| 394 |
+
default_creation_method="from_config",
|
| 395 |
+
),
|
| 396 |
+
]
|
| 397 |
+
|
| 398 |
+
@property
|
| 399 |
+
def expected_configs(self) -> list[ConfigSpec]:
|
| 400 |
+
return [
|
| 401 |
+
ConfigSpec(name="is_distilled", default=False),
|
| 402 |
+
]
|
| 403 |
+
|
| 404 |
+
@property
|
| 405 |
+
def inputs(self) -> list[InputParam]:
|
| 406 |
+
return [
|
| 407 |
+
InputParam("prompt"),
|
| 408 |
+
InputParam("max_sequence_length", type_hint=int, default=512, required=False),
|
| 409 |
+
InputParam("text_encoder_out_layers", type_hint=tuple[int], default=(9, 18, 27), required=False),
|
| 410 |
+
]
|
| 411 |
+
|
| 412 |
+
@property
|
| 413 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 414 |
+
return [
|
| 415 |
+
OutputParam(
|
| 416 |
+
"prompt_embeds",
|
| 417 |
+
kwargs_type="denoiser_input_fields",
|
| 418 |
+
type_hint=torch.Tensor,
|
| 419 |
+
description="Text embeddings from qwen3 used to guide the image generation",
|
| 420 |
+
),
|
| 421 |
+
OutputParam(
|
| 422 |
+
"negative_prompt_embeds",
|
| 423 |
+
kwargs_type="denoiser_input_fields",
|
| 424 |
+
type_hint=torch.Tensor,
|
| 425 |
+
description="Negative text embeddings from qwen3 used to guide the image generation",
|
| 426 |
+
),
|
| 427 |
+
]
|
| 428 |
+
|
| 429 |
+
@staticmethod
|
| 430 |
+
def check_inputs(block_state):
|
| 431 |
+
prompt = block_state.prompt
|
| 432 |
+
|
| 433 |
+
if prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
|
| 434 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
| 435 |
+
|
| 436 |
+
@staticmethod
|
| 437 |
+
# Copied from diffusers.pipelines.flux2.pipeline_flux2_klein.Flux2KleinPipeline._get_qwen3_prompt_embeds
|
| 438 |
+
def _get_qwen3_prompt_embeds(
|
| 439 |
+
text_encoder: Qwen3ForCausalLM,
|
| 440 |
+
tokenizer: Qwen2TokenizerFast,
|
| 441 |
+
prompt: str | list[str],
|
| 442 |
+
dtype: torch.dtype | None = None,
|
| 443 |
+
device: torch.device | None = None,
|
| 444 |
+
max_sequence_length: int = 512,
|
| 445 |
+
hidden_states_layers: list[int] = (9, 18, 27),
|
| 446 |
+
):
|
| 447 |
+
dtype = text_encoder.dtype if dtype is None else dtype
|
| 448 |
+
device = text_encoder.device if device is None else device
|
| 449 |
+
|
| 450 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 451 |
+
|
| 452 |
+
all_input_ids = []
|
| 453 |
+
all_attention_masks = []
|
| 454 |
+
|
| 455 |
+
for single_prompt in prompt:
|
| 456 |
+
messages = [{"role": "user", "content": single_prompt}]
|
| 457 |
+
text = tokenizer.apply_chat_template(
|
| 458 |
+
messages,
|
| 459 |
+
tokenize=False,
|
| 460 |
+
add_generation_prompt=True,
|
| 461 |
+
enable_thinking=False,
|
| 462 |
+
)
|
| 463 |
+
inputs = tokenizer(
|
| 464 |
+
text,
|
| 465 |
+
return_tensors="pt",
|
| 466 |
+
padding="max_length",
|
| 467 |
+
truncation=True,
|
| 468 |
+
max_length=max_sequence_length,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
all_input_ids.append(inputs["input_ids"])
|
| 472 |
+
all_attention_masks.append(inputs["attention_mask"])
|
| 473 |
+
|
| 474 |
+
input_ids = torch.cat(all_input_ids, dim=0).to(device)
|
| 475 |
+
attention_mask = torch.cat(all_attention_masks, dim=0).to(device)
|
| 476 |
+
|
| 477 |
+
# Forward pass through the model
|
| 478 |
+
output = text_encoder(
|
| 479 |
+
input_ids=input_ids,
|
| 480 |
+
attention_mask=attention_mask,
|
| 481 |
+
output_hidden_states=True,
|
| 482 |
+
use_cache=False,
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
# Only use outputs from intermediate layers and stack them
|
| 486 |
+
out = torch.stack([output.hidden_states[k] for k in hidden_states_layers], dim=1)
|
| 487 |
+
out = out.to(dtype=dtype, device=device)
|
| 488 |
+
|
| 489 |
+
batch_size, num_channels, seq_len, hidden_dim = out.shape
|
| 490 |
+
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
|
| 491 |
+
|
| 492 |
+
return prompt_embeds
|
| 493 |
+
|
| 494 |
+
@torch.no_grad()
|
| 495 |
+
def __call__(self, components: Flux2KleinModularPipeline, state: PipelineState) -> PipelineState:
|
| 496 |
+
block_state = self.get_block_state(state)
|
| 497 |
+
self.check_inputs(block_state)
|
| 498 |
+
|
| 499 |
+
device = components._execution_device
|
| 500 |
+
|
| 501 |
+
prompt = block_state.prompt
|
| 502 |
+
if prompt is None:
|
| 503 |
+
prompt = ""
|
| 504 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 505 |
+
|
| 506 |
+
block_state.prompt_embeds = self._get_qwen3_prompt_embeds(
|
| 507 |
+
text_encoder=components.text_encoder,
|
| 508 |
+
tokenizer=components.tokenizer,
|
| 509 |
+
prompt=prompt,
|
| 510 |
+
device=device,
|
| 511 |
+
max_sequence_length=block_state.max_sequence_length,
|
| 512 |
+
hidden_states_layers=block_state.text_encoder_out_layers,
|
| 513 |
+
)
|
| 514 |
+
|
| 515 |
+
if components.requires_unconditional_embeds:
|
| 516 |
+
negative_prompt = [""] * len(prompt)
|
| 517 |
+
block_state.negative_prompt_embeds = self._get_qwen3_prompt_embeds(
|
| 518 |
+
text_encoder=components.text_encoder,
|
| 519 |
+
tokenizer=components.tokenizer,
|
| 520 |
+
prompt=negative_prompt,
|
| 521 |
+
device=device,
|
| 522 |
+
max_sequence_length=block_state.max_sequence_length,
|
| 523 |
+
hidden_states_layers=block_state.text_encoder_out_layers,
|
| 524 |
+
)
|
| 525 |
+
else:
|
| 526 |
+
block_state.negative_prompt_embeds = None
|
| 527 |
+
|
| 528 |
+
self.set_block_state(state, block_state)
|
| 529 |
+
return components, state
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
class Flux2VaeEncoderStep(ModularPipelineBlocks):
|
| 533 |
+
model_name = "flux2"
|
| 534 |
+
|
| 535 |
+
@property
|
| 536 |
+
def description(self) -> str:
|
| 537 |
+
return "VAE Encoder step that encodes preprocessed images into latent representations for Flux2."
|
| 538 |
+
|
| 539 |
+
@property
|
| 540 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 541 |
+
return [ComponentSpec("vae", AutoencoderKLFlux2)]
|
| 542 |
+
|
| 543 |
+
@property
|
| 544 |
+
def inputs(self) -> list[InputParam]:
|
| 545 |
+
return [
|
| 546 |
+
InputParam("condition_images", type_hint=list[torch.Tensor]),
|
| 547 |
+
InputParam("generator"),
|
| 548 |
+
]
|
| 549 |
+
|
| 550 |
+
@property
|
| 551 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 552 |
+
return [
|
| 553 |
+
OutputParam(
|
| 554 |
+
"image_latents",
|
| 555 |
+
type_hint=list[torch.Tensor],
|
| 556 |
+
description="List of latent representations for each reference image",
|
| 557 |
+
),
|
| 558 |
+
]
|
| 559 |
+
|
| 560 |
+
@staticmethod
|
| 561 |
+
def _patchify_latents(latents):
|
| 562 |
+
"""Convert latents to patchified format for Flux2."""
|
| 563 |
+
batch_size, num_channels_latents, height, width = latents.shape
|
| 564 |
+
latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
|
| 565 |
+
latents = latents.permute(0, 1, 3, 5, 2, 4)
|
| 566 |
+
latents = latents.reshape(batch_size, num_channels_latents * 4, height // 2, width // 2)
|
| 567 |
+
return latents
|
| 568 |
+
|
| 569 |
+
def _encode_vae_image(self, vae: AutoencoderKLFlux2, image: torch.Tensor, generator: torch.Generator):
|
| 570 |
+
"""Encode a single image using Flux2 VAE with batch norm normalization."""
|
| 571 |
+
if image.ndim != 4:
|
| 572 |
+
raise ValueError(f"Expected image dims 4, got {image.ndim}.")
|
| 573 |
+
|
| 574 |
+
image_latents = retrieve_latents(vae.encode(image), generator=generator, sample_mode="argmax")
|
| 575 |
+
image_latents = self._patchify_latents(image_latents)
|
| 576 |
+
|
| 577 |
+
latents_bn_mean = vae.bn.running_mean.view(1, -1, 1, 1).to(image_latents.device, image_latents.dtype)
|
| 578 |
+
latents_bn_std = torch.sqrt(vae.bn.running_var.view(1, -1, 1, 1) + vae.config.batch_norm_eps)
|
| 579 |
+
latents_bn_std = latents_bn_std.to(image_latents.device, image_latents.dtype)
|
| 580 |
+
image_latents = (image_latents - latents_bn_mean) / latents_bn_std
|
| 581 |
+
|
| 582 |
+
return image_latents
|
| 583 |
+
|
| 584 |
+
@torch.no_grad()
|
| 585 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 586 |
+
block_state = self.get_block_state(state)
|
| 587 |
+
condition_images = block_state.condition_images
|
| 588 |
+
|
| 589 |
+
if condition_images is None:
|
| 590 |
+
return components, state
|
| 591 |
+
|
| 592 |
+
device = components._execution_device
|
| 593 |
+
dtype = components.vae.dtype
|
| 594 |
+
|
| 595 |
+
image_latents = []
|
| 596 |
+
for image in condition_images:
|
| 597 |
+
image = image.to(device=device, dtype=dtype)
|
| 598 |
+
latent = self._encode_vae_image(
|
| 599 |
+
vae=components.vae,
|
| 600 |
+
image=image,
|
| 601 |
+
generator=block_state.generator,
|
| 602 |
+
)
|
| 603 |
+
image_latents.append(latent)
|
| 604 |
+
|
| 605 |
+
block_state.image_latents = image_latents
|
| 606 |
+
|
| 607 |
+
self.set_block_state(state, block_state)
|
| 608 |
+
return components, state
|
diffusers/modular_pipelines/flux2/inputs.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ...configuration_utils import FrozenDict
|
| 18 |
+
from ...pipelines.flux2.image_processor import Flux2ImageProcessor
|
| 19 |
+
from ...utils import logging
|
| 20 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 21 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 22 |
+
from .modular_pipeline import Flux2ModularPipeline
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
logger = logging.get_logger(__name__)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Flux2TextInputStep(ModularPipelineBlocks):
|
| 29 |
+
model_name = "flux2"
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def description(self) -> str:
|
| 33 |
+
return (
|
| 34 |
+
"This step:\n"
|
| 35 |
+
" 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
|
| 36 |
+
" 2. Ensures all text embeddings have consistent batch sizes (batch_size * num_images_per_prompt)"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def inputs(self) -> list[InputParam]:
|
| 41 |
+
return [
|
| 42 |
+
InputParam("num_images_per_prompt", default=1),
|
| 43 |
+
InputParam(
|
| 44 |
+
"prompt_embeds",
|
| 45 |
+
required=True,
|
| 46 |
+
kwargs_type="denoiser_input_fields",
|
| 47 |
+
type_hint=torch.Tensor,
|
| 48 |
+
description="Pre-generated text embeddings. Can be generated from text_encoder step.",
|
| 49 |
+
),
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def intermediate_outputs(self) -> list[str]:
|
| 54 |
+
return [
|
| 55 |
+
OutputParam(
|
| 56 |
+
"batch_size",
|
| 57 |
+
type_hint=int,
|
| 58 |
+
description="Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt",
|
| 59 |
+
),
|
| 60 |
+
OutputParam(
|
| 61 |
+
"dtype",
|
| 62 |
+
type_hint=torch.dtype,
|
| 63 |
+
description="Data type of model tensor inputs (determined by `prompt_embeds`)",
|
| 64 |
+
),
|
| 65 |
+
OutputParam(
|
| 66 |
+
"prompt_embeds",
|
| 67 |
+
type_hint=torch.Tensor,
|
| 68 |
+
kwargs_type="denoiser_input_fields",
|
| 69 |
+
description="Text embeddings used to guide the image generation",
|
| 70 |
+
),
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
@torch.no_grad()
|
| 74 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 75 |
+
block_state = self.get_block_state(state)
|
| 76 |
+
|
| 77 |
+
block_state.batch_size = block_state.prompt_embeds.shape[0]
|
| 78 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 79 |
+
|
| 80 |
+
_, seq_len, _ = block_state.prompt_embeds.shape
|
| 81 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_images_per_prompt, 1)
|
| 82 |
+
block_state.prompt_embeds = block_state.prompt_embeds.view(
|
| 83 |
+
block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
self.set_block_state(state, block_state)
|
| 87 |
+
return components, state
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class Flux2KleinBaseTextInputStep(ModularPipelineBlocks):
|
| 91 |
+
model_name = "flux2-klein"
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def description(self) -> str:
|
| 95 |
+
return (
|
| 96 |
+
"This step:\n"
|
| 97 |
+
" 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
|
| 98 |
+
" 2. Ensures all text embeddings have consistent batch sizes (batch_size * num_images_per_prompt)"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def inputs(self) -> list[InputParam]:
|
| 103 |
+
return [
|
| 104 |
+
InputParam("num_images_per_prompt", default=1),
|
| 105 |
+
InputParam(
|
| 106 |
+
"prompt_embeds",
|
| 107 |
+
required=True,
|
| 108 |
+
kwargs_type="denoiser_input_fields",
|
| 109 |
+
type_hint=torch.Tensor,
|
| 110 |
+
description="Pre-generated text embeddings. Can be generated from text_encoder step.",
|
| 111 |
+
),
|
| 112 |
+
InputParam(
|
| 113 |
+
"negative_prompt_embeds",
|
| 114 |
+
required=False,
|
| 115 |
+
kwargs_type="denoiser_input_fields",
|
| 116 |
+
type_hint=torch.Tensor,
|
| 117 |
+
description="Pre-generated negative text embeddings. Can be generated from text_encoder step.",
|
| 118 |
+
),
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def intermediate_outputs(self) -> list[str]:
|
| 123 |
+
return [
|
| 124 |
+
OutputParam(
|
| 125 |
+
"batch_size",
|
| 126 |
+
type_hint=int,
|
| 127 |
+
description="Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt",
|
| 128 |
+
),
|
| 129 |
+
OutputParam(
|
| 130 |
+
"dtype",
|
| 131 |
+
type_hint=torch.dtype,
|
| 132 |
+
description="Data type of model tensor inputs (determined by `prompt_embeds`)",
|
| 133 |
+
),
|
| 134 |
+
OutputParam(
|
| 135 |
+
"prompt_embeds",
|
| 136 |
+
type_hint=torch.Tensor,
|
| 137 |
+
kwargs_type="denoiser_input_fields",
|
| 138 |
+
description="Text embeddings used to guide the image generation",
|
| 139 |
+
),
|
| 140 |
+
OutputParam(
|
| 141 |
+
"negative_prompt_embeds",
|
| 142 |
+
type_hint=torch.Tensor,
|
| 143 |
+
kwargs_type="denoiser_input_fields",
|
| 144 |
+
description="Negative text embeddings used to guide the image generation",
|
| 145 |
+
),
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
@torch.no_grad()
|
| 149 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 150 |
+
block_state = self.get_block_state(state)
|
| 151 |
+
|
| 152 |
+
block_state.batch_size = block_state.prompt_embeds.shape[0]
|
| 153 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 154 |
+
|
| 155 |
+
_, seq_len, _ = block_state.prompt_embeds.shape
|
| 156 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_images_per_prompt, 1)
|
| 157 |
+
block_state.prompt_embeds = block_state.prompt_embeds.view(
|
| 158 |
+
block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
if block_state.negative_prompt_embeds is not None:
|
| 162 |
+
_, seq_len, _ = block_state.negative_prompt_embeds.shape
|
| 163 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(
|
| 164 |
+
1, block_state.num_images_per_prompt, 1
|
| 165 |
+
)
|
| 166 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
|
| 167 |
+
block_state.batch_size * block_state.num_images_per_prompt, seq_len, -1
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
self.set_block_state(state, block_state)
|
| 171 |
+
return components, state
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class Flux2ProcessImagesInputStep(ModularPipelineBlocks):
|
| 175 |
+
model_name = "flux2"
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def description(self) -> str:
|
| 179 |
+
return "Image preprocess step for Flux2. Validates and preprocesses reference images."
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 183 |
+
return [
|
| 184 |
+
ComponentSpec(
|
| 185 |
+
"image_processor",
|
| 186 |
+
Flux2ImageProcessor,
|
| 187 |
+
config=FrozenDict({"vae_scale_factor": 16, "vae_latent_channels": 32}),
|
| 188 |
+
default_creation_method="from_config",
|
| 189 |
+
),
|
| 190 |
+
]
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def inputs(self) -> list[InputParam]:
|
| 194 |
+
return [
|
| 195 |
+
InputParam("image"),
|
| 196 |
+
InputParam("height"),
|
| 197 |
+
InputParam("width"),
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 202 |
+
return [OutputParam(name="condition_images", type_hint=list[torch.Tensor])]
|
| 203 |
+
|
| 204 |
+
@torch.no_grad()
|
| 205 |
+
def __call__(self, components: Flux2ModularPipeline, state: PipelineState):
|
| 206 |
+
block_state = self.get_block_state(state)
|
| 207 |
+
images = block_state.image
|
| 208 |
+
|
| 209 |
+
if images is None:
|
| 210 |
+
block_state.condition_images = None
|
| 211 |
+
self.set_block_state(state, block_state)
|
| 212 |
+
return components, state
|
| 213 |
+
|
| 214 |
+
if not isinstance(images, list):
|
| 215 |
+
images = [images]
|
| 216 |
+
|
| 217 |
+
condition_images = []
|
| 218 |
+
for img in images:
|
| 219 |
+
components.image_processor.check_image_input(img)
|
| 220 |
+
|
| 221 |
+
image_width, image_height = img.size
|
| 222 |
+
if image_width * image_height > 1024 * 1024:
|
| 223 |
+
img = components.image_processor._resize_to_target_area(img, 1024 * 1024)
|
| 224 |
+
image_width, image_height = img.size
|
| 225 |
+
|
| 226 |
+
multiple_of = components.vae_scale_factor * 2
|
| 227 |
+
image_width = (image_width // multiple_of) * multiple_of
|
| 228 |
+
image_height = (image_height // multiple_of) * multiple_of
|
| 229 |
+
condition_img = components.image_processor.preprocess(
|
| 230 |
+
img, height=image_height, width=image_width, resize_mode="crop"
|
| 231 |
+
)
|
| 232 |
+
condition_images.append(condition_img)
|
| 233 |
+
|
| 234 |
+
if block_state.height is None:
|
| 235 |
+
block_state.height = image_height
|
| 236 |
+
if block_state.width is None:
|
| 237 |
+
block_state.width = image_width
|
| 238 |
+
|
| 239 |
+
block_state.condition_images = condition_images
|
| 240 |
+
|
| 241 |
+
self.set_block_state(state, block_state)
|
| 242 |
+
return components, state
|
diffusers/modular_pipelines/flux2/modular_blocks_flux2.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Flux2PrepareGuidanceStep,
|
| 21 |
+
Flux2PrepareImageLatentsStep,
|
| 22 |
+
Flux2PrepareLatentsStep,
|
| 23 |
+
Flux2RoPEInputsStep,
|
| 24 |
+
Flux2SetTimestepsStep,
|
| 25 |
+
)
|
| 26 |
+
from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
|
| 27 |
+
from .denoise import Flux2DenoiseStep
|
| 28 |
+
from .encoders import (
|
| 29 |
+
Flux2TextEncoderStep,
|
| 30 |
+
Flux2VaeEncoderStep,
|
| 31 |
+
)
|
| 32 |
+
from .inputs import (
|
| 33 |
+
Flux2ProcessImagesInputStep,
|
| 34 |
+
Flux2TextInputStep,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# auto_docstring
|
| 42 |
+
class Flux2VaeEncoderSequentialStep(SequentialPipelineBlocks):
|
| 43 |
+
"""
|
| 44 |
+
VAE encoder step that preprocesses, encodes, and prepares image latents for Flux2 conditioning.
|
| 45 |
+
|
| 46 |
+
Components:
|
| 47 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 48 |
+
|
| 49 |
+
Inputs:
|
| 50 |
+
image (`None`, *optional*):
|
| 51 |
+
TODO: Add description.
|
| 52 |
+
height (`None`, *optional*):
|
| 53 |
+
TODO: Add description.
|
| 54 |
+
width (`None`, *optional*):
|
| 55 |
+
TODO: Add description.
|
| 56 |
+
generator (`None`, *optional*):
|
| 57 |
+
TODO: Add description.
|
| 58 |
+
|
| 59 |
+
Outputs:
|
| 60 |
+
condition_images (`list`):
|
| 61 |
+
TODO: Add description.
|
| 62 |
+
image_latents (`list`):
|
| 63 |
+
List of latent representations for each reference image
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
model_name = "flux2"
|
| 67 |
+
|
| 68 |
+
block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
|
| 69 |
+
block_names = ["preprocess", "encode"]
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def description(self) -> str:
|
| 73 |
+
return "VAE encoder step that preprocesses, encodes, and prepares image latents for Flux2 conditioning."
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# auto_docstring
|
| 77 |
+
class Flux2AutoVaeEncoderStep(AutoPipelineBlocks):
|
| 78 |
+
"""
|
| 79 |
+
VAE encoder step that encodes the image inputs into their latent representations.
|
| 80 |
+
This is an auto pipeline block that works for image conditioning tasks.
|
| 81 |
+
- `Flux2VaeEncoderSequentialStep` is used when `image` is provided.
|
| 82 |
+
- If `image` is not provided, step will be skipped.
|
| 83 |
+
|
| 84 |
+
Components:
|
| 85 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 86 |
+
|
| 87 |
+
Inputs:
|
| 88 |
+
image (`None`, *optional*):
|
| 89 |
+
TODO: Add description.
|
| 90 |
+
height (`None`, *optional*):
|
| 91 |
+
TODO: Add description.
|
| 92 |
+
width (`None`, *optional*):
|
| 93 |
+
TODO: Add description.
|
| 94 |
+
generator (`None`, *optional*):
|
| 95 |
+
TODO: Add description.
|
| 96 |
+
|
| 97 |
+
Outputs:
|
| 98 |
+
condition_images (`list`):
|
| 99 |
+
TODO: Add description.
|
| 100 |
+
image_latents (`list`):
|
| 101 |
+
List of latent representations for each reference image
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
block_classes = [Flux2VaeEncoderSequentialStep]
|
| 105 |
+
block_names = ["img_conditioning"]
|
| 106 |
+
block_trigger_inputs = ["image"]
|
| 107 |
+
|
| 108 |
+
@property
|
| 109 |
+
def description(self):
|
| 110 |
+
return (
|
| 111 |
+
"VAE encoder step that encodes the image inputs into their latent representations.\n"
|
| 112 |
+
"This is an auto pipeline block that works for image conditioning tasks.\n"
|
| 113 |
+
" - `Flux2VaeEncoderSequentialStep` is used when `image` is provided.\n"
|
| 114 |
+
" - If `image` is not provided, step will be skipped."
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
Flux2CoreDenoiseBlocks = InsertableDict(
|
| 119 |
+
[
|
| 120 |
+
("input", Flux2TextInputStep()),
|
| 121 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 122 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 123 |
+
("prepare_guidance", Flux2PrepareGuidanceStep()),
|
| 124 |
+
("prepare_rope_inputs", Flux2RoPEInputsStep()),
|
| 125 |
+
("denoise", Flux2DenoiseStep()),
|
| 126 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 127 |
+
]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# auto_docstring
|
| 132 |
+
class Flux2CoreDenoiseStep(SequentialPipelineBlocks):
|
| 133 |
+
"""
|
| 134 |
+
Core denoise step that performs the denoising process for Flux2-dev.
|
| 135 |
+
|
| 136 |
+
Components:
|
| 137 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 138 |
+
|
| 139 |
+
Inputs:
|
| 140 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 141 |
+
TODO: Add description.
|
| 142 |
+
prompt_embeds (`Tensor`):
|
| 143 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 144 |
+
height (`int`, *optional*):
|
| 145 |
+
TODO: Add description.
|
| 146 |
+
width (`int`, *optional*):
|
| 147 |
+
TODO: Add description.
|
| 148 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 149 |
+
TODO: Add description.
|
| 150 |
+
generator (`None`, *optional*):
|
| 151 |
+
TODO: Add description.
|
| 152 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 153 |
+
TODO: Add description.
|
| 154 |
+
timesteps (`None`, *optional*):
|
| 155 |
+
TODO: Add description.
|
| 156 |
+
sigmas (`None`, *optional*):
|
| 157 |
+
TODO: Add description.
|
| 158 |
+
guidance_scale (`None`, *optional*, defaults to 4.0):
|
| 159 |
+
TODO: Add description.
|
| 160 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 161 |
+
TODO: Add description.
|
| 162 |
+
image_latents (`Tensor`, *optional*):
|
| 163 |
+
Packed image latents for conditioning. Shape: (B, img_seq_len, C)
|
| 164 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 165 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 166 |
+
|
| 167 |
+
Outputs:
|
| 168 |
+
latents (`Tensor`):
|
| 169 |
+
Denoised latents.
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
model_name = "flux2"
|
| 173 |
+
|
| 174 |
+
block_classes = Flux2CoreDenoiseBlocks.values()
|
| 175 |
+
block_names = Flux2CoreDenoiseBlocks.keys()
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def description(self):
|
| 179 |
+
return "Core denoise step that performs the denoising process for Flux2-dev."
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def outputs(self):
|
| 183 |
+
return [
|
| 184 |
+
OutputParam.template("latents"),
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
Flux2ImageConditionedCoreDenoiseBlocks = InsertableDict(
|
| 189 |
+
[
|
| 190 |
+
("input", Flux2TextInputStep()),
|
| 191 |
+
("prepare_image_latents", Flux2PrepareImageLatentsStep()),
|
| 192 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 193 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 194 |
+
("prepare_guidance", Flux2PrepareGuidanceStep()),
|
| 195 |
+
("prepare_rope_inputs", Flux2RoPEInputsStep()),
|
| 196 |
+
("denoise", Flux2DenoiseStep()),
|
| 197 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 198 |
+
]
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# auto_docstring
|
| 203 |
+
class Flux2ImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
|
| 204 |
+
"""
|
| 205 |
+
Core denoise step that performs the denoising process for Flux2-dev with image conditioning.
|
| 206 |
+
|
| 207 |
+
Components:
|
| 208 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 209 |
+
|
| 210 |
+
Inputs:
|
| 211 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 212 |
+
TODO: Add description.
|
| 213 |
+
prompt_embeds (`Tensor`):
|
| 214 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 215 |
+
image_latents (`list`, *optional*):
|
| 216 |
+
TODO: Add description.
|
| 217 |
+
height (`int`, *optional*):
|
| 218 |
+
TODO: Add description.
|
| 219 |
+
width (`int`, *optional*):
|
| 220 |
+
TODO: Add description.
|
| 221 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 222 |
+
TODO: Add description.
|
| 223 |
+
generator (`None`, *optional*):
|
| 224 |
+
TODO: Add description.
|
| 225 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 226 |
+
TODO: Add description.
|
| 227 |
+
timesteps (`None`, *optional*):
|
| 228 |
+
TODO: Add description.
|
| 229 |
+
sigmas (`None`, *optional*):
|
| 230 |
+
TODO: Add description.
|
| 231 |
+
guidance_scale (`None`, *optional*, defaults to 4.0):
|
| 232 |
+
TODO: Add description.
|
| 233 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 234 |
+
TODO: Add description.
|
| 235 |
+
|
| 236 |
+
Outputs:
|
| 237 |
+
latents (`Tensor`):
|
| 238 |
+
Denoised latents.
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
model_name = "flux2"
|
| 242 |
+
|
| 243 |
+
block_classes = Flux2ImageConditionedCoreDenoiseBlocks.values()
|
| 244 |
+
block_names = Flux2ImageConditionedCoreDenoiseBlocks.keys()
|
| 245 |
+
|
| 246 |
+
@property
|
| 247 |
+
def description(self):
|
| 248 |
+
return "Core denoise step that performs the denoising process for Flux2-dev with image conditioning."
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def outputs(self):
|
| 252 |
+
return [
|
| 253 |
+
OutputParam.template("latents"),
|
| 254 |
+
]
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
class Flux2AutoCoreDenoiseStep(AutoPipelineBlocks):
|
| 258 |
+
model_name = "flux2"
|
| 259 |
+
|
| 260 |
+
block_classes = [Flux2ImageConditionedCoreDenoiseStep, Flux2CoreDenoiseStep]
|
| 261 |
+
block_names = ["image_conditioned", "text2image"]
|
| 262 |
+
block_trigger_inputs = ["image_latents", None]
|
| 263 |
+
|
| 264 |
+
@property
|
| 265 |
+
def description(self):
|
| 266 |
+
return (
|
| 267 |
+
"Auto core denoise step that performs the denoising process for Flux2-dev."
|
| 268 |
+
"This is an auto pipeline block that works for text-to-image and image-conditioned generation."
|
| 269 |
+
" - `Flux2CoreDenoiseStep` is used for text-to-image generation.\n"
|
| 270 |
+
" - `Flux2ImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
AUTO_BLOCKS = InsertableDict(
|
| 275 |
+
[
|
| 276 |
+
("text_encoder", Flux2TextEncoderStep()),
|
| 277 |
+
("vae_encoder", Flux2AutoVaeEncoderStep()),
|
| 278 |
+
("denoise", Flux2AutoCoreDenoiseStep()),
|
| 279 |
+
("decode", Flux2DecodeStep()),
|
| 280 |
+
]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# auto_docstring
|
| 285 |
+
class Flux2AutoBlocks(SequentialPipelineBlocks):
|
| 286 |
+
"""
|
| 287 |
+
Auto Modular pipeline for text-to-image and image-conditioned generation using Flux2.
|
| 288 |
+
|
| 289 |
+
Supported workflows:
|
| 290 |
+
- `text2image`: requires `prompt`
|
| 291 |
+
- `image_conditioned`: requires `image`, `prompt`
|
| 292 |
+
|
| 293 |
+
Components:
|
| 294 |
+
text_encoder (`Mistral3ForConditionalGeneration`) tokenizer (`AutoProcessor`) image_processor
|
| 295 |
+
(`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
|
| 296 |
+
(`Flux2Transformer2DModel`)
|
| 297 |
+
|
| 298 |
+
Inputs:
|
| 299 |
+
prompt (`None`, *optional*):
|
| 300 |
+
TODO: Add description.
|
| 301 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 302 |
+
TODO: Add description.
|
| 303 |
+
text_encoder_out_layers (`tuple`, *optional*, defaults to (10, 20, 30)):
|
| 304 |
+
TODO: Add description.
|
| 305 |
+
image (`None`, *optional*):
|
| 306 |
+
TODO: Add description.
|
| 307 |
+
height (`None`, *optional*):
|
| 308 |
+
TODO: Add description.
|
| 309 |
+
width (`None`, *optional*):
|
| 310 |
+
TODO: Add description.
|
| 311 |
+
generator (`None`, *optional*):
|
| 312 |
+
TODO: Add description.
|
| 313 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 314 |
+
TODO: Add description.
|
| 315 |
+
image_latents (`list`, *optional*):
|
| 316 |
+
TODO: Add description.
|
| 317 |
+
latents (`Tensor | NoneType`):
|
| 318 |
+
TODO: Add description.
|
| 319 |
+
num_inference_steps (`None`):
|
| 320 |
+
TODO: Add description.
|
| 321 |
+
timesteps (`None`):
|
| 322 |
+
TODO: Add description.
|
| 323 |
+
sigmas (`None`, *optional*):
|
| 324 |
+
TODO: Add description.
|
| 325 |
+
guidance_scale (`None`, *optional*, defaults to 4.0):
|
| 326 |
+
TODO: Add description.
|
| 327 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 328 |
+
TODO: Add description.
|
| 329 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 330 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 331 |
+
output_type (`None`, *optional*, defaults to pil):
|
| 332 |
+
TODO: Add description.
|
| 333 |
+
|
| 334 |
+
Outputs:
|
| 335 |
+
images (`list`):
|
| 336 |
+
Generated images.
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
model_name = "flux2"
|
| 340 |
+
|
| 341 |
+
block_classes = AUTO_BLOCKS.values()
|
| 342 |
+
block_names = AUTO_BLOCKS.keys()
|
| 343 |
+
_workflow_map = {
|
| 344 |
+
"text2image": {"prompt": True},
|
| 345 |
+
"image_conditioned": {"image": True, "prompt": True},
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
@property
|
| 349 |
+
def description(self):
|
| 350 |
+
return "Auto Modular pipeline for text-to-image and image-conditioned generation using Flux2."
|
| 351 |
+
|
| 352 |
+
@property
|
| 353 |
+
def outputs(self):
|
| 354 |
+
return [
|
| 355 |
+
OutputParam.template("images"),
|
| 356 |
+
]
|
diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Flux2PrepareImageLatentsStep,
|
| 21 |
+
Flux2PrepareLatentsStep,
|
| 22 |
+
Flux2RoPEInputsStep,
|
| 23 |
+
Flux2SetTimestepsStep,
|
| 24 |
+
)
|
| 25 |
+
from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
|
| 26 |
+
from .denoise import Flux2KleinDenoiseStep
|
| 27 |
+
from .encoders import (
|
| 28 |
+
Flux2KleinTextEncoderStep,
|
| 29 |
+
Flux2VaeEncoderStep,
|
| 30 |
+
)
|
| 31 |
+
from .inputs import (
|
| 32 |
+
Flux2ProcessImagesInputStep,
|
| 33 |
+
Flux2TextInputStep,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 38 |
+
|
| 39 |
+
################
|
| 40 |
+
# VAE encoder
|
| 41 |
+
################
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# auto_docstring
|
| 45 |
+
class Flux2KleinVaeEncoderSequentialStep(SequentialPipelineBlocks):
|
| 46 |
+
"""
|
| 47 |
+
VAE encoder step that preprocesses and encodes the image inputs into their latent representations.
|
| 48 |
+
|
| 49 |
+
Components:
|
| 50 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 51 |
+
|
| 52 |
+
Inputs:
|
| 53 |
+
image (`None`, *optional*):
|
| 54 |
+
TODO: Add description.
|
| 55 |
+
height (`None`, *optional*):
|
| 56 |
+
TODO: Add description.
|
| 57 |
+
width (`None`, *optional*):
|
| 58 |
+
TODO: Add description.
|
| 59 |
+
generator (`None`, *optional*):
|
| 60 |
+
TODO: Add description.
|
| 61 |
+
|
| 62 |
+
Outputs:
|
| 63 |
+
condition_images (`list`):
|
| 64 |
+
TODO: Add description.
|
| 65 |
+
image_latents (`list`):
|
| 66 |
+
List of latent representations for each reference image
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
model_name = "flux2-klein"
|
| 70 |
+
|
| 71 |
+
block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
|
| 72 |
+
block_names = ["preprocess", "encode"]
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def description(self) -> str:
|
| 76 |
+
return "VAE encoder step that preprocesses and encodes the image inputs into their latent representations."
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# auto_docstring
|
| 80 |
+
class Flux2KleinAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 81 |
+
"""
|
| 82 |
+
VAE encoder step that encodes the image inputs into their latent representations.
|
| 83 |
+
This is an auto pipeline block that works for image conditioning tasks.
|
| 84 |
+
- `Flux2KleinVaeEncoderSequentialStep` is used when `image` is provided.
|
| 85 |
+
- If `image` is not provided, step will be skipped.
|
| 86 |
+
|
| 87 |
+
Components:
|
| 88 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 89 |
+
|
| 90 |
+
Inputs:
|
| 91 |
+
image (`None`, *optional*):
|
| 92 |
+
TODO: Add description.
|
| 93 |
+
height (`None`, *optional*):
|
| 94 |
+
TODO: Add description.
|
| 95 |
+
width (`None`, *optional*):
|
| 96 |
+
TODO: Add description.
|
| 97 |
+
generator (`None`, *optional*):
|
| 98 |
+
TODO: Add description.
|
| 99 |
+
|
| 100 |
+
Outputs:
|
| 101 |
+
condition_images (`list`):
|
| 102 |
+
TODO: Add description.
|
| 103 |
+
image_latents (`list`):
|
| 104 |
+
List of latent representations for each reference image
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
model_name = "flux2-klein"
|
| 108 |
+
|
| 109 |
+
block_classes = [Flux2KleinVaeEncoderSequentialStep]
|
| 110 |
+
block_names = ["img_conditioning"]
|
| 111 |
+
block_trigger_inputs = ["image"]
|
| 112 |
+
|
| 113 |
+
@property
|
| 114 |
+
def description(self):
|
| 115 |
+
return (
|
| 116 |
+
"VAE encoder step that encodes the image inputs into their latent representations.\n"
|
| 117 |
+
"This is an auto pipeline block that works for image conditioning tasks.\n"
|
| 118 |
+
" - `Flux2KleinVaeEncoderSequentialStep` is used when `image` is provided.\n"
|
| 119 |
+
" - If `image` is not provided, step will be skipped."
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
###
|
| 124 |
+
### Core denoise
|
| 125 |
+
###
|
| 126 |
+
|
| 127 |
+
Flux2KleinCoreDenoiseBlocks = InsertableDict(
|
| 128 |
+
[
|
| 129 |
+
("input", Flux2TextInputStep()),
|
| 130 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 131 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 132 |
+
("prepare_rope_inputs", Flux2RoPEInputsStep()),
|
| 133 |
+
("denoise", Flux2KleinDenoiseStep()),
|
| 134 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 135 |
+
]
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# auto_docstring
|
| 140 |
+
class Flux2KleinCoreDenoiseStep(SequentialPipelineBlocks):
|
| 141 |
+
"""
|
| 142 |
+
Core denoise step that performs the denoising process for Flux2-Klein (distilled model), for text-to-image
|
| 143 |
+
generation.
|
| 144 |
+
|
| 145 |
+
Components:
|
| 146 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 147 |
+
|
| 148 |
+
Inputs:
|
| 149 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 150 |
+
TODO: Add description.
|
| 151 |
+
prompt_embeds (`Tensor`):
|
| 152 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 153 |
+
height (`int`, *optional*):
|
| 154 |
+
TODO: Add description.
|
| 155 |
+
width (`int`, *optional*):
|
| 156 |
+
TODO: Add description.
|
| 157 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 158 |
+
TODO: Add description.
|
| 159 |
+
generator (`None`, *optional*):
|
| 160 |
+
TODO: Add description.
|
| 161 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 162 |
+
TODO: Add description.
|
| 163 |
+
timesteps (`None`, *optional*):
|
| 164 |
+
TODO: Add description.
|
| 165 |
+
sigmas (`None`, *optional*):
|
| 166 |
+
TODO: Add description.
|
| 167 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 168 |
+
TODO: Add description.
|
| 169 |
+
image_latents (`Tensor`, *optional*):
|
| 170 |
+
Packed image latents for conditioning. Shape: (B, img_seq_len, C)
|
| 171 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 172 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 173 |
+
|
| 174 |
+
Outputs:
|
| 175 |
+
latents (`Tensor`):
|
| 176 |
+
Denoised latents.
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
model_name = "flux2-klein"
|
| 180 |
+
|
| 181 |
+
block_classes = Flux2KleinCoreDenoiseBlocks.values()
|
| 182 |
+
block_names = Flux2KleinCoreDenoiseBlocks.keys()
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def description(self):
|
| 186 |
+
return "Core denoise step that performs the denoising process for Flux2-Klein (distilled model), for text-to-image generation."
|
| 187 |
+
|
| 188 |
+
@property
|
| 189 |
+
def outputs(self):
|
| 190 |
+
return [
|
| 191 |
+
OutputParam.template("latents"),
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
Flux2KleinImageConditionedCoreDenoiseBlocks = InsertableDict(
|
| 196 |
+
[
|
| 197 |
+
("input", Flux2TextInputStep()),
|
| 198 |
+
("prepare_image_latents", Flux2PrepareImageLatentsStep()),
|
| 199 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 200 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 201 |
+
("prepare_rope_inputs", Flux2RoPEInputsStep()),
|
| 202 |
+
("denoise", Flux2KleinDenoiseStep()),
|
| 203 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 204 |
+
]
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# auto_docstring
|
| 209 |
+
class Flux2KleinImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
|
| 210 |
+
"""
|
| 211 |
+
Core denoise step that performs the denoising process for Flux2-Klein (distilled model) with image conditioning.
|
| 212 |
+
|
| 213 |
+
Components:
|
| 214 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 215 |
+
|
| 216 |
+
Inputs:
|
| 217 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 218 |
+
TODO: Add description.
|
| 219 |
+
prompt_embeds (`Tensor`):
|
| 220 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 221 |
+
image_latents (`list`, *optional*):
|
| 222 |
+
TODO: Add description.
|
| 223 |
+
height (`int`, *optional*):
|
| 224 |
+
TODO: Add description.
|
| 225 |
+
width (`int`, *optional*):
|
| 226 |
+
TODO: Add description.
|
| 227 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 228 |
+
TODO: Add description.
|
| 229 |
+
generator (`None`, *optional*):
|
| 230 |
+
TODO: Add description.
|
| 231 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 232 |
+
TODO: Add description.
|
| 233 |
+
timesteps (`None`, *optional*):
|
| 234 |
+
TODO: Add description.
|
| 235 |
+
sigmas (`None`, *optional*):
|
| 236 |
+
TODO: Add description.
|
| 237 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 238 |
+
TODO: Add description.
|
| 239 |
+
|
| 240 |
+
Outputs:
|
| 241 |
+
latents (`Tensor`):
|
| 242 |
+
Denoised latents.
|
| 243 |
+
"""
|
| 244 |
+
|
| 245 |
+
model_name = "flux2-klein"
|
| 246 |
+
|
| 247 |
+
block_classes = Flux2KleinImageConditionedCoreDenoiseBlocks.values()
|
| 248 |
+
block_names = Flux2KleinImageConditionedCoreDenoiseBlocks.keys()
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def description(self):
|
| 252 |
+
return "Core denoise step that performs the denoising process for Flux2-Klein (distilled model) with image conditioning."
|
| 253 |
+
|
| 254 |
+
@property
|
| 255 |
+
def outputs(self):
|
| 256 |
+
return [
|
| 257 |
+
OutputParam.template("latents"),
|
| 258 |
+
]
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
# auto_docstring
|
| 262 |
+
class Flux2KleinAutoCoreDenoiseStep(AutoPipelineBlocks):
|
| 263 |
+
"""
|
| 264 |
+
Auto core denoise step that performs the denoising process for Flux2-Klein.
|
| 265 |
+
This is an auto pipeline block that works for text-to-image and image-conditioned generation.
|
| 266 |
+
- `Flux2KleinCoreDenoiseStep` is used for text-to-image generation.
|
| 267 |
+
- `Flux2KleinImageConditionedCoreDenoiseStep` is used for image-conditioned generation.
|
| 268 |
+
|
| 269 |
+
Components:
|
| 270 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 271 |
+
|
| 272 |
+
Inputs:
|
| 273 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 274 |
+
TODO: Add description.
|
| 275 |
+
prompt_embeds (`Tensor`):
|
| 276 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 277 |
+
image_latents (`list`, *optional*):
|
| 278 |
+
TODO: Add description.
|
| 279 |
+
height (`int`, *optional*):
|
| 280 |
+
TODO: Add description.
|
| 281 |
+
width (`int`, *optional*):
|
| 282 |
+
TODO: Add description.
|
| 283 |
+
latents (`Tensor | NoneType`):
|
| 284 |
+
TODO: Add description.
|
| 285 |
+
generator (`None`, *optional*):
|
| 286 |
+
TODO: Add description.
|
| 287 |
+
num_inference_steps (`None`):
|
| 288 |
+
TODO: Add description.
|
| 289 |
+
timesteps (`None`):
|
| 290 |
+
TODO: Add description.
|
| 291 |
+
sigmas (`None`, *optional*):
|
| 292 |
+
TODO: Add description.
|
| 293 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 294 |
+
TODO: Add description.
|
| 295 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 296 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 297 |
+
|
| 298 |
+
Outputs:
|
| 299 |
+
latents (`Tensor`):
|
| 300 |
+
Denoised latents.
|
| 301 |
+
"""
|
| 302 |
+
|
| 303 |
+
model_name = "flux2-klein"
|
| 304 |
+
block_classes = [Flux2KleinImageConditionedCoreDenoiseStep, Flux2KleinCoreDenoiseStep]
|
| 305 |
+
block_names = ["image_conditioned", "text2image"]
|
| 306 |
+
block_trigger_inputs = ["image_latents", None]
|
| 307 |
+
|
| 308 |
+
@property
|
| 309 |
+
def description(self):
|
| 310 |
+
return (
|
| 311 |
+
"Auto core denoise step that performs the denoising process for Flux2-Klein.\n"
|
| 312 |
+
"This is an auto pipeline block that works for text-to-image and image-conditioned generation.\n"
|
| 313 |
+
" - `Flux2KleinCoreDenoiseStep` is used for text-to-image generation.\n"
|
| 314 |
+
" - `Flux2KleinImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
###
|
| 319 |
+
### Auto blocks
|
| 320 |
+
###
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
# auto_docstring
|
| 324 |
+
class Flux2KleinAutoBlocks(SequentialPipelineBlocks):
|
| 325 |
+
"""
|
| 326 |
+
Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein.
|
| 327 |
+
|
| 328 |
+
Supported workflows:
|
| 329 |
+
- `text2image`: requires `prompt`
|
| 330 |
+
- `image_conditioned`: requires `image`, `prompt`
|
| 331 |
+
|
| 332 |
+
Components:
|
| 333 |
+
text_encoder (`Qwen3ForCausalLM`) tokenizer (`Qwen2Tokenizer`) image_processor (`Flux2ImageProcessor`) vae
|
| 334 |
+
(`AutoencoderKLFlux2`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 335 |
+
|
| 336 |
+
Configs:
|
| 337 |
+
is_distilled (default: True)
|
| 338 |
+
|
| 339 |
+
Inputs:
|
| 340 |
+
prompt (`None`, *optional*):
|
| 341 |
+
TODO: Add description.
|
| 342 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 343 |
+
TODO: Add description.
|
| 344 |
+
text_encoder_out_layers (`tuple`, *optional*, defaults to (9, 18, 27)):
|
| 345 |
+
TODO: Add description.
|
| 346 |
+
image (`None`, *optional*):
|
| 347 |
+
TODO: Add description.
|
| 348 |
+
height (`None`, *optional*):
|
| 349 |
+
TODO: Add description.
|
| 350 |
+
width (`None`, *optional*):
|
| 351 |
+
TODO: Add description.
|
| 352 |
+
generator (`None`, *optional*):
|
| 353 |
+
TODO: Add description.
|
| 354 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 355 |
+
TODO: Add description.
|
| 356 |
+
image_latents (`list`, *optional*):
|
| 357 |
+
TODO: Add description.
|
| 358 |
+
latents (`Tensor | NoneType`):
|
| 359 |
+
TODO: Add description.
|
| 360 |
+
num_inference_steps (`None`):
|
| 361 |
+
TODO: Add description.
|
| 362 |
+
timesteps (`None`):
|
| 363 |
+
TODO: Add description.
|
| 364 |
+
sigmas (`None`, *optional*):
|
| 365 |
+
TODO: Add description.
|
| 366 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 367 |
+
TODO: Add description.
|
| 368 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 369 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 370 |
+
output_type (`None`, *optional*, defaults to pil):
|
| 371 |
+
TODO: Add description.
|
| 372 |
+
|
| 373 |
+
Outputs:
|
| 374 |
+
images (`list`):
|
| 375 |
+
Generated images.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
model_name = "flux2-klein"
|
| 379 |
+
block_classes = [
|
| 380 |
+
Flux2KleinTextEncoderStep(),
|
| 381 |
+
Flux2KleinAutoVaeEncoderStep(),
|
| 382 |
+
Flux2KleinAutoCoreDenoiseStep(),
|
| 383 |
+
Flux2DecodeStep(),
|
| 384 |
+
]
|
| 385 |
+
block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
|
| 386 |
+
_workflow_map = {
|
| 387 |
+
"text2image": {"prompt": True},
|
| 388 |
+
"image_conditioned": {"image": True, "prompt": True},
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
@property
|
| 392 |
+
def description(self):
|
| 393 |
+
return "Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein."
|
| 394 |
+
|
| 395 |
+
@property
|
| 396 |
+
def outputs(self):
|
| 397 |
+
return [
|
| 398 |
+
OutputParam.template("images"),
|
| 399 |
+
]
|
diffusers/modular_pipelines/flux2/modular_blocks_flux2_klein_base.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Flux2KleinBaseRoPEInputsStep,
|
| 21 |
+
Flux2PrepareImageLatentsStep,
|
| 22 |
+
Flux2PrepareLatentsStep,
|
| 23 |
+
Flux2SetTimestepsStep,
|
| 24 |
+
)
|
| 25 |
+
from .decoders import Flux2DecodeStep, Flux2UnpackLatentsStep
|
| 26 |
+
from .denoise import Flux2KleinBaseDenoiseStep
|
| 27 |
+
from .encoders import (
|
| 28 |
+
Flux2KleinBaseTextEncoderStep,
|
| 29 |
+
Flux2VaeEncoderStep,
|
| 30 |
+
)
|
| 31 |
+
from .inputs import (
|
| 32 |
+
Flux2KleinBaseTextInputStep,
|
| 33 |
+
Flux2ProcessImagesInputStep,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 38 |
+
|
| 39 |
+
################
|
| 40 |
+
# VAE encoder
|
| 41 |
+
################
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# auto_docstring
|
| 45 |
+
class Flux2KleinBaseVaeEncoderSequentialStep(SequentialPipelineBlocks):
|
| 46 |
+
"""
|
| 47 |
+
VAE encoder step that preprocesses and encodes the image inputs into their latent representations.
|
| 48 |
+
|
| 49 |
+
Components:
|
| 50 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 51 |
+
|
| 52 |
+
Inputs:
|
| 53 |
+
image (`None`, *optional*):
|
| 54 |
+
TODO: Add description.
|
| 55 |
+
height (`None`, *optional*):
|
| 56 |
+
TODO: Add description.
|
| 57 |
+
width (`None`, *optional*):
|
| 58 |
+
TODO: Add description.
|
| 59 |
+
generator (`None`, *optional*):
|
| 60 |
+
TODO: Add description.
|
| 61 |
+
|
| 62 |
+
Outputs:
|
| 63 |
+
condition_images (`list`):
|
| 64 |
+
TODO: Add description.
|
| 65 |
+
image_latents (`list`):
|
| 66 |
+
List of latent representations for each reference image
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
model_name = "flux2"
|
| 70 |
+
|
| 71 |
+
block_classes = [Flux2ProcessImagesInputStep(), Flux2VaeEncoderStep()]
|
| 72 |
+
block_names = ["preprocess", "encode"]
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def description(self) -> str:
|
| 76 |
+
return "VAE encoder step that preprocesses and encodes the image inputs into their latent representations."
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# auto_docstring
|
| 80 |
+
class Flux2KleinBaseAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 81 |
+
"""
|
| 82 |
+
VAE encoder step that encodes the image inputs into their latent representations.
|
| 83 |
+
This is an auto pipeline block that works for image conditioning tasks.
|
| 84 |
+
- `Flux2KleinBaseVaeEncoderSequentialStep` is used when `image` is provided.
|
| 85 |
+
- If `image` is not provided, step will be skipped.
|
| 86 |
+
|
| 87 |
+
Components:
|
| 88 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`)
|
| 89 |
+
|
| 90 |
+
Inputs:
|
| 91 |
+
image (`None`, *optional*):
|
| 92 |
+
TODO: Add description.
|
| 93 |
+
height (`None`, *optional*):
|
| 94 |
+
TODO: Add description.
|
| 95 |
+
width (`None`, *optional*):
|
| 96 |
+
TODO: Add description.
|
| 97 |
+
generator (`None`, *optional*):
|
| 98 |
+
TODO: Add description.
|
| 99 |
+
|
| 100 |
+
Outputs:
|
| 101 |
+
condition_images (`list`):
|
| 102 |
+
TODO: Add description.
|
| 103 |
+
image_latents (`list`):
|
| 104 |
+
List of latent representations for each reference image
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
block_classes = [Flux2KleinBaseVaeEncoderSequentialStep]
|
| 108 |
+
block_names = ["img_conditioning"]
|
| 109 |
+
block_trigger_inputs = ["image"]
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def description(self):
|
| 113 |
+
return (
|
| 114 |
+
"VAE encoder step that encodes the image inputs into their latent representations.\n"
|
| 115 |
+
"This is an auto pipeline block that works for image conditioning tasks.\n"
|
| 116 |
+
" - `Flux2KleinBaseVaeEncoderSequentialStep` is used when `image` is provided.\n"
|
| 117 |
+
" - If `image` is not provided, step will be skipped."
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
###
|
| 122 |
+
### Core denoise
|
| 123 |
+
###
|
| 124 |
+
|
| 125 |
+
Flux2KleinBaseCoreDenoiseBlocks = InsertableDict(
|
| 126 |
+
[
|
| 127 |
+
("input", Flux2KleinBaseTextInputStep()),
|
| 128 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 129 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 130 |
+
("prepare_rope_inputs", Flux2KleinBaseRoPEInputsStep()),
|
| 131 |
+
("denoise", Flux2KleinBaseDenoiseStep()),
|
| 132 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 133 |
+
]
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# auto_docstring
|
| 138 |
+
class Flux2KleinBaseCoreDenoiseStep(SequentialPipelineBlocks):
|
| 139 |
+
"""
|
| 140 |
+
Core denoise step that performs the denoising process for Flux2-Klein (base model), for text-to-image generation.
|
| 141 |
+
|
| 142 |
+
Components:
|
| 143 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
|
| 144 |
+
(`ClassifierFreeGuidance`)
|
| 145 |
+
|
| 146 |
+
Configs:
|
| 147 |
+
is_distilled (default: False)
|
| 148 |
+
|
| 149 |
+
Inputs:
|
| 150 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 151 |
+
TODO: Add description.
|
| 152 |
+
prompt_embeds (`Tensor`):
|
| 153 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 154 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 155 |
+
Pre-generated negative text embeddings. Can be generated from text_encoder step.
|
| 156 |
+
height (`int`, *optional*):
|
| 157 |
+
TODO: Add description.
|
| 158 |
+
width (`int`, *optional*):
|
| 159 |
+
TODO: Add description.
|
| 160 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 161 |
+
TODO: Add description.
|
| 162 |
+
generator (`None`, *optional*):
|
| 163 |
+
TODO: Add description.
|
| 164 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 165 |
+
TODO: Add description.
|
| 166 |
+
timesteps (`None`, *optional*):
|
| 167 |
+
TODO: Add description.
|
| 168 |
+
sigmas (`None`, *optional*):
|
| 169 |
+
TODO: Add description.
|
| 170 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 171 |
+
TODO: Add description.
|
| 172 |
+
image_latents (`Tensor`, *optional*):
|
| 173 |
+
Packed image latents for conditioning. Shape: (B, img_seq_len, C)
|
| 174 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 175 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 176 |
+
|
| 177 |
+
Outputs:
|
| 178 |
+
latents (`Tensor`):
|
| 179 |
+
Denoised latents.
|
| 180 |
+
"""
|
| 181 |
+
|
| 182 |
+
model_name = "flux2-klein"
|
| 183 |
+
block_classes = Flux2KleinBaseCoreDenoiseBlocks.values()
|
| 184 |
+
block_names = Flux2KleinBaseCoreDenoiseBlocks.keys()
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def description(self):
|
| 188 |
+
return "Core denoise step that performs the denoising process for Flux2-Klein (base model), for text-to-image generation."
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def outputs(self):
|
| 192 |
+
return [
|
| 193 |
+
OutputParam.template("latents"),
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
Flux2KleinBaseImageConditionedCoreDenoiseBlocks = InsertableDict(
|
| 198 |
+
[
|
| 199 |
+
("input", Flux2KleinBaseTextInputStep()),
|
| 200 |
+
("prepare_latents", Flux2PrepareLatentsStep()),
|
| 201 |
+
("prepare_image_latents", Flux2PrepareImageLatentsStep()),
|
| 202 |
+
("set_timesteps", Flux2SetTimestepsStep()),
|
| 203 |
+
("prepare_rope_inputs", Flux2KleinBaseRoPEInputsStep()),
|
| 204 |
+
("denoise", Flux2KleinBaseDenoiseStep()),
|
| 205 |
+
("after_denoise", Flux2UnpackLatentsStep()),
|
| 206 |
+
]
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# auto_docstring
|
| 211 |
+
class Flux2KleinBaseImageConditionedCoreDenoiseStep(SequentialPipelineBlocks):
|
| 212 |
+
"""
|
| 213 |
+
Core denoise step that performs the denoising process for Flux2-Klein (base model) with image conditioning.
|
| 214 |
+
|
| 215 |
+
Components:
|
| 216 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
|
| 217 |
+
(`ClassifierFreeGuidance`)
|
| 218 |
+
|
| 219 |
+
Configs:
|
| 220 |
+
is_distilled (default: False)
|
| 221 |
+
|
| 222 |
+
Inputs:
|
| 223 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 224 |
+
TODO: Add description.
|
| 225 |
+
prompt_embeds (`Tensor`):
|
| 226 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 227 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 228 |
+
Pre-generated negative text embeddings. Can be generated from text_encoder step.
|
| 229 |
+
height (`int`, *optional*):
|
| 230 |
+
TODO: Add description.
|
| 231 |
+
width (`int`, *optional*):
|
| 232 |
+
TODO: Add description.
|
| 233 |
+
latents (`Tensor | NoneType`, *optional*):
|
| 234 |
+
TODO: Add description.
|
| 235 |
+
generator (`None`, *optional*):
|
| 236 |
+
TODO: Add description.
|
| 237 |
+
image_latents (`list`, *optional*):
|
| 238 |
+
TODO: Add description.
|
| 239 |
+
num_inference_steps (`None`, *optional*, defaults to 50):
|
| 240 |
+
TODO: Add description.
|
| 241 |
+
timesteps (`None`, *optional*):
|
| 242 |
+
TODO: Add description.
|
| 243 |
+
sigmas (`None`, *optional*):
|
| 244 |
+
TODO: Add description.
|
| 245 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 246 |
+
TODO: Add description.
|
| 247 |
+
|
| 248 |
+
Outputs:
|
| 249 |
+
latents (`Tensor`):
|
| 250 |
+
Denoised latents.
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
model_name = "flux2-klein"
|
| 254 |
+
block_classes = Flux2KleinBaseImageConditionedCoreDenoiseBlocks.values()
|
| 255 |
+
block_names = Flux2KleinBaseImageConditionedCoreDenoiseBlocks.keys()
|
| 256 |
+
|
| 257 |
+
@property
|
| 258 |
+
def description(self):
|
| 259 |
+
return "Core denoise step that performs the denoising process for Flux2-Klein (base model) with image conditioning."
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def outputs(self):
|
| 263 |
+
return [
|
| 264 |
+
OutputParam.template("latents"),
|
| 265 |
+
]
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# auto_docstring
|
| 269 |
+
class Flux2KleinBaseAutoCoreDenoiseStep(AutoPipelineBlocks):
|
| 270 |
+
"""
|
| 271 |
+
Auto core denoise step that performs the denoising process for Flux2-Klein (base model).
|
| 272 |
+
This is an auto pipeline block that works for text-to-image and image-conditioned generation.
|
| 273 |
+
- `Flux2KleinBaseCoreDenoiseStep` is used for text-to-image generation.
|
| 274 |
+
- `Flux2KleinBaseImageConditionedCoreDenoiseStep` is used for image-conditioned generation.
|
| 275 |
+
|
| 276 |
+
Components:
|
| 277 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`) guider
|
| 278 |
+
(`ClassifierFreeGuidance`)
|
| 279 |
+
|
| 280 |
+
Configs:
|
| 281 |
+
is_distilled (default: False)
|
| 282 |
+
|
| 283 |
+
Inputs:
|
| 284 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 285 |
+
TODO: Add description.
|
| 286 |
+
prompt_embeds (`Tensor`):
|
| 287 |
+
Pre-generated text embeddings. Can be generated from text_encoder step.
|
| 288 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 289 |
+
Pre-generated negative text embeddings. Can be generated from text_encoder step.
|
| 290 |
+
height (`int`, *optional*):
|
| 291 |
+
TODO: Add description.
|
| 292 |
+
width (`int`, *optional*):
|
| 293 |
+
TODO: Add description.
|
| 294 |
+
latents (`Tensor | NoneType`):
|
| 295 |
+
TODO: Add description.
|
| 296 |
+
generator (`None`, *optional*):
|
| 297 |
+
TODO: Add description.
|
| 298 |
+
image_latents (`list`, *optional*):
|
| 299 |
+
TODO: Add description.
|
| 300 |
+
num_inference_steps (`None`):
|
| 301 |
+
TODO: Add description.
|
| 302 |
+
timesteps (`None`):
|
| 303 |
+
TODO: Add description.
|
| 304 |
+
sigmas (`None`, *optional*):
|
| 305 |
+
TODO: Add description.
|
| 306 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 307 |
+
TODO: Add description.
|
| 308 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 309 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 310 |
+
|
| 311 |
+
Outputs:
|
| 312 |
+
latents (`Tensor`):
|
| 313 |
+
Denoised latents.
|
| 314 |
+
"""
|
| 315 |
+
|
| 316 |
+
model_name = "flux2-klein"
|
| 317 |
+
block_classes = [Flux2KleinBaseImageConditionedCoreDenoiseStep, Flux2KleinBaseCoreDenoiseStep]
|
| 318 |
+
block_names = ["image_conditioned", "text2image"]
|
| 319 |
+
block_trigger_inputs = ["image_latents", None]
|
| 320 |
+
|
| 321 |
+
@property
|
| 322 |
+
def description(self):
|
| 323 |
+
return (
|
| 324 |
+
"Auto core denoise step that performs the denoising process for Flux2-Klein (base model).\n"
|
| 325 |
+
"This is an auto pipeline block that works for text-to-image and image-conditioned generation.\n"
|
| 326 |
+
" - `Flux2KleinBaseCoreDenoiseStep` is used for text-to-image generation.\n"
|
| 327 |
+
" - `Flux2KleinBaseImageConditionedCoreDenoiseStep` is used for image-conditioned generation.\n"
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
###
|
| 332 |
+
### Auto blocks
|
| 333 |
+
###
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# auto_docstring
|
| 337 |
+
class Flux2KleinBaseAutoBlocks(SequentialPipelineBlocks):
|
| 338 |
+
"""
|
| 339 |
+
Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein (base model).
|
| 340 |
+
|
| 341 |
+
Supported workflows:
|
| 342 |
+
- `text2image`: requires `prompt`
|
| 343 |
+
- `image_conditioned`: requires `image`, `prompt`
|
| 344 |
+
|
| 345 |
+
Components:
|
| 346 |
+
text_encoder (`Qwen3ForCausalLM`) tokenizer (`Qwen2Tokenizer`) guider (`ClassifierFreeGuidance`)
|
| 347 |
+
image_processor (`Flux2ImageProcessor`) vae (`AutoencoderKLFlux2`) scheduler
|
| 348 |
+
(`FlowMatchEulerDiscreteScheduler`) transformer (`Flux2Transformer2DModel`)
|
| 349 |
+
|
| 350 |
+
Configs:
|
| 351 |
+
is_distilled (default: False)
|
| 352 |
+
|
| 353 |
+
Inputs:
|
| 354 |
+
prompt (`None`, *optional*):
|
| 355 |
+
TODO: Add description.
|
| 356 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 357 |
+
TODO: Add description.
|
| 358 |
+
text_encoder_out_layers (`tuple`, *optional*, defaults to (9, 18, 27)):
|
| 359 |
+
TODO: Add description.
|
| 360 |
+
image (`None`, *optional*):
|
| 361 |
+
TODO: Add description.
|
| 362 |
+
height (`None`, *optional*):
|
| 363 |
+
TODO: Add description.
|
| 364 |
+
width (`None`, *optional*):
|
| 365 |
+
TODO: Add description.
|
| 366 |
+
generator (`None`, *optional*):
|
| 367 |
+
TODO: Add description.
|
| 368 |
+
num_images_per_prompt (`None`, *optional*, defaults to 1):
|
| 369 |
+
TODO: Add description.
|
| 370 |
+
latents (`Tensor | NoneType`):
|
| 371 |
+
TODO: Add description.
|
| 372 |
+
image_latents (`list`, *optional*):
|
| 373 |
+
TODO: Add description.
|
| 374 |
+
num_inference_steps (`None`):
|
| 375 |
+
TODO: Add description.
|
| 376 |
+
timesteps (`None`):
|
| 377 |
+
TODO: Add description.
|
| 378 |
+
sigmas (`None`, *optional*):
|
| 379 |
+
TODO: Add description.
|
| 380 |
+
joint_attention_kwargs (`None`, *optional*):
|
| 381 |
+
TODO: Add description.
|
| 382 |
+
image_latent_ids (`Tensor`, *optional*):
|
| 383 |
+
Position IDs for image latents. Shape: (B, img_seq_len, 4)
|
| 384 |
+
output_type (`None`, *optional*, defaults to pil):
|
| 385 |
+
TODO: Add description.
|
| 386 |
+
|
| 387 |
+
Outputs:
|
| 388 |
+
images (`list`):
|
| 389 |
+
Generated images.
|
| 390 |
+
"""
|
| 391 |
+
|
| 392 |
+
model_name = "flux2-klein"
|
| 393 |
+
block_classes = [
|
| 394 |
+
Flux2KleinBaseTextEncoderStep(),
|
| 395 |
+
Flux2KleinBaseAutoVaeEncoderStep(),
|
| 396 |
+
Flux2KleinBaseAutoCoreDenoiseStep(),
|
| 397 |
+
Flux2DecodeStep(),
|
| 398 |
+
]
|
| 399 |
+
block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
|
| 400 |
+
_workflow_map = {
|
| 401 |
+
"text2image": {"prompt": True},
|
| 402 |
+
"image_conditioned": {"image": True, "prompt": True},
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def description(self):
|
| 407 |
+
return "Auto blocks that perform the text-to-image and image-conditioned generation using Flux2-Klein (base model)."
|
| 408 |
+
|
| 409 |
+
@property
|
| 410 |
+
def outputs(self):
|
| 411 |
+
return [
|
| 412 |
+
OutputParam.template("images"),
|
| 413 |
+
]
|
diffusers/modular_pipelines/flux2/modular_pipeline.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...loaders import Flux2LoraLoaderMixin
|
| 17 |
+
from ...utils import logging
|
| 18 |
+
from ..modular_pipeline import ModularPipeline
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Flux2ModularPipeline(ModularPipeline, Flux2LoraLoaderMixin):
|
| 25 |
+
"""
|
| 26 |
+
A ModularPipeline for Flux2.
|
| 27 |
+
|
| 28 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
default_blocks_name = "Flux2AutoBlocks"
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def default_height(self):
|
| 35 |
+
return self.default_sample_size * self.vae_scale_factor
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def default_width(self):
|
| 39 |
+
return self.default_sample_size * self.vae_scale_factor
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def default_sample_size(self):
|
| 43 |
+
return 128
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def vae_scale_factor(self):
|
| 47 |
+
vae_scale_factor = 8
|
| 48 |
+
if getattr(self, "vae", None) is not None:
|
| 49 |
+
vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
|
| 50 |
+
return vae_scale_factor
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def num_channels_latents(self):
|
| 54 |
+
num_channels_latents = 32
|
| 55 |
+
if getattr(self, "transformer", None):
|
| 56 |
+
num_channels_latents = self.transformer.config.in_channels // 4
|
| 57 |
+
return num_channels_latents
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class Flux2KleinModularPipeline(Flux2ModularPipeline):
|
| 61 |
+
"""
|
| 62 |
+
A ModularPipeline for Flux2-Klein (distilled model).
|
| 63 |
+
|
| 64 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
default_blocks_name = "Flux2KleinAutoBlocks"
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def requires_unconditional_embeds(self):
|
| 71 |
+
if hasattr(self.config, "is_distilled") and self.config.is_distilled:
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
requires_unconditional_embeds = False
|
| 75 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 76 |
+
requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
|
| 77 |
+
|
| 78 |
+
return requires_unconditional_embeds
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Flux2KleinBaseModularPipeline(Flux2ModularPipeline):
|
| 82 |
+
"""
|
| 83 |
+
A ModularPipeline for Flux2-Klein (base model).
|
| 84 |
+
|
| 85 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
default_blocks_name = "Flux2KleinBaseAutoBlocks"
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def requires_unconditional_embeds(self):
|
| 92 |
+
if hasattr(self.config, "is_distilled") and self.config.is_distilled:
|
| 93 |
+
return False
|
| 94 |
+
|
| 95 |
+
requires_unconditional_embeds = False
|
| 96 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 97 |
+
requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
|
| 98 |
+
|
| 99 |
+
return requires_unconditional_embeds
|
diffusers/modular_pipelines/helios/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_helios"] = ["HeliosAutoBlocks"]
|
| 25 |
+
_import_structure["modular_blocks_helios_pyramid"] = ["HeliosPyramidAutoBlocks"]
|
| 26 |
+
_import_structure["modular_blocks_helios_pyramid_distilled"] = ["HeliosPyramidDistilledAutoBlocks"]
|
| 27 |
+
_import_structure["modular_pipeline"] = [
|
| 28 |
+
"HeliosModularPipeline",
|
| 29 |
+
"HeliosPyramidDistilledModularPipeline",
|
| 30 |
+
"HeliosPyramidModularPipeline",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 34 |
+
try:
|
| 35 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 36 |
+
raise OptionalDependencyNotAvailable()
|
| 37 |
+
except OptionalDependencyNotAvailable:
|
| 38 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 39 |
+
else:
|
| 40 |
+
from .modular_blocks_helios import HeliosAutoBlocks
|
| 41 |
+
from .modular_blocks_helios_pyramid import HeliosPyramidAutoBlocks
|
| 42 |
+
from .modular_blocks_helios_pyramid_distilled import HeliosPyramidDistilledAutoBlocks
|
| 43 |
+
from .modular_pipeline import (
|
| 44 |
+
HeliosModularPipeline,
|
| 45 |
+
HeliosPyramidDistilledModularPipeline,
|
| 46 |
+
HeliosPyramidModularPipeline,
|
| 47 |
+
)
|
| 48 |
+
else:
|
| 49 |
+
import sys
|
| 50 |
+
|
| 51 |
+
sys.modules[__name__] = _LazyModule(
|
| 52 |
+
__name__,
|
| 53 |
+
globals()["__file__"],
|
| 54 |
+
_import_structure,
|
| 55 |
+
module_spec=__spec__,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
for name, value in _dummy_objects.items():
|
| 59 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/helios/before_denoise.py
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...models import HeliosTransformer3DModel
|
| 19 |
+
from ...schedulers import HeliosScheduler
|
| 20 |
+
from ...utils import logging
|
| 21 |
+
from ...utils.torch_utils import randn_tensor
|
| 22 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 23 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 24 |
+
from .modular_pipeline import HeliosModularPipeline
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift
|
| 31 |
+
def calculate_shift(
|
| 32 |
+
image_seq_len,
|
| 33 |
+
base_seq_len: int = 256,
|
| 34 |
+
max_seq_len: int = 4096,
|
| 35 |
+
base_shift: float = 0.5,
|
| 36 |
+
max_shift: float = 1.15,
|
| 37 |
+
):
|
| 38 |
+
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
| 39 |
+
b = base_shift - m * base_seq_len
|
| 40 |
+
mu = image_seq_len * m + b
|
| 41 |
+
return mu
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class HeliosTextInputStep(ModularPipelineBlocks):
|
| 45 |
+
model_name = "helios"
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def description(self) -> str:
|
| 49 |
+
return (
|
| 50 |
+
"Input processing step that:\n"
|
| 51 |
+
" 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
|
| 52 |
+
" 2. Adjusts input tensor shapes based on `batch_size` (number of prompts) and `num_videos_per_prompt`\n\n"
|
| 53 |
+
"All input tensors are expected to have either batch_size=1 or match the batch_size\n"
|
| 54 |
+
"of prompt_embeds. The tensors will be duplicated across the batch dimension to\n"
|
| 55 |
+
"have a final batch_size of batch_size * num_videos_per_prompt."
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def inputs(self) -> list[InputParam]:
|
| 60 |
+
return [
|
| 61 |
+
InputParam(
|
| 62 |
+
"num_videos_per_prompt",
|
| 63 |
+
default=1,
|
| 64 |
+
type_hint=int,
|
| 65 |
+
description="Number of videos to generate per prompt.",
|
| 66 |
+
),
|
| 67 |
+
InputParam.template("prompt_embeds"),
|
| 68 |
+
InputParam.template("negative_prompt_embeds"),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def intermediate_outputs(self) -> list[str]:
|
| 73 |
+
return [
|
| 74 |
+
OutputParam(
|
| 75 |
+
"batch_size",
|
| 76 |
+
type_hint=int,
|
| 77 |
+
description="Number of prompts, the final batch size of model inputs should be batch_size * num_videos_per_prompt",
|
| 78 |
+
),
|
| 79 |
+
OutputParam(
|
| 80 |
+
"dtype",
|
| 81 |
+
type_hint=torch.dtype,
|
| 82 |
+
description="Data type of model tensor inputs (determined by `prompt_embeds.dtype`)",
|
| 83 |
+
),
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
def check_inputs(self, components, block_state):
|
| 87 |
+
if block_state.prompt_embeds is not None and block_state.negative_prompt_embeds is not None:
|
| 88 |
+
if block_state.prompt_embeds.shape != block_state.negative_prompt_embeds.shape:
|
| 89 |
+
raise ValueError(
|
| 90 |
+
"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but"
|
| 91 |
+
f" got: `prompt_embeds` {block_state.prompt_embeds.shape} != `negative_prompt_embeds`"
|
| 92 |
+
f" {block_state.negative_prompt_embeds.shape}."
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
@torch.no_grad()
|
| 96 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 97 |
+
block_state = self.get_block_state(state)
|
| 98 |
+
self.check_inputs(components, block_state)
|
| 99 |
+
|
| 100 |
+
block_state.batch_size = block_state.prompt_embeds.shape[0]
|
| 101 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 102 |
+
|
| 103 |
+
_, seq_len, _ = block_state.prompt_embeds.shape
|
| 104 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, block_state.num_videos_per_prompt, 1)
|
| 105 |
+
block_state.prompt_embeds = block_state.prompt_embeds.view(
|
| 106 |
+
block_state.batch_size * block_state.num_videos_per_prompt, seq_len, -1
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
if block_state.negative_prompt_embeds is not None:
|
| 110 |
+
_, seq_len, _ = block_state.negative_prompt_embeds.shape
|
| 111 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(
|
| 112 |
+
1, block_state.num_videos_per_prompt, 1
|
| 113 |
+
)
|
| 114 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
|
| 115 |
+
block_state.batch_size * block_state.num_videos_per_prompt, seq_len, -1
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
self.set_block_state(state, block_state)
|
| 119 |
+
|
| 120 |
+
return components, state
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# Copied from diffusers.modular_pipelines.wan.before_denoise.repeat_tensor_to_batch_size
|
| 124 |
+
def repeat_tensor_to_batch_size(
|
| 125 |
+
input_name: str,
|
| 126 |
+
input_tensor: torch.Tensor,
|
| 127 |
+
batch_size: int,
|
| 128 |
+
num_videos_per_prompt: int = 1,
|
| 129 |
+
) -> torch.Tensor:
|
| 130 |
+
"""Repeat tensor elements to match the final batch size.
|
| 131 |
+
|
| 132 |
+
This function expands a tensor's batch dimension to match the final batch size (batch_size * num_videos_per_prompt)
|
| 133 |
+
by repeating each element along dimension 0.
|
| 134 |
+
|
| 135 |
+
The input tensor must have batch size 1 or batch_size. The function will:
|
| 136 |
+
- If batch size is 1: repeat each element (batch_size * num_videos_per_prompt) times
|
| 137 |
+
- If batch size equals batch_size: repeat each element num_videos_per_prompt times
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
input_name (str): Name of the input tensor (used for error messages)
|
| 141 |
+
input_tensor (torch.Tensor): The tensor to repeat. Must have batch size 1 or batch_size.
|
| 142 |
+
batch_size (int): The base batch size (number of prompts)
|
| 143 |
+
num_videos_per_prompt (int, optional): Number of videos to generate per prompt. Defaults to 1.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
torch.Tensor: The repeated tensor with final batch size (batch_size * num_videos_per_prompt)
|
| 147 |
+
|
| 148 |
+
Raises:
|
| 149 |
+
ValueError: If input_tensor is not a torch.Tensor or has invalid batch size
|
| 150 |
+
|
| 151 |
+
Examples:
|
| 152 |
+
tensor = torch.tensor([[1, 2, 3]]) # shape: [1, 3] repeated = repeat_tensor_to_batch_size("image", tensor,
|
| 153 |
+
batch_size=2, num_videos_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]]) - shape:
|
| 154 |
+
[4, 3]
|
| 155 |
+
|
| 156 |
+
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]]) # shape: [2, 3] repeated = repeat_tensor_to_batch_size("image",
|
| 157 |
+
tensor, batch_size=2, num_videos_per_prompt=2) repeated # tensor([[1, 2, 3], [1, 2, 3], [4, 5, 6], [4, 5, 6]])
|
| 158 |
+
- shape: [4, 3]
|
| 159 |
+
"""
|
| 160 |
+
# make sure input is a tensor
|
| 161 |
+
if not isinstance(input_tensor, torch.Tensor):
|
| 162 |
+
raise ValueError(f"`{input_name}` must be a tensor")
|
| 163 |
+
|
| 164 |
+
# make sure input tensor e.g. image_latents has batch size 1 or batch_size same as prompts
|
| 165 |
+
if input_tensor.shape[0] == 1:
|
| 166 |
+
repeat_by = batch_size * num_videos_per_prompt
|
| 167 |
+
elif input_tensor.shape[0] == batch_size:
|
| 168 |
+
repeat_by = num_videos_per_prompt
|
| 169 |
+
else:
|
| 170 |
+
raise ValueError(
|
| 171 |
+
f"`{input_name}` must have have batch size 1 or {batch_size}, but got {input_tensor.shape[0]}"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# expand the tensor to match the batch_size * num_videos_per_prompt
|
| 175 |
+
input_tensor = input_tensor.repeat_interleave(repeat_by, dim=0)
|
| 176 |
+
|
| 177 |
+
return input_tensor
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# Copied from diffusers.modular_pipelines.wan.before_denoise.calculate_dimension_from_latents
|
| 181 |
+
def calculate_dimension_from_latents(
|
| 182 |
+
latents: torch.Tensor, vae_scale_factor_temporal: int, vae_scale_factor_spatial: int
|
| 183 |
+
) -> tuple[int, int]:
|
| 184 |
+
"""Calculate image dimensions from latent tensor dimensions.
|
| 185 |
+
|
| 186 |
+
This function converts latent temporal and spatial dimensions to image temporal and spatial dimensions by
|
| 187 |
+
multiplying the latent num_frames/height/width by the VAE scale factor.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
latents (torch.Tensor): The latent tensor. Must have 4 or 5 dimensions.
|
| 191 |
+
Expected shapes: [batch, channels, height, width] or [batch, channels, frames, height, width]
|
| 192 |
+
vae_scale_factor_temporal (int): The scale factor used by the VAE to compress temporal dimension.
|
| 193 |
+
Typically 4 for most VAEs (video is 4x larger than latents in temporal dimension)
|
| 194 |
+
vae_scale_factor_spatial (int): The scale factor used by the VAE to compress spatial dimension.
|
| 195 |
+
Typically 8 for most VAEs (image is 8x larger than latents in each dimension)
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
tuple[int, int]: The calculated image dimensions as (height, width)
|
| 199 |
+
|
| 200 |
+
Raises:
|
| 201 |
+
ValueError: If latents tensor doesn't have 4 or 5 dimensions
|
| 202 |
+
|
| 203 |
+
"""
|
| 204 |
+
if latents.ndim != 5:
|
| 205 |
+
raise ValueError(f"latents must have 5 dimensions, but got {latents.ndim}")
|
| 206 |
+
|
| 207 |
+
_, _, num_latent_frames, latent_height, latent_width = latents.shape
|
| 208 |
+
|
| 209 |
+
num_frames = (num_latent_frames - 1) * vae_scale_factor_temporal + 1
|
| 210 |
+
height = latent_height * vae_scale_factor_spatial
|
| 211 |
+
width = latent_width * vae_scale_factor_spatial
|
| 212 |
+
|
| 213 |
+
return num_frames, height, width
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class HeliosAdditionalInputsStep(ModularPipelineBlocks):
|
| 217 |
+
"""Configurable step that standardizes inputs for the denoising step.
|
| 218 |
+
|
| 219 |
+
This step handles:
|
| 220 |
+
1. For encoded image latents: Computes height/width from latents and expands batch size
|
| 221 |
+
2. For additional_batch_inputs: Expands batch dimensions to match final batch size
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
model_name = "helios"
|
| 225 |
+
|
| 226 |
+
def __init__(
|
| 227 |
+
self,
|
| 228 |
+
image_latent_inputs: list[InputParam] | None = None,
|
| 229 |
+
additional_batch_inputs: list[InputParam] | None = None,
|
| 230 |
+
):
|
| 231 |
+
if image_latent_inputs is None:
|
| 232 |
+
image_latent_inputs = [InputParam.template("image_latents")]
|
| 233 |
+
if additional_batch_inputs is None:
|
| 234 |
+
additional_batch_inputs = []
|
| 235 |
+
|
| 236 |
+
if not isinstance(image_latent_inputs, list):
|
| 237 |
+
raise ValueError(f"image_latent_inputs must be a list, but got {type(image_latent_inputs)}")
|
| 238 |
+
else:
|
| 239 |
+
for input_param in image_latent_inputs:
|
| 240 |
+
if not isinstance(input_param, InputParam):
|
| 241 |
+
raise ValueError(f"image_latent_inputs must be a list of InputParam, but got {type(input_param)}")
|
| 242 |
+
|
| 243 |
+
if not isinstance(additional_batch_inputs, list):
|
| 244 |
+
raise ValueError(f"additional_batch_inputs must be a list, but got {type(additional_batch_inputs)}")
|
| 245 |
+
else:
|
| 246 |
+
for input_param in additional_batch_inputs:
|
| 247 |
+
if not isinstance(input_param, InputParam):
|
| 248 |
+
raise ValueError(
|
| 249 |
+
f"additional_batch_inputs must be a list of InputParam, but got {type(input_param)}"
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
self._image_latent_inputs = image_latent_inputs
|
| 253 |
+
self._additional_batch_inputs = additional_batch_inputs
|
| 254 |
+
super().__init__()
|
| 255 |
+
|
| 256 |
+
@property
|
| 257 |
+
def description(self) -> str:
|
| 258 |
+
summary_section = (
|
| 259 |
+
"Input processing step that:\n"
|
| 260 |
+
" 1. For image latent inputs: Computes height/width from latents and expands batch size\n"
|
| 261 |
+
" 2. For additional batch inputs: Expands batch dimensions to match final batch size"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
inputs_info = ""
|
| 265 |
+
if self._image_latent_inputs or self._additional_batch_inputs:
|
| 266 |
+
inputs_info = "\n\nConfigured inputs:"
|
| 267 |
+
if self._image_latent_inputs:
|
| 268 |
+
inputs_info += f"\n - Image latent inputs: {[p.name for p in self._image_latent_inputs]}"
|
| 269 |
+
if self._additional_batch_inputs:
|
| 270 |
+
inputs_info += f"\n - Additional batch inputs: {[p.name for p in self._additional_batch_inputs]}"
|
| 271 |
+
|
| 272 |
+
placement_section = "\n\nThis block should be placed after the encoder steps and the text input step."
|
| 273 |
+
|
| 274 |
+
return summary_section + inputs_info + placement_section
|
| 275 |
+
|
| 276 |
+
@property
|
| 277 |
+
def inputs(self) -> list[InputParam]:
|
| 278 |
+
inputs = [
|
| 279 |
+
InputParam(name="num_videos_per_prompt", default=1),
|
| 280 |
+
InputParam(name="batch_size", required=True),
|
| 281 |
+
]
|
| 282 |
+
inputs += self._image_latent_inputs + self._additional_batch_inputs
|
| 283 |
+
|
| 284 |
+
return inputs
|
| 285 |
+
|
| 286 |
+
@property
|
| 287 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 288 |
+
outputs = [
|
| 289 |
+
OutputParam("height", type_hint=int),
|
| 290 |
+
OutputParam("width", type_hint=int),
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
for input_param in self._image_latent_inputs:
|
| 294 |
+
outputs.append(OutputParam(input_param.name, type_hint=torch.Tensor))
|
| 295 |
+
|
| 296 |
+
for input_param in self._additional_batch_inputs:
|
| 297 |
+
outputs.append(OutputParam(input_param.name, type_hint=torch.Tensor))
|
| 298 |
+
|
| 299 |
+
return outputs
|
| 300 |
+
|
| 301 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 302 |
+
block_state = self.get_block_state(state)
|
| 303 |
+
|
| 304 |
+
for input_param in self._image_latent_inputs:
|
| 305 |
+
image_latent_tensor = getattr(block_state, input_param.name)
|
| 306 |
+
if image_latent_tensor is None:
|
| 307 |
+
continue
|
| 308 |
+
|
| 309 |
+
# Calculate height/width from latents
|
| 310 |
+
_, height, width = calculate_dimension_from_latents(
|
| 311 |
+
image_latent_tensor, components.vae_scale_factor_temporal, components.vae_scale_factor_spatial
|
| 312 |
+
)
|
| 313 |
+
block_state.height = height
|
| 314 |
+
block_state.width = width
|
| 315 |
+
|
| 316 |
+
# Expand batch size
|
| 317 |
+
image_latent_tensor = repeat_tensor_to_batch_size(
|
| 318 |
+
input_name=input_param.name,
|
| 319 |
+
input_tensor=image_latent_tensor,
|
| 320 |
+
num_videos_per_prompt=block_state.num_videos_per_prompt,
|
| 321 |
+
batch_size=block_state.batch_size,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
setattr(block_state, input_param.name, image_latent_tensor)
|
| 325 |
+
|
| 326 |
+
for input_param in self._additional_batch_inputs:
|
| 327 |
+
input_tensor = getattr(block_state, input_param.name)
|
| 328 |
+
if input_tensor is None:
|
| 329 |
+
continue
|
| 330 |
+
|
| 331 |
+
input_tensor = repeat_tensor_to_batch_size(
|
| 332 |
+
input_name=input_param.name,
|
| 333 |
+
input_tensor=input_tensor,
|
| 334 |
+
num_videos_per_prompt=block_state.num_videos_per_prompt,
|
| 335 |
+
batch_size=block_state.batch_size,
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
setattr(block_state, input_param.name, input_tensor)
|
| 339 |
+
|
| 340 |
+
self.set_block_state(state, block_state)
|
| 341 |
+
return components, state
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class HeliosAddNoiseToImageLatentsStep(ModularPipelineBlocks):
|
| 345 |
+
"""Adds noise to image_latents and fake_image_latents for I2V conditioning.
|
| 346 |
+
|
| 347 |
+
Applies single-sigma noise to image_latents (using image_noise_sigma range) and single-sigma noise to
|
| 348 |
+
fake_image_latents (using video_noise_sigma range).
|
| 349 |
+
"""
|
| 350 |
+
|
| 351 |
+
model_name = "helios"
|
| 352 |
+
|
| 353 |
+
@property
|
| 354 |
+
def description(self) -> str:
|
| 355 |
+
return (
|
| 356 |
+
"Adds noise to image_latents and fake_image_latents for I2V conditioning. "
|
| 357 |
+
"Uses random sigma from configured ranges for each."
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def inputs(self) -> list[InputParam]:
|
| 362 |
+
return [
|
| 363 |
+
InputParam.template("image_latents"),
|
| 364 |
+
InputParam(
|
| 365 |
+
"fake_image_latents",
|
| 366 |
+
required=True,
|
| 367 |
+
type_hint=torch.Tensor,
|
| 368 |
+
description="Fake image latents used as history seed for I2V generation.",
|
| 369 |
+
),
|
| 370 |
+
InputParam(
|
| 371 |
+
"image_noise_sigma_min",
|
| 372 |
+
default=0.111,
|
| 373 |
+
type_hint=float,
|
| 374 |
+
description="Minimum sigma for image latent noise.",
|
| 375 |
+
),
|
| 376 |
+
InputParam(
|
| 377 |
+
"image_noise_sigma_max",
|
| 378 |
+
default=0.135,
|
| 379 |
+
type_hint=float,
|
| 380 |
+
description="Maximum sigma for image latent noise.",
|
| 381 |
+
),
|
| 382 |
+
InputParam(
|
| 383 |
+
"video_noise_sigma_min",
|
| 384 |
+
default=0.111,
|
| 385 |
+
type_hint=float,
|
| 386 |
+
description="Minimum sigma for video/fake-image latent noise.",
|
| 387 |
+
),
|
| 388 |
+
InputParam(
|
| 389 |
+
"video_noise_sigma_max",
|
| 390 |
+
default=0.135,
|
| 391 |
+
type_hint=float,
|
| 392 |
+
description="Maximum sigma for video/fake-image latent noise.",
|
| 393 |
+
),
|
| 394 |
+
InputParam.template("generator"),
|
| 395 |
+
]
|
| 396 |
+
|
| 397 |
+
@property
|
| 398 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 399 |
+
return [
|
| 400 |
+
OutputParam.template("image_latents"),
|
| 401 |
+
OutputParam("fake_image_latents", type_hint=torch.Tensor, description="Noisy fake image latents"),
|
| 402 |
+
]
|
| 403 |
+
|
| 404 |
+
@torch.no_grad()
|
| 405 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 406 |
+
block_state = self.get_block_state(state)
|
| 407 |
+
|
| 408 |
+
device = components._execution_device
|
| 409 |
+
image_latents = block_state.image_latents
|
| 410 |
+
fake_image_latents = block_state.fake_image_latents
|
| 411 |
+
|
| 412 |
+
# Add noise to image_latents
|
| 413 |
+
image_noise_sigma = (
|
| 414 |
+
torch.rand(1, device=device, generator=block_state.generator)
|
| 415 |
+
* (block_state.image_noise_sigma_max - block_state.image_noise_sigma_min)
|
| 416 |
+
+ block_state.image_noise_sigma_min
|
| 417 |
+
)
|
| 418 |
+
image_latents = (
|
| 419 |
+
image_noise_sigma * randn_tensor(image_latents.shape, generator=block_state.generator, device=device)
|
| 420 |
+
+ (1 - image_noise_sigma) * image_latents
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Add noise to fake_image_latents
|
| 424 |
+
fake_image_noise_sigma = (
|
| 425 |
+
torch.rand(1, device=device, generator=block_state.generator)
|
| 426 |
+
* (block_state.video_noise_sigma_max - block_state.video_noise_sigma_min)
|
| 427 |
+
+ block_state.video_noise_sigma_min
|
| 428 |
+
)
|
| 429 |
+
fake_image_latents = (
|
| 430 |
+
fake_image_noise_sigma
|
| 431 |
+
* randn_tensor(fake_image_latents.shape, generator=block_state.generator, device=device)
|
| 432 |
+
+ (1 - fake_image_noise_sigma) * fake_image_latents
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
|
| 436 |
+
block_state.fake_image_latents = fake_image_latents.to(device=device, dtype=torch.float32)
|
| 437 |
+
|
| 438 |
+
self.set_block_state(state, block_state)
|
| 439 |
+
return components, state
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
class HeliosAddNoiseToVideoLatentsStep(ModularPipelineBlocks):
|
| 443 |
+
"""Adds noise to image_latents and video_latents for V2V conditioning.
|
| 444 |
+
|
| 445 |
+
Applies single-sigma noise to image_latents (using image_noise_sigma range) and per-frame noise to video_latents in
|
| 446 |
+
chunks (using video_noise_sigma range).
|
| 447 |
+
"""
|
| 448 |
+
|
| 449 |
+
model_name = "helios"
|
| 450 |
+
|
| 451 |
+
@property
|
| 452 |
+
def description(self) -> str:
|
| 453 |
+
return (
|
| 454 |
+
"Adds noise to image_latents and video_latents for V2V conditioning. "
|
| 455 |
+
"Uses single-sigma noise for image_latents and per-frame noise for video chunks."
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
@property
|
| 459 |
+
def inputs(self) -> list[InputParam]:
|
| 460 |
+
return [
|
| 461 |
+
InputParam.template("image_latents"),
|
| 462 |
+
InputParam(
|
| 463 |
+
"video_latents",
|
| 464 |
+
required=True,
|
| 465 |
+
type_hint=torch.Tensor,
|
| 466 |
+
description="Encoded video latents for V2V generation.",
|
| 467 |
+
),
|
| 468 |
+
InputParam(
|
| 469 |
+
"num_latent_frames_per_chunk",
|
| 470 |
+
default=9,
|
| 471 |
+
type_hint=int,
|
| 472 |
+
description="Number of latent frames per temporal chunk.",
|
| 473 |
+
),
|
| 474 |
+
InputParam(
|
| 475 |
+
"image_noise_sigma_min",
|
| 476 |
+
default=0.111,
|
| 477 |
+
type_hint=float,
|
| 478 |
+
description="Minimum sigma for image latent noise.",
|
| 479 |
+
),
|
| 480 |
+
InputParam(
|
| 481 |
+
"image_noise_sigma_max",
|
| 482 |
+
default=0.135,
|
| 483 |
+
type_hint=float,
|
| 484 |
+
description="Maximum sigma for image latent noise.",
|
| 485 |
+
),
|
| 486 |
+
InputParam(
|
| 487 |
+
"video_noise_sigma_min",
|
| 488 |
+
default=0.111,
|
| 489 |
+
type_hint=float,
|
| 490 |
+
description="Minimum sigma for video latent noise.",
|
| 491 |
+
),
|
| 492 |
+
InputParam(
|
| 493 |
+
"video_noise_sigma_max",
|
| 494 |
+
default=0.135,
|
| 495 |
+
type_hint=float,
|
| 496 |
+
description="Maximum sigma for video latent noise.",
|
| 497 |
+
),
|
| 498 |
+
InputParam.template("generator"),
|
| 499 |
+
]
|
| 500 |
+
|
| 501 |
+
@property
|
| 502 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 503 |
+
return [
|
| 504 |
+
OutputParam.template("image_latents"),
|
| 505 |
+
OutputParam("video_latents", type_hint=torch.Tensor, description="Noisy video latents"),
|
| 506 |
+
]
|
| 507 |
+
|
| 508 |
+
@torch.no_grad()
|
| 509 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 510 |
+
block_state = self.get_block_state(state)
|
| 511 |
+
|
| 512 |
+
device = components._execution_device
|
| 513 |
+
image_latents = block_state.image_latents
|
| 514 |
+
video_latents = block_state.video_latents
|
| 515 |
+
num_latent_frames_per_chunk = block_state.num_latent_frames_per_chunk
|
| 516 |
+
|
| 517 |
+
# Add noise to first frame (single sigma)
|
| 518 |
+
image_noise_sigma = (
|
| 519 |
+
torch.rand(1, device=device, generator=block_state.generator)
|
| 520 |
+
* (block_state.image_noise_sigma_max - block_state.image_noise_sigma_min)
|
| 521 |
+
+ block_state.image_noise_sigma_min
|
| 522 |
+
)
|
| 523 |
+
image_latents = (
|
| 524 |
+
image_noise_sigma * randn_tensor(image_latents.shape, generator=block_state.generator, device=device)
|
| 525 |
+
+ (1 - image_noise_sigma) * image_latents
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
# Add per-frame noise to video chunks
|
| 529 |
+
noisy_latents_chunks = []
|
| 530 |
+
num_latent_chunks = video_latents.shape[2] // num_latent_frames_per_chunk
|
| 531 |
+
for i in range(num_latent_chunks):
|
| 532 |
+
chunk_start = i * num_latent_frames_per_chunk
|
| 533 |
+
chunk_end = chunk_start + num_latent_frames_per_chunk
|
| 534 |
+
latent_chunk = video_latents[:, :, chunk_start:chunk_end, :, :]
|
| 535 |
+
|
| 536 |
+
chunk_frames = latent_chunk.shape[2]
|
| 537 |
+
frame_sigmas = (
|
| 538 |
+
torch.rand(chunk_frames, device=device, generator=block_state.generator)
|
| 539 |
+
* (block_state.video_noise_sigma_max - block_state.video_noise_sigma_min)
|
| 540 |
+
+ block_state.video_noise_sigma_min
|
| 541 |
+
)
|
| 542 |
+
frame_sigmas = frame_sigmas.view(1, 1, chunk_frames, 1, 1)
|
| 543 |
+
|
| 544 |
+
noisy_chunk = (
|
| 545 |
+
frame_sigmas * randn_tensor(latent_chunk.shape, generator=block_state.generator, device=device)
|
| 546 |
+
+ (1 - frame_sigmas) * latent_chunk
|
| 547 |
+
)
|
| 548 |
+
noisy_latents_chunks.append(noisy_chunk)
|
| 549 |
+
video_latents = torch.cat(noisy_latents_chunks, dim=2)
|
| 550 |
+
|
| 551 |
+
block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
|
| 552 |
+
block_state.video_latents = video_latents.to(device=device, dtype=torch.float32)
|
| 553 |
+
|
| 554 |
+
self.set_block_state(state, block_state)
|
| 555 |
+
return components, state
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
class HeliosPrepareHistoryStep(ModularPipelineBlocks):
|
| 559 |
+
"""Prepares chunk/history indices and initializes history state for the chunk loop."""
|
| 560 |
+
|
| 561 |
+
model_name = "helios"
|
| 562 |
+
|
| 563 |
+
@property
|
| 564 |
+
def description(self) -> str:
|
| 565 |
+
return (
|
| 566 |
+
"Prepares the chunk loop by computing latent dimensions, number of chunks, "
|
| 567 |
+
"history indices, and initializing history state (history_latents, image_latents, latent_chunks)."
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
@property
|
| 571 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 572 |
+
return [
|
| 573 |
+
ComponentSpec("transformer", HeliosTransformer3DModel),
|
| 574 |
+
]
|
| 575 |
+
|
| 576 |
+
@property
|
| 577 |
+
def inputs(self) -> list[InputParam]:
|
| 578 |
+
return [
|
| 579 |
+
InputParam.template("height", default=384),
|
| 580 |
+
InputParam.template("width", default=640),
|
| 581 |
+
InputParam(
|
| 582 |
+
"num_frames", default=132, type_hint=int, description="Total number of video frames to generate."
|
| 583 |
+
),
|
| 584 |
+
InputParam("batch_size", required=True, type_hint=int),
|
| 585 |
+
InputParam(
|
| 586 |
+
"num_latent_frames_per_chunk",
|
| 587 |
+
default=9,
|
| 588 |
+
type_hint=int,
|
| 589 |
+
description="Number of latent frames per temporal chunk.",
|
| 590 |
+
),
|
| 591 |
+
InputParam(
|
| 592 |
+
"history_sizes",
|
| 593 |
+
default=[16, 2, 1],
|
| 594 |
+
type_hint=list,
|
| 595 |
+
description="Sizes of long/mid/short history buffers for temporal context.",
|
| 596 |
+
),
|
| 597 |
+
InputParam(
|
| 598 |
+
"keep_first_frame",
|
| 599 |
+
default=True,
|
| 600 |
+
type_hint=bool,
|
| 601 |
+
description="Whether to keep the first frame as a prefix in history.",
|
| 602 |
+
),
|
| 603 |
+
]
|
| 604 |
+
|
| 605 |
+
@property
|
| 606 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 607 |
+
return [
|
| 608 |
+
OutputParam("num_latent_chunk", type_hint=int, description="Number of temporal chunks"),
|
| 609 |
+
OutputParam("latent_shape", type_hint=tuple, description="Shape of latent tensor per chunk"),
|
| 610 |
+
OutputParam("history_sizes", type_hint=list, description="Adjusted history sizes (sorted, descending)"),
|
| 611 |
+
OutputParam("indices_hidden_states", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
|
| 612 |
+
OutputParam("indices_latents_history_short", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
|
| 613 |
+
OutputParam("indices_latents_history_mid", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
|
| 614 |
+
OutputParam("indices_latents_history_long", type_hint=torch.Tensor, kwargs_type="denoiser_input_fields"),
|
| 615 |
+
OutputParam("history_latents", type_hint=torch.Tensor, description="Initialized zero history latents"),
|
| 616 |
+
]
|
| 617 |
+
|
| 618 |
+
@torch.no_grad()
|
| 619 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 620 |
+
block_state = self.get_block_state(state)
|
| 621 |
+
|
| 622 |
+
batch_size = block_state.batch_size
|
| 623 |
+
device = components._execution_device
|
| 624 |
+
|
| 625 |
+
block_state.num_frames = max(block_state.num_frames, 1)
|
| 626 |
+
history_sizes = sorted(block_state.history_sizes, reverse=True)
|
| 627 |
+
|
| 628 |
+
num_channels_latents = components.num_channels_latents
|
| 629 |
+
h_latent = block_state.height // components.vae_scale_factor_spatial
|
| 630 |
+
w_latent = block_state.width // components.vae_scale_factor_spatial
|
| 631 |
+
|
| 632 |
+
# Compute number of chunks
|
| 633 |
+
block_state.window_num_frames = (
|
| 634 |
+
block_state.num_latent_frames_per_chunk - 1
|
| 635 |
+
) * components.vae_scale_factor_temporal + 1
|
| 636 |
+
block_state.num_latent_chunk = max(
|
| 637 |
+
1, (block_state.num_frames + block_state.window_num_frames - 1) // block_state.window_num_frames
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
# Modify history_sizes for non-keep_first_frame (matching pipeline behavior)
|
| 641 |
+
if not block_state.keep_first_frame:
|
| 642 |
+
history_sizes = history_sizes.copy()
|
| 643 |
+
history_sizes[-1] = history_sizes[-1] + 1
|
| 644 |
+
|
| 645 |
+
# Compute indices ONCE (same structure for all chunks)
|
| 646 |
+
if block_state.keep_first_frame:
|
| 647 |
+
indices = torch.arange(0, sum([1, *history_sizes, block_state.num_latent_frames_per_chunk]))
|
| 648 |
+
(
|
| 649 |
+
indices_prefix,
|
| 650 |
+
indices_latents_history_long,
|
| 651 |
+
indices_latents_history_mid,
|
| 652 |
+
indices_latents_history_1x,
|
| 653 |
+
indices_hidden_states,
|
| 654 |
+
) = indices.split([1, *history_sizes, block_state.num_latent_frames_per_chunk], dim=0)
|
| 655 |
+
indices_latents_history_short = torch.cat([indices_prefix, indices_latents_history_1x], dim=0)
|
| 656 |
+
else:
|
| 657 |
+
indices = torch.arange(0, sum([*history_sizes, block_state.num_latent_frames_per_chunk]))
|
| 658 |
+
(
|
| 659 |
+
indices_latents_history_long,
|
| 660 |
+
indices_latents_history_mid,
|
| 661 |
+
indices_latents_history_short,
|
| 662 |
+
indices_hidden_states,
|
| 663 |
+
) = indices.split([*history_sizes, block_state.num_latent_frames_per_chunk], dim=0)
|
| 664 |
+
|
| 665 |
+
# Latent shape per chunk
|
| 666 |
+
block_state.latent_shape = (
|
| 667 |
+
batch_size,
|
| 668 |
+
num_channels_latents,
|
| 669 |
+
block_state.num_latent_frames_per_chunk,
|
| 670 |
+
h_latent,
|
| 671 |
+
w_latent,
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
# Set outputs
|
| 675 |
+
block_state.history_sizes = history_sizes
|
| 676 |
+
block_state.indices_hidden_states = indices_hidden_states.unsqueeze(0)
|
| 677 |
+
block_state.indices_latents_history_short = indices_latents_history_short.unsqueeze(0)
|
| 678 |
+
block_state.indices_latents_history_mid = indices_latents_history_mid.unsqueeze(0)
|
| 679 |
+
block_state.indices_latents_history_long = indices_latents_history_long.unsqueeze(0)
|
| 680 |
+
block_state.history_latents = torch.zeros(
|
| 681 |
+
batch_size,
|
| 682 |
+
num_channels_latents,
|
| 683 |
+
sum(history_sizes),
|
| 684 |
+
h_latent,
|
| 685 |
+
w_latent,
|
| 686 |
+
device=device,
|
| 687 |
+
dtype=torch.float32,
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
self.set_block_state(state, block_state)
|
| 691 |
+
|
| 692 |
+
return components, state
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
class HeliosI2VSeedHistoryStep(ModularPipelineBlocks):
|
| 696 |
+
"""Seeds history_latents with fake_image_latents for I2V pipelines.
|
| 697 |
+
|
| 698 |
+
This small additive step runs after HeliosPrepareHistoryStep and appends fake_image_latents to the initialized
|
| 699 |
+
history_latents tensor.
|
| 700 |
+
"""
|
| 701 |
+
|
| 702 |
+
model_name = "helios"
|
| 703 |
+
|
| 704 |
+
@property
|
| 705 |
+
def description(self) -> str:
|
| 706 |
+
return "I2V history seeding: appends fake_image_latents to history_latents."
|
| 707 |
+
|
| 708 |
+
@property
|
| 709 |
+
def inputs(self) -> list[InputParam]:
|
| 710 |
+
return [
|
| 711 |
+
InputParam("history_latents", required=True, type_hint=torch.Tensor),
|
| 712 |
+
InputParam("fake_image_latents", required=True, type_hint=torch.Tensor),
|
| 713 |
+
]
|
| 714 |
+
|
| 715 |
+
@property
|
| 716 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 717 |
+
return [
|
| 718 |
+
OutputParam(
|
| 719 |
+
"history_latents", type_hint=torch.Tensor, description="History latents seeded with fake_image_latents"
|
| 720 |
+
),
|
| 721 |
+
]
|
| 722 |
+
|
| 723 |
+
@torch.no_grad()
|
| 724 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 725 |
+
block_state = self.get_block_state(state)
|
| 726 |
+
|
| 727 |
+
block_state.history_latents = torch.cat([block_state.history_latents, block_state.fake_image_latents], dim=2)
|
| 728 |
+
|
| 729 |
+
self.set_block_state(state, block_state)
|
| 730 |
+
return components, state
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
class HeliosV2VSeedHistoryStep(ModularPipelineBlocks):
|
| 734 |
+
"""Seeds history_latents with video_latents for V2V pipelines.
|
| 735 |
+
|
| 736 |
+
This step runs after HeliosPrepareHistoryStep and replaces the tail of history_latents with video_latents. If the
|
| 737 |
+
video has fewer frames than the history, the beginning of history is preserved.
|
| 738 |
+
"""
|
| 739 |
+
|
| 740 |
+
model_name = "helios"
|
| 741 |
+
|
| 742 |
+
@property
|
| 743 |
+
def description(self) -> str:
|
| 744 |
+
return "V2V history seeding: replaces the tail of history_latents with video_latents."
|
| 745 |
+
|
| 746 |
+
@property
|
| 747 |
+
def inputs(self) -> list[InputParam]:
|
| 748 |
+
return [
|
| 749 |
+
InputParam("history_latents", required=True, type_hint=torch.Tensor),
|
| 750 |
+
InputParam("video_latents", required=True, type_hint=torch.Tensor),
|
| 751 |
+
]
|
| 752 |
+
|
| 753 |
+
@property
|
| 754 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 755 |
+
return [
|
| 756 |
+
OutputParam(
|
| 757 |
+
"history_latents", type_hint=torch.Tensor, description="History latents seeded with video_latents"
|
| 758 |
+
),
|
| 759 |
+
]
|
| 760 |
+
|
| 761 |
+
@torch.no_grad()
|
| 762 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 763 |
+
block_state = self.get_block_state(state)
|
| 764 |
+
|
| 765 |
+
history_latents = block_state.history_latents
|
| 766 |
+
video_latents = block_state.video_latents
|
| 767 |
+
|
| 768 |
+
history_frames = history_latents.shape[2]
|
| 769 |
+
video_frames = video_latents.shape[2]
|
| 770 |
+
if video_frames < history_frames:
|
| 771 |
+
keep_frames = history_frames - video_frames
|
| 772 |
+
history_latents = torch.cat([history_latents[:, :, :keep_frames, :, :], video_latents], dim=2)
|
| 773 |
+
else:
|
| 774 |
+
history_latents = video_latents
|
| 775 |
+
|
| 776 |
+
block_state.history_latents = history_latents
|
| 777 |
+
|
| 778 |
+
self.set_block_state(state, block_state)
|
| 779 |
+
return components, state
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
class HeliosSetTimestepsStep(ModularPipelineBlocks):
|
| 783 |
+
"""Computes scheduler parameters (mu, sigmas) for the chunk loop."""
|
| 784 |
+
|
| 785 |
+
model_name = "helios"
|
| 786 |
+
|
| 787 |
+
@property
|
| 788 |
+
def description(self) -> str:
|
| 789 |
+
return "Computes scheduler shift parameter (mu) and default sigmas for the Helios chunk loop."
|
| 790 |
+
|
| 791 |
+
@property
|
| 792 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 793 |
+
return [
|
| 794 |
+
ComponentSpec("transformer", HeliosTransformer3DModel),
|
| 795 |
+
ComponentSpec("scheduler", HeliosScheduler),
|
| 796 |
+
]
|
| 797 |
+
|
| 798 |
+
@property
|
| 799 |
+
def inputs(self) -> list[InputParam]:
|
| 800 |
+
return [
|
| 801 |
+
InputParam("latent_shape", required=True, type_hint=tuple),
|
| 802 |
+
InputParam.template("num_inference_steps"),
|
| 803 |
+
InputParam.template("sigmas"),
|
| 804 |
+
]
|
| 805 |
+
|
| 806 |
+
@property
|
| 807 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 808 |
+
return [
|
| 809 |
+
OutputParam("mu", type_hint=float, description="Scheduler shift parameter"),
|
| 810 |
+
OutputParam("sigmas", type_hint=list, description="Sigma schedule for diffusion"),
|
| 811 |
+
]
|
| 812 |
+
|
| 813 |
+
@torch.no_grad()
|
| 814 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 815 |
+
block_state = self.get_block_state(state)
|
| 816 |
+
|
| 817 |
+
patch_size = components.transformer.config.patch_size
|
| 818 |
+
latent_shape = block_state.latent_shape
|
| 819 |
+
image_seq_len = (latent_shape[-1] * latent_shape[-2] * latent_shape[-3]) // (
|
| 820 |
+
patch_size[0] * patch_size[1] * patch_size[2]
|
| 821 |
+
)
|
| 822 |
+
|
| 823 |
+
if block_state.sigmas is None:
|
| 824 |
+
block_state.sigmas = np.linspace(0.999, 0.0, block_state.num_inference_steps + 1)[:-1]
|
| 825 |
+
|
| 826 |
+
block_state.mu = calculate_shift(
|
| 827 |
+
image_seq_len,
|
| 828 |
+
components.scheduler.config.get("base_image_seq_len", 256),
|
| 829 |
+
components.scheduler.config.get("max_image_seq_len", 4096),
|
| 830 |
+
components.scheduler.config.get("base_shift", 0.5),
|
| 831 |
+
components.scheduler.config.get("max_shift", 1.15),
|
| 832 |
+
)
|
| 833 |
+
|
| 834 |
+
self.set_block_state(state, block_state)
|
| 835 |
+
|
| 836 |
+
return components, state
|
diffusers/modular_pipelines/helios/decoders.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import PIL
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...models import AutoencoderKLWan
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ...video_processor import VideoProcessor
|
| 23 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 24 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class HeliosDecodeStep(ModularPipelineBlocks):
|
| 31 |
+
"""Decode all chunk latents with VAE, trim frames, and postprocess into final video output."""
|
| 32 |
+
|
| 33 |
+
model_name = "helios"
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def description(self) -> str:
|
| 37 |
+
return (
|
| 38 |
+
"Decodes all chunk latents with the VAE, concatenates them, "
|
| 39 |
+
"trims to the target frame count, and postprocesses into the final video output."
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
@property
|
| 43 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 44 |
+
return [
|
| 45 |
+
ComponentSpec("vae", AutoencoderKLWan),
|
| 46 |
+
ComponentSpec(
|
| 47 |
+
"video_processor",
|
| 48 |
+
VideoProcessor,
|
| 49 |
+
config=FrozenDict({"vae_scale_factor": 8}),
|
| 50 |
+
default_creation_method="from_config",
|
| 51 |
+
),
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def inputs(self) -> list[InputParam]:
|
| 56 |
+
return [
|
| 57 |
+
InputParam(
|
| 58 |
+
"latent_chunks", required=True, type_hint=list, description="List of per-chunk denoised latent tensors"
|
| 59 |
+
),
|
| 60 |
+
InputParam("num_frames", required=True, type_hint=int, description="The target number of output frames"),
|
| 61 |
+
InputParam.template("output_type", default="np"),
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
@property
|
| 65 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 66 |
+
return [
|
| 67 |
+
OutputParam(
|
| 68 |
+
"videos",
|
| 69 |
+
type_hint=list[list[PIL.Image.Image]] | list[torch.Tensor] | list[np.ndarray],
|
| 70 |
+
description="The generated videos, can be a PIL.Image.Image, torch.Tensor or a numpy array",
|
| 71 |
+
),
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
@torch.no_grad()
|
| 75 |
+
def __call__(self, components, state: PipelineState) -> PipelineState:
|
| 76 |
+
block_state = self.get_block_state(state)
|
| 77 |
+
|
| 78 |
+
vae = components.vae
|
| 79 |
+
device = components._execution_device
|
| 80 |
+
decode_dtype = vae.dtype
|
| 81 |
+
|
| 82 |
+
latents_mean = (
|
| 83 |
+
torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(device, decode_dtype)
|
| 84 |
+
)
|
| 85 |
+
latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
|
| 86 |
+
device, decode_dtype
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
history_video = None
|
| 90 |
+
for chunk_latents in block_state.latent_chunks:
|
| 91 |
+
current_latents = chunk_latents.to(device=device, dtype=decode_dtype) / latents_std + latents_mean
|
| 92 |
+
current_video = vae.decode(current_latents, return_dict=False)[0]
|
| 93 |
+
|
| 94 |
+
if history_video is None:
|
| 95 |
+
history_video = current_video
|
| 96 |
+
else:
|
| 97 |
+
history_video = torch.cat([history_video, current_video], dim=2)
|
| 98 |
+
|
| 99 |
+
# Trim to proper frame count
|
| 100 |
+
generated_frames = history_video.size(2)
|
| 101 |
+
generated_frames = (
|
| 102 |
+
generated_frames - 1
|
| 103 |
+
) // components.vae_scale_factor_temporal * components.vae_scale_factor_temporal + 1
|
| 104 |
+
history_video = history_video[:, :, :generated_frames]
|
| 105 |
+
|
| 106 |
+
block_state.videos = components.video_processor.postprocess_video(
|
| 107 |
+
history_video, output_type=block_state.output_type
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
self.set_block_state(state, block_state)
|
| 111 |
+
|
| 112 |
+
return components, state
|
diffusers/modular_pipelines/helios/denoise.py
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import inspect
|
| 16 |
+
import math
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from tqdm.auto import tqdm
|
| 21 |
+
|
| 22 |
+
from ...configuration_utils import FrozenDict
|
| 23 |
+
from ...guiders import ClassifierFreeGuidance, ClassifierFreeZeroStarGuidance
|
| 24 |
+
from ...models import HeliosTransformer3DModel
|
| 25 |
+
from ...schedulers import HeliosScheduler
|
| 26 |
+
from ...utils import logging
|
| 27 |
+
from ...utils.torch_utils import randn_tensor
|
| 28 |
+
from ..modular_pipeline import (
|
| 29 |
+
BlockState,
|
| 30 |
+
LoopSequentialPipelineBlocks,
|
| 31 |
+
ModularPipelineBlocks,
|
| 32 |
+
PipelineState,
|
| 33 |
+
)
|
| 34 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 35 |
+
from .before_denoise import calculate_shift
|
| 36 |
+
from .modular_pipeline import HeliosModularPipeline
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def sample_block_noise(
|
| 43 |
+
batch_size,
|
| 44 |
+
channel,
|
| 45 |
+
num_frames,
|
| 46 |
+
height,
|
| 47 |
+
width,
|
| 48 |
+
gamma,
|
| 49 |
+
patch_size=(1, 2, 2),
|
| 50 |
+
device=None,
|
| 51 |
+
generator=None,
|
| 52 |
+
):
|
| 53 |
+
"""Generate spatially-correlated block noise for pyramid upsampling correction.
|
| 54 |
+
|
| 55 |
+
Uses a multivariate normal distribution with covariance based on `gamma` to produce noise with block structure,
|
| 56 |
+
matching the upsampling artifacts that need correction.
|
| 57 |
+
"""
|
| 58 |
+
# NOTE: A generator must be provided to ensure correct and reproducible results.
|
| 59 |
+
# Creating a default generator here is a fallback only — without a fixed seed,
|
| 60 |
+
# the output will be non-deterministic and may produce incorrect results in CP context.
|
| 61 |
+
if generator is None:
|
| 62 |
+
generator = torch.Generator(device=device)
|
| 63 |
+
elif isinstance(generator, list):
|
| 64 |
+
generator = generator[0]
|
| 65 |
+
|
| 66 |
+
_, ph, pw = patch_size
|
| 67 |
+
block_size = ph * pw
|
| 68 |
+
|
| 69 |
+
cov = (
|
| 70 |
+
torch.eye(block_size, device=device) * (1 + gamma) - torch.ones(block_size, block_size, device=device) * gamma
|
| 71 |
+
)
|
| 72 |
+
cov += torch.eye(block_size, device=device) * 1e-8
|
| 73 |
+
cov = cov.float() # Upcast to fp32 for numerical stability — cholesky is unreliable in fp16/bf16.
|
| 74 |
+
|
| 75 |
+
L = torch.linalg.cholesky(cov)
|
| 76 |
+
block_number = batch_size * channel * num_frames * (height // ph) * (width // pw)
|
| 77 |
+
z = torch.randn(block_number, block_size, device=generator.device, generator=generator).to(device)
|
| 78 |
+
noise = z @ L.T
|
| 79 |
+
|
| 80 |
+
noise = noise.view(batch_size, channel, num_frames, height // ph, width // pw, ph, pw)
|
| 81 |
+
noise = noise.permute(0, 1, 2, 3, 5, 4, 6).reshape(batch_size, channel, num_frames, height, width)
|
| 82 |
+
return noise
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# ========================================
|
| 86 |
+
# Chunk Loop Leaf Blocks
|
| 87 |
+
# ========================================
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class HeliosChunkHistorySliceStep(ModularPipelineBlocks):
|
| 91 |
+
"""Slices history latents into short/mid/long for a T2V chunk.
|
| 92 |
+
|
| 93 |
+
At k==0 with no image_latents, creates a zero prefix. Otherwise uses image_latents (either provided or captured
|
| 94 |
+
from first chunk by HeliosChunkUpdateStep).
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
model_name = "helios"
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def description(self) -> str:
|
| 101 |
+
return (
|
| 102 |
+
"T2V history slice: splits history into long/mid/short. At k==0 with no image_latents, "
|
| 103 |
+
"creates a zero prefix; otherwise uses image_latents as prefix for short history."
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def inputs(self) -> list[InputParam]:
|
| 108 |
+
return [
|
| 109 |
+
InputParam(
|
| 110 |
+
"keep_first_frame",
|
| 111 |
+
default=True,
|
| 112 |
+
type_hint=bool,
|
| 113 |
+
description="Whether to keep the first frame as a prefix in history.",
|
| 114 |
+
),
|
| 115 |
+
InputParam(
|
| 116 |
+
"history_sizes",
|
| 117 |
+
required=True,
|
| 118 |
+
type_hint=list,
|
| 119 |
+
description="Sizes of long/mid/short history buffers for temporal context.",
|
| 120 |
+
),
|
| 121 |
+
InputParam(
|
| 122 |
+
"history_latents",
|
| 123 |
+
required=True,
|
| 124 |
+
type_hint=torch.Tensor,
|
| 125 |
+
description="Accumulated history latents from previous chunks.",
|
| 126 |
+
),
|
| 127 |
+
InputParam("latent_shape", required=True, type_hint=tuple),
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 132 |
+
return []
|
| 133 |
+
|
| 134 |
+
@torch.no_grad()
|
| 135 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 136 |
+
keep_first_frame = block_state.keep_first_frame
|
| 137 |
+
history_sizes = block_state.history_sizes
|
| 138 |
+
image_latents = block_state.image_latents
|
| 139 |
+
device = components._execution_device
|
| 140 |
+
|
| 141 |
+
batch_size, num_channels_latents, _, h_latent, w_latent = block_state.latent_shape
|
| 142 |
+
|
| 143 |
+
if keep_first_frame:
|
| 144 |
+
latents_history_long, latents_history_mid, latents_history_1x = block_state.history_latents[
|
| 145 |
+
:, :, -sum(history_sizes) :
|
| 146 |
+
].split(history_sizes, dim=2)
|
| 147 |
+
if image_latents is None and k == 0:
|
| 148 |
+
latents_prefix = torch.zeros(
|
| 149 |
+
batch_size,
|
| 150 |
+
num_channels_latents,
|
| 151 |
+
1,
|
| 152 |
+
h_latent,
|
| 153 |
+
w_latent,
|
| 154 |
+
device=device,
|
| 155 |
+
dtype=torch.float32,
|
| 156 |
+
)
|
| 157 |
+
else:
|
| 158 |
+
latents_prefix = image_latents
|
| 159 |
+
latents_history_short = torch.cat([latents_prefix, latents_history_1x], dim=2)
|
| 160 |
+
else:
|
| 161 |
+
latents_history_long, latents_history_mid, latents_history_short = block_state.history_latents[
|
| 162 |
+
:, :, -sum(history_sizes) :
|
| 163 |
+
].split(history_sizes, dim=2)
|
| 164 |
+
|
| 165 |
+
block_state.latents_history_short = latents_history_short
|
| 166 |
+
block_state.latents_history_mid = latents_history_mid
|
| 167 |
+
block_state.latents_history_long = latents_history_long
|
| 168 |
+
|
| 169 |
+
return components, block_state
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class HeliosI2VChunkHistorySliceStep(ModularPipelineBlocks):
|
| 173 |
+
"""Slices history latents into short/mid/long for an I2V chunk.
|
| 174 |
+
|
| 175 |
+
Always uses image_latents as prefix (assumes history pre-seeded with fake_image_latents).
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
model_name = "helios"
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def description(self) -> str:
|
| 182 |
+
return (
|
| 183 |
+
"I2V history slice: splits pre-seeded history into long/mid/short, "
|
| 184 |
+
"always using image_latents as prefix for short history."
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
@property
|
| 188 |
+
def inputs(self) -> list[InputParam]:
|
| 189 |
+
return [
|
| 190 |
+
InputParam(
|
| 191 |
+
"keep_first_frame",
|
| 192 |
+
default=True,
|
| 193 |
+
type_hint=bool,
|
| 194 |
+
description="Whether to keep the first frame as a prefix in history.",
|
| 195 |
+
),
|
| 196 |
+
InputParam(
|
| 197 |
+
"history_sizes",
|
| 198 |
+
required=True,
|
| 199 |
+
type_hint=list,
|
| 200 |
+
description="Sizes of long/mid/short history buffers for temporal context.",
|
| 201 |
+
),
|
| 202 |
+
InputParam(
|
| 203 |
+
"history_latents",
|
| 204 |
+
required=True,
|
| 205 |
+
type_hint=torch.Tensor,
|
| 206 |
+
description="Accumulated history latents from previous chunks.",
|
| 207 |
+
),
|
| 208 |
+
InputParam(
|
| 209 |
+
"image_latents",
|
| 210 |
+
required=True,
|
| 211 |
+
type_hint=torch.Tensor,
|
| 212 |
+
description="First-frame latents used as prefix for short history.",
|
| 213 |
+
),
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
@property
|
| 217 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 218 |
+
return []
|
| 219 |
+
|
| 220 |
+
@torch.no_grad()
|
| 221 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 222 |
+
keep_first_frame = block_state.keep_first_frame
|
| 223 |
+
history_sizes = block_state.history_sizes
|
| 224 |
+
image_latents = block_state.image_latents
|
| 225 |
+
|
| 226 |
+
if keep_first_frame:
|
| 227 |
+
latents_history_long, latents_history_mid, latents_history_1x = block_state.history_latents[
|
| 228 |
+
:, :, -sum(history_sizes) :
|
| 229 |
+
].split(history_sizes, dim=2)
|
| 230 |
+
latents_history_short = torch.cat([image_latents, latents_history_1x], dim=2)
|
| 231 |
+
else:
|
| 232 |
+
latents_history_long, latents_history_mid, latents_history_short = block_state.history_latents[
|
| 233 |
+
:, :, -sum(history_sizes) :
|
| 234 |
+
].split(history_sizes, dim=2)
|
| 235 |
+
|
| 236 |
+
block_state.latents_history_short = latents_history_short
|
| 237 |
+
block_state.latents_history_mid = latents_history_mid
|
| 238 |
+
block_state.latents_history_long = latents_history_long
|
| 239 |
+
|
| 240 |
+
return components, block_state
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class HeliosChunkNoiseGenStep(ModularPipelineBlocks):
|
| 244 |
+
"""Generates noise latents for a chunk using randn_tensor."""
|
| 245 |
+
|
| 246 |
+
model_name = "helios"
|
| 247 |
+
|
| 248 |
+
@property
|
| 249 |
+
def description(self) -> str:
|
| 250 |
+
return "Generates random noise latents at full resolution for a single chunk."
|
| 251 |
+
|
| 252 |
+
@property
|
| 253 |
+
def inputs(self) -> list[InputParam]:
|
| 254 |
+
return [
|
| 255 |
+
InputParam("latent_shape", required=True, type_hint=tuple),
|
| 256 |
+
InputParam.template("generator"),
|
| 257 |
+
]
|
| 258 |
+
|
| 259 |
+
@torch.no_grad()
|
| 260 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 261 |
+
device = components._execution_device
|
| 262 |
+
block_state.latents = randn_tensor(
|
| 263 |
+
block_state.latent_shape, generator=block_state.generator, device=device, dtype=torch.float32
|
| 264 |
+
)
|
| 265 |
+
return components, block_state
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class HeliosPyramidChunkNoiseGenStep(ModularPipelineBlocks):
|
| 269 |
+
"""Generates noise latents and downsamples to smallest pyramid level."""
|
| 270 |
+
|
| 271 |
+
model_name = "helios-pyramid"
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def description(self) -> str:
|
| 275 |
+
return (
|
| 276 |
+
"Generates random noise at full resolution, then downsamples to the smallest "
|
| 277 |
+
"pyramid level via bilinear interpolation."
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
@property
|
| 281 |
+
def inputs(self) -> list[InputParam]:
|
| 282 |
+
return [
|
| 283 |
+
InputParam("latent_shape", required=True, type_hint=tuple),
|
| 284 |
+
InputParam(
|
| 285 |
+
"pyramid_num_inference_steps_list",
|
| 286 |
+
default=[10, 10, 10],
|
| 287 |
+
type_hint=list,
|
| 288 |
+
description="Number of denoising steps per pyramid stage.",
|
| 289 |
+
),
|
| 290 |
+
InputParam.template("generator"),
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
@torch.no_grad()
|
| 294 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 295 |
+
device = components._execution_device
|
| 296 |
+
batch_size, num_channels_latents, num_latent_frames, h_latent, w_latent = block_state.latent_shape
|
| 297 |
+
|
| 298 |
+
latents = randn_tensor(
|
| 299 |
+
block_state.latent_shape, generator=block_state.generator, device=device, dtype=torch.float32
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
# Downsample to smallest pyramid level
|
| 303 |
+
h, w = h_latent, w_latent
|
| 304 |
+
latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_latent_frames, num_channels_latents, h, w)
|
| 305 |
+
for _ in range(len(block_state.pyramid_num_inference_steps_list) - 1):
|
| 306 |
+
h //= 2
|
| 307 |
+
w //= 2
|
| 308 |
+
latents = F.interpolate(latents, size=(h, w), mode="bilinear") * 2
|
| 309 |
+
block_state.latents = latents.reshape(batch_size, num_latent_frames, num_channels_latents, h, w).permute(
|
| 310 |
+
0, 2, 1, 3, 4
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
return components, block_state
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
class HeliosChunkSchedulerResetStep(ModularPipelineBlocks):
|
| 317 |
+
"""Resets the scheduler with timesteps for a single chunk."""
|
| 318 |
+
|
| 319 |
+
model_name = "helios"
|
| 320 |
+
|
| 321 |
+
@property
|
| 322 |
+
def description(self) -> str:
|
| 323 |
+
return "Resets the scheduler with the correct timesteps and shift parameter (mu) for this chunk."
|
| 324 |
+
|
| 325 |
+
@property
|
| 326 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 327 |
+
return [
|
| 328 |
+
ComponentSpec("scheduler", HeliosScheduler),
|
| 329 |
+
]
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def inputs(self) -> list[InputParam]:
|
| 333 |
+
return [
|
| 334 |
+
InputParam("mu", required=True, type_hint=float),
|
| 335 |
+
InputParam.template("sigmas", required=True),
|
| 336 |
+
InputParam.template("num_inference_steps"),
|
| 337 |
+
]
|
| 338 |
+
|
| 339 |
+
@torch.no_grad()
|
| 340 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 341 |
+
device = components._execution_device
|
| 342 |
+
components.scheduler.set_timesteps(
|
| 343 |
+
block_state.num_inference_steps, device=device, sigmas=block_state.sigmas, mu=block_state.mu
|
| 344 |
+
)
|
| 345 |
+
block_state.timesteps = components.scheduler.timesteps
|
| 346 |
+
|
| 347 |
+
return components, block_state
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
# ========================================
|
| 351 |
+
# Inner Denoising Blocks
|
| 352 |
+
# ========================================
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
class HeliosChunkDenoiseInner(ModularPipelineBlocks):
|
| 356 |
+
"""Inner timestep loop for denoising a single chunk, using guider for guidance."""
|
| 357 |
+
|
| 358 |
+
model_name = "helios"
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def description(self) -> str:
|
| 362 |
+
return (
|
| 363 |
+
"Inner denoising loop that iterates over timesteps for a single chunk. "
|
| 364 |
+
"Uses the guider to manage conditional/unconditional forward passes with cache_context, "
|
| 365 |
+
"applies guidance, and runs scheduler step."
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
@property
|
| 369 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 370 |
+
return [
|
| 371 |
+
ComponentSpec("transformer", HeliosTransformer3DModel),
|
| 372 |
+
ComponentSpec("scheduler", HeliosScheduler),
|
| 373 |
+
ComponentSpec(
|
| 374 |
+
"guider",
|
| 375 |
+
ClassifierFreeGuidance,
|
| 376 |
+
config=FrozenDict({"guidance_scale": 5.0}),
|
| 377 |
+
default_creation_method="from_config",
|
| 378 |
+
),
|
| 379 |
+
]
|
| 380 |
+
|
| 381 |
+
@property
|
| 382 |
+
def inputs(self) -> list[InputParam]:
|
| 383 |
+
return [
|
| 384 |
+
InputParam.template("latents"),
|
| 385 |
+
InputParam.template("timesteps"),
|
| 386 |
+
InputParam("prompt_embeds", type_hint=torch.Tensor),
|
| 387 |
+
InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
|
| 388 |
+
InputParam.template("denoiser_input_fields"),
|
| 389 |
+
InputParam.template("num_inference_steps"),
|
| 390 |
+
InputParam.template("attention_kwargs"),
|
| 391 |
+
InputParam.template("generator"),
|
| 392 |
+
]
|
| 393 |
+
|
| 394 |
+
@torch.no_grad()
|
| 395 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 396 |
+
latents = block_state.latents
|
| 397 |
+
timesteps = block_state.timesteps
|
| 398 |
+
num_inference_steps = block_state.num_inference_steps
|
| 399 |
+
|
| 400 |
+
transformer_dtype = components.transformer.dtype
|
| 401 |
+
num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
|
| 402 |
+
|
| 403 |
+
# Guider inputs: only encoder_hidden_states differs between cond/uncond
|
| 404 |
+
guider_inputs = {
|
| 405 |
+
"encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
# Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
|
| 409 |
+
transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
|
| 410 |
+
shared_kwargs = {}
|
| 411 |
+
for field_name, field_value in block_state.denoiser_input_fields.items():
|
| 412 |
+
if field_name in transformer_args and field_name not in guider_inputs:
|
| 413 |
+
shared_kwargs[field_name] = field_value
|
| 414 |
+
|
| 415 |
+
# Add loop-internal history latents with dtype casting
|
| 416 |
+
shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
|
| 417 |
+
shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
|
| 418 |
+
shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
|
| 419 |
+
shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
|
| 420 |
+
|
| 421 |
+
with tqdm(total=num_inference_steps) as progress_bar:
|
| 422 |
+
for i, t in enumerate(timesteps):
|
| 423 |
+
timestep = t.expand(latents.shape[0]).to(torch.int64)
|
| 424 |
+
latent_model_input = latents.to(transformer_dtype)
|
| 425 |
+
|
| 426 |
+
components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
|
| 427 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 428 |
+
|
| 429 |
+
for guider_state_batch in guider_state:
|
| 430 |
+
components.guider.prepare_models(components.transformer)
|
| 431 |
+
cond_kwargs = {k: getattr(guider_state_batch, k) for k in guider_inputs.keys()}
|
| 432 |
+
|
| 433 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key)
|
| 434 |
+
with components.transformer.cache_context(context_name):
|
| 435 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 436 |
+
hidden_states=latent_model_input,
|
| 437 |
+
timestep=timestep,
|
| 438 |
+
return_dict=False,
|
| 439 |
+
**cond_kwargs,
|
| 440 |
+
**shared_kwargs,
|
| 441 |
+
)[0]
|
| 442 |
+
components.guider.cleanup_models(components.transformer)
|
| 443 |
+
|
| 444 |
+
noise_pred = components.guider(guider_state)[0]
|
| 445 |
+
|
| 446 |
+
# Scheduler step
|
| 447 |
+
latents = components.scheduler.step(
|
| 448 |
+
noise_pred,
|
| 449 |
+
t,
|
| 450 |
+
latents,
|
| 451 |
+
generator=block_state.generator,
|
| 452 |
+
return_dict=False,
|
| 453 |
+
)[0]
|
| 454 |
+
|
| 455 |
+
if i == len(timesteps) - 1 or (
|
| 456 |
+
(i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 457 |
+
):
|
| 458 |
+
progress_bar.update()
|
| 459 |
+
|
| 460 |
+
block_state.latents = latents
|
| 461 |
+
return components, block_state
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
class HeliosPyramidChunkDenoiseInner(ModularPipelineBlocks):
|
| 465 |
+
"""Nested pyramid stage loop with inner timestep denoising.
|
| 466 |
+
|
| 467 |
+
For each pyramid stage (small -> full resolution):
|
| 468 |
+
1. Upsample latents + block noise correction (stages > 0)
|
| 469 |
+
2. Compute mu from current resolution, set scheduler timesteps
|
| 470 |
+
3. Run timestep denoising loop (same logic as HeliosChunkDenoiseInner)
|
| 471 |
+
"""
|
| 472 |
+
|
| 473 |
+
model_name = "helios-pyramid"
|
| 474 |
+
|
| 475 |
+
@property
|
| 476 |
+
def description(self) -> str:
|
| 477 |
+
return (
|
| 478 |
+
"Pyramid denoising inner block: loops over pyramid stages from smallest to full resolution. "
|
| 479 |
+
"Each stage upsamples latents (with block noise correction), recomputes scheduler parameters, "
|
| 480 |
+
"and runs the timestep denoising loop."
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
@property
|
| 484 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 485 |
+
return [
|
| 486 |
+
ComponentSpec("transformer", HeliosTransformer3DModel),
|
| 487 |
+
ComponentSpec("scheduler", HeliosScheduler),
|
| 488 |
+
ComponentSpec(
|
| 489 |
+
"guider",
|
| 490 |
+
ClassifierFreeZeroStarGuidance,
|
| 491 |
+
config=FrozenDict({"guidance_scale": 5.0, "zero_init_steps": 2}),
|
| 492 |
+
default_creation_method="from_config",
|
| 493 |
+
),
|
| 494 |
+
]
|
| 495 |
+
|
| 496 |
+
@property
|
| 497 |
+
def inputs(self) -> list[InputParam]:
|
| 498 |
+
return [
|
| 499 |
+
InputParam.template("latents"),
|
| 500 |
+
InputParam("prompt_embeds", type_hint=torch.Tensor),
|
| 501 |
+
InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
|
| 502 |
+
InputParam.template("denoiser_input_fields"),
|
| 503 |
+
InputParam(
|
| 504 |
+
"pyramid_num_inference_steps_list",
|
| 505 |
+
default=[10, 10, 10],
|
| 506 |
+
type_hint=list,
|
| 507 |
+
description="Number of denoising steps per pyramid stage.",
|
| 508 |
+
),
|
| 509 |
+
InputParam.template("attention_kwargs"),
|
| 510 |
+
InputParam.template("generator"),
|
| 511 |
+
]
|
| 512 |
+
|
| 513 |
+
@torch.no_grad()
|
| 514 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 515 |
+
device = components._execution_device
|
| 516 |
+
transformer_dtype = components.transformer.dtype
|
| 517 |
+
latents = block_state.latents
|
| 518 |
+
pyramid_num_stages = len(block_state.pyramid_num_inference_steps_list)
|
| 519 |
+
|
| 520 |
+
# Guider inputs: only encoder_hidden_states differs between cond/uncond
|
| 521 |
+
guider_inputs = {
|
| 522 |
+
"encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
# Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
|
| 526 |
+
transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
|
| 527 |
+
shared_kwargs = {}
|
| 528 |
+
for field_name, field_value in block_state.denoiser_input_fields.items():
|
| 529 |
+
if field_name in transformer_args and field_name not in guider_inputs:
|
| 530 |
+
shared_kwargs[field_name] = field_value
|
| 531 |
+
|
| 532 |
+
# Add loop-internal history latents with dtype casting
|
| 533 |
+
shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
|
| 534 |
+
shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
|
| 535 |
+
shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
|
| 536 |
+
shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
|
| 537 |
+
|
| 538 |
+
# Save original zero_init_steps if the guider supports it (e.g. ClassifierFreeZeroStarGuidance).
|
| 539 |
+
# Helios only applies zero init in pyramid stage 0 (lowest resolution), so we disable it
|
| 540 |
+
# for subsequent stages by temporarily setting zero_init_steps=0.
|
| 541 |
+
orig_zero_init_steps = getattr(components.guider, "zero_init_steps", None)
|
| 542 |
+
|
| 543 |
+
for i_s in range(pyramid_num_stages):
|
| 544 |
+
# --- Stage setup ---
|
| 545 |
+
|
| 546 |
+
# Disable zero init for stages > 0 (only stage 0 should have zero init)
|
| 547 |
+
if orig_zero_init_steps is not None and i_s > 0:
|
| 548 |
+
components.guider.zero_init_steps = 0
|
| 549 |
+
|
| 550 |
+
# a. Compute mu from current resolution (before upsample, matching standard pipeline)
|
| 551 |
+
patch_size = components.transformer.config.patch_size
|
| 552 |
+
image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
|
| 553 |
+
patch_size[0] * patch_size[1] * patch_size[2]
|
| 554 |
+
)
|
| 555 |
+
mu = calculate_shift(
|
| 556 |
+
image_seq_len,
|
| 557 |
+
components.scheduler.config.get("base_image_seq_len", 256),
|
| 558 |
+
components.scheduler.config.get("max_image_seq_len", 4096),
|
| 559 |
+
components.scheduler.config.get("base_shift", 0.5),
|
| 560 |
+
components.scheduler.config.get("max_shift", 1.15),
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
# b. Set scheduler timesteps for this stage
|
| 564 |
+
num_inference_steps = block_state.pyramid_num_inference_steps_list[i_s]
|
| 565 |
+
components.scheduler.set_timesteps(
|
| 566 |
+
num_inference_steps,
|
| 567 |
+
i_s,
|
| 568 |
+
device=device,
|
| 569 |
+
mu=mu,
|
| 570 |
+
)
|
| 571 |
+
timesteps = components.scheduler.timesteps
|
| 572 |
+
|
| 573 |
+
# c. Upsample + block noise correction for stages > 0
|
| 574 |
+
if i_s > 0:
|
| 575 |
+
batch_size, num_channels_latents, num_frames, current_h, current_w = latents.shape
|
| 576 |
+
new_h = current_h * 2
|
| 577 |
+
new_w = current_w * 2
|
| 578 |
+
|
| 579 |
+
latents = latents.permute(0, 2, 1, 3, 4).reshape(
|
| 580 |
+
batch_size * num_frames, num_channels_latents, current_h, current_w
|
| 581 |
+
)
|
| 582 |
+
latents = F.interpolate(latents, size=(new_h, new_w), mode="nearest")
|
| 583 |
+
latents = latents.reshape(batch_size, num_frames, num_channels_latents, new_h, new_w).permute(
|
| 584 |
+
0, 2, 1, 3, 4
|
| 585 |
+
)
|
| 586 |
+
|
| 587 |
+
# Block noise correction
|
| 588 |
+
ori_sigma = 1 - components.scheduler.ori_start_sigmas[i_s]
|
| 589 |
+
gamma = components.scheduler.config.gamma
|
| 590 |
+
alpha = 1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
|
| 591 |
+
beta = alpha * (1 - ori_sigma) / math.sqrt(gamma)
|
| 592 |
+
|
| 593 |
+
batch_size, num_channels_latents, num_frames, h, w = latents.shape
|
| 594 |
+
noise = sample_block_noise(
|
| 595 |
+
batch_size,
|
| 596 |
+
num_channels_latents,
|
| 597 |
+
num_frames,
|
| 598 |
+
h,
|
| 599 |
+
w,
|
| 600 |
+
gamma,
|
| 601 |
+
patch_size,
|
| 602 |
+
device=device,
|
| 603 |
+
generator=block_state.generator,
|
| 604 |
+
)
|
| 605 |
+
noise = noise.to(dtype=transformer_dtype)
|
| 606 |
+
latents = alpha * latents + beta * noise
|
| 607 |
+
|
| 608 |
+
# --- Timestep denoising loop ---
|
| 609 |
+
num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
|
| 610 |
+
|
| 611 |
+
with tqdm(total=num_inference_steps) as progress_bar:
|
| 612 |
+
for i, t in enumerate(timesteps):
|
| 613 |
+
timestep = t.expand(latents.shape[0]).to(torch.int64)
|
| 614 |
+
latent_model_input = latents.to(transformer_dtype)
|
| 615 |
+
|
| 616 |
+
components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
|
| 617 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 618 |
+
|
| 619 |
+
for guider_state_batch in guider_state:
|
| 620 |
+
components.guider.prepare_models(components.transformer)
|
| 621 |
+
cond_kwargs = {kk: getattr(guider_state_batch, kk) for kk in guider_inputs.keys()}
|
| 622 |
+
|
| 623 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key)
|
| 624 |
+
with components.transformer.cache_context(context_name):
|
| 625 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 626 |
+
hidden_states=latent_model_input,
|
| 627 |
+
timestep=timestep,
|
| 628 |
+
return_dict=False,
|
| 629 |
+
**cond_kwargs,
|
| 630 |
+
**shared_kwargs,
|
| 631 |
+
)[0]
|
| 632 |
+
components.guider.cleanup_models(components.transformer)
|
| 633 |
+
|
| 634 |
+
noise_pred = components.guider(guider_state)[0]
|
| 635 |
+
|
| 636 |
+
# Scheduler step
|
| 637 |
+
latents = components.scheduler.step(
|
| 638 |
+
noise_pred,
|
| 639 |
+
t,
|
| 640 |
+
latents,
|
| 641 |
+
generator=block_state.generator,
|
| 642 |
+
return_dict=False,
|
| 643 |
+
)[0]
|
| 644 |
+
|
| 645 |
+
if i == len(timesteps) - 1 or (
|
| 646 |
+
(i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 647 |
+
):
|
| 648 |
+
progress_bar.update()
|
| 649 |
+
|
| 650 |
+
# Restore original zero_init_steps
|
| 651 |
+
if orig_zero_init_steps is not None:
|
| 652 |
+
components.guider.zero_init_steps = orig_zero_init_steps
|
| 653 |
+
|
| 654 |
+
block_state.latents = latents
|
| 655 |
+
return components, block_state
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
# ========================================
|
| 659 |
+
# Post-Denoise Update
|
| 660 |
+
# ========================================
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
class HeliosChunkUpdateStep(ModularPipelineBlocks):
|
| 664 |
+
"""Updates chunk collection and history after denoising a single chunk."""
|
| 665 |
+
|
| 666 |
+
model_name = "helios"
|
| 667 |
+
|
| 668 |
+
@property
|
| 669 |
+
def description(self) -> str:
|
| 670 |
+
return (
|
| 671 |
+
"Post-denoising update step: appends the denoised latents to the chunk list, "
|
| 672 |
+
"captures image_latents from the first chunk if needed, and extends history_latents."
|
| 673 |
+
)
|
| 674 |
+
|
| 675 |
+
@property
|
| 676 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 677 |
+
return []
|
| 678 |
+
|
| 679 |
+
@property
|
| 680 |
+
def inputs(self) -> list[InputParam]:
|
| 681 |
+
return [
|
| 682 |
+
InputParam("latents", type_hint=torch.Tensor),
|
| 683 |
+
InputParam("history_latents", type_hint=torch.Tensor),
|
| 684 |
+
InputParam("keep_first_frame", default=True, type_hint=bool),
|
| 685 |
+
]
|
| 686 |
+
|
| 687 |
+
@torch.no_grad()
|
| 688 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 689 |
+
# e. Collect denoised latents for this chunk
|
| 690 |
+
block_state.latent_chunks.append(block_state.latents)
|
| 691 |
+
|
| 692 |
+
# f. Update history
|
| 693 |
+
if block_state.keep_first_frame and k == 0 and block_state.image_latents is None:
|
| 694 |
+
block_state.image_latents = block_state.latents[:, :, 0:1, :, :]
|
| 695 |
+
|
| 696 |
+
block_state.history_latents = torch.cat([block_state.history_latents, block_state.latents], dim=2)
|
| 697 |
+
|
| 698 |
+
return components, block_state
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
# ========================================
|
| 702 |
+
# Chunk Loop Wrapper
|
| 703 |
+
# ========================================
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
class HeliosChunkLoopWrapper(LoopSequentialPipelineBlocks):
|
| 707 |
+
"""Outer chunk loop that iterates over temporal chunks.
|
| 708 |
+
|
| 709 |
+
History indices, scheduler params, and history state are prepared by HeliosPrepareHistoryStep and
|
| 710 |
+
HeliosSetTimestepsStep before this block runs. Sub-blocks handle per-chunk preparation, denoising, and history
|
| 711 |
+
updates.
|
| 712 |
+
"""
|
| 713 |
+
|
| 714 |
+
model_name = "helios"
|
| 715 |
+
|
| 716 |
+
@property
|
| 717 |
+
def description(self) -> str:
|
| 718 |
+
return (
|
| 719 |
+
"Pipeline block that iterates over temporal chunks for progressive video generation. "
|
| 720 |
+
"At each chunk iteration, it runs sub-blocks for preparation, denoising, and history updates."
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
@property
|
| 724 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 725 |
+
return [
|
| 726 |
+
InputParam("num_latent_chunk", required=True, type_hint=int),
|
| 727 |
+
]
|
| 728 |
+
|
| 729 |
+
@property
|
| 730 |
+
def loop_intermediate_outputs(self) -> list[OutputParam]:
|
| 731 |
+
return [
|
| 732 |
+
OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors"),
|
| 733 |
+
]
|
| 734 |
+
|
| 735 |
+
@torch.no_grad()
|
| 736 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 737 |
+
block_state = self.get_block_state(state)
|
| 738 |
+
block_state.latent_chunks = []
|
| 739 |
+
|
| 740 |
+
if not hasattr(block_state, "image_latents"):
|
| 741 |
+
block_state.image_latents = None
|
| 742 |
+
|
| 743 |
+
for k in range(block_state.num_latent_chunk):
|
| 744 |
+
components, block_state = self.loop_step(components, block_state, k=k)
|
| 745 |
+
|
| 746 |
+
self.set_block_state(state, block_state)
|
| 747 |
+
|
| 748 |
+
return components, state
|
| 749 |
+
|
| 750 |
+
|
| 751 |
+
# ========================================
|
| 752 |
+
# Composed Chunk Denoise Steps
|
| 753 |
+
# ========================================
|
| 754 |
+
|
| 755 |
+
|
| 756 |
+
class HeliosChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 757 |
+
"""T2V chunk-based denoising: history slice -> noise gen -> scheduler reset -> denoise -> update."""
|
| 758 |
+
|
| 759 |
+
block_classes = [
|
| 760 |
+
HeliosChunkHistorySliceStep,
|
| 761 |
+
HeliosChunkNoiseGenStep,
|
| 762 |
+
HeliosChunkSchedulerResetStep,
|
| 763 |
+
HeliosChunkDenoiseInner,
|
| 764 |
+
HeliosChunkUpdateStep,
|
| 765 |
+
]
|
| 766 |
+
block_names = ["history_slice", "noise_gen", "scheduler_reset", "denoise_inner", "update_chunk"]
|
| 767 |
+
|
| 768 |
+
@property
|
| 769 |
+
def description(self) -> str:
|
| 770 |
+
return (
|
| 771 |
+
"T2V chunk denoise step that iterates over temporal chunks.\n"
|
| 772 |
+
"At each chunk: history_slice -> noise_gen -> scheduler_reset -> denoise_inner -> update_chunk."
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
class HeliosI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 777 |
+
"""I2V chunk-based denoising: I2V history slice -> noise gen -> scheduler reset -> denoise -> update."""
|
| 778 |
+
|
| 779 |
+
block_classes = [
|
| 780 |
+
HeliosI2VChunkHistorySliceStep,
|
| 781 |
+
HeliosChunkNoiseGenStep,
|
| 782 |
+
HeliosChunkSchedulerResetStep,
|
| 783 |
+
HeliosChunkDenoiseInner,
|
| 784 |
+
HeliosChunkUpdateStep,
|
| 785 |
+
]
|
| 786 |
+
block_names = ["history_slice", "noise_gen", "scheduler_reset", "denoise_inner", "update_chunk"]
|
| 787 |
+
|
| 788 |
+
@property
|
| 789 |
+
def description(self) -> str:
|
| 790 |
+
return (
|
| 791 |
+
"I2V chunk denoise step that iterates over temporal chunks.\n"
|
| 792 |
+
"At each chunk: history_slice (I2V) -> noise_gen -> scheduler_reset -> denoise_inner -> update_chunk."
|
| 793 |
+
)
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
class HeliosPyramidDistilledChunkDenoiseInner(ModularPipelineBlocks):
|
| 797 |
+
"""Nested pyramid stage loop with DMD denoising for distilled checkpoints.
|
| 798 |
+
|
| 799 |
+
Same progressive multi-resolution strategy as HeliosPyramidChunkDenoiseInner, but:
|
| 800 |
+
- Guidance is disabled (guidance_scale=1.0, no unconditional pass)
|
| 801 |
+
- Supports is_amplify_first_chunk (doubles first chunk's timesteps via scheduler)
|
| 802 |
+
- Tracks start_point_list and passes DMD-specific args to scheduler.step()
|
| 803 |
+
"""
|
| 804 |
+
|
| 805 |
+
model_name = "helios-pyramid"
|
| 806 |
+
|
| 807 |
+
@property
|
| 808 |
+
def description(self) -> str:
|
| 809 |
+
return (
|
| 810 |
+
"Distilled pyramid denoising inner block for DMD checkpoints. Loops over pyramid stages "
|
| 811 |
+
"from smallest to full resolution with guidance disabled and DMD scheduler support."
|
| 812 |
+
)
|
| 813 |
+
|
| 814 |
+
@property
|
| 815 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 816 |
+
return [
|
| 817 |
+
ComponentSpec("transformer", HeliosTransformer3DModel),
|
| 818 |
+
ComponentSpec("scheduler", HeliosScheduler),
|
| 819 |
+
ComponentSpec(
|
| 820 |
+
"guider",
|
| 821 |
+
ClassifierFreeGuidance,
|
| 822 |
+
config=FrozenDict({"guidance_scale": 1.0}),
|
| 823 |
+
default_creation_method="from_config",
|
| 824 |
+
),
|
| 825 |
+
]
|
| 826 |
+
|
| 827 |
+
@property
|
| 828 |
+
def inputs(self) -> list[InputParam]:
|
| 829 |
+
return [
|
| 830 |
+
InputParam.template("latents"),
|
| 831 |
+
InputParam("prompt_embeds", type_hint=torch.Tensor),
|
| 832 |
+
InputParam("negative_prompt_embeds", type_hint=torch.Tensor),
|
| 833 |
+
InputParam.template("denoiser_input_fields"),
|
| 834 |
+
InputParam(
|
| 835 |
+
"pyramid_num_inference_steps_list",
|
| 836 |
+
default=[2, 2, 2],
|
| 837 |
+
type_hint=list,
|
| 838 |
+
description="Number of denoising steps per pyramid stage.",
|
| 839 |
+
),
|
| 840 |
+
InputParam(
|
| 841 |
+
"is_amplify_first_chunk",
|
| 842 |
+
default=True,
|
| 843 |
+
type_hint=bool,
|
| 844 |
+
description="Whether to double the first chunk's timesteps via the scheduler for amplified generation.",
|
| 845 |
+
),
|
| 846 |
+
InputParam.template("attention_kwargs"),
|
| 847 |
+
InputParam.template("generator"),
|
| 848 |
+
]
|
| 849 |
+
|
| 850 |
+
@torch.no_grad()
|
| 851 |
+
def __call__(self, components: HeliosModularPipeline, block_state: BlockState, k: int):
|
| 852 |
+
device = components._execution_device
|
| 853 |
+
transformer_dtype = components.transformer.dtype
|
| 854 |
+
latents = block_state.latents
|
| 855 |
+
pyramid_num_stages = len(block_state.pyramid_num_inference_steps_list)
|
| 856 |
+
is_first_chunk = k == 0
|
| 857 |
+
|
| 858 |
+
# Track start points for DMD scheduler
|
| 859 |
+
start_point_list = [latents]
|
| 860 |
+
|
| 861 |
+
# Guider inputs: only encoder_hidden_states differs between cond/uncond
|
| 862 |
+
guider_inputs = {
|
| 863 |
+
"encoder_hidden_states": (block_state.prompt_embeds, block_state.negative_prompt_embeds),
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
# Build shared kwargs from denoiser_input_fields (excludes guider-managed ones)
|
| 867 |
+
transformer_args = set(inspect.signature(components.transformer.forward).parameters.keys())
|
| 868 |
+
shared_kwargs = {}
|
| 869 |
+
for field_name, field_value in block_state.denoiser_input_fields.items():
|
| 870 |
+
if field_name in transformer_args and field_name not in guider_inputs:
|
| 871 |
+
shared_kwargs[field_name] = field_value
|
| 872 |
+
|
| 873 |
+
# Add loop-internal history latents with dtype casting
|
| 874 |
+
shared_kwargs["latents_history_short"] = block_state.latents_history_short.to(transformer_dtype)
|
| 875 |
+
shared_kwargs["latents_history_mid"] = block_state.latents_history_mid.to(transformer_dtype)
|
| 876 |
+
shared_kwargs["latents_history_long"] = block_state.latents_history_long.to(transformer_dtype)
|
| 877 |
+
shared_kwargs["attention_kwargs"] = block_state.attention_kwargs
|
| 878 |
+
|
| 879 |
+
for i_s in range(pyramid_num_stages):
|
| 880 |
+
# --- Stage setup ---
|
| 881 |
+
patch_size = components.transformer.config.patch_size
|
| 882 |
+
|
| 883 |
+
# a. Compute mu from current resolution (before upsample, matching standard pipeline)
|
| 884 |
+
image_seq_len = (latents.shape[-1] * latents.shape[-2] * latents.shape[-3]) // (
|
| 885 |
+
patch_size[0] * patch_size[1] * patch_size[2]
|
| 886 |
+
)
|
| 887 |
+
mu = calculate_shift(
|
| 888 |
+
image_seq_len,
|
| 889 |
+
components.scheduler.config.get("base_image_seq_len", 256),
|
| 890 |
+
components.scheduler.config.get("max_image_seq_len", 4096),
|
| 891 |
+
components.scheduler.config.get("base_shift", 0.5),
|
| 892 |
+
components.scheduler.config.get("max_shift", 1.15),
|
| 893 |
+
)
|
| 894 |
+
|
| 895 |
+
# b. Set scheduler timesteps for this stage (with DMD amplification)
|
| 896 |
+
num_inference_steps = block_state.pyramid_num_inference_steps_list[i_s]
|
| 897 |
+
components.scheduler.set_timesteps(
|
| 898 |
+
num_inference_steps,
|
| 899 |
+
i_s,
|
| 900 |
+
device=device,
|
| 901 |
+
mu=mu,
|
| 902 |
+
is_amplify_first_chunk=block_state.is_amplify_first_chunk and is_first_chunk,
|
| 903 |
+
)
|
| 904 |
+
timesteps = components.scheduler.timesteps
|
| 905 |
+
|
| 906 |
+
# c. Upsample + block noise correction for stages > 0
|
| 907 |
+
if i_s > 0:
|
| 908 |
+
batch_size, num_channels_latents, num_frames, current_h, current_w = latents.shape
|
| 909 |
+
new_h = current_h * 2
|
| 910 |
+
new_w = current_w * 2
|
| 911 |
+
|
| 912 |
+
latents = latents.permute(0, 2, 1, 3, 4).reshape(
|
| 913 |
+
batch_size * num_frames, num_channels_latents, current_h, current_w
|
| 914 |
+
)
|
| 915 |
+
latents = F.interpolate(latents, size=(new_h, new_w), mode="nearest")
|
| 916 |
+
latents = latents.reshape(batch_size, num_frames, num_channels_latents, new_h, new_w).permute(
|
| 917 |
+
0, 2, 1, 3, 4
|
| 918 |
+
)
|
| 919 |
+
|
| 920 |
+
# Block noise correction
|
| 921 |
+
ori_sigma = 1 - components.scheduler.ori_start_sigmas[i_s]
|
| 922 |
+
gamma = components.scheduler.config.gamma
|
| 923 |
+
alpha = 1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
|
| 924 |
+
beta = alpha * (1 - ori_sigma) / math.sqrt(gamma)
|
| 925 |
+
|
| 926 |
+
batch_size, num_channels_latents, num_frames, h, w = latents.shape
|
| 927 |
+
noise = sample_block_noise(
|
| 928 |
+
batch_size,
|
| 929 |
+
num_channels_latents,
|
| 930 |
+
num_frames,
|
| 931 |
+
h,
|
| 932 |
+
w,
|
| 933 |
+
gamma,
|
| 934 |
+
patch_size,
|
| 935 |
+
device=device,
|
| 936 |
+
generator=block_state.generator,
|
| 937 |
+
)
|
| 938 |
+
noise = noise.to(dtype=transformer_dtype)
|
| 939 |
+
latents = alpha * latents + beta * noise
|
| 940 |
+
|
| 941 |
+
start_point_list.append(latents)
|
| 942 |
+
|
| 943 |
+
# --- Timestep denoising loop ---
|
| 944 |
+
num_warmup_steps = len(timesteps) - num_inference_steps * components.scheduler.order
|
| 945 |
+
|
| 946 |
+
with tqdm(total=num_inference_steps) as progress_bar:
|
| 947 |
+
for i, t in enumerate(timesteps):
|
| 948 |
+
timestep = t.expand(latents.shape[0]).to(torch.int64)
|
| 949 |
+
latent_model_input = latents.to(transformer_dtype)
|
| 950 |
+
|
| 951 |
+
components.guider.set_state(step=i, num_inference_steps=num_inference_steps, timestep=t)
|
| 952 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 953 |
+
|
| 954 |
+
for guider_state_batch in guider_state:
|
| 955 |
+
components.guider.prepare_models(components.transformer)
|
| 956 |
+
cond_kwargs = {k: getattr(guider_state_batch, k) for k in guider_inputs.keys()}
|
| 957 |
+
|
| 958 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key)
|
| 959 |
+
with components.transformer.cache_context(context_name):
|
| 960 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 961 |
+
hidden_states=latent_model_input,
|
| 962 |
+
timestep=timestep,
|
| 963 |
+
return_dict=False,
|
| 964 |
+
**cond_kwargs,
|
| 965 |
+
**shared_kwargs,
|
| 966 |
+
)[0]
|
| 967 |
+
components.guider.cleanup_models(components.transformer)
|
| 968 |
+
|
| 969 |
+
noise_pred = components.guider(guider_state)[0]
|
| 970 |
+
|
| 971 |
+
# Scheduler step with DMD args
|
| 972 |
+
latents = components.scheduler.step(
|
| 973 |
+
noise_pred,
|
| 974 |
+
t,
|
| 975 |
+
latents,
|
| 976 |
+
generator=block_state.generator,
|
| 977 |
+
return_dict=False,
|
| 978 |
+
cur_sampling_step=i,
|
| 979 |
+
dmd_noisy_tensor=start_point_list[i_s],
|
| 980 |
+
dmd_sigmas=components.scheduler.sigmas,
|
| 981 |
+
dmd_timesteps=components.scheduler.timesteps,
|
| 982 |
+
all_timesteps=timesteps,
|
| 983 |
+
)[0]
|
| 984 |
+
|
| 985 |
+
if i == len(timesteps) - 1 or (
|
| 986 |
+
(i + 1) > num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 987 |
+
):
|
| 988 |
+
progress_bar.update()
|
| 989 |
+
|
| 990 |
+
block_state.latents = latents
|
| 991 |
+
return components, block_state
|
| 992 |
+
|
| 993 |
+
|
| 994 |
+
class HeliosPyramidChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 995 |
+
"""T2V pyramid chunk denoising: history slice -> pyramid noise gen -> pyramid denoise inner -> update."""
|
| 996 |
+
|
| 997 |
+
block_classes = [
|
| 998 |
+
HeliosChunkHistorySliceStep,
|
| 999 |
+
HeliosPyramidChunkNoiseGenStep,
|
| 1000 |
+
HeliosPyramidChunkDenoiseInner,
|
| 1001 |
+
HeliosChunkUpdateStep,
|
| 1002 |
+
]
|
| 1003 |
+
block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
|
| 1004 |
+
|
| 1005 |
+
@property
|
| 1006 |
+
def description(self) -> str:
|
| 1007 |
+
return (
|
| 1008 |
+
"T2V pyramid chunk denoise step that iterates over temporal chunks.\n"
|
| 1009 |
+
"At each chunk: history_slice -> noise_gen (pyramid) -> denoise_inner (pyramid stages) -> update_chunk.\n"
|
| 1010 |
+
"Denoising starts at the smallest resolution and progressively upsamples."
|
| 1011 |
+
)
|
| 1012 |
+
|
| 1013 |
+
|
| 1014 |
+
class HeliosPyramidI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 1015 |
+
"""I2V pyramid chunk denoising: I2V history slice -> pyramid noise gen -> pyramid denoise inner -> update."""
|
| 1016 |
+
|
| 1017 |
+
block_classes = [
|
| 1018 |
+
HeliosI2VChunkHistorySliceStep,
|
| 1019 |
+
HeliosPyramidChunkNoiseGenStep,
|
| 1020 |
+
HeliosPyramidChunkDenoiseInner,
|
| 1021 |
+
HeliosChunkUpdateStep,
|
| 1022 |
+
]
|
| 1023 |
+
block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
|
| 1024 |
+
|
| 1025 |
+
@property
|
| 1026 |
+
def description(self) -> str:
|
| 1027 |
+
return (
|
| 1028 |
+
"I2V pyramid chunk denoise step that iterates over temporal chunks.\n"
|
| 1029 |
+
"At each chunk: history_slice (I2V) -> noise_gen (pyramid) -> denoise_inner (pyramid stages) -> update_chunk.\n"
|
| 1030 |
+
"Denoising starts at the smallest resolution and progressively upsamples."
|
| 1031 |
+
)
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
class HeliosPyramidDistilledChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 1035 |
+
"""T2V distilled pyramid chunk denoising with DMD scheduler and no CFG."""
|
| 1036 |
+
|
| 1037 |
+
block_classes = [
|
| 1038 |
+
HeliosChunkHistorySliceStep,
|
| 1039 |
+
HeliosPyramidChunkNoiseGenStep,
|
| 1040 |
+
HeliosPyramidDistilledChunkDenoiseInner,
|
| 1041 |
+
HeliosChunkUpdateStep,
|
| 1042 |
+
]
|
| 1043 |
+
block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
|
| 1044 |
+
|
| 1045 |
+
@property
|
| 1046 |
+
def description(self) -> str:
|
| 1047 |
+
return (
|
| 1048 |
+
"T2V distilled pyramid chunk denoise step with DMD scheduler.\n"
|
| 1049 |
+
"At each chunk: history_slice -> noise_gen (pyramid) -> denoise_inner (distilled/DMD) -> update_chunk."
|
| 1050 |
+
)
|
| 1051 |
+
|
| 1052 |
+
|
| 1053 |
+
class HeliosPyramidDistilledI2VChunkDenoiseStep(HeliosChunkLoopWrapper):
|
| 1054 |
+
"""I2V distilled pyramid chunk denoising with DMD scheduler and no CFG."""
|
| 1055 |
+
|
| 1056 |
+
block_classes = [
|
| 1057 |
+
HeliosI2VChunkHistorySliceStep,
|
| 1058 |
+
HeliosPyramidChunkNoiseGenStep,
|
| 1059 |
+
HeliosPyramidDistilledChunkDenoiseInner,
|
| 1060 |
+
HeliosChunkUpdateStep,
|
| 1061 |
+
]
|
| 1062 |
+
block_names = ["history_slice", "noise_gen", "denoise_inner", "update_chunk"]
|
| 1063 |
+
|
| 1064 |
+
@property
|
| 1065 |
+
def description(self) -> str:
|
| 1066 |
+
return (
|
| 1067 |
+
"I2V distilled pyramid chunk denoise step with DMD scheduler.\n"
|
| 1068 |
+
"At each chunk: history_slice (I2V) -> noise_gen (pyramid) -> denoise_inner (distilled/DMD) -> update_chunk."
|
| 1069 |
+
)
|
diffusers/modular_pipelines/helios/encoders.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import html
|
| 16 |
+
|
| 17 |
+
import regex as re
|
| 18 |
+
import torch
|
| 19 |
+
from transformers import AutoTokenizer, UMT5EncoderModel
|
| 20 |
+
|
| 21 |
+
from ...configuration_utils import FrozenDict
|
| 22 |
+
from ...guiders import ClassifierFreeGuidance
|
| 23 |
+
from ...models import AutoencoderKLWan
|
| 24 |
+
from ...utils import is_ftfy_available, logging
|
| 25 |
+
from ...video_processor import VideoProcessor
|
| 26 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 27 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 28 |
+
from .modular_pipeline import HeliosModularPipeline
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
if is_ftfy_available():
|
| 32 |
+
import ftfy
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def basic_clean(text):
|
| 39 |
+
text = ftfy.fix_text(text)
|
| 40 |
+
text = html.unescape(html.unescape(text))
|
| 41 |
+
return text.strip()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def whitespace_clean(text):
|
| 45 |
+
text = re.sub(r"\s+", " ", text)
|
| 46 |
+
text = text.strip()
|
| 47 |
+
return text
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def prompt_clean(text):
|
| 51 |
+
text = whitespace_clean(basic_clean(text))
|
| 52 |
+
return text
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_t5_prompt_embeds(
|
| 56 |
+
text_encoder: UMT5EncoderModel,
|
| 57 |
+
tokenizer: AutoTokenizer,
|
| 58 |
+
prompt: str | list[str],
|
| 59 |
+
max_sequence_length: int,
|
| 60 |
+
device: torch.device,
|
| 61 |
+
dtype: torch.dtype | None = None,
|
| 62 |
+
):
|
| 63 |
+
"""Encode text prompts into T5 embeddings for Helios.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
text_encoder: The T5 text encoder model.
|
| 67 |
+
tokenizer: The tokenizer for the text encoder.
|
| 68 |
+
prompt: The prompt or prompts to encode.
|
| 69 |
+
max_sequence_length: Maximum sequence length for tokenization.
|
| 70 |
+
device: Device to place tensors on.
|
| 71 |
+
dtype: Optional dtype override. Defaults to `text_encoder.dtype`.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
A tuple of `(prompt_embeds, attention_mask)` where `prompt_embeds` is the encoded text embeddings and
|
| 75 |
+
`attention_mask` is a boolean mask.
|
| 76 |
+
"""
|
| 77 |
+
dtype = dtype or text_encoder.dtype
|
| 78 |
+
|
| 79 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 80 |
+
prompt = [prompt_clean(u) for u in prompt]
|
| 81 |
+
|
| 82 |
+
text_inputs = tokenizer(
|
| 83 |
+
prompt,
|
| 84 |
+
padding="max_length",
|
| 85 |
+
max_length=max_sequence_length,
|
| 86 |
+
truncation=True,
|
| 87 |
+
add_special_tokens=True,
|
| 88 |
+
return_attention_mask=True,
|
| 89 |
+
return_tensors="pt",
|
| 90 |
+
)
|
| 91 |
+
text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
|
| 92 |
+
seq_lens = mask.gt(0).sum(dim=1).long()
|
| 93 |
+
|
| 94 |
+
prompt_embeds = text_encoder(text_input_ids.to(device), mask.to(device)).last_hidden_state
|
| 95 |
+
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
|
| 96 |
+
prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
|
| 97 |
+
prompt_embeds = torch.stack(
|
| 98 |
+
[torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds], dim=0
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return prompt_embeds, text_inputs.attention_mask.bool()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class HeliosTextEncoderStep(ModularPipelineBlocks):
|
| 105 |
+
model_name = "helios"
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def description(self) -> str:
|
| 109 |
+
return "Text Encoder step that generates text embeddings to guide the video generation"
|
| 110 |
+
|
| 111 |
+
@property
|
| 112 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 113 |
+
return [
|
| 114 |
+
ComponentSpec("text_encoder", UMT5EncoderModel),
|
| 115 |
+
ComponentSpec("tokenizer", AutoTokenizer),
|
| 116 |
+
ComponentSpec(
|
| 117 |
+
"guider",
|
| 118 |
+
ClassifierFreeGuidance,
|
| 119 |
+
config=FrozenDict({"guidance_scale": 5.0}),
|
| 120 |
+
default_creation_method="from_config",
|
| 121 |
+
),
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def inputs(self) -> list[InputParam]:
|
| 126 |
+
return [
|
| 127 |
+
InputParam.template("prompt"),
|
| 128 |
+
InputParam.template("negative_prompt"),
|
| 129 |
+
InputParam.template("max_sequence_length"),
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 134 |
+
return [
|
| 135 |
+
OutputParam.template("prompt_embeds"),
|
| 136 |
+
OutputParam.template("negative_prompt_embeds"),
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def check_inputs(prompt, negative_prompt):
|
| 141 |
+
if prompt is not None and not isinstance(prompt, (str, list)):
|
| 142 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
| 143 |
+
|
| 144 |
+
if negative_prompt is not None and not isinstance(negative_prompt, (str, list)):
|
| 145 |
+
raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
|
| 146 |
+
|
| 147 |
+
if prompt is not None and negative_prompt is not None:
|
| 148 |
+
prompt_list = [prompt] if isinstance(prompt, str) else prompt
|
| 149 |
+
neg_list = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
|
| 150 |
+
if type(prompt_list) is not type(neg_list):
|
| 151 |
+
raise TypeError(
|
| 152 |
+
f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
|
| 153 |
+
f" {type(prompt)}."
|
| 154 |
+
)
|
| 155 |
+
if len(prompt_list) != len(neg_list):
|
| 156 |
+
raise ValueError(
|
| 157 |
+
f"`negative_prompt` has batch size {len(neg_list)}, but `prompt` has batch size"
|
| 158 |
+
f" {len(prompt_list)}. Please make sure that passed `negative_prompt` matches"
|
| 159 |
+
" the batch size of `prompt`."
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
@torch.no_grad()
|
| 163 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 164 |
+
block_state = self.get_block_state(state)
|
| 165 |
+
|
| 166 |
+
prompt = block_state.prompt
|
| 167 |
+
negative_prompt = block_state.negative_prompt
|
| 168 |
+
max_sequence_length = block_state.max_sequence_length
|
| 169 |
+
device = components._execution_device
|
| 170 |
+
|
| 171 |
+
self.check_inputs(prompt, negative_prompt)
|
| 172 |
+
|
| 173 |
+
# Encode prompt
|
| 174 |
+
block_state.prompt_embeds, _ = get_t5_prompt_embeds(
|
| 175 |
+
text_encoder=components.text_encoder,
|
| 176 |
+
tokenizer=components.tokenizer,
|
| 177 |
+
prompt=prompt,
|
| 178 |
+
max_sequence_length=max_sequence_length,
|
| 179 |
+
device=device,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Encode negative prompt
|
| 183 |
+
block_state.negative_prompt_embeds = None
|
| 184 |
+
if components.requires_unconditional_embeds:
|
| 185 |
+
negative_prompt = negative_prompt or ""
|
| 186 |
+
if isinstance(prompt, list) and isinstance(negative_prompt, str):
|
| 187 |
+
negative_prompt = len(prompt) * [negative_prompt]
|
| 188 |
+
|
| 189 |
+
block_state.negative_prompt_embeds, _ = get_t5_prompt_embeds(
|
| 190 |
+
text_encoder=components.text_encoder,
|
| 191 |
+
tokenizer=components.tokenizer,
|
| 192 |
+
prompt=negative_prompt,
|
| 193 |
+
max_sequence_length=max_sequence_length,
|
| 194 |
+
device=device,
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
self.set_block_state(state, block_state)
|
| 198 |
+
return components, state
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class HeliosImageVaeEncoderStep(ModularPipelineBlocks):
|
| 202 |
+
"""Encodes an input image into VAE latent space for image-to-video generation."""
|
| 203 |
+
|
| 204 |
+
model_name = "helios"
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def description(self) -> str:
|
| 208 |
+
return (
|
| 209 |
+
"Image Encoder step that encodes an input image into VAE latent space, "
|
| 210 |
+
"producing image_latents (first frame prefix) and fake_image_latents (history seed) "
|
| 211 |
+
"for image-to-video generation."
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
@property
|
| 215 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 216 |
+
return [
|
| 217 |
+
ComponentSpec("vae", AutoencoderKLWan),
|
| 218 |
+
ComponentSpec(
|
| 219 |
+
"video_processor",
|
| 220 |
+
VideoProcessor,
|
| 221 |
+
config=FrozenDict({"vae_scale_factor": 8}),
|
| 222 |
+
default_creation_method="from_config",
|
| 223 |
+
),
|
| 224 |
+
]
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def inputs(self) -> list[InputParam]:
|
| 228 |
+
return [
|
| 229 |
+
InputParam.template("image"),
|
| 230 |
+
InputParam.template("height", default=384),
|
| 231 |
+
InputParam.template("width", default=640),
|
| 232 |
+
InputParam(
|
| 233 |
+
"num_latent_frames_per_chunk",
|
| 234 |
+
default=9,
|
| 235 |
+
type_hint=int,
|
| 236 |
+
description="Number of latent frames per temporal chunk.",
|
| 237 |
+
),
|
| 238 |
+
InputParam.template("generator"),
|
| 239 |
+
]
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 243 |
+
return [
|
| 244 |
+
OutputParam.template("image_latents"),
|
| 245 |
+
OutputParam(
|
| 246 |
+
"fake_image_latents", type_hint=torch.Tensor, description="Fake image latents for history seeding"
|
| 247 |
+
),
|
| 248 |
+
]
|
| 249 |
+
|
| 250 |
+
@torch.no_grad()
|
| 251 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 252 |
+
block_state = self.get_block_state(state)
|
| 253 |
+
|
| 254 |
+
vae = components.vae
|
| 255 |
+
device = components._execution_device
|
| 256 |
+
|
| 257 |
+
latents_mean = (
|
| 258 |
+
torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype)
|
| 259 |
+
)
|
| 260 |
+
latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
|
| 261 |
+
vae.device, vae.dtype
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Preprocess image to 4D tensor (B, C, H, W)
|
| 265 |
+
image = components.video_processor.preprocess(
|
| 266 |
+
block_state.image, height=block_state.height, width=block_state.width
|
| 267 |
+
)
|
| 268 |
+
image_5d = image.unsqueeze(2).to(device=device, dtype=vae.dtype) # (B, C, 1, H, W)
|
| 269 |
+
|
| 270 |
+
# Encode image to get image_latents
|
| 271 |
+
image_latents = vae.encode(image_5d).latent_dist.sample(generator=block_state.generator)
|
| 272 |
+
image_latents = (image_latents - latents_mean) * latents_std
|
| 273 |
+
|
| 274 |
+
# Encode fake video to get fake_image_latents
|
| 275 |
+
min_frames = (block_state.num_latent_frames_per_chunk - 1) * components.vae_scale_factor_temporal + 1
|
| 276 |
+
fake_video = image_5d.repeat(1, 1, min_frames, 1, 1) # (B, C, min_frames, H, W)
|
| 277 |
+
fake_latents_full = vae.encode(fake_video).latent_dist.sample(generator=block_state.generator)
|
| 278 |
+
fake_latents_full = (fake_latents_full - latents_mean) * latents_std
|
| 279 |
+
fake_image_latents = fake_latents_full[:, :, -1:, :, :]
|
| 280 |
+
|
| 281 |
+
block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
|
| 282 |
+
block_state.fake_image_latents = fake_image_latents.to(device=device, dtype=torch.float32)
|
| 283 |
+
|
| 284 |
+
self.set_block_state(state, block_state)
|
| 285 |
+
return components, state
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
class HeliosVideoVaeEncoderStep(ModularPipelineBlocks):
|
| 289 |
+
"""Encodes an input video into VAE latent space for video-to-video generation.
|
| 290 |
+
|
| 291 |
+
Produces `image_latents` (first frame) and `video_latents` (remaining frames encoded in chunks).
|
| 292 |
+
"""
|
| 293 |
+
|
| 294 |
+
model_name = "helios"
|
| 295 |
+
|
| 296 |
+
@property
|
| 297 |
+
def description(self) -> str:
|
| 298 |
+
return (
|
| 299 |
+
"Video Encoder step that encodes an input video into VAE latent space, "
|
| 300 |
+
"producing image_latents (first frame) and video_latents (chunked video frames) "
|
| 301 |
+
"for video-to-video generation."
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
@property
|
| 305 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 306 |
+
return [
|
| 307 |
+
ComponentSpec("vae", AutoencoderKLWan),
|
| 308 |
+
ComponentSpec(
|
| 309 |
+
"video_processor",
|
| 310 |
+
VideoProcessor,
|
| 311 |
+
config=FrozenDict({"vae_scale_factor": 8}),
|
| 312 |
+
default_creation_method="from_config",
|
| 313 |
+
),
|
| 314 |
+
]
|
| 315 |
+
|
| 316 |
+
@property
|
| 317 |
+
def inputs(self) -> list[InputParam]:
|
| 318 |
+
return [
|
| 319 |
+
InputParam("video", required=True, description="Input video for video-to-video generation"),
|
| 320 |
+
InputParam.template("height", default=384),
|
| 321 |
+
InputParam.template("width", default=640),
|
| 322 |
+
InputParam(
|
| 323 |
+
"num_latent_frames_per_chunk",
|
| 324 |
+
default=9,
|
| 325 |
+
type_hint=int,
|
| 326 |
+
description="Number of latent frames per temporal chunk.",
|
| 327 |
+
),
|
| 328 |
+
InputParam.template("generator"),
|
| 329 |
+
]
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 333 |
+
return [
|
| 334 |
+
OutputParam.template("image_latents"),
|
| 335 |
+
OutputParam("video_latents", type_hint=torch.Tensor, description="Encoded video latents (chunked)"),
|
| 336 |
+
]
|
| 337 |
+
|
| 338 |
+
@torch.no_grad()
|
| 339 |
+
def __call__(self, components: HeliosModularPipeline, state: PipelineState) -> PipelineState:
|
| 340 |
+
block_state = self.get_block_state(state)
|
| 341 |
+
|
| 342 |
+
vae = components.vae
|
| 343 |
+
device = components._execution_device
|
| 344 |
+
num_latent_frames_per_chunk = block_state.num_latent_frames_per_chunk
|
| 345 |
+
|
| 346 |
+
latents_mean = (
|
| 347 |
+
torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(vae.device, vae.dtype)
|
| 348 |
+
)
|
| 349 |
+
latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
|
| 350 |
+
vae.device, vae.dtype
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
# Preprocess video
|
| 354 |
+
video = components.video_processor.preprocess_video(
|
| 355 |
+
block_state.video, height=block_state.height, width=block_state.width
|
| 356 |
+
)
|
| 357 |
+
video = video.to(device=device, dtype=vae.dtype)
|
| 358 |
+
|
| 359 |
+
# Encode video into latents
|
| 360 |
+
num_frames = video.shape[2]
|
| 361 |
+
min_frames = (num_latent_frames_per_chunk - 1) * 4 + 1
|
| 362 |
+
num_chunks = num_frames // min_frames
|
| 363 |
+
if num_chunks == 0:
|
| 364 |
+
raise ValueError(
|
| 365 |
+
f"Video must have at least {min_frames} frames "
|
| 366 |
+
f"(got {num_frames} frames). "
|
| 367 |
+
f"Required: (num_latent_frames_per_chunk - 1) * 4 + 1 = ({num_latent_frames_per_chunk} - 1) * 4 + 1 = {min_frames}"
|
| 368 |
+
)
|
| 369 |
+
total_valid_frames = num_chunks * min_frames
|
| 370 |
+
start_frame = num_frames - total_valid_frames
|
| 371 |
+
|
| 372 |
+
# Encode first frame
|
| 373 |
+
first_frame = video[:, :, 0:1, :, :]
|
| 374 |
+
image_latents = vae.encode(first_frame).latent_dist.sample(generator=block_state.generator)
|
| 375 |
+
image_latents = (image_latents - latents_mean) * latents_std
|
| 376 |
+
|
| 377 |
+
# Encode remaining frames in chunks
|
| 378 |
+
latents_chunks = []
|
| 379 |
+
for i in range(num_chunks):
|
| 380 |
+
chunk_start = start_frame + i * min_frames
|
| 381 |
+
chunk_end = chunk_start + min_frames
|
| 382 |
+
video_chunk = video[:, :, chunk_start:chunk_end, :, :]
|
| 383 |
+
chunk_latents = vae.encode(video_chunk).latent_dist.sample(generator=block_state.generator)
|
| 384 |
+
chunk_latents = (chunk_latents - latents_mean) * latents_std
|
| 385 |
+
latents_chunks.append(chunk_latents)
|
| 386 |
+
video_latents = torch.cat(latents_chunks, dim=2)
|
| 387 |
+
|
| 388 |
+
block_state.image_latents = image_latents.to(device=device, dtype=torch.float32)
|
| 389 |
+
block_state.video_latents = video_latents.to(device=device, dtype=torch.float32)
|
| 390 |
+
|
| 391 |
+
self.set_block_state(state, block_state)
|
| 392 |
+
return components, state
|
diffusers/modular_pipelines/helios/modular_blocks_helios.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ...utils import logging
|
| 18 |
+
from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
|
| 19 |
+
from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
|
| 20 |
+
from .before_denoise import (
|
| 21 |
+
HeliosAdditionalInputsStep,
|
| 22 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 23 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 24 |
+
HeliosI2VSeedHistoryStep,
|
| 25 |
+
HeliosPrepareHistoryStep,
|
| 26 |
+
HeliosSetTimestepsStep,
|
| 27 |
+
HeliosTextInputStep,
|
| 28 |
+
HeliosV2VSeedHistoryStep,
|
| 29 |
+
)
|
| 30 |
+
from .decoders import HeliosDecodeStep
|
| 31 |
+
from .denoise import HeliosChunkDenoiseStep, HeliosI2VChunkDenoiseStep
|
| 32 |
+
from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ====================
|
| 39 |
+
# 1. Vae Encoder
|
| 40 |
+
# ====================
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# auto_docstring
|
| 44 |
+
class HeliosAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 45 |
+
"""
|
| 46 |
+
Encoder step that encodes video or image inputs. This is an auto pipeline block.
|
| 47 |
+
- `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
|
| 48 |
+
- `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
|
| 49 |
+
- If neither is provided, step will be skipped.
|
| 50 |
+
|
| 51 |
+
Components:
|
| 52 |
+
vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
|
| 53 |
+
|
| 54 |
+
Inputs:
|
| 55 |
+
video (`None`, *optional*):
|
| 56 |
+
Input video for video-to-video generation
|
| 57 |
+
height (`int`, *optional*, defaults to 384):
|
| 58 |
+
The height in pixels of the generated image.
|
| 59 |
+
width (`int`, *optional*, defaults to 640):
|
| 60 |
+
The width in pixels of the generated image.
|
| 61 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 62 |
+
Number of latent frames per temporal chunk.
|
| 63 |
+
generator (`Generator`, *optional*):
|
| 64 |
+
Torch generator for deterministic generation.
|
| 65 |
+
image (`Image | list`, *optional*):
|
| 66 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 67 |
+
|
| 68 |
+
Outputs:
|
| 69 |
+
image_latents (`Tensor`):
|
| 70 |
+
The latent representation of the input image.
|
| 71 |
+
video_latents (`Tensor`):
|
| 72 |
+
Encoded video latents (chunked)
|
| 73 |
+
fake_image_latents (`Tensor`):
|
| 74 |
+
Fake image latents for history seeding
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
|
| 78 |
+
block_names = ["video_encoder", "image_encoder"]
|
| 79 |
+
block_trigger_inputs = ["video", "image"]
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def description(self):
|
| 83 |
+
return (
|
| 84 |
+
"Encoder step that encodes video or image inputs. This is an auto pipeline block.\n"
|
| 85 |
+
" - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
|
| 86 |
+
" - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
|
| 87 |
+
" - If neither is provided, step will be skipped."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# ====================
|
| 92 |
+
# 2. DENOISE
|
| 93 |
+
# ====================
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# DENOISE (T2V)
|
| 97 |
+
# auto_docstring
|
| 98 |
+
class HeliosCoreDenoiseStep(SequentialPipelineBlocks):
|
| 99 |
+
"""
|
| 100 |
+
Denoise block that takes encoded conditions and runs the chunk-based denoising process.
|
| 101 |
+
|
| 102 |
+
Components:
|
| 103 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 104 |
+
|
| 105 |
+
Inputs:
|
| 106 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 107 |
+
Number of videos to generate per prompt.
|
| 108 |
+
prompt_embeds (`Tensor`):
|
| 109 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 110 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 111 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 112 |
+
height (`int`, *optional*, defaults to 384):
|
| 113 |
+
The height in pixels of the generated image.
|
| 114 |
+
width (`int`, *optional*, defaults to 640):
|
| 115 |
+
The width in pixels of the generated image.
|
| 116 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 117 |
+
Total number of video frames to generate.
|
| 118 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 119 |
+
Number of latent frames per temporal chunk.
|
| 120 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 121 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 122 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 123 |
+
Whether to keep the first frame as a prefix in history.
|
| 124 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 125 |
+
The number of denoising steps.
|
| 126 |
+
sigmas (`list`, *optional*):
|
| 127 |
+
Custom sigmas for the denoising process.
|
| 128 |
+
generator (`Generator`, *optional*):
|
| 129 |
+
Torch generator for deterministic generation.
|
| 130 |
+
latents (`Tensor`, *optional*):
|
| 131 |
+
Pre-generated noisy latents for image generation.
|
| 132 |
+
timesteps (`Tensor`, *optional*):
|
| 133 |
+
Timesteps for the denoising process.
|
| 134 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 135 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 136 |
+
attention_kwargs (`dict`, *optional*):
|
| 137 |
+
Additional kwargs for attention processors.
|
| 138 |
+
|
| 139 |
+
Outputs:
|
| 140 |
+
latent_chunks (`list`):
|
| 141 |
+
List of per-chunk denoised latent tensors
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
model_name = "helios"
|
| 145 |
+
block_classes = [
|
| 146 |
+
HeliosTextInputStep,
|
| 147 |
+
HeliosPrepareHistoryStep,
|
| 148 |
+
HeliosSetTimestepsStep,
|
| 149 |
+
HeliosChunkDenoiseStep,
|
| 150 |
+
]
|
| 151 |
+
block_names = ["input", "prepare_history", "set_timesteps", "chunk_denoise"]
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def description(self):
|
| 155 |
+
return "Denoise block that takes encoded conditions and runs the chunk-based denoising process."
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def outputs(self):
|
| 159 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# DENOISE (I2V)
|
| 163 |
+
# auto_docstring
|
| 164 |
+
class HeliosI2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 165 |
+
"""
|
| 166 |
+
I2V denoise block that seeds history with image latents and uses I2V-aware chunk preparation.
|
| 167 |
+
|
| 168 |
+
Components:
|
| 169 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 170 |
+
|
| 171 |
+
Inputs:
|
| 172 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 173 |
+
Number of videos to generate per prompt.
|
| 174 |
+
prompt_embeds (`Tensor`):
|
| 175 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 176 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 177 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 178 |
+
image_latents (`Tensor`):
|
| 179 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 180 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 181 |
+
Fake image latents used as history seed for I2V generation.
|
| 182 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 183 |
+
Minimum sigma for image latent noise.
|
| 184 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 185 |
+
Maximum sigma for image latent noise.
|
| 186 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 187 |
+
Minimum sigma for video/fake-image latent noise.
|
| 188 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 189 |
+
Maximum sigma for video/fake-image latent noise.
|
| 190 |
+
generator (`Generator`, *optional*):
|
| 191 |
+
Torch generator for deterministic generation.
|
| 192 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 193 |
+
Total number of video frames to generate.
|
| 194 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 195 |
+
Number of latent frames per temporal chunk.
|
| 196 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 197 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 198 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 199 |
+
Whether to keep the first frame as a prefix in history.
|
| 200 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 201 |
+
The number of denoising steps.
|
| 202 |
+
sigmas (`list`, *optional*):
|
| 203 |
+
Custom sigmas for the denoising process.
|
| 204 |
+
latents (`Tensor`, *optional*):
|
| 205 |
+
Pre-generated noisy latents for image generation.
|
| 206 |
+
timesteps (`Tensor`, *optional*):
|
| 207 |
+
Timesteps for the denoising process.
|
| 208 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 209 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 210 |
+
attention_kwargs (`dict`, *optional*):
|
| 211 |
+
Additional kwargs for attention processors.
|
| 212 |
+
|
| 213 |
+
Outputs:
|
| 214 |
+
latent_chunks (`list`):
|
| 215 |
+
List of per-chunk denoised latent tensors
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
model_name = "helios"
|
| 219 |
+
block_classes = [
|
| 220 |
+
HeliosTextInputStep,
|
| 221 |
+
HeliosAdditionalInputsStep(
|
| 222 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 223 |
+
additional_batch_inputs=[
|
| 224 |
+
InputParam(
|
| 225 |
+
"fake_image_latents",
|
| 226 |
+
type_hint=torch.Tensor,
|
| 227 |
+
description="Fake image latents used as history seed for I2V generation.",
|
| 228 |
+
),
|
| 229 |
+
],
|
| 230 |
+
),
|
| 231 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 232 |
+
HeliosPrepareHistoryStep,
|
| 233 |
+
HeliosI2VSeedHistoryStep,
|
| 234 |
+
HeliosSetTimestepsStep,
|
| 235 |
+
HeliosI2VChunkDenoiseStep,
|
| 236 |
+
]
|
| 237 |
+
block_names = [
|
| 238 |
+
"input",
|
| 239 |
+
"additional_inputs",
|
| 240 |
+
"add_noise_image",
|
| 241 |
+
"prepare_history",
|
| 242 |
+
"seed_history",
|
| 243 |
+
"set_timesteps",
|
| 244 |
+
"chunk_denoise",
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
def description(self):
|
| 249 |
+
return "I2V denoise block that seeds history with image latents and uses I2V-aware chunk preparation."
|
| 250 |
+
|
| 251 |
+
@property
|
| 252 |
+
def outputs(self):
|
| 253 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# DENOISE (V2V)
|
| 257 |
+
# auto_docstring
|
| 258 |
+
class HeliosV2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 259 |
+
"""
|
| 260 |
+
V2V denoise block that seeds history with video latents and uses I2V-aware chunk preparation.
|
| 261 |
+
|
| 262 |
+
Components:
|
| 263 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 264 |
+
|
| 265 |
+
Inputs:
|
| 266 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 267 |
+
Number of videos to generate per prompt.
|
| 268 |
+
prompt_embeds (`Tensor`):
|
| 269 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 270 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 271 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 272 |
+
image_latents (`Tensor`, *optional*):
|
| 273 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 274 |
+
video_latents (`Tensor`, *optional*):
|
| 275 |
+
Encoded video latents for V2V generation.
|
| 276 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 277 |
+
Number of latent frames per temporal chunk.
|
| 278 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 279 |
+
Minimum sigma for image latent noise.
|
| 280 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 281 |
+
Maximum sigma for image latent noise.
|
| 282 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 283 |
+
Minimum sigma for video latent noise.
|
| 284 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 285 |
+
Maximum sigma for video latent noise.
|
| 286 |
+
generator (`Generator`, *optional*):
|
| 287 |
+
Torch generator for deterministic generation.
|
| 288 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 289 |
+
Total number of video frames to generate.
|
| 290 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 291 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 292 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 293 |
+
Whether to keep the first frame as a prefix in history.
|
| 294 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 295 |
+
The number of denoising steps.
|
| 296 |
+
sigmas (`list`, *optional*):
|
| 297 |
+
Custom sigmas for the denoising process.
|
| 298 |
+
latents (`Tensor`, *optional*):
|
| 299 |
+
Pre-generated noisy latents for image generation.
|
| 300 |
+
timesteps (`Tensor`, *optional*):
|
| 301 |
+
Timesteps for the denoising process.
|
| 302 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 303 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 304 |
+
attention_kwargs (`dict`, *optional*):
|
| 305 |
+
Additional kwargs for attention processors.
|
| 306 |
+
|
| 307 |
+
Outputs:
|
| 308 |
+
latent_chunks (`list`):
|
| 309 |
+
List of per-chunk denoised latent tensors
|
| 310 |
+
"""
|
| 311 |
+
|
| 312 |
+
model_name = "helios"
|
| 313 |
+
block_classes = [
|
| 314 |
+
HeliosTextInputStep,
|
| 315 |
+
HeliosAdditionalInputsStep(
|
| 316 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 317 |
+
additional_batch_inputs=[
|
| 318 |
+
InputParam(
|
| 319 |
+
"video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
|
| 320 |
+
),
|
| 321 |
+
],
|
| 322 |
+
),
|
| 323 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 324 |
+
HeliosPrepareHistoryStep,
|
| 325 |
+
HeliosV2VSeedHistoryStep,
|
| 326 |
+
HeliosSetTimestepsStep,
|
| 327 |
+
HeliosI2VChunkDenoiseStep,
|
| 328 |
+
]
|
| 329 |
+
block_names = [
|
| 330 |
+
"input",
|
| 331 |
+
"additional_inputs",
|
| 332 |
+
"add_noise_video",
|
| 333 |
+
"prepare_history",
|
| 334 |
+
"seed_history",
|
| 335 |
+
"set_timesteps",
|
| 336 |
+
"chunk_denoise",
|
| 337 |
+
]
|
| 338 |
+
|
| 339 |
+
@property
|
| 340 |
+
def description(self):
|
| 341 |
+
return "V2V denoise block that seeds history with video latents and uses I2V-aware chunk preparation."
|
| 342 |
+
|
| 343 |
+
@property
|
| 344 |
+
def outputs(self):
|
| 345 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# AUTO DENOISE
|
| 349 |
+
# auto_docstring
|
| 350 |
+
class HeliosAutoCoreDenoiseStep(ConditionalPipelineBlocks):
|
| 351 |
+
"""
|
| 352 |
+
Core denoise step that selects the appropriate denoising block.
|
| 353 |
+
- `HeliosV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
|
| 354 |
+
- `HeliosI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
|
| 355 |
+
- `HeliosCoreDenoiseStep` (text2video) for text-to-video tasks.
|
| 356 |
+
|
| 357 |
+
Components:
|
| 358 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 359 |
+
|
| 360 |
+
Inputs:
|
| 361 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 362 |
+
Number of videos to generate per prompt.
|
| 363 |
+
prompt_embeds (`Tensor`):
|
| 364 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 365 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 366 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 367 |
+
image_latents (`Tensor`, *optional*):
|
| 368 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 369 |
+
video_latents (`Tensor`, *optional*):
|
| 370 |
+
Encoded video latents for V2V generation.
|
| 371 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 372 |
+
Number of latent frames per temporal chunk.
|
| 373 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 374 |
+
Minimum sigma for image latent noise.
|
| 375 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 376 |
+
Maximum sigma for image latent noise.
|
| 377 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 378 |
+
Minimum sigma for video latent noise.
|
| 379 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 380 |
+
Maximum sigma for video latent noise.
|
| 381 |
+
generator (`Generator`, *optional*):
|
| 382 |
+
Torch generator for deterministic generation.
|
| 383 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 384 |
+
Total number of video frames to generate.
|
| 385 |
+
history_sizes (`list`):
|
| 386 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 387 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 388 |
+
Whether to keep the first frame as a prefix in history.
|
| 389 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 390 |
+
The number of denoising steps.
|
| 391 |
+
sigmas (`list`):
|
| 392 |
+
Custom sigmas for the denoising process.
|
| 393 |
+
latents (`Tensor`, *optional*):
|
| 394 |
+
Pre-generated noisy latents for image generation.
|
| 395 |
+
timesteps (`Tensor`, *optional*):
|
| 396 |
+
Timesteps for the denoising process.
|
| 397 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 398 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 399 |
+
attention_kwargs (`dict`, *optional*):
|
| 400 |
+
Additional kwargs for attention processors.
|
| 401 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 402 |
+
Fake image latents used as history seed for I2V generation.
|
| 403 |
+
height (`int`, *optional*, defaults to 384):
|
| 404 |
+
The height in pixels of the generated image.
|
| 405 |
+
width (`int`, *optional*, defaults to 640):
|
| 406 |
+
The width in pixels of the generated image.
|
| 407 |
+
|
| 408 |
+
Outputs:
|
| 409 |
+
latent_chunks (`list`):
|
| 410 |
+
List of per-chunk denoised latent tensors
|
| 411 |
+
"""
|
| 412 |
+
|
| 413 |
+
block_classes = [HeliosV2VCoreDenoiseStep, HeliosI2VCoreDenoiseStep, HeliosCoreDenoiseStep]
|
| 414 |
+
block_names = ["video2video", "image2video", "text2video"]
|
| 415 |
+
block_trigger_inputs = ["video_latents", "fake_image_latents"]
|
| 416 |
+
default_block_name = "text2video"
|
| 417 |
+
|
| 418 |
+
def select_block(self, video_latents=None, fake_image_latents=None):
|
| 419 |
+
if video_latents is not None:
|
| 420 |
+
return "video2video"
|
| 421 |
+
elif fake_image_latents is not None:
|
| 422 |
+
return "image2video"
|
| 423 |
+
return None
|
| 424 |
+
|
| 425 |
+
@property
|
| 426 |
+
def description(self):
|
| 427 |
+
return (
|
| 428 |
+
"Core denoise step that selects the appropriate denoising block.\n"
|
| 429 |
+
" - `HeliosV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
|
| 430 |
+
" - `HeliosI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
|
| 431 |
+
" - `HeliosCoreDenoiseStep` (text2video) for text-to-video tasks."
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
AUTO_BLOCKS = InsertableDict(
|
| 436 |
+
[
|
| 437 |
+
("text_encoder", HeliosTextEncoderStep()),
|
| 438 |
+
("vae_encoder", HeliosAutoVaeEncoderStep()),
|
| 439 |
+
("denoise", HeliosAutoCoreDenoiseStep()),
|
| 440 |
+
("decode", HeliosDecodeStep()),
|
| 441 |
+
]
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
# ====================
|
| 445 |
+
# 3. Auto Blocks
|
| 446 |
+
# ====================
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
# auto_docstring
|
| 450 |
+
class HeliosAutoBlocks(SequentialPipelineBlocks):
|
| 451 |
+
"""
|
| 452 |
+
Auto Modular pipeline for text-to-video, image-to-video, and video-to-video tasks using Helios.
|
| 453 |
+
|
| 454 |
+
Supported workflows:
|
| 455 |
+
- `text2video`: requires `prompt`
|
| 456 |
+
- `image2video`: requires `prompt`, `image`
|
| 457 |
+
- `video2video`: requires `prompt`, `video`
|
| 458 |
+
|
| 459 |
+
Components:
|
| 460 |
+
text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
|
| 461 |
+
(`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
|
| 462 |
+
(`HeliosScheduler`)
|
| 463 |
+
|
| 464 |
+
Inputs:
|
| 465 |
+
prompt (`str`):
|
| 466 |
+
The prompt or prompts to guide image generation.
|
| 467 |
+
negative_prompt (`str`, *optional*):
|
| 468 |
+
The prompt or prompts not to guide the image generation.
|
| 469 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 470 |
+
Maximum sequence length for prompt encoding.
|
| 471 |
+
video (`None`, *optional*):
|
| 472 |
+
Input video for video-to-video generation
|
| 473 |
+
height (`int`, *optional*, defaults to 384):
|
| 474 |
+
The height in pixels of the generated image.
|
| 475 |
+
width (`int`, *optional*, defaults to 640):
|
| 476 |
+
The width in pixels of the generated image.
|
| 477 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 478 |
+
Number of latent frames per temporal chunk.
|
| 479 |
+
generator (`Generator`, *optional*):
|
| 480 |
+
Torch generator for deterministic generation.
|
| 481 |
+
image (`Image | list`, *optional*):
|
| 482 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 483 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 484 |
+
Number of videos to generate per prompt.
|
| 485 |
+
image_latents (`Tensor`, *optional*):
|
| 486 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 487 |
+
video_latents (`Tensor`, *optional*):
|
| 488 |
+
Encoded video latents for V2V generation.
|
| 489 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 490 |
+
Minimum sigma for image latent noise.
|
| 491 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 492 |
+
Maximum sigma for image latent noise.
|
| 493 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 494 |
+
Minimum sigma for video latent noise.
|
| 495 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 496 |
+
Maximum sigma for video latent noise.
|
| 497 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 498 |
+
Total number of video frames to generate.
|
| 499 |
+
history_sizes (`list`):
|
| 500 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 501 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 502 |
+
Whether to keep the first frame as a prefix in history.
|
| 503 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 504 |
+
The number of denoising steps.
|
| 505 |
+
sigmas (`list`):
|
| 506 |
+
Custom sigmas for the denoising process.
|
| 507 |
+
latents (`Tensor`, *optional*):
|
| 508 |
+
Pre-generated noisy latents for image generation.
|
| 509 |
+
timesteps (`Tensor`, *optional*):
|
| 510 |
+
Timesteps for the denoising process.
|
| 511 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 512 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 513 |
+
attention_kwargs (`dict`, *optional*):
|
| 514 |
+
Additional kwargs for attention processors.
|
| 515 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 516 |
+
Fake image latents used as history seed for I2V generation.
|
| 517 |
+
output_type (`str`, *optional*, defaults to np):
|
| 518 |
+
Output format: 'pil', 'np', 'pt'.
|
| 519 |
+
|
| 520 |
+
Outputs:
|
| 521 |
+
videos (`list`):
|
| 522 |
+
The generated videos.
|
| 523 |
+
"""
|
| 524 |
+
|
| 525 |
+
model_name = "helios"
|
| 526 |
+
|
| 527 |
+
block_classes = AUTO_BLOCKS.values()
|
| 528 |
+
block_names = AUTO_BLOCKS.keys()
|
| 529 |
+
|
| 530 |
+
_workflow_map = {
|
| 531 |
+
"text2video": {"prompt": True},
|
| 532 |
+
"image2video": {"prompt": True, "image": True},
|
| 533 |
+
"video2video": {"prompt": True, "video": True},
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
@property
|
| 537 |
+
def description(self):
|
| 538 |
+
return "Auto Modular pipeline for text-to-video, image-to-video, and video-to-video tasks using Helios."
|
| 539 |
+
|
| 540 |
+
@property
|
| 541 |
+
def outputs(self):
|
| 542 |
+
return [OutputParam.template("videos")]
|
diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ...utils import logging
|
| 18 |
+
from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
|
| 19 |
+
from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
|
| 20 |
+
from .before_denoise import (
|
| 21 |
+
HeliosAdditionalInputsStep,
|
| 22 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 23 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 24 |
+
HeliosI2VSeedHistoryStep,
|
| 25 |
+
HeliosPrepareHistoryStep,
|
| 26 |
+
HeliosTextInputStep,
|
| 27 |
+
HeliosV2VSeedHistoryStep,
|
| 28 |
+
)
|
| 29 |
+
from .decoders import HeliosDecodeStep
|
| 30 |
+
from .denoise import HeliosPyramidChunkDenoiseStep, HeliosPyramidI2VChunkDenoiseStep
|
| 31 |
+
from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ====================
|
| 38 |
+
# 1. Vae Encoder
|
| 39 |
+
# ====================
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# auto_docstring
|
| 43 |
+
class HeliosPyramidAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 44 |
+
"""
|
| 45 |
+
Encoder step that encodes video or image inputs. This is an auto pipeline block.
|
| 46 |
+
- `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
|
| 47 |
+
- `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
|
| 48 |
+
- If neither is provided, step will be skipped.
|
| 49 |
+
|
| 50 |
+
Components:
|
| 51 |
+
vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
|
| 52 |
+
|
| 53 |
+
Inputs:
|
| 54 |
+
video (`None`, *optional*):
|
| 55 |
+
Input video for video-to-video generation
|
| 56 |
+
height (`int`, *optional*, defaults to 384):
|
| 57 |
+
The height in pixels of the generated image.
|
| 58 |
+
width (`int`, *optional*, defaults to 640):
|
| 59 |
+
The width in pixels of the generated image.
|
| 60 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 61 |
+
Number of latent frames per temporal chunk.
|
| 62 |
+
generator (`Generator`, *optional*):
|
| 63 |
+
Torch generator for deterministic generation.
|
| 64 |
+
image (`Image | list`, *optional*):
|
| 65 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 66 |
+
|
| 67 |
+
Outputs:
|
| 68 |
+
image_latents (`Tensor`):
|
| 69 |
+
The latent representation of the input image.
|
| 70 |
+
video_latents (`Tensor`):
|
| 71 |
+
Encoded video latents (chunked)
|
| 72 |
+
fake_image_latents (`Tensor`):
|
| 73 |
+
Fake image latents for history seeding
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
|
| 77 |
+
block_names = ["video_encoder", "image_encoder"]
|
| 78 |
+
block_trigger_inputs = ["video", "image"]
|
| 79 |
+
|
| 80 |
+
@property
|
| 81 |
+
def description(self):
|
| 82 |
+
return (
|
| 83 |
+
"Encoder step that encodes video or image inputs. This is an auto pipeline block.\n"
|
| 84 |
+
" - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
|
| 85 |
+
" - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
|
| 86 |
+
" - If neither is provided, step will be skipped."
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ====================
|
| 91 |
+
# 2. DENOISE
|
| 92 |
+
# ====================
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# DENOISE (T2V)
|
| 96 |
+
# auto_docstring
|
| 97 |
+
class HeliosPyramidCoreDenoiseStep(SequentialPipelineBlocks):
|
| 98 |
+
"""
|
| 99 |
+
T2V pyramid denoise block with progressive multi-resolution denoising.
|
| 100 |
+
|
| 101 |
+
Components:
|
| 102 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
|
| 103 |
+
(`ClassifierFreeZeroStarGuidance`)
|
| 104 |
+
|
| 105 |
+
Inputs:
|
| 106 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 107 |
+
Number of videos to generate per prompt.
|
| 108 |
+
prompt_embeds (`Tensor`):
|
| 109 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 110 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 111 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 112 |
+
height (`int`, *optional*, defaults to 384):
|
| 113 |
+
The height in pixels of the generated image.
|
| 114 |
+
width (`int`, *optional*, defaults to 640):
|
| 115 |
+
The width in pixels of the generated image.
|
| 116 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 117 |
+
Total number of video frames to generate.
|
| 118 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 119 |
+
Number of latent frames per temporal chunk.
|
| 120 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 121 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 122 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 123 |
+
Whether to keep the first frame as a prefix in history.
|
| 124 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 125 |
+
Number of denoising steps per pyramid stage.
|
| 126 |
+
generator (`Generator`, *optional*):
|
| 127 |
+
Torch generator for deterministic generation.
|
| 128 |
+
latents (`Tensor`, *optional*):
|
| 129 |
+
Pre-generated noisy latents for image generation.
|
| 130 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 131 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 132 |
+
attention_kwargs (`dict`, *optional*):
|
| 133 |
+
Additional kwargs for attention processors.
|
| 134 |
+
|
| 135 |
+
Outputs:
|
| 136 |
+
latent_chunks (`list`):
|
| 137 |
+
List of per-chunk denoised latent tensors
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
model_name = "helios-pyramid"
|
| 141 |
+
block_classes = [
|
| 142 |
+
HeliosTextInputStep,
|
| 143 |
+
HeliosPrepareHistoryStep,
|
| 144 |
+
HeliosPyramidChunkDenoiseStep,
|
| 145 |
+
]
|
| 146 |
+
block_names = ["input", "prepare_history", "pyramid_chunk_denoise"]
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def description(self):
|
| 150 |
+
return "T2V pyramid denoise block with progressive multi-resolution denoising."
|
| 151 |
+
|
| 152 |
+
@property
|
| 153 |
+
def outputs(self):
|
| 154 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# DENOISE (I2V)
|
| 158 |
+
# auto_docstring
|
| 159 |
+
class HeliosPyramidI2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 160 |
+
"""
|
| 161 |
+
I2V pyramid denoise block with progressive multi-resolution denoising.
|
| 162 |
+
|
| 163 |
+
Components:
|
| 164 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
|
| 165 |
+
(`ClassifierFreeZeroStarGuidance`)
|
| 166 |
+
|
| 167 |
+
Inputs:
|
| 168 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 169 |
+
Number of videos to generate per prompt.
|
| 170 |
+
prompt_embeds (`Tensor`):
|
| 171 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 172 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 173 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 174 |
+
image_latents (`Tensor`):
|
| 175 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 176 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 177 |
+
Fake image latents used as history seed for I2V generation.
|
| 178 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 179 |
+
Minimum sigma for image latent noise.
|
| 180 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 181 |
+
Maximum sigma for image latent noise.
|
| 182 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 183 |
+
Minimum sigma for video/fake-image latent noise.
|
| 184 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 185 |
+
Maximum sigma for video/fake-image latent noise.
|
| 186 |
+
generator (`Generator`, *optional*):
|
| 187 |
+
Torch generator for deterministic generation.
|
| 188 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 189 |
+
Total number of video frames to generate.
|
| 190 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 191 |
+
Number of latent frames per temporal chunk.
|
| 192 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 193 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 194 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 195 |
+
Whether to keep the first frame as a prefix in history.
|
| 196 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 197 |
+
Number of denoising steps per pyramid stage.
|
| 198 |
+
latents (`Tensor`, *optional*):
|
| 199 |
+
Pre-generated noisy latents for image generation.
|
| 200 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 201 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 202 |
+
attention_kwargs (`dict`, *optional*):
|
| 203 |
+
Additional kwargs for attention processors.
|
| 204 |
+
|
| 205 |
+
Outputs:
|
| 206 |
+
latent_chunks (`list`):
|
| 207 |
+
List of per-chunk denoised latent tensors
|
| 208 |
+
"""
|
| 209 |
+
|
| 210 |
+
model_name = "helios-pyramid"
|
| 211 |
+
block_classes = [
|
| 212 |
+
HeliosTextInputStep,
|
| 213 |
+
HeliosAdditionalInputsStep(
|
| 214 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 215 |
+
additional_batch_inputs=[
|
| 216 |
+
InputParam(
|
| 217 |
+
"fake_image_latents",
|
| 218 |
+
type_hint=torch.Tensor,
|
| 219 |
+
description="Fake image latents used as history seed for I2V generation.",
|
| 220 |
+
),
|
| 221 |
+
],
|
| 222 |
+
),
|
| 223 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 224 |
+
HeliosPrepareHistoryStep,
|
| 225 |
+
HeliosI2VSeedHistoryStep,
|
| 226 |
+
HeliosPyramidI2VChunkDenoiseStep,
|
| 227 |
+
]
|
| 228 |
+
block_names = [
|
| 229 |
+
"input",
|
| 230 |
+
"additional_inputs",
|
| 231 |
+
"add_noise_image",
|
| 232 |
+
"prepare_history",
|
| 233 |
+
"seed_history",
|
| 234 |
+
"pyramid_chunk_denoise",
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
@property
|
| 238 |
+
def description(self):
|
| 239 |
+
return "I2V pyramid denoise block with progressive multi-resolution denoising."
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def outputs(self):
|
| 243 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# DENOISE (V2V)
|
| 247 |
+
# auto_docstring
|
| 248 |
+
class HeliosPyramidV2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 249 |
+
"""
|
| 250 |
+
V2V pyramid denoise block with progressive multi-resolution denoising.
|
| 251 |
+
|
| 252 |
+
Components:
|
| 253 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
|
| 254 |
+
(`ClassifierFreeZeroStarGuidance`)
|
| 255 |
+
|
| 256 |
+
Inputs:
|
| 257 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 258 |
+
Number of videos to generate per prompt.
|
| 259 |
+
prompt_embeds (`Tensor`):
|
| 260 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 261 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 262 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 263 |
+
image_latents (`Tensor`, *optional*):
|
| 264 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 265 |
+
video_latents (`Tensor`, *optional*):
|
| 266 |
+
Encoded video latents for V2V generation.
|
| 267 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 268 |
+
Number of latent frames per temporal chunk.
|
| 269 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 270 |
+
Minimum sigma for image latent noise.
|
| 271 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 272 |
+
Maximum sigma for image latent noise.
|
| 273 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 274 |
+
Minimum sigma for video latent noise.
|
| 275 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 276 |
+
Maximum sigma for video latent noise.
|
| 277 |
+
generator (`Generator`, *optional*):
|
| 278 |
+
Torch generator for deterministic generation.
|
| 279 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 280 |
+
Total number of video frames to generate.
|
| 281 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 282 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 283 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 284 |
+
Whether to keep the first frame as a prefix in history.
|
| 285 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 286 |
+
Number of denoising steps per pyramid stage.
|
| 287 |
+
latents (`Tensor`, *optional*):
|
| 288 |
+
Pre-generated noisy latents for image generation.
|
| 289 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 290 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 291 |
+
attention_kwargs (`dict`, *optional*):
|
| 292 |
+
Additional kwargs for attention processors.
|
| 293 |
+
|
| 294 |
+
Outputs:
|
| 295 |
+
latent_chunks (`list`):
|
| 296 |
+
List of per-chunk denoised latent tensors
|
| 297 |
+
"""
|
| 298 |
+
|
| 299 |
+
model_name = "helios-pyramid"
|
| 300 |
+
block_classes = [
|
| 301 |
+
HeliosTextInputStep,
|
| 302 |
+
HeliosAdditionalInputsStep(
|
| 303 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 304 |
+
additional_batch_inputs=[
|
| 305 |
+
InputParam(
|
| 306 |
+
"video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
|
| 307 |
+
),
|
| 308 |
+
],
|
| 309 |
+
),
|
| 310 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 311 |
+
HeliosPrepareHistoryStep,
|
| 312 |
+
HeliosV2VSeedHistoryStep,
|
| 313 |
+
HeliosPyramidI2VChunkDenoiseStep,
|
| 314 |
+
]
|
| 315 |
+
block_names = [
|
| 316 |
+
"input",
|
| 317 |
+
"additional_inputs",
|
| 318 |
+
"add_noise_video",
|
| 319 |
+
"prepare_history",
|
| 320 |
+
"seed_history",
|
| 321 |
+
"pyramid_chunk_denoise",
|
| 322 |
+
]
|
| 323 |
+
|
| 324 |
+
@property
|
| 325 |
+
def description(self):
|
| 326 |
+
return "V2V pyramid denoise block with progressive multi-resolution denoising."
|
| 327 |
+
|
| 328 |
+
@property
|
| 329 |
+
def outputs(self):
|
| 330 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# AUTO DENOISE
|
| 334 |
+
# auto_docstring
|
| 335 |
+
class HeliosPyramidAutoCoreDenoiseStep(ConditionalPipelineBlocks):
|
| 336 |
+
"""
|
| 337 |
+
Pyramid core denoise step that selects the appropriate denoising block.
|
| 338 |
+
- `HeliosPyramidV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
|
| 339 |
+
- `HeliosPyramidI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
|
| 340 |
+
- `HeliosPyramidCoreDenoiseStep` (text2video) for text-to-video tasks.
|
| 341 |
+
|
| 342 |
+
Components:
|
| 343 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider
|
| 344 |
+
(`ClassifierFreeZeroStarGuidance`)
|
| 345 |
+
|
| 346 |
+
Inputs:
|
| 347 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 348 |
+
Number of videos to generate per prompt.
|
| 349 |
+
prompt_embeds (`Tensor`):
|
| 350 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 351 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 352 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 353 |
+
image_latents (`Tensor`, *optional*):
|
| 354 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 355 |
+
video_latents (`Tensor`, *optional*):
|
| 356 |
+
Encoded video latents for V2V generation.
|
| 357 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 358 |
+
Number of latent frames per temporal chunk.
|
| 359 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 360 |
+
Minimum sigma for image latent noise.
|
| 361 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 362 |
+
Maximum sigma for image latent noise.
|
| 363 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 364 |
+
Minimum sigma for video latent noise.
|
| 365 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 366 |
+
Maximum sigma for video latent noise.
|
| 367 |
+
generator (`Generator`, *optional*):
|
| 368 |
+
Torch generator for deterministic generation.
|
| 369 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 370 |
+
Total number of video frames to generate.
|
| 371 |
+
history_sizes (`list`):
|
| 372 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 373 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 374 |
+
Whether to keep the first frame as a prefix in history.
|
| 375 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 376 |
+
Number of denoising steps per pyramid stage.
|
| 377 |
+
latents (`Tensor`, *optional*):
|
| 378 |
+
Pre-generated noisy latents for image generation.
|
| 379 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 380 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 381 |
+
attention_kwargs (`dict`, *optional*):
|
| 382 |
+
Additional kwargs for attention processors.
|
| 383 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 384 |
+
Fake image latents used as history seed for I2V generation.
|
| 385 |
+
height (`int`, *optional*, defaults to 384):
|
| 386 |
+
The height in pixels of the generated image.
|
| 387 |
+
width (`int`, *optional*, defaults to 640):
|
| 388 |
+
The width in pixels of the generated image.
|
| 389 |
+
|
| 390 |
+
Outputs:
|
| 391 |
+
latent_chunks (`list`):
|
| 392 |
+
List of per-chunk denoised latent tensors
|
| 393 |
+
"""
|
| 394 |
+
|
| 395 |
+
block_classes = [HeliosPyramidV2VCoreDenoiseStep, HeliosPyramidI2VCoreDenoiseStep, HeliosPyramidCoreDenoiseStep]
|
| 396 |
+
block_names = ["video2video", "image2video", "text2video"]
|
| 397 |
+
block_trigger_inputs = ["video_latents", "fake_image_latents"]
|
| 398 |
+
default_block_name = "text2video"
|
| 399 |
+
|
| 400 |
+
def select_block(self, video_latents=None, fake_image_latents=None):
|
| 401 |
+
if video_latents is not None:
|
| 402 |
+
return "video2video"
|
| 403 |
+
elif fake_image_latents is not None:
|
| 404 |
+
return "image2video"
|
| 405 |
+
return None
|
| 406 |
+
|
| 407 |
+
@property
|
| 408 |
+
def description(self):
|
| 409 |
+
return (
|
| 410 |
+
"Pyramid core denoise step that selects the appropriate denoising block.\n"
|
| 411 |
+
" - `HeliosPyramidV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
|
| 412 |
+
" - `HeliosPyramidI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
|
| 413 |
+
" - `HeliosPyramidCoreDenoiseStep` (text2video) for text-to-video tasks."
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# ====================
|
| 418 |
+
# 3. Auto Blocks
|
| 419 |
+
# ====================
|
| 420 |
+
|
| 421 |
+
PYRAMID_AUTO_BLOCKS = InsertableDict(
|
| 422 |
+
[
|
| 423 |
+
("text_encoder", HeliosTextEncoderStep()),
|
| 424 |
+
("vae_encoder", HeliosPyramidAutoVaeEncoderStep()),
|
| 425 |
+
("denoise", HeliosPyramidAutoCoreDenoiseStep()),
|
| 426 |
+
("decode", HeliosDecodeStep()),
|
| 427 |
+
]
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
# auto_docstring
|
| 432 |
+
class HeliosPyramidAutoBlocks(SequentialPipelineBlocks):
|
| 433 |
+
"""
|
| 434 |
+
Auto Modular pipeline for pyramid progressive generation (T2V/I2V/V2V) using Helios.
|
| 435 |
+
|
| 436 |
+
Supported workflows:
|
| 437 |
+
- `text2video`: requires `prompt`
|
| 438 |
+
- `image2video`: requires `prompt`, `image`
|
| 439 |
+
- `video2video`: requires `prompt`, `video`
|
| 440 |
+
|
| 441 |
+
Components:
|
| 442 |
+
text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
|
| 443 |
+
(`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
|
| 444 |
+
(`HeliosScheduler`)
|
| 445 |
+
|
| 446 |
+
Inputs:
|
| 447 |
+
prompt (`str`):
|
| 448 |
+
The prompt or prompts to guide image generation.
|
| 449 |
+
negative_prompt (`str`, *optional*):
|
| 450 |
+
The prompt or prompts not to guide the image generation.
|
| 451 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 452 |
+
Maximum sequence length for prompt encoding.
|
| 453 |
+
video (`None`, *optional*):
|
| 454 |
+
Input video for video-to-video generation
|
| 455 |
+
height (`int`, *optional*, defaults to 384):
|
| 456 |
+
The height in pixels of the generated image.
|
| 457 |
+
width (`int`, *optional*, defaults to 640):
|
| 458 |
+
The width in pixels of the generated image.
|
| 459 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 460 |
+
Number of latent frames per temporal chunk.
|
| 461 |
+
generator (`Generator`, *optional*):
|
| 462 |
+
Torch generator for deterministic generation.
|
| 463 |
+
image (`Image | list`, *optional*):
|
| 464 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 465 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 466 |
+
Number of videos to generate per prompt.
|
| 467 |
+
image_latents (`Tensor`, *optional*):
|
| 468 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 469 |
+
video_latents (`Tensor`, *optional*):
|
| 470 |
+
Encoded video latents for V2V generation.
|
| 471 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 472 |
+
Minimum sigma for image latent noise.
|
| 473 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 474 |
+
Maximum sigma for image latent noise.
|
| 475 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 476 |
+
Minimum sigma for video latent noise.
|
| 477 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 478 |
+
Maximum sigma for video latent noise.
|
| 479 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 480 |
+
Total number of video frames to generate.
|
| 481 |
+
history_sizes (`list`):
|
| 482 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 483 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 484 |
+
Whether to keep the first frame as a prefix in history.
|
| 485 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 486 |
+
Number of denoising steps per pyramid stage.
|
| 487 |
+
latents (`Tensor`, *optional*):
|
| 488 |
+
Pre-generated noisy latents for image generation.
|
| 489 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 490 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 491 |
+
attention_kwargs (`dict`, *optional*):
|
| 492 |
+
Additional kwargs for attention processors.
|
| 493 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 494 |
+
Fake image latents used as history seed for I2V generation.
|
| 495 |
+
output_type (`str`, *optional*, defaults to np):
|
| 496 |
+
Output format: 'pil', 'np', 'pt'.
|
| 497 |
+
|
| 498 |
+
Outputs:
|
| 499 |
+
videos (`list`):
|
| 500 |
+
The generated videos.
|
| 501 |
+
"""
|
| 502 |
+
|
| 503 |
+
model_name = "helios-pyramid"
|
| 504 |
+
|
| 505 |
+
block_classes = PYRAMID_AUTO_BLOCKS.values()
|
| 506 |
+
block_names = PYRAMID_AUTO_BLOCKS.keys()
|
| 507 |
+
|
| 508 |
+
_workflow_map = {
|
| 509 |
+
"text2video": {"prompt": True},
|
| 510 |
+
"image2video": {"prompt": True, "image": True},
|
| 511 |
+
"video2video": {"prompt": True, "video": True},
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
@property
|
| 515 |
+
def description(self):
|
| 516 |
+
return "Auto Modular pipeline for pyramid progressive generation (T2V/I2V/V2V) using Helios."
|
| 517 |
+
|
| 518 |
+
@property
|
| 519 |
+
def outputs(self):
|
| 520 |
+
return [OutputParam.template("videos")]
|
diffusers/modular_pipelines/helios/modular_blocks_helios_pyramid_distilled.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ...utils import logging
|
| 18 |
+
from ..modular_pipeline import AutoPipelineBlocks, ConditionalPipelineBlocks, SequentialPipelineBlocks
|
| 19 |
+
from ..modular_pipeline_utils import InputParam, InsertableDict, OutputParam
|
| 20 |
+
from .before_denoise import (
|
| 21 |
+
HeliosAdditionalInputsStep,
|
| 22 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 23 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 24 |
+
HeliosI2VSeedHistoryStep,
|
| 25 |
+
HeliosPrepareHistoryStep,
|
| 26 |
+
HeliosTextInputStep,
|
| 27 |
+
HeliosV2VSeedHistoryStep,
|
| 28 |
+
)
|
| 29 |
+
from .decoders import HeliosDecodeStep
|
| 30 |
+
from .denoise import HeliosPyramidDistilledChunkDenoiseStep, HeliosPyramidDistilledI2VChunkDenoiseStep
|
| 31 |
+
from .encoders import HeliosImageVaeEncoderStep, HeliosTextEncoderStep, HeliosVideoVaeEncoderStep
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ====================
|
| 38 |
+
# 1. Vae Encoder
|
| 39 |
+
# ====================
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# auto_docstring
|
| 43 |
+
class HeliosPyramidDistilledAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 44 |
+
"""
|
| 45 |
+
Encoder step for distilled pyramid pipeline.
|
| 46 |
+
- `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.
|
| 47 |
+
- `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.
|
| 48 |
+
- If neither is provided, step will be skipped.
|
| 49 |
+
|
| 50 |
+
Components:
|
| 51 |
+
vae (`AutoencoderKLWan`) video_processor (`VideoProcessor`)
|
| 52 |
+
|
| 53 |
+
Inputs:
|
| 54 |
+
video (`None`, *optional*):
|
| 55 |
+
Input video for video-to-video generation
|
| 56 |
+
height (`int`, *optional*, defaults to 384):
|
| 57 |
+
The height in pixels of the generated image.
|
| 58 |
+
width (`int`, *optional*, defaults to 640):
|
| 59 |
+
The width in pixels of the generated image.
|
| 60 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 61 |
+
Number of latent frames per temporal chunk.
|
| 62 |
+
generator (`Generator`, *optional*):
|
| 63 |
+
Torch generator for deterministic generation.
|
| 64 |
+
image (`Image | list`, *optional*):
|
| 65 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 66 |
+
|
| 67 |
+
Outputs:
|
| 68 |
+
image_latents (`Tensor`):
|
| 69 |
+
The latent representation of the input image.
|
| 70 |
+
video_latents (`Tensor`):
|
| 71 |
+
Encoded video latents (chunked)
|
| 72 |
+
fake_image_latents (`Tensor`):
|
| 73 |
+
Fake image latents for history seeding
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
block_classes = [HeliosVideoVaeEncoderStep, HeliosImageVaeEncoderStep]
|
| 77 |
+
block_names = ["video_encoder", "image_encoder"]
|
| 78 |
+
block_trigger_inputs = ["video", "image"]
|
| 79 |
+
|
| 80 |
+
@property
|
| 81 |
+
def description(self):
|
| 82 |
+
return (
|
| 83 |
+
"Encoder step for distilled pyramid pipeline.\n"
|
| 84 |
+
" - `HeliosVideoVaeEncoderStep` (video_encoder) is used when `video` is provided.\n"
|
| 85 |
+
" - `HeliosImageVaeEncoderStep` (image_encoder) is used when `image` is provided.\n"
|
| 86 |
+
" - If neither is provided, step will be skipped."
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# ====================
|
| 91 |
+
# 2. DENOISE
|
| 92 |
+
# ====================
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# DENOISE (T2V)
|
| 96 |
+
# auto_docstring
|
| 97 |
+
class HeliosPyramidDistilledCoreDenoiseStep(SequentialPipelineBlocks):
|
| 98 |
+
"""
|
| 99 |
+
T2V distilled pyramid denoise block with DMD scheduler and no CFG.
|
| 100 |
+
|
| 101 |
+
Components:
|
| 102 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 103 |
+
|
| 104 |
+
Inputs:
|
| 105 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 106 |
+
Number of videos to generate per prompt.
|
| 107 |
+
prompt_embeds (`Tensor`):
|
| 108 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 109 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 110 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 111 |
+
height (`int`, *optional*, defaults to 384):
|
| 112 |
+
The height in pixels of the generated image.
|
| 113 |
+
width (`int`, *optional*, defaults to 640):
|
| 114 |
+
The width in pixels of the generated image.
|
| 115 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 116 |
+
Total number of video frames to generate.
|
| 117 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 118 |
+
Number of latent frames per temporal chunk.
|
| 119 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 120 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 121 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 122 |
+
Whether to keep the first frame as a prefix in history.
|
| 123 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 124 |
+
Number of denoising steps per pyramid stage.
|
| 125 |
+
generator (`Generator`, *optional*):
|
| 126 |
+
Torch generator for deterministic generation.
|
| 127 |
+
latents (`Tensor`, *optional*):
|
| 128 |
+
Pre-generated noisy latents for image generation.
|
| 129 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 130 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 131 |
+
is_amplify_first_chunk (`bool`, *optional*, defaults to True):
|
| 132 |
+
Whether to double the first chunk's timesteps via the scheduler for amplified generation.
|
| 133 |
+
attention_kwargs (`dict`, *optional*):
|
| 134 |
+
Additional kwargs for attention processors.
|
| 135 |
+
|
| 136 |
+
Outputs:
|
| 137 |
+
latent_chunks (`list`):
|
| 138 |
+
List of per-chunk denoised latent tensors
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
model_name = "helios-pyramid"
|
| 142 |
+
block_classes = [
|
| 143 |
+
HeliosTextInputStep,
|
| 144 |
+
HeliosPrepareHistoryStep,
|
| 145 |
+
HeliosPyramidDistilledChunkDenoiseStep,
|
| 146 |
+
]
|
| 147 |
+
block_names = ["input", "prepare_history", "pyramid_chunk_denoise"]
|
| 148 |
+
|
| 149 |
+
@property
|
| 150 |
+
def description(self):
|
| 151 |
+
return "T2V distilled pyramid denoise block with DMD scheduler and no CFG."
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def outputs(self):
|
| 155 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# DENOISE (I2V)
|
| 159 |
+
# auto_docstring
|
| 160 |
+
class HeliosPyramidDistilledI2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 161 |
+
"""
|
| 162 |
+
I2V distilled pyramid denoise block with DMD scheduler and no CFG.
|
| 163 |
+
|
| 164 |
+
Components:
|
| 165 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 166 |
+
|
| 167 |
+
Inputs:
|
| 168 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 169 |
+
Number of videos to generate per prompt.
|
| 170 |
+
prompt_embeds (`Tensor`):
|
| 171 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 172 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 173 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 174 |
+
image_latents (`Tensor`):
|
| 175 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 176 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 177 |
+
Fake image latents used as history seed for I2V generation.
|
| 178 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 179 |
+
Minimum sigma for image latent noise.
|
| 180 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 181 |
+
Maximum sigma for image latent noise.
|
| 182 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 183 |
+
Minimum sigma for video/fake-image latent noise.
|
| 184 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 185 |
+
Maximum sigma for video/fake-image latent noise.
|
| 186 |
+
generator (`Generator`, *optional*):
|
| 187 |
+
Torch generator for deterministic generation.
|
| 188 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 189 |
+
Total number of video frames to generate.
|
| 190 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 191 |
+
Number of latent frames per temporal chunk.
|
| 192 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 193 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 194 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 195 |
+
Whether to keep the first frame as a prefix in history.
|
| 196 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 197 |
+
Number of denoising steps per pyramid stage.
|
| 198 |
+
latents (`Tensor`, *optional*):
|
| 199 |
+
Pre-generated noisy latents for image generation.
|
| 200 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 201 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 202 |
+
is_amplify_first_chunk (`bool`, *optional*, defaults to True):
|
| 203 |
+
Whether to double the first chunk's timesteps via the scheduler for amplified generation.
|
| 204 |
+
attention_kwargs (`dict`, *optional*):
|
| 205 |
+
Additional kwargs for attention processors.
|
| 206 |
+
|
| 207 |
+
Outputs:
|
| 208 |
+
latent_chunks (`list`):
|
| 209 |
+
List of per-chunk denoised latent tensors
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
model_name = "helios-pyramid"
|
| 213 |
+
block_classes = [
|
| 214 |
+
HeliosTextInputStep,
|
| 215 |
+
HeliosAdditionalInputsStep(
|
| 216 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 217 |
+
additional_batch_inputs=[
|
| 218 |
+
InputParam(
|
| 219 |
+
"fake_image_latents",
|
| 220 |
+
type_hint=torch.Tensor,
|
| 221 |
+
description="Fake image latents used as history seed for I2V generation.",
|
| 222 |
+
),
|
| 223 |
+
],
|
| 224 |
+
),
|
| 225 |
+
HeliosAddNoiseToImageLatentsStep,
|
| 226 |
+
HeliosPrepareHistoryStep,
|
| 227 |
+
HeliosI2VSeedHistoryStep,
|
| 228 |
+
HeliosPyramidDistilledI2VChunkDenoiseStep,
|
| 229 |
+
]
|
| 230 |
+
block_names = [
|
| 231 |
+
"input",
|
| 232 |
+
"additional_inputs",
|
| 233 |
+
"add_noise_image",
|
| 234 |
+
"prepare_history",
|
| 235 |
+
"seed_history",
|
| 236 |
+
"pyramid_chunk_denoise",
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
@property
|
| 240 |
+
def description(self):
|
| 241 |
+
return "I2V distilled pyramid denoise block with DMD scheduler and no CFG."
|
| 242 |
+
|
| 243 |
+
@property
|
| 244 |
+
def outputs(self):
|
| 245 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# DENOISE (V2V)
|
| 249 |
+
# auto_docstring
|
| 250 |
+
class HeliosPyramidDistilledV2VCoreDenoiseStep(SequentialPipelineBlocks):
|
| 251 |
+
"""
|
| 252 |
+
V2V distilled pyramid denoise block with DMD scheduler and no CFG.
|
| 253 |
+
|
| 254 |
+
Components:
|
| 255 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 256 |
+
|
| 257 |
+
Inputs:
|
| 258 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 259 |
+
Number of videos to generate per prompt.
|
| 260 |
+
prompt_embeds (`Tensor`):
|
| 261 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 262 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 263 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 264 |
+
image_latents (`Tensor`, *optional*):
|
| 265 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 266 |
+
video_latents (`Tensor`, *optional*):
|
| 267 |
+
Encoded video latents for V2V generation.
|
| 268 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 269 |
+
Number of latent frames per temporal chunk.
|
| 270 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 271 |
+
Minimum sigma for image latent noise.
|
| 272 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 273 |
+
Maximum sigma for image latent noise.
|
| 274 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 275 |
+
Minimum sigma for video latent noise.
|
| 276 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 277 |
+
Maximum sigma for video latent noise.
|
| 278 |
+
generator (`Generator`, *optional*):
|
| 279 |
+
Torch generator for deterministic generation.
|
| 280 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 281 |
+
Total number of video frames to generate.
|
| 282 |
+
history_sizes (`list`, *optional*, defaults to [16, 2, 1]):
|
| 283 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 284 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 285 |
+
Whether to keep the first frame as a prefix in history.
|
| 286 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 287 |
+
Number of denoising steps per pyramid stage.
|
| 288 |
+
latents (`Tensor`, *optional*):
|
| 289 |
+
Pre-generated noisy latents for image generation.
|
| 290 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 291 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 292 |
+
is_amplify_first_chunk (`bool`, *optional*, defaults to True):
|
| 293 |
+
Whether to double the first chunk's timesteps via the scheduler for amplified generation.
|
| 294 |
+
attention_kwargs (`dict`, *optional*):
|
| 295 |
+
Additional kwargs for attention processors.
|
| 296 |
+
|
| 297 |
+
Outputs:
|
| 298 |
+
latent_chunks (`list`):
|
| 299 |
+
List of per-chunk denoised latent tensors
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
model_name = "helios-pyramid"
|
| 303 |
+
block_classes = [
|
| 304 |
+
HeliosTextInputStep,
|
| 305 |
+
HeliosAdditionalInputsStep(
|
| 306 |
+
image_latent_inputs=[InputParam.template("image_latents")],
|
| 307 |
+
additional_batch_inputs=[
|
| 308 |
+
InputParam(
|
| 309 |
+
"video_latents", type_hint=torch.Tensor, description="Encoded video latents for V2V generation."
|
| 310 |
+
),
|
| 311 |
+
],
|
| 312 |
+
),
|
| 313 |
+
HeliosAddNoiseToVideoLatentsStep,
|
| 314 |
+
HeliosPrepareHistoryStep,
|
| 315 |
+
HeliosV2VSeedHistoryStep,
|
| 316 |
+
HeliosPyramidDistilledI2VChunkDenoiseStep,
|
| 317 |
+
]
|
| 318 |
+
block_names = [
|
| 319 |
+
"input",
|
| 320 |
+
"additional_inputs",
|
| 321 |
+
"add_noise_video",
|
| 322 |
+
"prepare_history",
|
| 323 |
+
"seed_history",
|
| 324 |
+
"pyramid_chunk_denoise",
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
@property
|
| 328 |
+
def description(self):
|
| 329 |
+
return "V2V distilled pyramid denoise block with DMD scheduler and no CFG."
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def outputs(self):
|
| 333 |
+
return [OutputParam("latent_chunks", type_hint=list, description="List of per-chunk denoised latent tensors")]
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# AUTO DENOISE
|
| 337 |
+
# auto_docstring
|
| 338 |
+
class HeliosPyramidDistilledAutoCoreDenoiseStep(ConditionalPipelineBlocks):
|
| 339 |
+
"""
|
| 340 |
+
Distilled pyramid core denoise step that selects the appropriate denoising block.
|
| 341 |
+
- `HeliosPyramidDistilledV2VCoreDenoiseStep` (video2video) for video-to-video tasks.
|
| 342 |
+
- `HeliosPyramidDistilledI2VCoreDenoiseStep` (image2video) for image-to-video tasks.
|
| 343 |
+
- `HeliosPyramidDistilledCoreDenoiseStep` (text2video) for text-to-video tasks.
|
| 344 |
+
|
| 345 |
+
Components:
|
| 346 |
+
transformer (`HeliosTransformer3DModel`) scheduler (`HeliosScheduler`) guider (`ClassifierFreeGuidance`)
|
| 347 |
+
|
| 348 |
+
Inputs:
|
| 349 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 350 |
+
Number of videos to generate per prompt.
|
| 351 |
+
prompt_embeds (`Tensor`):
|
| 352 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 353 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 354 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 355 |
+
image_latents (`Tensor`, *optional*):
|
| 356 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 357 |
+
video_latents (`Tensor`, *optional*):
|
| 358 |
+
Encoded video latents for V2V generation.
|
| 359 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 360 |
+
Number of latent frames per temporal chunk.
|
| 361 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 362 |
+
Minimum sigma for image latent noise.
|
| 363 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 364 |
+
Maximum sigma for image latent noise.
|
| 365 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 366 |
+
Minimum sigma for video latent noise.
|
| 367 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 368 |
+
Maximum sigma for video latent noise.
|
| 369 |
+
generator (`Generator`, *optional*):
|
| 370 |
+
Torch generator for deterministic generation.
|
| 371 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 372 |
+
Total number of video frames to generate.
|
| 373 |
+
history_sizes (`list`):
|
| 374 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 375 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 376 |
+
Whether to keep the first frame as a prefix in history.
|
| 377 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 378 |
+
Number of denoising steps per pyramid stage.
|
| 379 |
+
latents (`Tensor`, *optional*):
|
| 380 |
+
Pre-generated noisy latents for image generation.
|
| 381 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 382 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 383 |
+
is_amplify_first_chunk (`bool`, *optional*, defaults to True):
|
| 384 |
+
Whether to double the first chunk's timesteps via the scheduler for amplified generation.
|
| 385 |
+
attention_kwargs (`dict`, *optional*):
|
| 386 |
+
Additional kwargs for attention processors.
|
| 387 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 388 |
+
Fake image latents used as history seed for I2V generation.
|
| 389 |
+
height (`int`, *optional*, defaults to 384):
|
| 390 |
+
The height in pixels of the generated image.
|
| 391 |
+
width (`int`, *optional*, defaults to 640):
|
| 392 |
+
The width in pixels of the generated image.
|
| 393 |
+
|
| 394 |
+
Outputs:
|
| 395 |
+
latent_chunks (`list`):
|
| 396 |
+
List of per-chunk denoised latent tensors
|
| 397 |
+
"""
|
| 398 |
+
|
| 399 |
+
block_classes = [
|
| 400 |
+
HeliosPyramidDistilledV2VCoreDenoiseStep,
|
| 401 |
+
HeliosPyramidDistilledI2VCoreDenoiseStep,
|
| 402 |
+
HeliosPyramidDistilledCoreDenoiseStep,
|
| 403 |
+
]
|
| 404 |
+
block_names = ["video2video", "image2video", "text2video"]
|
| 405 |
+
block_trigger_inputs = ["video_latents", "fake_image_latents"]
|
| 406 |
+
default_block_name = "text2video"
|
| 407 |
+
|
| 408 |
+
def select_block(self, video_latents=None, fake_image_latents=None):
|
| 409 |
+
if video_latents is not None:
|
| 410 |
+
return "video2video"
|
| 411 |
+
elif fake_image_latents is not None:
|
| 412 |
+
return "image2video"
|
| 413 |
+
return None
|
| 414 |
+
|
| 415 |
+
@property
|
| 416 |
+
def description(self):
|
| 417 |
+
return (
|
| 418 |
+
"Distilled pyramid core denoise step that selects the appropriate denoising block.\n"
|
| 419 |
+
" - `HeliosPyramidDistilledV2VCoreDenoiseStep` (video2video) for video-to-video tasks.\n"
|
| 420 |
+
" - `HeliosPyramidDistilledI2VCoreDenoiseStep` (image2video) for image-to-video tasks.\n"
|
| 421 |
+
" - `HeliosPyramidDistilledCoreDenoiseStep` (text2video) for text-to-video tasks."
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
# ====================
|
| 426 |
+
# 3. Auto Blocks
|
| 427 |
+
# ====================
|
| 428 |
+
|
| 429 |
+
DISTILLED_PYRAMID_AUTO_BLOCKS = InsertableDict(
|
| 430 |
+
[
|
| 431 |
+
("text_encoder", HeliosTextEncoderStep()),
|
| 432 |
+
("vae_encoder", HeliosPyramidDistilledAutoVaeEncoderStep()),
|
| 433 |
+
("denoise", HeliosPyramidDistilledAutoCoreDenoiseStep()),
|
| 434 |
+
("decode", HeliosDecodeStep()),
|
| 435 |
+
]
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
# auto_docstring
|
| 440 |
+
class HeliosPyramidDistilledAutoBlocks(SequentialPipelineBlocks):
|
| 441 |
+
"""
|
| 442 |
+
Auto Modular pipeline for distilled pyramid progressive generation (T2V/I2V/V2V) using Helios.
|
| 443 |
+
|
| 444 |
+
Supported workflows:
|
| 445 |
+
- `text2video`: requires `prompt`
|
| 446 |
+
- `image2video`: requires `prompt`, `image`
|
| 447 |
+
- `video2video`: requires `prompt`, `video`
|
| 448 |
+
|
| 449 |
+
Components:
|
| 450 |
+
text_encoder (`UMT5EncoderModel`) tokenizer (`AutoTokenizer`) guider (`ClassifierFreeGuidance`) vae
|
| 451 |
+
(`AutoencoderKLWan`) video_processor (`VideoProcessor`) transformer (`HeliosTransformer3DModel`) scheduler
|
| 452 |
+
(`HeliosScheduler`)
|
| 453 |
+
|
| 454 |
+
Inputs:
|
| 455 |
+
prompt (`str`):
|
| 456 |
+
The prompt or prompts to guide image generation.
|
| 457 |
+
negative_prompt (`str`, *optional*):
|
| 458 |
+
The prompt or prompts not to guide the image generation.
|
| 459 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 460 |
+
Maximum sequence length for prompt encoding.
|
| 461 |
+
video (`None`, *optional*):
|
| 462 |
+
Input video for video-to-video generation
|
| 463 |
+
height (`int`, *optional*, defaults to 384):
|
| 464 |
+
The height in pixels of the generated image.
|
| 465 |
+
width (`int`, *optional*, defaults to 640):
|
| 466 |
+
The width in pixels of the generated image.
|
| 467 |
+
num_latent_frames_per_chunk (`int`, *optional*, defaults to 9):
|
| 468 |
+
Number of latent frames per temporal chunk.
|
| 469 |
+
generator (`Generator`, *optional*):
|
| 470 |
+
Torch generator for deterministic generation.
|
| 471 |
+
image (`Image | list`, *optional*):
|
| 472 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 473 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 474 |
+
Number of videos to generate per prompt.
|
| 475 |
+
image_latents (`Tensor`, *optional*):
|
| 476 |
+
image latents used to guide the image generation. Can be generated from vae_encoder step.
|
| 477 |
+
video_latents (`Tensor`, *optional*):
|
| 478 |
+
Encoded video latents for V2V generation.
|
| 479 |
+
image_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 480 |
+
Minimum sigma for image latent noise.
|
| 481 |
+
image_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 482 |
+
Maximum sigma for image latent noise.
|
| 483 |
+
video_noise_sigma_min (`float`, *optional*, defaults to 0.111):
|
| 484 |
+
Minimum sigma for video latent noise.
|
| 485 |
+
video_noise_sigma_max (`float`, *optional*, defaults to 0.135):
|
| 486 |
+
Maximum sigma for video latent noise.
|
| 487 |
+
num_frames (`int`, *optional*, defaults to 132):
|
| 488 |
+
Total number of video frames to generate.
|
| 489 |
+
history_sizes (`list`):
|
| 490 |
+
Sizes of long/mid/short history buffers for temporal context.
|
| 491 |
+
keep_first_frame (`bool`, *optional*, defaults to True):
|
| 492 |
+
Whether to keep the first frame as a prefix in history.
|
| 493 |
+
pyramid_num_inference_steps_list (`list`, *optional*, defaults to [10, 10, 10]):
|
| 494 |
+
Number of denoising steps per pyramid stage.
|
| 495 |
+
latents (`Tensor`, *optional*):
|
| 496 |
+
Pre-generated noisy latents for image generation.
|
| 497 |
+
**denoiser_input_fields (`None`, *optional*):
|
| 498 |
+
conditional model inputs for the denoiser: e.g. prompt_embeds, negative_prompt_embeds, etc.
|
| 499 |
+
is_amplify_first_chunk (`bool`, *optional*, defaults to True):
|
| 500 |
+
Whether to double the first chunk's timesteps via the scheduler for amplified generation.
|
| 501 |
+
attention_kwargs (`dict`, *optional*):
|
| 502 |
+
Additional kwargs for attention processors.
|
| 503 |
+
fake_image_latents (`Tensor`, *optional*):
|
| 504 |
+
Fake image latents used as history seed for I2V generation.
|
| 505 |
+
output_type (`str`, *optional*, defaults to np):
|
| 506 |
+
Output format: 'pil', 'np', 'pt'.
|
| 507 |
+
|
| 508 |
+
Outputs:
|
| 509 |
+
videos (`list`):
|
| 510 |
+
The generated videos.
|
| 511 |
+
"""
|
| 512 |
+
|
| 513 |
+
model_name = "helios-pyramid"
|
| 514 |
+
|
| 515 |
+
block_classes = DISTILLED_PYRAMID_AUTO_BLOCKS.values()
|
| 516 |
+
block_names = DISTILLED_PYRAMID_AUTO_BLOCKS.keys()
|
| 517 |
+
|
| 518 |
+
_workflow_map = {
|
| 519 |
+
"text2video": {"prompt": True},
|
| 520 |
+
"image2video": {"prompt": True, "image": True},
|
| 521 |
+
"video2video": {"prompt": True, "video": True},
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
@property
|
| 525 |
+
def description(self):
|
| 526 |
+
return "Auto Modular pipeline for distilled pyramid progressive generation (T2V/I2V/V2V) using Helios."
|
| 527 |
+
|
| 528 |
+
@property
|
| 529 |
+
def outputs(self):
|
| 530 |
+
return [OutputParam.template("videos")]
|
diffusers/modular_pipelines/helios/modular_pipeline.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...loaders import HeliosLoraLoaderMixin
|
| 17 |
+
from ...utils import logging
|
| 18 |
+
from ..modular_pipeline import ModularPipeline
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class HeliosModularPipeline(
|
| 25 |
+
ModularPipeline,
|
| 26 |
+
HeliosLoraLoaderMixin,
|
| 27 |
+
):
|
| 28 |
+
"""
|
| 29 |
+
A ModularPipeline for Helios text-to-video generation.
|
| 30 |
+
|
| 31 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
default_blocks_name = "HeliosAutoBlocks"
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def vae_scale_factor_spatial(self):
|
| 38 |
+
vae_scale_factor = 8
|
| 39 |
+
if hasattr(self, "vae") and self.vae is not None:
|
| 40 |
+
vae_scale_factor = self.vae.config.scale_factor_spatial
|
| 41 |
+
return vae_scale_factor
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def vae_scale_factor_temporal(self):
|
| 45 |
+
vae_scale_factor = 4
|
| 46 |
+
if hasattr(self, "vae") and self.vae is not None:
|
| 47 |
+
vae_scale_factor = self.vae.config.scale_factor_temporal
|
| 48 |
+
return vae_scale_factor
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def num_channels_latents(self):
|
| 52 |
+
# YiYi TODO: find out default value
|
| 53 |
+
num_channels_latents = 16
|
| 54 |
+
if hasattr(self, "transformer") and self.transformer is not None:
|
| 55 |
+
num_channels_latents = self.transformer.config.in_channels
|
| 56 |
+
return num_channels_latents
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def requires_unconditional_embeds(self):
|
| 60 |
+
requires_unconditional_embeds = False
|
| 61 |
+
|
| 62 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 63 |
+
requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
|
| 64 |
+
|
| 65 |
+
return requires_unconditional_embeds
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class HeliosPyramidModularPipeline(HeliosModularPipeline):
|
| 69 |
+
"""
|
| 70 |
+
A ModularPipeline for Helios pyramid (progressive resolution) video generation.
|
| 71 |
+
|
| 72 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
default_blocks_name = "HeliosPyramidAutoBlocks"
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class HeliosPyramidDistilledModularPipeline(HeliosModularPipeline):
|
| 79 |
+
"""
|
| 80 |
+
A ModularPipeline for Helios distilled pyramid video generation using DMD scheduler.
|
| 81 |
+
|
| 82 |
+
Uses guidance_scale=1.0 (no CFG) and supports is_amplify_first_chunk for the DMD scheduler.
|
| 83 |
+
|
| 84 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
default_blocks_name = "HeliosPyramidDistilledAutoBlocks"
|
diffusers/modular_pipelines/hunyuan_video1_5/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_hunyuan_video1_5"] = [
|
| 25 |
+
"HunyuanVideo15AutoBlocks",
|
| 26 |
+
]
|
| 27 |
+
_import_structure["modular_pipeline"] = ["HunyuanVideo15ModularPipeline"]
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 30 |
+
try:
|
| 31 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 32 |
+
raise OptionalDependencyNotAvailable()
|
| 33 |
+
except OptionalDependencyNotAvailable:
|
| 34 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 35 |
+
else:
|
| 36 |
+
from .modular_blocks_hunyuan_video1_5 import HunyuanVideo15AutoBlocks
|
| 37 |
+
from .modular_pipeline import HunyuanVideo15ModularPipeline
|
| 38 |
+
else:
|
| 39 |
+
import sys
|
| 40 |
+
|
| 41 |
+
sys.modules[__name__] = _LazyModule(
|
| 42 |
+
__name__,
|
| 43 |
+
globals()["__file__"],
|
| 44 |
+
_import_structure,
|
| 45 |
+
module_spec=__spec__,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
for name, value in _dummy_objects.items():
|
| 49 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/hunyuan_video1_5/before_denoise.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import inspect
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ...configuration_utils import FrozenDict
|
| 21 |
+
from ...models import HunyuanVideo15Transformer3DModel
|
| 22 |
+
from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
|
| 23 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 24 |
+
from ...utils import logging
|
| 25 |
+
from ...utils.torch_utils import randn_tensor
|
| 26 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 27 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 28 |
+
from .modular_pipeline import HunyuanVideo15ModularPipeline
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
logger = logging.get_logger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
|
| 35 |
+
def retrieve_timesteps(
|
| 36 |
+
scheduler,
|
| 37 |
+
num_inference_steps: int | None = None,
|
| 38 |
+
device: str | torch.device | None = None,
|
| 39 |
+
timesteps: list[int] | None = None,
|
| 40 |
+
sigmas: list[float] | None = None,
|
| 41 |
+
**kwargs,
|
| 42 |
+
):
|
| 43 |
+
r"""
|
| 44 |
+
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
|
| 45 |
+
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
scheduler (`SchedulerMixin`):
|
| 49 |
+
The scheduler to get timesteps from.
|
| 50 |
+
num_inference_steps (`int`):
|
| 51 |
+
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
|
| 52 |
+
must be `None`.
|
| 53 |
+
device (`str` or `torch.device`, *optional*):
|
| 54 |
+
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
| 55 |
+
timesteps (`list[int]`, *optional*):
|
| 56 |
+
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
|
| 57 |
+
`num_inference_steps` and `sigmas` must be `None`.
|
| 58 |
+
sigmas (`list[float]`, *optional*):
|
| 59 |
+
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
|
| 60 |
+
`num_inference_steps` and `timesteps` must be `None`.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
`tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
|
| 64 |
+
second element is the number of inference steps.
|
| 65 |
+
"""
|
| 66 |
+
if timesteps is not None and sigmas is not None:
|
| 67 |
+
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
|
| 68 |
+
if timesteps is not None:
|
| 69 |
+
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
| 70 |
+
if not accepts_timesteps:
|
| 71 |
+
raise ValueError(
|
| 72 |
+
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
| 73 |
+
f" timestep schedules. Please check whether you are using the correct scheduler."
|
| 74 |
+
)
|
| 75 |
+
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
| 76 |
+
timesteps = scheduler.timesteps
|
| 77 |
+
num_inference_steps = len(timesteps)
|
| 78 |
+
elif sigmas is not None:
|
| 79 |
+
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
| 80 |
+
if not accept_sigmas:
|
| 81 |
+
raise ValueError(
|
| 82 |
+
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
| 83 |
+
f" sigmas schedules. Please check whether you are using the correct scheduler."
|
| 84 |
+
)
|
| 85 |
+
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
| 86 |
+
timesteps = scheduler.timesteps
|
| 87 |
+
num_inference_steps = len(timesteps)
|
| 88 |
+
else:
|
| 89 |
+
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
| 90 |
+
timesteps = scheduler.timesteps
|
| 91 |
+
return timesteps, num_inference_steps
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class HunyuanVideo15TextInputStep(ModularPipelineBlocks):
|
| 95 |
+
model_name = "hunyuan-video-1.5"
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def description(self) -> str:
|
| 99 |
+
return "Input processing step that determines batch_size"
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def inputs(self) -> list[InputParam]:
|
| 103 |
+
return [
|
| 104 |
+
InputParam.template("prompt_embeds"),
|
| 105 |
+
InputParam.template("batch_size", default=None),
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
@property
|
| 109 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 110 |
+
return [
|
| 111 |
+
OutputParam("batch_size", type_hint=int),
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
@torch.no_grad()
|
| 115 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 116 |
+
block_state = self.get_block_state(state)
|
| 117 |
+
block_state.batch_size = getattr(block_state, "batch_size", None) or block_state.prompt_embeds.shape[0]
|
| 118 |
+
self.set_block_state(state, block_state)
|
| 119 |
+
return components, state
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class HunyuanVideo15SetTimestepsStep(ModularPipelineBlocks):
|
| 123 |
+
model_name = "hunyuan-video-1.5"
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 127 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def description(self) -> str:
|
| 131 |
+
return "Step that sets the scheduler's timesteps for inference"
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def inputs(self) -> list[InputParam]:
|
| 135 |
+
return [
|
| 136 |
+
InputParam.template("num_inference_steps"),
|
| 137 |
+
InputParam.template("sigmas"),
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
@property
|
| 141 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 142 |
+
return [
|
| 143 |
+
OutputParam("timesteps", type_hint=torch.Tensor),
|
| 144 |
+
OutputParam("num_inference_steps", type_hint=int),
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
@torch.no_grad()
|
| 148 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 149 |
+
block_state = self.get_block_state(state)
|
| 150 |
+
device = components._execution_device
|
| 151 |
+
|
| 152 |
+
sigmas = block_state.sigmas
|
| 153 |
+
if sigmas is None:
|
| 154 |
+
sigmas = np.linspace(1.0, 0.0, block_state.num_inference_steps + 1)[:-1]
|
| 155 |
+
|
| 156 |
+
block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
|
| 157 |
+
components.scheduler, block_state.num_inference_steps, device, sigmas=sigmas
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
self.set_block_state(state, block_state)
|
| 161 |
+
return components, state
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class HunyuanVideo15PrepareLatentsStep(ModularPipelineBlocks):
|
| 165 |
+
model_name = "hunyuan-video-1.5"
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def description(self) -> str:
|
| 169 |
+
return "Prepare latents, conditioning latents, mask, and image_embeds for T2V"
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 173 |
+
return [
|
| 174 |
+
ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
|
| 175 |
+
ComponentSpec(
|
| 176 |
+
"video_processor",
|
| 177 |
+
HunyuanVideo15ImageProcessor,
|
| 178 |
+
config=FrozenDict({"vae_scale_factor": 16}),
|
| 179 |
+
default_creation_method="from_config",
|
| 180 |
+
),
|
| 181 |
+
]
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def inputs(self) -> list[InputParam]:
|
| 185 |
+
return [
|
| 186 |
+
InputParam.template("height"),
|
| 187 |
+
InputParam.template("width"),
|
| 188 |
+
InputParam("num_frames", type_hint=int, default=121, description="Number of video frames to generate."),
|
| 189 |
+
InputParam.template("latents"),
|
| 190 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 191 |
+
InputParam.template("generator"),
|
| 192 |
+
InputParam.template("batch_size", required=True, default=None),
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 197 |
+
return [
|
| 198 |
+
OutputParam("latents", type_hint=torch.Tensor, description="Pure noise latents"),
|
| 199 |
+
OutputParam("cond_latents_concat", type_hint=torch.Tensor),
|
| 200 |
+
OutputParam("mask_concat", type_hint=torch.Tensor),
|
| 201 |
+
OutputParam("image_embeds", type_hint=torch.Tensor),
|
| 202 |
+
]
|
| 203 |
+
|
| 204 |
+
@torch.no_grad()
|
| 205 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 206 |
+
block_state = self.get_block_state(state)
|
| 207 |
+
device = components._execution_device
|
| 208 |
+
dtype = components.transformer.dtype
|
| 209 |
+
|
| 210 |
+
height = block_state.height
|
| 211 |
+
width = block_state.width
|
| 212 |
+
if height is None and width is None:
|
| 213 |
+
height, width = components.video_processor.calculate_default_height_width(
|
| 214 |
+
components.default_aspect_ratio[1], components.default_aspect_ratio[0], components.target_size
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
batch_size = block_state.batch_size * block_state.num_videos_per_prompt
|
| 218 |
+
num_frames = block_state.num_frames
|
| 219 |
+
|
| 220 |
+
latents = block_state.latents
|
| 221 |
+
if latents is not None:
|
| 222 |
+
latents = latents.to(device=device, dtype=dtype)
|
| 223 |
+
else:
|
| 224 |
+
shape = (
|
| 225 |
+
batch_size,
|
| 226 |
+
components.num_channels_latents,
|
| 227 |
+
(num_frames - 1) // components.vae_scale_factor_temporal + 1,
|
| 228 |
+
int(height) // components.vae_scale_factor_spatial,
|
| 229 |
+
int(width) // components.vae_scale_factor_spatial,
|
| 230 |
+
)
|
| 231 |
+
if isinstance(block_state.generator, list) and len(block_state.generator) != batch_size:
|
| 232 |
+
raise ValueError(
|
| 233 |
+
f"You have passed a list of generators of length {len(block_state.generator)}, but requested an effective batch"
|
| 234 |
+
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
| 235 |
+
)
|
| 236 |
+
latents = randn_tensor(shape, generator=block_state.generator, device=device, dtype=dtype)
|
| 237 |
+
|
| 238 |
+
block_state.latents = latents
|
| 239 |
+
|
| 240 |
+
b, c, f, h, w = latents.shape
|
| 241 |
+
block_state.cond_latents_concat = torch.zeros(b, c, f, h, w, dtype=dtype, device=device)
|
| 242 |
+
block_state.mask_concat = torch.zeros(b, 1, f, h, w, dtype=dtype, device=device)
|
| 243 |
+
|
| 244 |
+
block_state.image_embeds = torch.zeros(
|
| 245 |
+
block_state.batch_size,
|
| 246 |
+
components.vision_num_semantic_tokens,
|
| 247 |
+
components.vision_states_dim,
|
| 248 |
+
dtype=dtype,
|
| 249 |
+
device=device,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
self.set_block_state(state, block_state)
|
| 253 |
+
return components, state
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class HunyuanVideo15Image2VideoPrepareLatentsStep(ModularPipelineBlocks):
|
| 257 |
+
model_name = "hunyuan-video-1.5"
|
| 258 |
+
|
| 259 |
+
@property
|
| 260 |
+
def description(self) -> str:
|
| 261 |
+
return (
|
| 262 |
+
"Prepare I2V conditioning from image_latents and image_embeds. "
|
| 263 |
+
"Expects pure noise `latents` from HunyuanVideo15PrepareLatentsStep. "
|
| 264 |
+
"Builds cond_latents_concat and mask_concat for the denoiser."
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
@property
|
| 268 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 269 |
+
return [ComponentSpec("transformer", HunyuanVideo15Transformer3DModel)]
|
| 270 |
+
|
| 271 |
+
@property
|
| 272 |
+
def inputs(self) -> list[InputParam]:
|
| 273 |
+
return [
|
| 274 |
+
InputParam(
|
| 275 |
+
"image_latents",
|
| 276 |
+
type_hint=torch.Tensor,
|
| 277 |
+
required=True,
|
| 278 |
+
description="Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.",
|
| 279 |
+
),
|
| 280 |
+
InputParam(
|
| 281 |
+
"image_embeds",
|
| 282 |
+
type_hint=torch.Tensor,
|
| 283 |
+
required=True,
|
| 284 |
+
description="Siglip image embeddings from the image encoder step, used as extra conditioning for I2V.",
|
| 285 |
+
),
|
| 286 |
+
InputParam.template("latents", required=True),
|
| 287 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 288 |
+
InputParam.template("batch_size", required=True, default=None),
|
| 289 |
+
]
|
| 290 |
+
|
| 291 |
+
@property
|
| 292 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 293 |
+
return [
|
| 294 |
+
OutputParam("cond_latents_concat", type_hint=torch.Tensor),
|
| 295 |
+
OutputParam("mask_concat", type_hint=torch.Tensor),
|
| 296 |
+
OutputParam("image_embeds", type_hint=torch.Tensor),
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
@torch.no_grad()
|
| 300 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 301 |
+
block_state = self.get_block_state(state)
|
| 302 |
+
device = components._execution_device
|
| 303 |
+
dtype = components.transformer.dtype
|
| 304 |
+
|
| 305 |
+
batch_size = block_state.batch_size * block_state.num_videos_per_prompt
|
| 306 |
+
|
| 307 |
+
b, c, f, h, w = block_state.latents.shape
|
| 308 |
+
|
| 309 |
+
latent_condition = block_state.image_latents.to(device=device, dtype=dtype)
|
| 310 |
+
latent_condition = latent_condition.repeat(batch_size, 1, f, 1, 1)
|
| 311 |
+
latent_condition[:, :, 1:, :, :] = 0
|
| 312 |
+
block_state.cond_latents_concat = latent_condition
|
| 313 |
+
|
| 314 |
+
latent_mask = torch.zeros(b, 1, f, h, w, dtype=dtype, device=device)
|
| 315 |
+
latent_mask[:, :, 0, :, :] = 1.0
|
| 316 |
+
block_state.mask_concat = latent_mask
|
| 317 |
+
|
| 318 |
+
image_embeds = block_state.image_embeds.to(device=device, dtype=dtype)
|
| 319 |
+
if image_embeds.shape[0] == 1 and batch_size > 1:
|
| 320 |
+
image_embeds = image_embeds.repeat(batch_size, 1, 1)
|
| 321 |
+
block_state.image_embeds = image_embeds
|
| 322 |
+
|
| 323 |
+
self.set_block_state(state, block_state)
|
| 324 |
+
return components, state
|
diffusers/modular_pipelines/hunyuan_video1_5/decoders.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...models import AutoencoderKLHunyuanVideo15
|
| 20 |
+
from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 23 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
logger = logging.get_logger(__name__)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class HunyuanVideo15VaeDecoderStep(ModularPipelineBlocks):
|
| 30 |
+
model_name = "hunyuan-video-1.5"
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 34 |
+
return [
|
| 35 |
+
ComponentSpec("vae", AutoencoderKLHunyuanVideo15),
|
| 36 |
+
ComponentSpec(
|
| 37 |
+
"video_processor",
|
| 38 |
+
HunyuanVideo15ImageProcessor,
|
| 39 |
+
config=FrozenDict({"vae_scale_factor": 16}),
|
| 40 |
+
default_creation_method="from_config",
|
| 41 |
+
),
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def description(self) -> str:
|
| 46 |
+
return "Step that decodes the denoised latents into videos"
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def inputs(self) -> list[InputParam]:
|
| 50 |
+
return [
|
| 51 |
+
InputParam.template("latents", required=True),
|
| 52 |
+
InputParam.template("output_type", default="np"),
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 57 |
+
return [
|
| 58 |
+
OutputParam.template("videos"),
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
@torch.no_grad()
|
| 62 |
+
def __call__(self, components, state: PipelineState) -> PipelineState:
|
| 63 |
+
block_state = self.get_block_state(state)
|
| 64 |
+
|
| 65 |
+
latents = block_state.latents.to(components.vae.dtype) / components.vae.config.scaling_factor
|
| 66 |
+
video = components.vae.decode(latents, return_dict=False)[0]
|
| 67 |
+
block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type)
|
| 68 |
+
|
| 69 |
+
self.set_block_state(state, block_state)
|
| 70 |
+
return components, state
|
diffusers/modular_pipelines/hunyuan_video1_5/denoise.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...guiders import ClassifierFreeGuidance
|
| 20 |
+
from ...models import HunyuanVideo15Transformer3DModel
|
| 21 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 22 |
+
from ...utils import logging
|
| 23 |
+
from ..modular_pipeline import (
|
| 24 |
+
BlockState,
|
| 25 |
+
LoopSequentialPipelineBlocks,
|
| 26 |
+
ModularPipelineBlocks,
|
| 27 |
+
PipelineState,
|
| 28 |
+
)
|
| 29 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam
|
| 30 |
+
from .modular_pipeline import HunyuanVideo15ModularPipeline
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = logging.get_logger(__name__)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HunyuanVideo15LoopBeforeDenoiser(ModularPipelineBlocks):
|
| 37 |
+
model_name = "hunyuan-video-1.5"
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def description(self) -> str:
|
| 41 |
+
return "Step within the denoising loop that prepares the latent input"
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def inputs(self) -> list[InputParam]:
|
| 45 |
+
return [
|
| 46 |
+
InputParam.template("latents", required=True),
|
| 47 |
+
InputParam("cond_latents_concat", required=True, type_hint=torch.Tensor),
|
| 48 |
+
InputParam("mask_concat", required=True, type_hint=torch.Tensor),
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
@torch.no_grad()
|
| 52 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 53 |
+
block_state.latent_model_input = torch.cat(
|
| 54 |
+
[block_state.latents, block_state.cond_latents_concat, block_state.mask_concat], dim=1
|
| 55 |
+
)
|
| 56 |
+
return components, block_state
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class HunyuanVideo15LoopDenoiser(ModularPipelineBlocks):
|
| 60 |
+
model_name = "hunyuan-video-1.5"
|
| 61 |
+
|
| 62 |
+
def __init__(self, guider_input_fields=None):
|
| 63 |
+
if guider_input_fields is None:
|
| 64 |
+
guider_input_fields = {
|
| 65 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 66 |
+
"encoder_attention_mask": ("prompt_embeds_mask", "negative_prompt_embeds_mask"),
|
| 67 |
+
"encoder_hidden_states_2": ("prompt_embeds_2", "negative_prompt_embeds_2"),
|
| 68 |
+
"encoder_attention_mask_2": ("prompt_embeds_mask_2", "negative_prompt_embeds_mask_2"),
|
| 69 |
+
}
|
| 70 |
+
if not isinstance(guider_input_fields, dict):
|
| 71 |
+
raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
|
| 72 |
+
self._guider_input_fields = guider_input_fields
|
| 73 |
+
super().__init__()
|
| 74 |
+
|
| 75 |
+
@property
|
| 76 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 77 |
+
return [
|
| 78 |
+
ComponentSpec(
|
| 79 |
+
"guider",
|
| 80 |
+
ClassifierFreeGuidance,
|
| 81 |
+
config=FrozenDict({"guidance_scale": 7.5}),
|
| 82 |
+
default_creation_method="from_config",
|
| 83 |
+
),
|
| 84 |
+
ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def description(self) -> str:
|
| 89 |
+
return "Step within the denoising loop that denoises the latents with guidance"
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def inputs(self) -> list[InputParam]:
|
| 93 |
+
inputs = [
|
| 94 |
+
InputParam.template("attention_kwargs"),
|
| 95 |
+
InputParam.template("num_inference_steps", required=True, default=None),
|
| 96 |
+
InputParam(
|
| 97 |
+
"image_embeds",
|
| 98 |
+
type_hint=torch.Tensor,
|
| 99 |
+
description="Siglip image embeddings used as extra conditioning for I2V. Zero-filled for T2V.",
|
| 100 |
+
),
|
| 101 |
+
]
|
| 102 |
+
for value in self._guider_input_fields.values():
|
| 103 |
+
if isinstance(value, tuple):
|
| 104 |
+
inputs.append(
|
| 105 |
+
InputParam(
|
| 106 |
+
name=value[0],
|
| 107 |
+
required=True,
|
| 108 |
+
type_hint=torch.Tensor,
|
| 109 |
+
description=f"Positive branch of the {value[0]!r} field fed into the guider.",
|
| 110 |
+
)
|
| 111 |
+
)
|
| 112 |
+
for neg_name in value[1:]:
|
| 113 |
+
inputs.append(
|
| 114 |
+
InputParam(
|
| 115 |
+
name=neg_name,
|
| 116 |
+
type_hint=torch.Tensor,
|
| 117 |
+
description=f"Negative branch of the {neg_name!r} field fed into the guider.",
|
| 118 |
+
)
|
| 119 |
+
)
|
| 120 |
+
else:
|
| 121 |
+
inputs.append(
|
| 122 |
+
InputParam(
|
| 123 |
+
name=value,
|
| 124 |
+
required=True,
|
| 125 |
+
type_hint=torch.Tensor,
|
| 126 |
+
description=f"{value!r} field fed into the guider.",
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
return inputs
|
| 130 |
+
|
| 131 |
+
@torch.no_grad()
|
| 132 |
+
def __call__(
|
| 133 |
+
self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 134 |
+
) -> PipelineState:
|
| 135 |
+
timestep = t.expand(block_state.latent_model_input.shape[0]).to(block_state.latent_model_input.dtype)
|
| 136 |
+
|
| 137 |
+
# Step 1: Collect model inputs
|
| 138 |
+
guider_inputs = {
|
| 139 |
+
input_name: tuple(getattr(block_state, v) for v in value)
|
| 140 |
+
if isinstance(value, tuple)
|
| 141 |
+
else getattr(block_state, value)
|
| 142 |
+
for input_name, value in self._guider_input_fields.items()
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Step 2: Update guider state
|
| 146 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 147 |
+
|
| 148 |
+
# Step 3: Prepare batched inputs
|
| 149 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 150 |
+
|
| 151 |
+
# Step 4: Run denoiser for each batch
|
| 152 |
+
for guider_state_batch in guider_state:
|
| 153 |
+
components.guider.prepare_models(components.transformer)
|
| 154 |
+
|
| 155 |
+
cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
|
| 156 |
+
|
| 157 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key)
|
| 158 |
+
with components.transformer.cache_context(context_name):
|
| 159 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 160 |
+
hidden_states=block_state.latent_model_input,
|
| 161 |
+
image_embeds=block_state.image_embeds,
|
| 162 |
+
timestep=timestep,
|
| 163 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 164 |
+
return_dict=False,
|
| 165 |
+
**cond_kwargs,
|
| 166 |
+
)[0]
|
| 167 |
+
|
| 168 |
+
components.guider.cleanup_models(components.transformer)
|
| 169 |
+
|
| 170 |
+
# Step 5: Combine predictions
|
| 171 |
+
block_state.noise_pred = components.guider(guider_state)[0]
|
| 172 |
+
|
| 173 |
+
return components, block_state
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
class HunyuanVideo15LoopAfterDenoiser(ModularPipelineBlocks):
|
| 177 |
+
model_name = "hunyuan-video-1.5"
|
| 178 |
+
|
| 179 |
+
@property
|
| 180 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 181 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def description(self) -> str:
|
| 185 |
+
return "Step within the denoising loop that updates the latents"
|
| 186 |
+
|
| 187 |
+
@torch.no_grad()
|
| 188 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 189 |
+
latents_dtype = block_state.latents.dtype
|
| 190 |
+
block_state.latents = components.scheduler.step(
|
| 191 |
+
block_state.noise_pred, t, block_state.latents, return_dict=False
|
| 192 |
+
)[0]
|
| 193 |
+
|
| 194 |
+
if block_state.latents.dtype != latents_dtype:
|
| 195 |
+
if torch.backends.mps.is_available():
|
| 196 |
+
block_state.latents = block_state.latents.to(latents_dtype)
|
| 197 |
+
|
| 198 |
+
return components, block_state
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class HunyuanVideo15DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
|
| 202 |
+
model_name = "hunyuan-video-1.5"
|
| 203 |
+
|
| 204 |
+
@property
|
| 205 |
+
def description(self) -> str:
|
| 206 |
+
return "Pipeline block that iteratively denoises the latents over timesteps"
|
| 207 |
+
|
| 208 |
+
@property
|
| 209 |
+
def loop_expected_components(self) -> list[ComponentSpec]:
|
| 210 |
+
return [
|
| 211 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 212 |
+
ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
@property
|
| 216 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 217 |
+
return [
|
| 218 |
+
InputParam.template("timesteps", required=True),
|
| 219 |
+
InputParam.template("num_inference_steps", required=True, default=None),
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
@torch.no_grad()
|
| 223 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 224 |
+
block_state = self.get_block_state(state)
|
| 225 |
+
|
| 226 |
+
block_state.num_warmup_steps = max(
|
| 227 |
+
len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
|
| 231 |
+
for i, t in enumerate(block_state.timesteps):
|
| 232 |
+
components, block_state = self.loop_step(components, block_state, i=i, t=t)
|
| 233 |
+
if i == len(block_state.timesteps) - 1 or (
|
| 234 |
+
(i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 235 |
+
):
|
| 236 |
+
progress_bar.update()
|
| 237 |
+
|
| 238 |
+
self.set_block_state(state, block_state)
|
| 239 |
+
return components, state
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class HunyuanVideo15DenoiseStep(HunyuanVideo15DenoiseLoopWrapper):
|
| 243 |
+
block_classes = [
|
| 244 |
+
HunyuanVideo15LoopBeforeDenoiser,
|
| 245 |
+
HunyuanVideo15LoopDenoiser(),
|
| 246 |
+
HunyuanVideo15LoopAfterDenoiser,
|
| 247 |
+
]
|
| 248 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def description(self) -> str:
|
| 252 |
+
return (
|
| 253 |
+
"Denoise step that iteratively denoises the latents.\n"
|
| 254 |
+
"At each iteration:\n"
|
| 255 |
+
" - `HunyuanVideo15LoopBeforeDenoiser`\n"
|
| 256 |
+
" - `HunyuanVideo15LoopDenoiser`\n"
|
| 257 |
+
" - `HunyuanVideo15LoopAfterDenoiser`\n"
|
| 258 |
+
"This block supports text-to-video tasks."
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
class HunyuanVideo15Image2VideoLoopDenoiser(ModularPipelineBlocks):
|
| 263 |
+
model_name = "hunyuan-video-1.5"
|
| 264 |
+
|
| 265 |
+
def __init__(self, guider_input_fields=None):
|
| 266 |
+
if guider_input_fields is None:
|
| 267 |
+
guider_input_fields = {
|
| 268 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 269 |
+
"encoder_attention_mask": ("prompt_embeds_mask", "negative_prompt_embeds_mask"),
|
| 270 |
+
"encoder_hidden_states_2": ("prompt_embeds_2", "negative_prompt_embeds_2"),
|
| 271 |
+
"encoder_attention_mask_2": ("prompt_embeds_mask_2", "negative_prompt_embeds_mask_2"),
|
| 272 |
+
}
|
| 273 |
+
if not isinstance(guider_input_fields, dict):
|
| 274 |
+
raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
|
| 275 |
+
self._guider_input_fields = guider_input_fields
|
| 276 |
+
super().__init__()
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 280 |
+
return [
|
| 281 |
+
ComponentSpec(
|
| 282 |
+
"guider",
|
| 283 |
+
ClassifierFreeGuidance,
|
| 284 |
+
config=FrozenDict({"guidance_scale": 7.5}),
|
| 285 |
+
default_creation_method="from_config",
|
| 286 |
+
),
|
| 287 |
+
ComponentSpec("transformer", HunyuanVideo15Transformer3DModel),
|
| 288 |
+
]
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def description(self) -> str:
|
| 292 |
+
return "I2V denoiser with MeanFlow timestep_r support"
|
| 293 |
+
|
| 294 |
+
@property
|
| 295 |
+
def inputs(self) -> list[InputParam]:
|
| 296 |
+
inputs = [
|
| 297 |
+
InputParam.template("attention_kwargs"),
|
| 298 |
+
InputParam.template("num_inference_steps", required=True, default=None),
|
| 299 |
+
InputParam(
|
| 300 |
+
"image_embeds",
|
| 301 |
+
type_hint=torch.Tensor,
|
| 302 |
+
description="Siglip image embeddings used as extra conditioning for I2V. Zero-filled for T2V.",
|
| 303 |
+
),
|
| 304 |
+
InputParam.template("timesteps", required=True),
|
| 305 |
+
]
|
| 306 |
+
for value in self._guider_input_fields.values():
|
| 307 |
+
if isinstance(value, tuple):
|
| 308 |
+
inputs.append(
|
| 309 |
+
InputParam(
|
| 310 |
+
name=value[0],
|
| 311 |
+
required=True,
|
| 312 |
+
type_hint=torch.Tensor,
|
| 313 |
+
description=f"Positive branch of the {value[0]!r} field fed into the guider.",
|
| 314 |
+
)
|
| 315 |
+
)
|
| 316 |
+
for neg_name in value[1:]:
|
| 317 |
+
inputs.append(
|
| 318 |
+
InputParam(
|
| 319 |
+
name=neg_name,
|
| 320 |
+
type_hint=torch.Tensor,
|
| 321 |
+
description=f"Negative branch of the {neg_name!r} field fed into the guider.",
|
| 322 |
+
)
|
| 323 |
+
)
|
| 324 |
+
else:
|
| 325 |
+
inputs.append(
|
| 326 |
+
InputParam(
|
| 327 |
+
name=value,
|
| 328 |
+
required=True,
|
| 329 |
+
type_hint=torch.Tensor,
|
| 330 |
+
description=f"{value!r} field fed into the guider.",
|
| 331 |
+
)
|
| 332 |
+
)
|
| 333 |
+
return inputs
|
| 334 |
+
|
| 335 |
+
@torch.no_grad()
|
| 336 |
+
def __call__(
|
| 337 |
+
self, components: HunyuanVideo15ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 338 |
+
) -> PipelineState:
|
| 339 |
+
timestep = t.expand(block_state.latent_model_input.shape[0]).to(block_state.latent_model_input.dtype)
|
| 340 |
+
|
| 341 |
+
# MeanFlow timestep_r (lines 855-862)
|
| 342 |
+
if components.transformer.config.use_meanflow:
|
| 343 |
+
if i == len(block_state.timesteps) - 1:
|
| 344 |
+
timestep_r = torch.tensor([0.0], device=timestep.device)
|
| 345 |
+
else:
|
| 346 |
+
timestep_r = block_state.timesteps[i + 1]
|
| 347 |
+
timestep_r = timestep_r.expand(block_state.latents.shape[0]).to(block_state.latents.dtype)
|
| 348 |
+
else:
|
| 349 |
+
timestep_r = None
|
| 350 |
+
|
| 351 |
+
guider_inputs = {
|
| 352 |
+
input_name: tuple(getattr(block_state, v) for v in value)
|
| 353 |
+
if isinstance(value, tuple)
|
| 354 |
+
else getattr(block_state, value)
|
| 355 |
+
for input_name, value in self._guider_input_fields.items()
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 359 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 360 |
+
|
| 361 |
+
for guider_state_batch in guider_state:
|
| 362 |
+
components.guider.prepare_models(components.transformer)
|
| 363 |
+
|
| 364 |
+
cond_kwargs = {input_name: getattr(guider_state_batch, input_name) for input_name in guider_inputs.keys()}
|
| 365 |
+
|
| 366 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key)
|
| 367 |
+
with components.transformer.cache_context(context_name):
|
| 368 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 369 |
+
hidden_states=block_state.latent_model_input,
|
| 370 |
+
image_embeds=block_state.image_embeds,
|
| 371 |
+
timestep=timestep,
|
| 372 |
+
timestep_r=timestep_r,
|
| 373 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 374 |
+
return_dict=False,
|
| 375 |
+
**cond_kwargs,
|
| 376 |
+
)[0]
|
| 377 |
+
|
| 378 |
+
components.guider.cleanup_models(components.transformer)
|
| 379 |
+
|
| 380 |
+
block_state.noise_pred = components.guider(guider_state)[0]
|
| 381 |
+
|
| 382 |
+
return components, block_state
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
class HunyuanVideo15Image2VideoDenoiseStep(HunyuanVideo15DenoiseLoopWrapper):
|
| 386 |
+
block_classes = [
|
| 387 |
+
HunyuanVideo15LoopBeforeDenoiser,
|
| 388 |
+
HunyuanVideo15Image2VideoLoopDenoiser(),
|
| 389 |
+
HunyuanVideo15LoopAfterDenoiser,
|
| 390 |
+
]
|
| 391 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 392 |
+
|
| 393 |
+
@property
|
| 394 |
+
def description(self) -> str:
|
| 395 |
+
return (
|
| 396 |
+
"Denoise step for image-to-video with MeanFlow support.\n"
|
| 397 |
+
"At each iteration:\n"
|
| 398 |
+
" - `HunyuanVideo15LoopBeforeDenoiser`\n"
|
| 399 |
+
" - `HunyuanVideo15Image2VideoLoopDenoiser`\n"
|
| 400 |
+
" - `HunyuanVideo15LoopAfterDenoiser`"
|
| 401 |
+
)
|
diffusers/modular_pipelines/hunyuan_video1_5/encoders.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import re
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from transformers import (
|
| 19 |
+
ByT5Tokenizer,
|
| 20 |
+
Qwen2_5_VLTextModel,
|
| 21 |
+
Qwen2TokenizerFast,
|
| 22 |
+
SiglipImageProcessor,
|
| 23 |
+
SiglipVisionModel,
|
| 24 |
+
T5EncoderModel,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
from ...configuration_utils import FrozenDict
|
| 28 |
+
from ...guiders import ClassifierFreeGuidance
|
| 29 |
+
from ...models import AutoencoderKLHunyuanVideo15
|
| 30 |
+
from ...pipelines.hunyuan_video1_5.image_processor import HunyuanVideo15ImageProcessor
|
| 31 |
+
from ...utils import logging
|
| 32 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 33 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 34 |
+
from .modular_pipeline import HunyuanVideo15ModularPipeline
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
logger = logging.get_logger(__name__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def format_text_input(prompt, system_message):
|
| 41 |
+
return [
|
| 42 |
+
[{"role": "system", "content": system_message}, {"role": "user", "content": p if p else " "}] for p in prompt
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def extract_glyph_texts(prompt):
|
| 47 |
+
pattern = r"\"(.*?)\"|\"(.*?)\""
|
| 48 |
+
matches = re.findall(pattern, prompt)
|
| 49 |
+
result = [match[0] or match[1] for match in matches]
|
| 50 |
+
result = list(dict.fromkeys(result)) if len(result) > 1 else result
|
| 51 |
+
if result:
|
| 52 |
+
formatted_result = ". ".join([f'Text "{text}"' for text in result]) + ". "
|
| 53 |
+
else:
|
| 54 |
+
formatted_result = None
|
| 55 |
+
return formatted_result
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _get_mllm_prompt_embeds(
|
| 59 |
+
text_encoder,
|
| 60 |
+
tokenizer,
|
| 61 |
+
prompt,
|
| 62 |
+
device,
|
| 63 |
+
tokenizer_max_length=1000,
|
| 64 |
+
num_hidden_layers_to_skip=2,
|
| 65 |
+
# fmt: off
|
| 66 |
+
system_message="You are a helpful assistant. Describe the video by detailing the following aspects: \
|
| 67 |
+
1. The main content and theme of the video. \
|
| 68 |
+
2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects. \
|
| 69 |
+
3. Actions, events, behaviors temporal relationships, physical movement changes of the objects. \
|
| 70 |
+
4. background environment, light, style and atmosphere. \
|
| 71 |
+
5. camera angles, movements, and transitions used in the video.",
|
| 72 |
+
# fmt: on
|
| 73 |
+
crop_start=108,
|
| 74 |
+
):
|
| 75 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 76 |
+
prompt = format_text_input(prompt, system_message)
|
| 77 |
+
|
| 78 |
+
text_inputs = tokenizer.apply_chat_template(
|
| 79 |
+
prompt,
|
| 80 |
+
add_generation_prompt=True,
|
| 81 |
+
tokenize=True,
|
| 82 |
+
return_dict=True,
|
| 83 |
+
padding="max_length",
|
| 84 |
+
max_length=tokenizer_max_length + crop_start,
|
| 85 |
+
truncation=True,
|
| 86 |
+
return_tensors="pt",
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
text_input_ids = text_inputs.input_ids.to(device=device)
|
| 90 |
+
prompt_attention_mask = text_inputs.attention_mask.to(device=device)
|
| 91 |
+
|
| 92 |
+
prompt_embeds = text_encoder(
|
| 93 |
+
input_ids=text_input_ids,
|
| 94 |
+
attention_mask=prompt_attention_mask,
|
| 95 |
+
output_hidden_states=True,
|
| 96 |
+
).hidden_states[-(num_hidden_layers_to_skip + 1)]
|
| 97 |
+
|
| 98 |
+
if crop_start is not None and crop_start > 0:
|
| 99 |
+
prompt_embeds = prompt_embeds[:, crop_start:]
|
| 100 |
+
prompt_attention_mask = prompt_attention_mask[:, crop_start:]
|
| 101 |
+
|
| 102 |
+
return prompt_embeds, prompt_attention_mask
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _get_byt5_prompt_embeds(tokenizer, text_encoder, prompt, device, tokenizer_max_length=256):
|
| 106 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 107 |
+
glyph_texts = [extract_glyph_texts(p) for p in prompt]
|
| 108 |
+
|
| 109 |
+
prompt_embeds_list = []
|
| 110 |
+
prompt_embeds_mask_list = []
|
| 111 |
+
|
| 112 |
+
for glyph_text in glyph_texts:
|
| 113 |
+
if glyph_text is None:
|
| 114 |
+
glyph_text_embeds = torch.zeros(
|
| 115 |
+
(1, tokenizer_max_length, text_encoder.config.d_model), device=device, dtype=text_encoder.dtype
|
| 116 |
+
)
|
| 117 |
+
glyph_text_embeds_mask = torch.zeros((1, tokenizer_max_length), device=device, dtype=torch.int64)
|
| 118 |
+
else:
|
| 119 |
+
txt_tokens = tokenizer(
|
| 120 |
+
glyph_text,
|
| 121 |
+
padding="max_length",
|
| 122 |
+
max_length=tokenizer_max_length,
|
| 123 |
+
truncation=True,
|
| 124 |
+
add_special_tokens=True,
|
| 125 |
+
return_tensors="pt",
|
| 126 |
+
).to(device)
|
| 127 |
+
|
| 128 |
+
glyph_text_embeds = text_encoder(
|
| 129 |
+
input_ids=txt_tokens.input_ids,
|
| 130 |
+
attention_mask=txt_tokens.attention_mask.float(),
|
| 131 |
+
)[0]
|
| 132 |
+
glyph_text_embeds = glyph_text_embeds.to(device=device)
|
| 133 |
+
glyph_text_embeds_mask = txt_tokens.attention_mask.to(device=device)
|
| 134 |
+
|
| 135 |
+
prompt_embeds_list.append(glyph_text_embeds)
|
| 136 |
+
prompt_embeds_mask_list.append(glyph_text_embeds_mask)
|
| 137 |
+
|
| 138 |
+
return torch.cat(prompt_embeds_list, dim=0), torch.cat(prompt_embeds_mask_list, dim=0)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class HunyuanVideo15TextEncoderStep(ModularPipelineBlocks):
|
| 142 |
+
model_name = "hunyuan-video-1.5"
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def description(self) -> str:
|
| 146 |
+
return "Dual text encoder step using Qwen2.5-VL (MLLM) and ByT5 (glyph text)"
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 150 |
+
return [
|
| 151 |
+
ComponentSpec("text_encoder", Qwen2_5_VLTextModel),
|
| 152 |
+
ComponentSpec("tokenizer", Qwen2TokenizerFast),
|
| 153 |
+
ComponentSpec("text_encoder_2", T5EncoderModel),
|
| 154 |
+
ComponentSpec("tokenizer_2", ByT5Tokenizer),
|
| 155 |
+
ComponentSpec(
|
| 156 |
+
"guider",
|
| 157 |
+
ClassifierFreeGuidance,
|
| 158 |
+
config=FrozenDict({"guidance_scale": 7.5}),
|
| 159 |
+
default_creation_method="from_config",
|
| 160 |
+
),
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
@property
|
| 164 |
+
def inputs(self) -> list[InputParam]:
|
| 165 |
+
return [
|
| 166 |
+
InputParam.template("prompt", required=False),
|
| 167 |
+
InputParam.template("negative_prompt"),
|
| 168 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 173 |
+
return [
|
| 174 |
+
OutputParam.template("prompt_embeds"),
|
| 175 |
+
OutputParam.template("prompt_embeds_mask"),
|
| 176 |
+
OutputParam.template("negative_prompt_embeds"),
|
| 177 |
+
OutputParam.template("negative_prompt_embeds_mask"),
|
| 178 |
+
OutputParam(
|
| 179 |
+
"prompt_embeds_2",
|
| 180 |
+
type_hint=torch.Tensor,
|
| 181 |
+
kwargs_type="denoiser_input_fields",
|
| 182 |
+
description="ByT5 glyph-text embeddings used as a second conditioning stream for the transformer.",
|
| 183 |
+
),
|
| 184 |
+
OutputParam(
|
| 185 |
+
"prompt_embeds_mask_2",
|
| 186 |
+
type_hint=torch.Tensor,
|
| 187 |
+
kwargs_type="denoiser_input_fields",
|
| 188 |
+
description="Attention mask for the ByT5 glyph-text embeddings.",
|
| 189 |
+
),
|
| 190 |
+
OutputParam(
|
| 191 |
+
"negative_prompt_embeds_2",
|
| 192 |
+
type_hint=torch.Tensor,
|
| 193 |
+
kwargs_type="denoiser_input_fields",
|
| 194 |
+
description="ByT5 glyph-text negative embeddings for classifier-free guidance.",
|
| 195 |
+
),
|
| 196 |
+
OutputParam(
|
| 197 |
+
"negative_prompt_embeds_mask_2",
|
| 198 |
+
type_hint=torch.Tensor,
|
| 199 |
+
kwargs_type="denoiser_input_fields",
|
| 200 |
+
description="Attention mask for the ByT5 glyph-text negative embeddings.",
|
| 201 |
+
),
|
| 202 |
+
]
|
| 203 |
+
|
| 204 |
+
@staticmethod
|
| 205 |
+
def encode_prompt(
|
| 206 |
+
components,
|
| 207 |
+
prompt,
|
| 208 |
+
device=None,
|
| 209 |
+
dtype=None,
|
| 210 |
+
batch_size=1,
|
| 211 |
+
num_videos_per_prompt=1,
|
| 212 |
+
):
|
| 213 |
+
device = device or components._execution_device
|
| 214 |
+
dtype = dtype or components.text_encoder.dtype
|
| 215 |
+
|
| 216 |
+
if prompt is None:
|
| 217 |
+
prompt = [""] * batch_size
|
| 218 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 219 |
+
|
| 220 |
+
prompt_embeds, prompt_embeds_mask = _get_mllm_prompt_embeds(
|
| 221 |
+
tokenizer=components.tokenizer,
|
| 222 |
+
text_encoder=components.text_encoder,
|
| 223 |
+
prompt=prompt,
|
| 224 |
+
device=device,
|
| 225 |
+
tokenizer_max_length=components.tokenizer_max_length,
|
| 226 |
+
system_message=components.system_message,
|
| 227 |
+
crop_start=components.prompt_template_encode_start_idx,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
prompt_embeds_2, prompt_embeds_mask_2 = _get_byt5_prompt_embeds(
|
| 231 |
+
tokenizer=components.tokenizer_2,
|
| 232 |
+
text_encoder=components.text_encoder_2,
|
| 233 |
+
prompt=prompt,
|
| 234 |
+
device=device,
|
| 235 |
+
tokenizer_max_length=components.tokenizer_2_max_length,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
_, seq_len, _ = prompt_embeds.shape
|
| 239 |
+
prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1).view(
|
| 240 |
+
batch_size * num_videos_per_prompt, seq_len, -1
|
| 241 |
+
)
|
| 242 |
+
prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_videos_per_prompt, 1).view(
|
| 243 |
+
batch_size * num_videos_per_prompt, seq_len
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
_, seq_len_2, _ = prompt_embeds_2.shape
|
| 247 |
+
prompt_embeds_2 = prompt_embeds_2.repeat(1, num_videos_per_prompt, 1).view(
|
| 248 |
+
batch_size * num_videos_per_prompt, seq_len_2, -1
|
| 249 |
+
)
|
| 250 |
+
prompt_embeds_mask_2 = prompt_embeds_mask_2.repeat(1, num_videos_per_prompt, 1).view(
|
| 251 |
+
batch_size * num_videos_per_prompt, seq_len_2
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
|
| 255 |
+
prompt_embeds_mask = prompt_embeds_mask.to(dtype=dtype, device=device)
|
| 256 |
+
prompt_embeds_2 = prompt_embeds_2.to(dtype=dtype, device=device)
|
| 257 |
+
prompt_embeds_mask_2 = prompt_embeds_mask_2.to(dtype=dtype, device=device)
|
| 258 |
+
|
| 259 |
+
return prompt_embeds, prompt_embeds_mask, prompt_embeds_2, prompt_embeds_mask_2
|
| 260 |
+
|
| 261 |
+
@torch.no_grad()
|
| 262 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 263 |
+
block_state = self.get_block_state(state)
|
| 264 |
+
device = components._execution_device
|
| 265 |
+
dtype = components.transformer.dtype
|
| 266 |
+
|
| 267 |
+
prompt = block_state.prompt
|
| 268 |
+
negative_prompt = block_state.negative_prompt
|
| 269 |
+
num_videos_per_prompt = block_state.num_videos_per_prompt
|
| 270 |
+
|
| 271 |
+
if prompt is not None and isinstance(prompt, str):
|
| 272 |
+
batch_size = 1
|
| 273 |
+
elif prompt is not None and isinstance(prompt, list):
|
| 274 |
+
batch_size = len(prompt)
|
| 275 |
+
else:
|
| 276 |
+
batch_size = 1
|
| 277 |
+
|
| 278 |
+
(
|
| 279 |
+
block_state.prompt_embeds,
|
| 280 |
+
block_state.prompt_embeds_mask,
|
| 281 |
+
block_state.prompt_embeds_2,
|
| 282 |
+
block_state.prompt_embeds_mask_2,
|
| 283 |
+
) = self.encode_prompt(
|
| 284 |
+
components,
|
| 285 |
+
prompt=prompt,
|
| 286 |
+
device=device,
|
| 287 |
+
dtype=dtype,
|
| 288 |
+
batch_size=batch_size,
|
| 289 |
+
num_videos_per_prompt=num_videos_per_prompt,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
if components.requires_unconditional_embeds:
|
| 293 |
+
(
|
| 294 |
+
block_state.negative_prompt_embeds,
|
| 295 |
+
block_state.negative_prompt_embeds_mask,
|
| 296 |
+
block_state.negative_prompt_embeds_2,
|
| 297 |
+
block_state.negative_prompt_embeds_mask_2,
|
| 298 |
+
) = self.encode_prompt(
|
| 299 |
+
components,
|
| 300 |
+
prompt=negative_prompt,
|
| 301 |
+
device=device,
|
| 302 |
+
dtype=dtype,
|
| 303 |
+
batch_size=batch_size,
|
| 304 |
+
num_videos_per_prompt=num_videos_per_prompt,
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
state.set("batch_size", batch_size)
|
| 308 |
+
|
| 309 |
+
self.set_block_state(state, block_state)
|
| 310 |
+
return components, state
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def retrieve_latents(
|
| 314 |
+
encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
|
| 315 |
+
):
|
| 316 |
+
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
|
| 317 |
+
return encoder_output.latent_dist.sample(generator)
|
| 318 |
+
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
|
| 319 |
+
return encoder_output.latent_dist.mode()
|
| 320 |
+
elif hasattr(encoder_output, "latents"):
|
| 321 |
+
return encoder_output.latents
|
| 322 |
+
else:
|
| 323 |
+
raise AttributeError("Could not access latents of provided encoder_output")
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
class HunyuanVideo15VaeEncoderStep(ModularPipelineBlocks):
|
| 327 |
+
model_name = "hunyuan-video-1.5"
|
| 328 |
+
|
| 329 |
+
@property
|
| 330 |
+
def description(self) -> str:
|
| 331 |
+
return "VAE Encoder step that encodes an input image into latent space for image-to-video generation"
|
| 332 |
+
|
| 333 |
+
@property
|
| 334 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 335 |
+
return [
|
| 336 |
+
ComponentSpec("vae", AutoencoderKLHunyuanVideo15),
|
| 337 |
+
ComponentSpec(
|
| 338 |
+
"video_processor",
|
| 339 |
+
HunyuanVideo15ImageProcessor,
|
| 340 |
+
config=FrozenDict({"vae_scale_factor": 16}),
|
| 341 |
+
default_creation_method="from_config",
|
| 342 |
+
),
|
| 343 |
+
]
|
| 344 |
+
|
| 345 |
+
@property
|
| 346 |
+
def inputs(self) -> list[InputParam]:
|
| 347 |
+
return [
|
| 348 |
+
InputParam.template("image", required=True),
|
| 349 |
+
InputParam.template("height"),
|
| 350 |
+
InputParam.template("width"),
|
| 351 |
+
]
|
| 352 |
+
|
| 353 |
+
@property
|
| 354 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 355 |
+
return [
|
| 356 |
+
OutputParam(
|
| 357 |
+
"image_latents",
|
| 358 |
+
type_hint=torch.Tensor,
|
| 359 |
+
description="Encoded image latents from the VAE encoder",
|
| 360 |
+
),
|
| 361 |
+
OutputParam("height", type_hint=int, description="Target height resolved from image"),
|
| 362 |
+
OutputParam("width", type_hint=int, description="Target width resolved from image"),
|
| 363 |
+
]
|
| 364 |
+
|
| 365 |
+
@torch.no_grad()
|
| 366 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 367 |
+
block_state = self.get_block_state(state)
|
| 368 |
+
device = components._execution_device
|
| 369 |
+
|
| 370 |
+
image = block_state.image
|
| 371 |
+
height = block_state.height
|
| 372 |
+
width = block_state.width
|
| 373 |
+
if height is None or width is None:
|
| 374 |
+
height, width = components.video_processor.calculate_default_height_width(
|
| 375 |
+
height=image.size[1], width=image.size[0], target_size=components.target_size
|
| 376 |
+
)
|
| 377 |
+
image = components.video_processor.resize(image, height=height, width=width, resize_mode="crop")
|
| 378 |
+
|
| 379 |
+
vae_dtype = components.vae.dtype
|
| 380 |
+
image_tensor = components.video_processor.preprocess(image, height=height, width=width).to(
|
| 381 |
+
device=device, dtype=vae_dtype
|
| 382 |
+
)
|
| 383 |
+
image_tensor = image_tensor.unsqueeze(2)
|
| 384 |
+
image_latents = retrieve_latents(components.vae.encode(image_tensor), sample_mode="argmax")
|
| 385 |
+
image_latents = image_latents * components.vae.config.scaling_factor
|
| 386 |
+
|
| 387 |
+
block_state.image_latents = image_latents
|
| 388 |
+
block_state.height = height
|
| 389 |
+
block_state.width = width
|
| 390 |
+
state.set("image", image)
|
| 391 |
+
|
| 392 |
+
self.set_block_state(state, block_state)
|
| 393 |
+
return components, state
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
class HunyuanVideo15ImageEncoderStep(ModularPipelineBlocks):
|
| 397 |
+
model_name = "hunyuan-video-1.5"
|
| 398 |
+
|
| 399 |
+
@property
|
| 400 |
+
def description(self) -> str:
|
| 401 |
+
return "Siglip image encoder step that produces image_embeds for image-to-video generation"
|
| 402 |
+
|
| 403 |
+
@property
|
| 404 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 405 |
+
return [
|
| 406 |
+
ComponentSpec("image_encoder", SiglipVisionModel),
|
| 407 |
+
ComponentSpec("feature_extractor", SiglipImageProcessor),
|
| 408 |
+
]
|
| 409 |
+
|
| 410 |
+
@property
|
| 411 |
+
def inputs(self) -> list[InputParam]:
|
| 412 |
+
return [
|
| 413 |
+
InputParam.template("image", required=True),
|
| 414 |
+
]
|
| 415 |
+
|
| 416 |
+
@property
|
| 417 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 418 |
+
return [
|
| 419 |
+
OutputParam(
|
| 420 |
+
"image_embeds",
|
| 421 |
+
type_hint=torch.Tensor,
|
| 422 |
+
description="Image embeddings from the Siglip vision encoder",
|
| 423 |
+
),
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
@torch.no_grad()
|
| 427 |
+
def __call__(self, components: HunyuanVideo15ModularPipeline, state: PipelineState) -> PipelineState:
|
| 428 |
+
block_state = self.get_block_state(state)
|
| 429 |
+
device = components._execution_device
|
| 430 |
+
|
| 431 |
+
image_encoder_dtype = next(components.image_encoder.parameters()).dtype
|
| 432 |
+
image_inputs = components.feature_extractor.preprocess(
|
| 433 |
+
images=block_state.image, do_resize=True, return_tensors="pt", do_convert_rgb=True
|
| 434 |
+
)
|
| 435 |
+
image_inputs = image_inputs.to(device=device, dtype=image_encoder_dtype)
|
| 436 |
+
image_embeds = components.image_encoder(**image_inputs).last_hidden_state
|
| 437 |
+
|
| 438 |
+
block_state.image_embeds = image_embeds
|
| 439 |
+
|
| 440 |
+
self.set_block_state(state, block_state)
|
| 441 |
+
return components, state
|
diffusers/modular_pipelines/hunyuan_video1_5/modular_blocks_hunyuan_video1_5.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ...utils import logging
|
| 16 |
+
from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
|
| 17 |
+
from ..modular_pipeline_utils import OutputParam
|
| 18 |
+
from .before_denoise import (
|
| 19 |
+
HunyuanVideo15Image2VideoPrepareLatentsStep,
|
| 20 |
+
HunyuanVideo15PrepareLatentsStep,
|
| 21 |
+
HunyuanVideo15SetTimestepsStep,
|
| 22 |
+
HunyuanVideo15TextInputStep,
|
| 23 |
+
)
|
| 24 |
+
from .decoders import HunyuanVideo15VaeDecoderStep
|
| 25 |
+
from .denoise import HunyuanVideo15DenoiseStep, HunyuanVideo15Image2VideoDenoiseStep
|
| 26 |
+
from .encoders import (
|
| 27 |
+
HunyuanVideo15ImageEncoderStep,
|
| 28 |
+
HunyuanVideo15TextEncoderStep,
|
| 29 |
+
HunyuanVideo15VaeEncoderStep,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = logging.get_logger(__name__)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# auto_docstring
|
| 37 |
+
class HunyuanVideo15CoreDenoiseStep(SequentialPipelineBlocks):
|
| 38 |
+
"""
|
| 39 |
+
Denoise block that takes encoded conditions and runs the denoising process.
|
| 40 |
+
|
| 41 |
+
Components:
|
| 42 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
|
| 43 |
+
video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
|
| 44 |
+
|
| 45 |
+
Inputs:
|
| 46 |
+
prompt_embeds (`Tensor`):
|
| 47 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 48 |
+
batch_size (`int`, *optional*):
|
| 49 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 50 |
+
be generated in input step.
|
| 51 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 52 |
+
The number of denoising steps.
|
| 53 |
+
sigmas (`list`, *optional*):
|
| 54 |
+
Custom sigmas for the denoising process.
|
| 55 |
+
height (`int`, *optional*):
|
| 56 |
+
The height in pixels of the generated image.
|
| 57 |
+
width (`int`, *optional*):
|
| 58 |
+
The width in pixels of the generated image.
|
| 59 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 60 |
+
Number of video frames to generate.
|
| 61 |
+
latents (`Tensor`, *optional*):
|
| 62 |
+
Pre-generated noisy latents for image generation.
|
| 63 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 64 |
+
The number of images to generate per prompt.
|
| 65 |
+
generator (`Generator`, *optional*):
|
| 66 |
+
Torch generator for deterministic generation.
|
| 67 |
+
attention_kwargs (`dict`, *optional*):
|
| 68 |
+
Additional kwargs for attention processors.
|
| 69 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 70 |
+
Negative branch of the 'negative_prompt_embeds' field fed into the guider.
|
| 71 |
+
prompt_embeds_mask (`Tensor`):
|
| 72 |
+
Positive branch of the 'prompt_embeds_mask' field fed into the guider.
|
| 73 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 74 |
+
Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
|
| 75 |
+
prompt_embeds_2 (`Tensor`):
|
| 76 |
+
Positive branch of the 'prompt_embeds_2' field fed into the guider.
|
| 77 |
+
negative_prompt_embeds_2 (`Tensor`, *optional*):
|
| 78 |
+
Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
|
| 79 |
+
prompt_embeds_mask_2 (`Tensor`):
|
| 80 |
+
Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
|
| 81 |
+
negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
|
| 82 |
+
Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
|
| 83 |
+
|
| 84 |
+
Outputs:
|
| 85 |
+
latents (`Tensor`):
|
| 86 |
+
Denoised latents.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
model_name = "hunyuan-video-1.5"
|
| 90 |
+
block_classes = [
|
| 91 |
+
HunyuanVideo15TextInputStep,
|
| 92 |
+
HunyuanVideo15SetTimestepsStep,
|
| 93 |
+
HunyuanVideo15PrepareLatentsStep,
|
| 94 |
+
HunyuanVideo15DenoiseStep,
|
| 95 |
+
]
|
| 96 |
+
block_names = ["input", "set_timesteps", "prepare_latents", "denoise"]
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def description(self):
|
| 100 |
+
return "Denoise block that takes encoded conditions and runs the denoising process."
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def outputs(self):
|
| 104 |
+
return [OutputParam.template("latents")]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# auto_docstring
|
| 108 |
+
class HunyuanVideo15Blocks(SequentialPipelineBlocks):
|
| 109 |
+
"""
|
| 110 |
+
Modular pipeline blocks for HunyuanVideo 1.5 text-to-video.
|
| 111 |
+
|
| 112 |
+
Components:
|
| 113 |
+
text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
|
| 114 |
+
tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 115 |
+
transformer (`HunyuanVideo15Transformer3DModel`) video_processor (`HunyuanVideo15ImageProcessor`) vae
|
| 116 |
+
(`AutoencoderKLHunyuanVideo15`)
|
| 117 |
+
|
| 118 |
+
Inputs:
|
| 119 |
+
prompt (`str`, *optional*):
|
| 120 |
+
The prompt or prompts to guide image generation.
|
| 121 |
+
negative_prompt (`str`, *optional*):
|
| 122 |
+
The prompt or prompts not to guide the image generation.
|
| 123 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 124 |
+
The number of images to generate per prompt.
|
| 125 |
+
batch_size (`int`, *optional*):
|
| 126 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 127 |
+
be generated in input step.
|
| 128 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 129 |
+
The number of denoising steps.
|
| 130 |
+
sigmas (`list`, *optional*):
|
| 131 |
+
Custom sigmas for the denoising process.
|
| 132 |
+
height (`int`, *optional*):
|
| 133 |
+
The height in pixels of the generated image.
|
| 134 |
+
width (`int`, *optional*):
|
| 135 |
+
The width in pixels of the generated image.
|
| 136 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 137 |
+
Number of video frames to generate.
|
| 138 |
+
latents (`Tensor`, *optional*):
|
| 139 |
+
Pre-generated noisy latents for image generation.
|
| 140 |
+
generator (`Generator`, *optional*):
|
| 141 |
+
Torch generator for deterministic generation.
|
| 142 |
+
attention_kwargs (`dict`, *optional*):
|
| 143 |
+
Additional kwargs for attention processors.
|
| 144 |
+
output_type (`str`, *optional*, defaults to np):
|
| 145 |
+
Output format: 'pil', 'np', 'pt'.
|
| 146 |
+
|
| 147 |
+
Outputs:
|
| 148 |
+
videos (`list`):
|
| 149 |
+
The generated videos.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
model_name = "hunyuan-video-1.5"
|
| 153 |
+
block_classes = [
|
| 154 |
+
HunyuanVideo15TextEncoderStep,
|
| 155 |
+
HunyuanVideo15CoreDenoiseStep,
|
| 156 |
+
HunyuanVideo15VaeDecoderStep,
|
| 157 |
+
]
|
| 158 |
+
block_names = ["text_encoder", "denoise", "decode"]
|
| 159 |
+
|
| 160 |
+
@property
|
| 161 |
+
def description(self):
|
| 162 |
+
return "Modular pipeline blocks for HunyuanVideo 1.5 text-to-video."
|
| 163 |
+
|
| 164 |
+
@property
|
| 165 |
+
def outputs(self):
|
| 166 |
+
return [OutputParam.template("videos")]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# auto_docstring
|
| 170 |
+
class HunyuanVideo15Image2VideoCoreDenoiseStep(SequentialPipelineBlocks):
|
| 171 |
+
"""
|
| 172 |
+
Denoise block for image-to-video that takes encoded conditions and runs the denoising process.
|
| 173 |
+
|
| 174 |
+
Components:
|
| 175 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
|
| 176 |
+
video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
|
| 177 |
+
|
| 178 |
+
Inputs:
|
| 179 |
+
prompt_embeds (`Tensor`):
|
| 180 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 181 |
+
batch_size (`int`, *optional*):
|
| 182 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 183 |
+
be generated in input step.
|
| 184 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 185 |
+
The number of denoising steps.
|
| 186 |
+
sigmas (`list`, *optional*):
|
| 187 |
+
Custom sigmas for the denoising process.
|
| 188 |
+
height (`int`, *optional*):
|
| 189 |
+
The height in pixels of the generated image.
|
| 190 |
+
width (`int`, *optional*):
|
| 191 |
+
The width in pixels of the generated image.
|
| 192 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 193 |
+
Number of video frames to generate.
|
| 194 |
+
latents (`Tensor`, *optional*):
|
| 195 |
+
Pre-generated noisy latents for image generation.
|
| 196 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 197 |
+
The number of images to generate per prompt.
|
| 198 |
+
generator (`Generator`, *optional*):
|
| 199 |
+
Torch generator for deterministic generation.
|
| 200 |
+
image_latents (`Tensor`):
|
| 201 |
+
Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
|
| 202 |
+
attention_kwargs (`dict`, *optional*):
|
| 203 |
+
Additional kwargs for attention processors.
|
| 204 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 205 |
+
Negative branch of the 'negative_prompt_embeds' field fed into the guider.
|
| 206 |
+
prompt_embeds_mask (`Tensor`):
|
| 207 |
+
Positive branch of the 'prompt_embeds_mask' field fed into the guider.
|
| 208 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 209 |
+
Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
|
| 210 |
+
prompt_embeds_2 (`Tensor`):
|
| 211 |
+
Positive branch of the 'prompt_embeds_2' field fed into the guider.
|
| 212 |
+
negative_prompt_embeds_2 (`Tensor`, *optional*):
|
| 213 |
+
Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
|
| 214 |
+
prompt_embeds_mask_2 (`Tensor`):
|
| 215 |
+
Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
|
| 216 |
+
negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
|
| 217 |
+
Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
|
| 218 |
+
|
| 219 |
+
Outputs:
|
| 220 |
+
latents (`Tensor`):
|
| 221 |
+
Denoised latents.
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
model_name = "hunyuan-video-1.5"
|
| 225 |
+
block_classes = [
|
| 226 |
+
HunyuanVideo15TextInputStep,
|
| 227 |
+
HunyuanVideo15SetTimestepsStep,
|
| 228 |
+
HunyuanVideo15PrepareLatentsStep,
|
| 229 |
+
HunyuanVideo15Image2VideoPrepareLatentsStep,
|
| 230 |
+
HunyuanVideo15Image2VideoDenoiseStep,
|
| 231 |
+
]
|
| 232 |
+
block_names = ["input", "set_timesteps", "prepare_latents", "prepare_i2v_latents", "denoise"]
|
| 233 |
+
|
| 234 |
+
@property
|
| 235 |
+
def description(self):
|
| 236 |
+
return "Denoise block for image-to-video that takes encoded conditions and runs the denoising process."
|
| 237 |
+
|
| 238 |
+
@property
|
| 239 |
+
def outputs(self):
|
| 240 |
+
return [OutputParam.template("latents")]
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# auto_docstring
|
| 244 |
+
class HunyuanVideo15AutoVaeEncoderStep(AutoPipelineBlocks):
|
| 245 |
+
"""
|
| 246 |
+
VAE encoder step that encodes the image input into its latent representation.
|
| 247 |
+
This is an auto pipeline block that works for image-to-video tasks.
|
| 248 |
+
- `HunyuanVideo15VaeEncoderStep` is used when `image` is provided.
|
| 249 |
+
- If `image` is not provided, step will be skipped.
|
| 250 |
+
|
| 251 |
+
Components:
|
| 252 |
+
vae (`AutoencoderKLHunyuanVideo15`) video_processor (`HunyuanVideo15ImageProcessor`)
|
| 253 |
+
|
| 254 |
+
Inputs:
|
| 255 |
+
image (`Image | list`, *optional*):
|
| 256 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 257 |
+
height (`int`, *optional*):
|
| 258 |
+
The height in pixels of the generated image.
|
| 259 |
+
width (`int`, *optional*):
|
| 260 |
+
The width in pixels of the generated image.
|
| 261 |
+
|
| 262 |
+
Outputs:
|
| 263 |
+
image_latents (`Tensor`):
|
| 264 |
+
Encoded image latents from the VAE encoder
|
| 265 |
+
height (`int`):
|
| 266 |
+
Target height resolved from image
|
| 267 |
+
width (`int`):
|
| 268 |
+
Target width resolved from image
|
| 269 |
+
"""
|
| 270 |
+
|
| 271 |
+
model_name = "hunyuan-video-1.5"
|
| 272 |
+
block_classes = [HunyuanVideo15VaeEncoderStep]
|
| 273 |
+
block_names = ["vae_encoder"]
|
| 274 |
+
block_trigger_inputs = ["image"]
|
| 275 |
+
|
| 276 |
+
@property
|
| 277 |
+
def description(self):
|
| 278 |
+
return (
|
| 279 |
+
"VAE encoder step that encodes the image input into its latent representation.\n"
|
| 280 |
+
"This is an auto pipeline block that works for image-to-video tasks.\n"
|
| 281 |
+
" - `HunyuanVideo15VaeEncoderStep` is used when `image` is provided.\n"
|
| 282 |
+
" - If `image` is not provided, step will be skipped."
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# auto_docstring
|
| 287 |
+
class HunyuanVideo15AutoImageEncoderStep(AutoPipelineBlocks):
|
| 288 |
+
"""
|
| 289 |
+
Siglip image encoder step that produces image_embeds.
|
| 290 |
+
This is an auto pipeline block that works for image-to-video tasks.
|
| 291 |
+
- `HunyuanVideo15ImageEncoderStep` is used when `image` is provided.
|
| 292 |
+
- If `image` is not provided, step will be skipped.
|
| 293 |
+
|
| 294 |
+
Components:
|
| 295 |
+
image_encoder (`SiglipVisionModel`) feature_extractor (`SiglipImageProcessor`)
|
| 296 |
+
|
| 297 |
+
Inputs:
|
| 298 |
+
image (`Image | list`, *optional*):
|
| 299 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 300 |
+
|
| 301 |
+
Outputs:
|
| 302 |
+
image_embeds (`Tensor`):
|
| 303 |
+
Image embeddings from the Siglip vision encoder
|
| 304 |
+
"""
|
| 305 |
+
|
| 306 |
+
model_name = "hunyuan-video-1.5"
|
| 307 |
+
block_classes = [HunyuanVideo15ImageEncoderStep]
|
| 308 |
+
block_names = ["image_encoder"]
|
| 309 |
+
block_trigger_inputs = ["image"]
|
| 310 |
+
|
| 311 |
+
@property
|
| 312 |
+
def description(self):
|
| 313 |
+
return (
|
| 314 |
+
"Siglip image encoder step that produces image_embeds.\n"
|
| 315 |
+
"This is an auto pipeline block that works for image-to-video tasks.\n"
|
| 316 |
+
" - `HunyuanVideo15ImageEncoderStep` is used when `image` is provided.\n"
|
| 317 |
+
" - If `image` is not provided, step will be skipped."
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
# auto_docstring
|
| 322 |
+
class HunyuanVideo15AutoCoreDenoiseStep(AutoPipelineBlocks):
|
| 323 |
+
"""
|
| 324 |
+
Auto denoise block that selects the appropriate denoise pipeline based on inputs.
|
| 325 |
+
- `HunyuanVideo15Image2VideoCoreDenoiseStep` is used when `image_latents` is provided.
|
| 326 |
+
- `HunyuanVideo15CoreDenoiseStep` is used otherwise (text-to-video).
|
| 327 |
+
|
| 328 |
+
Components:
|
| 329 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`HunyuanVideo15Transformer3DModel`)
|
| 330 |
+
video_processor (`HunyuanVideo15ImageProcessor`) guider (`ClassifierFreeGuidance`)
|
| 331 |
+
|
| 332 |
+
Inputs:
|
| 333 |
+
prompt_embeds (`Tensor`):
|
| 334 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 335 |
+
batch_size (`int`):
|
| 336 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 337 |
+
be generated in input step.
|
| 338 |
+
num_inference_steps (`int`):
|
| 339 |
+
The number of denoising steps.
|
| 340 |
+
sigmas (`list`, *optional*):
|
| 341 |
+
Custom sigmas for the denoising process.
|
| 342 |
+
height (`int`, *optional*):
|
| 343 |
+
The height in pixels of the generated image.
|
| 344 |
+
width (`int`, *optional*):
|
| 345 |
+
The width in pixels of the generated image.
|
| 346 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 347 |
+
Number of video frames to generate.
|
| 348 |
+
latents (`Tensor`):
|
| 349 |
+
Pre-generated noisy latents for image generation.
|
| 350 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 351 |
+
The number of images to generate per prompt.
|
| 352 |
+
generator (`Generator`, *optional*):
|
| 353 |
+
Torch generator for deterministic generation.
|
| 354 |
+
image_latents (`Tensor`, *optional*):
|
| 355 |
+
Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
|
| 356 |
+
attention_kwargs (`dict`, *optional*):
|
| 357 |
+
Additional kwargs for attention processors.
|
| 358 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 359 |
+
Negative branch of the 'negative_prompt_embeds' field fed into the guider.
|
| 360 |
+
prompt_embeds_mask (`Tensor`):
|
| 361 |
+
Positive branch of the 'prompt_embeds_mask' field fed into the guider.
|
| 362 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 363 |
+
Negative branch of the 'negative_prompt_embeds_mask' field fed into the guider.
|
| 364 |
+
prompt_embeds_2 (`Tensor`):
|
| 365 |
+
Positive branch of the 'prompt_embeds_2' field fed into the guider.
|
| 366 |
+
negative_prompt_embeds_2 (`Tensor`, *optional*):
|
| 367 |
+
Negative branch of the 'negative_prompt_embeds_2' field fed into the guider.
|
| 368 |
+
prompt_embeds_mask_2 (`Tensor`):
|
| 369 |
+
Positive branch of the 'prompt_embeds_mask_2' field fed into the guider.
|
| 370 |
+
negative_prompt_embeds_mask_2 (`Tensor`, *optional*):
|
| 371 |
+
Negative branch of the 'negative_prompt_embeds_mask_2' field fed into the guider.
|
| 372 |
+
|
| 373 |
+
Outputs:
|
| 374 |
+
latents (`Tensor`):
|
| 375 |
+
Denoised latents.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
model_name = "hunyuan-video-1.5"
|
| 379 |
+
block_classes = [HunyuanVideo15Image2VideoCoreDenoiseStep, HunyuanVideo15CoreDenoiseStep]
|
| 380 |
+
block_names = ["image2video", "text2video"]
|
| 381 |
+
block_trigger_inputs = ["image_latents", None]
|
| 382 |
+
|
| 383 |
+
@property
|
| 384 |
+
def description(self):
|
| 385 |
+
return (
|
| 386 |
+
"Auto denoise block that selects the appropriate denoise pipeline based on inputs.\n"
|
| 387 |
+
" - `HunyuanVideo15Image2VideoCoreDenoiseStep` is used when `image_latents` is provided.\n"
|
| 388 |
+
" - `HunyuanVideo15CoreDenoiseStep` is used otherwise (text-to-video)."
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
# auto_docstring
|
| 393 |
+
class HunyuanVideo15AutoBlocks(SequentialPipelineBlocks):
|
| 394 |
+
"""
|
| 395 |
+
Auto blocks for HunyuanVideo 1.5 that support both text-to-video and image-to-video workflows.
|
| 396 |
+
|
| 397 |
+
Supported workflows:
|
| 398 |
+
- `text2video`: requires `prompt`
|
| 399 |
+
- `image2video`: requires `image`, `prompt`
|
| 400 |
+
|
| 401 |
+
Components:
|
| 402 |
+
text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
|
| 403 |
+
tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) vae (`AutoencoderKLHunyuanVideo15`)
|
| 404 |
+
video_processor (`HunyuanVideo15ImageProcessor`) image_encoder (`SiglipVisionModel`) feature_extractor
|
| 405 |
+
(`SiglipImageProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
|
| 406 |
+
(`HunyuanVideo15Transformer3DModel`)
|
| 407 |
+
|
| 408 |
+
Inputs:
|
| 409 |
+
prompt (`str`, *optional*):
|
| 410 |
+
The prompt or prompts to guide image generation.
|
| 411 |
+
negative_prompt (`str`, *optional*):
|
| 412 |
+
The prompt or prompts not to guide the image generation.
|
| 413 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 414 |
+
The number of images to generate per prompt.
|
| 415 |
+
image (`Image | list`, *optional*):
|
| 416 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 417 |
+
height (`int`, *optional*):
|
| 418 |
+
The height in pixels of the generated image.
|
| 419 |
+
width (`int`, *optional*):
|
| 420 |
+
The width in pixels of the generated image.
|
| 421 |
+
batch_size (`int`):
|
| 422 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 423 |
+
be generated in input step.
|
| 424 |
+
num_inference_steps (`int`):
|
| 425 |
+
The number of denoising steps.
|
| 426 |
+
sigmas (`list`, *optional*):
|
| 427 |
+
Custom sigmas for the denoising process.
|
| 428 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 429 |
+
Number of video frames to generate.
|
| 430 |
+
latents (`Tensor`):
|
| 431 |
+
Pre-generated noisy latents for image generation.
|
| 432 |
+
generator (`Generator`, *optional*):
|
| 433 |
+
Torch generator for deterministic generation.
|
| 434 |
+
image_latents (`Tensor`, *optional*):
|
| 435 |
+
Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
|
| 436 |
+
attention_kwargs (`dict`, *optional*):
|
| 437 |
+
Additional kwargs for attention processors.
|
| 438 |
+
output_type (`str`, *optional*, defaults to np):
|
| 439 |
+
Output format: 'pil', 'np', 'pt'.
|
| 440 |
+
|
| 441 |
+
Outputs:
|
| 442 |
+
videos (`list`):
|
| 443 |
+
The generated videos.
|
| 444 |
+
"""
|
| 445 |
+
|
| 446 |
+
model_name = "hunyuan-video-1.5"
|
| 447 |
+
block_classes = [
|
| 448 |
+
HunyuanVideo15TextEncoderStep,
|
| 449 |
+
HunyuanVideo15AutoVaeEncoderStep,
|
| 450 |
+
HunyuanVideo15AutoImageEncoderStep,
|
| 451 |
+
HunyuanVideo15AutoCoreDenoiseStep,
|
| 452 |
+
HunyuanVideo15VaeDecoderStep,
|
| 453 |
+
]
|
| 454 |
+
block_names = ["text_encoder", "vae_encoder", "image_encoder", "denoise", "decode"]
|
| 455 |
+
_workflow_map = {
|
| 456 |
+
"text2video": {"prompt": True},
|
| 457 |
+
"image2video": {"image": True, "prompt": True},
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
@property
|
| 461 |
+
def description(self):
|
| 462 |
+
return "Auto blocks for HunyuanVideo 1.5 that support both text-to-video and image-to-video workflows."
|
| 463 |
+
|
| 464 |
+
@property
|
| 465 |
+
def outputs(self):
|
| 466 |
+
return [OutputParam.template("videos")]
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
# auto_docstring
|
| 470 |
+
class HunyuanVideo15Image2VideoBlocks(SequentialPipelineBlocks):
|
| 471 |
+
"""
|
| 472 |
+
Modular pipeline blocks for HunyuanVideo 1.5 image-to-video.
|
| 473 |
+
|
| 474 |
+
Components:
|
| 475 |
+
text_encoder (`Qwen2_5_VLTextModel`) tokenizer (`Qwen2Tokenizer`) text_encoder_2 (`T5EncoderModel`)
|
| 476 |
+
tokenizer_2 (`ByT5Tokenizer`) guider (`ClassifierFreeGuidance`) vae (`AutoencoderKLHunyuanVideo15`)
|
| 477 |
+
video_processor (`HunyuanVideo15ImageProcessor`) image_encoder (`SiglipVisionModel`) feature_extractor
|
| 478 |
+
(`SiglipImageProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`) transformer
|
| 479 |
+
(`HunyuanVideo15Transformer3DModel`)
|
| 480 |
+
|
| 481 |
+
Inputs:
|
| 482 |
+
prompt (`str`, *optional*):
|
| 483 |
+
The prompt or prompts to guide image generation.
|
| 484 |
+
negative_prompt (`str`, *optional*):
|
| 485 |
+
The prompt or prompts not to guide the image generation.
|
| 486 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 487 |
+
The number of images to generate per prompt.
|
| 488 |
+
image (`Image | list`, *optional*):
|
| 489 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 490 |
+
height (`int`, *optional*):
|
| 491 |
+
The height in pixels of the generated image.
|
| 492 |
+
width (`int`, *optional*):
|
| 493 |
+
The width in pixels of the generated image.
|
| 494 |
+
batch_size (`int`, *optional*):
|
| 495 |
+
Number of prompts, the final batch size of model inputs should be batch_size * num_images_per_prompt. Can
|
| 496 |
+
be generated in input step.
|
| 497 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 498 |
+
The number of denoising steps.
|
| 499 |
+
sigmas (`list`, *optional*):
|
| 500 |
+
Custom sigmas for the denoising process.
|
| 501 |
+
num_frames (`int`, *optional*, defaults to 121):
|
| 502 |
+
Number of video frames to generate.
|
| 503 |
+
latents (`Tensor`, *optional*):
|
| 504 |
+
Pre-generated noisy latents for image generation.
|
| 505 |
+
generator (`Generator`, *optional*):
|
| 506 |
+
Torch generator for deterministic generation.
|
| 507 |
+
image_latents (`Tensor`):
|
| 508 |
+
Pre-encoded image latents from the VAE encoder step, used as conditioning for I2V.
|
| 509 |
+
attention_kwargs (`dict`, *optional*):
|
| 510 |
+
Additional kwargs for attention processors.
|
| 511 |
+
output_type (`str`, *optional*, defaults to np):
|
| 512 |
+
Output format: 'pil', 'np', 'pt'.
|
| 513 |
+
|
| 514 |
+
Outputs:
|
| 515 |
+
videos (`list`):
|
| 516 |
+
The generated videos.
|
| 517 |
+
"""
|
| 518 |
+
|
| 519 |
+
model_name = "hunyuan-video-1.5"
|
| 520 |
+
block_classes = [
|
| 521 |
+
HunyuanVideo15TextEncoderStep,
|
| 522 |
+
HunyuanVideo15AutoVaeEncoderStep,
|
| 523 |
+
HunyuanVideo15AutoImageEncoderStep,
|
| 524 |
+
HunyuanVideo15Image2VideoCoreDenoiseStep,
|
| 525 |
+
HunyuanVideo15VaeDecoderStep,
|
| 526 |
+
]
|
| 527 |
+
block_names = ["text_encoder", "vae_encoder", "image_encoder", "denoise", "decode"]
|
| 528 |
+
|
| 529 |
+
@property
|
| 530 |
+
def description(self):
|
| 531 |
+
return "Modular pipeline blocks for HunyuanVideo 1.5 image-to-video."
|
| 532 |
+
|
| 533 |
+
@property
|
| 534 |
+
def outputs(self):
|
| 535 |
+
return [OutputParam.template("videos")]
|
diffusers/modular_pipelines/hunyuan_video1_5/modular_pipeline.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ...loaders import HunyuanVideoLoraLoaderMixin
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import ModularPipeline
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
logger = logging.get_logger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class HunyuanVideo15ModularPipeline(
|
| 24 |
+
ModularPipeline,
|
| 25 |
+
HunyuanVideoLoraLoaderMixin,
|
| 26 |
+
):
|
| 27 |
+
"""
|
| 28 |
+
A ModularPipeline for HunyuanVideo 1.5.
|
| 29 |
+
|
| 30 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
default_blocks_name = "HunyuanVideo15AutoBlocks"
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def vae_scale_factor_spatial(self):
|
| 37 |
+
return self.vae.spatial_compression_ratio if getattr(self, "vae", None) else 16
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def vae_scale_factor_temporal(self):
|
| 41 |
+
return self.vae.temporal_compression_ratio if getattr(self, "vae", None) else 4
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def num_channels_latents(self):
|
| 45 |
+
return self.vae.config.latent_channels if getattr(self, "vae", None) else 32
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def target_size(self):
|
| 49 |
+
return self.transformer.config.target_size if getattr(self, "transformer", None) else 640
|
| 50 |
+
|
| 51 |
+
@property
|
| 52 |
+
def default_aspect_ratio(self):
|
| 53 |
+
return (16, 9)
|
| 54 |
+
|
| 55 |
+
@property
|
| 56 |
+
def vision_num_semantic_tokens(self):
|
| 57 |
+
return 729
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def vision_states_dim(self):
|
| 61 |
+
return self.transformer.config.image_embed_dim if getattr(self, "transformer", None) else 1152
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def tokenizer_max_length(self):
|
| 65 |
+
return 1000
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def tokenizer_2_max_length(self):
|
| 69 |
+
return 256
|
| 70 |
+
|
| 71 |
+
# fmt: off
|
| 72 |
+
@property
|
| 73 |
+
def system_message(self):
|
| 74 |
+
return "You are a helpful assistant. Describe the video by detailing the following aspects: \
|
| 75 |
+
1. The main content and theme of the video. \
|
| 76 |
+
2. The color, shape, size, texture, quantity, text, and spatial relationships of the objects. \
|
| 77 |
+
3. Actions, events, behaviors temporal relationships, physical movement changes of the objects. \
|
| 78 |
+
4. background environment, light, style and atmosphere. \
|
| 79 |
+
5. camera angles, movements, and transitions used in the video."
|
| 80 |
+
# fmt: on
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def prompt_template_encode_start_idx(self):
|
| 84 |
+
return 108
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def requires_unconditional_embeds(self):
|
| 88 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 89 |
+
return self.guider._enabled and self.guider.num_conditions > 1
|
| 90 |
+
return False
|
diffusers/modular_pipelines/ideogram4/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_ideogram4"] = ["Ideogram4AutoBlocks"]
|
| 25 |
+
_import_structure["modular_pipeline"] = ["Ideogram4ModularPipeline"]
|
| 26 |
+
|
| 27 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 28 |
+
try:
|
| 29 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 30 |
+
raise OptionalDependencyNotAvailable()
|
| 31 |
+
except OptionalDependencyNotAvailable:
|
| 32 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 33 |
+
else:
|
| 34 |
+
from .modular_blocks_ideogram4 import Ideogram4AutoBlocks
|
| 35 |
+
from .modular_pipeline import Ideogram4ModularPipeline
|
| 36 |
+
else:
|
| 37 |
+
import sys
|
| 38 |
+
|
| 39 |
+
sys.modules[__name__] = _LazyModule(
|
| 40 |
+
__name__,
|
| 41 |
+
globals()["__file__"],
|
| 42 |
+
_import_structure,
|
| 43 |
+
module_spec=__spec__,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
for name, value in _dummy_objects.items():
|
| 47 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/ideogram4/before_denoise.py
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ...models.transformers.transformer_ideogram4 import (
|
| 21 |
+
IMAGE_POSITION_OFFSET,
|
| 22 |
+
LLM_TOKEN_INDICATOR,
|
| 23 |
+
OUTPUT_IMAGE_INDICATOR,
|
| 24 |
+
SEQUENCE_PADDING_INDICATOR,
|
| 25 |
+
Ideogram4Transformer2DModel,
|
| 26 |
+
)
|
| 27 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 28 |
+
from ...utils import logging
|
| 29 |
+
from ...utils.torch_utils import randn_tensor
|
| 30 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 31 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 32 |
+
from .modular_pipeline import Ideogram4ModularPipeline
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 36 |
+
|
| 37 |
+
# Default per-step guidance schedule (length must equal `num_inference_steps`): 7.0 for the main steps,
|
| 38 |
+
# dropping to 3.0 for the final 3 "polish" steps.
|
| 39 |
+
DEFAULT_GUIDANCE_SCHEDULE = (7.0,) * 45 + (3.0,) * 3
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._logit_normal_sigmas
|
| 43 |
+
def _logit_normal_sigmas(
|
| 44 |
+
num_inference_steps: int,
|
| 45 |
+
mu: float,
|
| 46 |
+
std: float = 1.0,
|
| 47 |
+
logsnr_min: float = -15.0,
|
| 48 |
+
logsnr_max: float = 18.0,
|
| 49 |
+
device: torch.device | None = None,
|
| 50 |
+
) -> torch.Tensor:
|
| 51 |
+
r"""
|
| 52 |
+
Build a length-`num_inference_steps` sigma schedule using the Ideogram4 logit-normal flow-matching schedule.
|
| 53 |
+
|
| 54 |
+
Sigmas are returned in `[0, 1]` in decreasing order (sigma close to 1 corresponds to pure noise, sigma close to 0
|
| 55 |
+
to clean data), matching diffusers conventions.
|
| 56 |
+
|
| 57 |
+
The Ideogram4 schedule applies `sigma(s) = 1 - logit_normal_cdf_inverse(1 - s)` to `s = linspace(0, 1, N + 1)` and
|
| 58 |
+
keeps the first `N` entries; a terminal zero is appended downstream by the scheduler.
|
| 59 |
+
"""
|
| 60 |
+
intervals = torch.linspace(0.0, 1.0, num_inference_steps + 1, dtype=torch.float64)
|
| 61 |
+
# Apply the inverse CDF of a normal then push through the logistic to obtain a logit-normal CDF inverse.
|
| 62 |
+
z = torch.special.ndtri(intervals)
|
| 63 |
+
y = mu + std * z
|
| 64 |
+
t = 1.0 - torch.special.expit(y)
|
| 65 |
+
t_min = 1.0 / (1.0 + math.exp(0.5 * logsnr_max))
|
| 66 |
+
t_max = 1.0 / (1.0 + math.exp(0.5 * logsnr_min))
|
| 67 |
+
t = t.clamp(t_min, t_max)
|
| 68 |
+
# Convert from model time (0 = noise, 1 = data) to diffusers sigma (1 = noise, 0 = data) and reverse.
|
| 69 |
+
sigmas = (1.0 - t).flip(0)
|
| 70 |
+
# Drop the trailing 0; FlowMatchEulerDiscreteScheduler.set_timesteps appends one back internally.
|
| 71 |
+
sigmas = sigmas[:-1].to(dtype=torch.float32, device=device)
|
| 72 |
+
return sigmas
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._resolution_aware_mu
|
| 76 |
+
def _resolution_aware_mu(
|
| 77 |
+
height: int,
|
| 78 |
+
width: int,
|
| 79 |
+
base_mu: float,
|
| 80 |
+
base_resolution: tuple[int, int] = (512, 512),
|
| 81 |
+
) -> float:
|
| 82 |
+
"""Shift the schedule mean as a function of image resolution."""
|
| 83 |
+
num_pixels = height * width
|
| 84 |
+
base_pixels = base_resolution[0] * base_resolution[1]
|
| 85 |
+
return base_mu + 0.5 * math.log(num_pixels / base_pixels)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4._expand_tensor_to_effective_batch
|
| 89 |
+
def _expand_tensor_to_effective_batch(
|
| 90 |
+
tensor: torch.Tensor,
|
| 91 |
+
batch_size: int,
|
| 92 |
+
num_per_prompt: int,
|
| 93 |
+
tensor_name: str | None = None,
|
| 94 |
+
) -> torch.Tensor:
|
| 95 |
+
"""Replicate `tensor` along dim 0 from `batch_size` (or 1) to `batch_size * num_per_prompt`."""
|
| 96 |
+
target_batch_size = batch_size * num_per_prompt
|
| 97 |
+
|
| 98 |
+
if tensor.shape[0] == target_batch_size:
|
| 99 |
+
return tensor
|
| 100 |
+
|
| 101 |
+
if tensor.shape[0] == 1:
|
| 102 |
+
repeat_by = target_batch_size
|
| 103 |
+
elif tensor.shape[0] == batch_size:
|
| 104 |
+
repeat_by = num_per_prompt
|
| 105 |
+
else:
|
| 106 |
+
tensor_name = f"`{tensor_name}`" if tensor_name is not None else "Tensor"
|
| 107 |
+
raise ValueError(
|
| 108 |
+
f"{tensor_name} batch size must be 1, `batch_size` ({batch_size}), or "
|
| 109 |
+
f"`batch_size * num_*_per_prompt` ({target_batch_size}), but got {tensor.shape[0]}."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
return torch.repeat_interleave(tensor, repeats=repeat_by, dim=0, output_size=tensor.shape[0] * repeat_by)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# auto_docstring
|
| 116 |
+
class Ideogram4TextInputsStep(ModularPipelineBlocks):
|
| 117 |
+
"""
|
| 118 |
+
Input step that determines `batch_size`/`dtype` from the per-prompt `text_features` and replicates the text outputs
|
| 119 |
+
to `batch_size * num_images_per_prompt`. Place after the text encoder.
|
| 120 |
+
|
| 121 |
+
Inputs:
|
| 122 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 123 |
+
The number of images to generate per prompt.
|
| 124 |
+
text_features (`Tensor`):
|
| 125 |
+
Per-prompt text features from the encoder.
|
| 126 |
+
text_lengths (`list`):
|
| 127 |
+
Per-prompt text-token counts from the encoder.
|
| 128 |
+
|
| 129 |
+
Outputs:
|
| 130 |
+
batch_size (`int`):
|
| 131 |
+
Effective batch size (num prompts * num_images_per_prompt).
|
| 132 |
+
dtype (`dtype`):
|
| 133 |
+
The dtype of the text features.
|
| 134 |
+
text_features (`Tensor`):
|
| 135 |
+
Text features, batch-expanded.
|
| 136 |
+
text_lengths (`list`):
|
| 137 |
+
Text-token counts, batch-expanded.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
model_name = "ideogram4"
|
| 141 |
+
|
| 142 |
+
@property
|
| 143 |
+
def description(self) -> str:
|
| 144 |
+
return (
|
| 145 |
+
"Input step that determines `batch_size`/`dtype` from the per-prompt `text_features` and replicates the "
|
| 146 |
+
"text outputs to `batch_size * num_images_per_prompt`. Place after the text encoder."
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
@property
|
| 150 |
+
def inputs(self) -> list[InputParam]:
|
| 151 |
+
return [
|
| 152 |
+
InputParam.template("num_images_per_prompt", default=1),
|
| 153 |
+
InputParam(
|
| 154 |
+
name="text_features",
|
| 155 |
+
required=True,
|
| 156 |
+
type_hint=torch.Tensor,
|
| 157 |
+
description="Per-prompt text features from the encoder.",
|
| 158 |
+
),
|
| 159 |
+
InputParam(
|
| 160 |
+
name="text_lengths",
|
| 161 |
+
required=True,
|
| 162 |
+
type_hint=list,
|
| 163 |
+
description="Per-prompt text-token counts from the encoder.",
|
| 164 |
+
),
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 169 |
+
return [
|
| 170 |
+
OutputParam(
|
| 171 |
+
name="batch_size",
|
| 172 |
+
type_hint=int,
|
| 173 |
+
description="Effective batch size (num prompts * num_images_per_prompt).",
|
| 174 |
+
),
|
| 175 |
+
OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
|
| 176 |
+
OutputParam(name="text_features", type_hint=torch.Tensor, description="Text features, batch-expanded."),
|
| 177 |
+
OutputParam(name="text_lengths", type_hint=list, description="Text-token counts, batch-expanded."),
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
@torch.no_grad()
|
| 181 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 182 |
+
block_state = self.get_block_state(state)
|
| 183 |
+
|
| 184 |
+
prompt_batch = block_state.text_features.shape[0]
|
| 185 |
+
num_per_prompt = block_state.num_images_per_prompt
|
| 186 |
+
|
| 187 |
+
block_state.dtype = block_state.text_features.dtype
|
| 188 |
+
block_state.text_features = _expand_tensor_to_effective_batch(
|
| 189 |
+
block_state.text_features, prompt_batch, num_per_prompt, "text_features"
|
| 190 |
+
)
|
| 191 |
+
block_state.text_lengths = [n for n in block_state.text_lengths for _ in range(num_per_prompt)]
|
| 192 |
+
block_state.batch_size = prompt_batch * num_per_prompt
|
| 193 |
+
|
| 194 |
+
self.set_block_state(state, block_state)
|
| 195 |
+
return components, state
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# auto_docstring
|
| 199 |
+
class Ideogram4PrepareLatentsStep(ModularPipelineBlocks):
|
| 200 |
+
"""
|
| 201 |
+
Step that prepares the packed image latents (B, num_image_tokens, latent_dim) for the denoising loop.
|
| 202 |
+
|
| 203 |
+
Components:
|
| 204 |
+
transformer (`Ideogram4Transformer2DModel`)
|
| 205 |
+
|
| 206 |
+
Inputs:
|
| 207 |
+
latents (`Tensor`, *optional*):
|
| 208 |
+
Pre-generated noisy latents for image generation.
|
| 209 |
+
height (`int`):
|
| 210 |
+
The height in pixels of the generated image.
|
| 211 |
+
width (`int`):
|
| 212 |
+
The width in pixels of the generated image.
|
| 213 |
+
generator (`Generator`, *optional*):
|
| 214 |
+
Torch generator for deterministic generation.
|
| 215 |
+
batch_size (`int`):
|
| 216 |
+
Effective batch size.
|
| 217 |
+
|
| 218 |
+
Outputs:
|
| 219 |
+
latents (`Tensor`):
|
| 220 |
+
The initial packed image latents (B, num_image_tokens, latent_dim).
|
| 221 |
+
num_image_tokens (`int`):
|
| 222 |
+
Number of image tokens (grid_h * grid_w).
|
| 223 |
+
"""
|
| 224 |
+
|
| 225 |
+
model_name = "ideogram4"
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def description(self) -> str:
|
| 229 |
+
return "Step that prepares the packed image latents (B, num_image_tokens, latent_dim) for the denoising loop."
|
| 230 |
+
|
| 231 |
+
@property
|
| 232 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 233 |
+
return [ComponentSpec("transformer", Ideogram4Transformer2DModel)]
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def inputs(self) -> list[InputParam]:
|
| 237 |
+
return [
|
| 238 |
+
InputParam.template("latents"),
|
| 239 |
+
InputParam.template("height", required=True),
|
| 240 |
+
InputParam.template("width", required=True),
|
| 241 |
+
InputParam.template("generator"),
|
| 242 |
+
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
|
| 243 |
+
]
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 247 |
+
return [
|
| 248 |
+
OutputParam(
|
| 249 |
+
name="latents",
|
| 250 |
+
type_hint=torch.Tensor,
|
| 251 |
+
description="The initial packed image latents (B, num_image_tokens, latent_dim).",
|
| 252 |
+
),
|
| 253 |
+
OutputParam(
|
| 254 |
+
name="num_image_tokens", type_hint=int, description="Number of image tokens (grid_h * grid_w)."
|
| 255 |
+
),
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
@torch.no_grad()
|
| 259 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 260 |
+
block_state = self.get_block_state(state)
|
| 261 |
+
|
| 262 |
+
device = components._execution_device
|
| 263 |
+
patch = components.patch_size
|
| 264 |
+
grid_h = block_state.height // (components.vae_scale_factor * patch)
|
| 265 |
+
grid_w = block_state.width // (components.vae_scale_factor * patch)
|
| 266 |
+
num_image_tokens = grid_h * grid_w
|
| 267 |
+
latent_dim = components.transformer.config.in_channels
|
| 268 |
+
|
| 269 |
+
shape = (block_state.batch_size, num_image_tokens, latent_dim)
|
| 270 |
+
if block_state.latents is None:
|
| 271 |
+
block_state.latents = randn_tensor(
|
| 272 |
+
shape, generator=block_state.generator, device=device, dtype=torch.float32
|
| 273 |
+
)
|
| 274 |
+
else:
|
| 275 |
+
block_state.latents = block_state.latents.to(device=device, dtype=torch.float32)
|
| 276 |
+
|
| 277 |
+
block_state.num_image_tokens = num_image_tokens
|
| 278 |
+
|
| 279 |
+
self.set_block_state(state, block_state)
|
| 280 |
+
return components, state
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# auto_docstring
|
| 284 |
+
class Ideogram4SetTimestepsStep(ModularPipelineBlocks):
|
| 285 |
+
"""
|
| 286 |
+
Step that sets the resolution-aware logit-normal sigma schedule on the scheduler and resolves the per-step guidance
|
| 287 |
+
weights.
|
| 288 |
+
|
| 289 |
+
Components:
|
| 290 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 291 |
+
|
| 292 |
+
Inputs:
|
| 293 |
+
num_inference_steps (`int`, *optional*, defaults to 48):
|
| 294 |
+
The number of denoising steps.
|
| 295 |
+
height (`int`):
|
| 296 |
+
The height in pixels of the generated image.
|
| 297 |
+
width (`int`):
|
| 298 |
+
The width in pixels of the generated image.
|
| 299 |
+
mu (`float`, *optional*, defaults to 0.0):
|
| 300 |
+
Base mean of the logit-normal schedule.
|
| 301 |
+
std (`float`, *optional*, defaults to 1.5):
|
| 302 |
+
Std of the logit-normal schedule.
|
| 303 |
+
guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 304 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 305 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
|
| 306 |
+
Per-step guidance scale schedule (length num_inference_steps).
|
| 307 |
+
|
| 308 |
+
Outputs:
|
| 309 |
+
timesteps (`Tensor`):
|
| 310 |
+
The denoising timesteps.
|
| 311 |
+
gw (`Tensor`):
|
| 312 |
+
Per-step guidance weights (num_inference_steps,).
|
| 313 |
+
"""
|
| 314 |
+
|
| 315 |
+
model_name = "ideogram4"
|
| 316 |
+
|
| 317 |
+
@property
|
| 318 |
+
def description(self) -> str:
|
| 319 |
+
return (
|
| 320 |
+
"Step that sets the resolution-aware logit-normal sigma schedule on the scheduler and resolves the "
|
| 321 |
+
"per-step guidance weights."
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
@property
|
| 325 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 326 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 327 |
+
|
| 328 |
+
@property
|
| 329 |
+
def inputs(self) -> list[InputParam]:
|
| 330 |
+
return [
|
| 331 |
+
InputParam.template("num_inference_steps", default=48),
|
| 332 |
+
InputParam.template("height", required=True),
|
| 333 |
+
InputParam.template("width", required=True),
|
| 334 |
+
InputParam(name="mu", default=0.0, type_hint=float, description="Base mean of the logit-normal schedule."),
|
| 335 |
+
InputParam(name="std", default=1.5, type_hint=float, description="Std of the logit-normal schedule."),
|
| 336 |
+
InputParam(
|
| 337 |
+
name="guidance_schedule",
|
| 338 |
+
default=DEFAULT_GUIDANCE_SCHEDULE,
|
| 339 |
+
type_hint=list,
|
| 340 |
+
description="Per-step guidance scale schedule (length num_inference_steps).",
|
| 341 |
+
),
|
| 342 |
+
]
|
| 343 |
+
|
| 344 |
+
@property
|
| 345 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 346 |
+
return [
|
| 347 |
+
OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps."),
|
| 348 |
+
OutputParam(
|
| 349 |
+
name="gw", type_hint=torch.Tensor, description="Per-step guidance weights (num_inference_steps,)."
|
| 350 |
+
),
|
| 351 |
+
]
|
| 352 |
+
|
| 353 |
+
@torch.no_grad()
|
| 354 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 355 |
+
block_state = self.get_block_state(state)
|
| 356 |
+
|
| 357 |
+
device = components._execution_device
|
| 358 |
+
if len(block_state.guidance_schedule) != block_state.num_inference_steps:
|
| 359 |
+
raise ValueError(
|
| 360 |
+
f"`guidance_schedule` must have length `num_inference_steps` ({block_state.num_inference_steps}), "
|
| 361 |
+
f"got {len(block_state.guidance_schedule)}."
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
schedule_mu = _resolution_aware_mu(height=block_state.height, width=block_state.width, base_mu=block_state.mu)
|
| 365 |
+
sigmas = _logit_normal_sigmas(block_state.num_inference_steps, schedule_mu, std=block_state.std, device=device)
|
| 366 |
+
components.scheduler.set_timesteps(sigmas=sigmas.tolist(), device=device)
|
| 367 |
+
|
| 368 |
+
block_state.timesteps = components.scheduler.timesteps
|
| 369 |
+
block_state.gw = torch.as_tensor(block_state.guidance_schedule, dtype=torch.float32, device=device)
|
| 370 |
+
|
| 371 |
+
self.set_block_state(state, block_state)
|
| 372 |
+
return components, state
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# auto_docstring
|
| 376 |
+
class Ideogram4PrepareAdditionalInputsStep(ModularPipelineBlocks):
|
| 377 |
+
"""
|
| 378 |
+
Step that prepares the additional denoiser inputs from the packed-sequence layout: the conditional
|
| 379 |
+
encoder_hidden_states (text features packed with image padding) and the position_ids/segment_ids/indicator, plus
|
| 380 |
+
the unconditional (image-only) counterparts. Place after prepare_latents.
|
| 381 |
+
|
| 382 |
+
Inputs:
|
| 383 |
+
height (`int`):
|
| 384 |
+
The height in pixels of the generated image.
|
| 385 |
+
width (`int`):
|
| 386 |
+
The width in pixels of the generated image.
|
| 387 |
+
text_features (`Tensor`):
|
| 388 |
+
Batch-expanded text features.
|
| 389 |
+
text_lengths (`list`):
|
| 390 |
+
Batch-expanded text-token counts.
|
| 391 |
+
batch_size (`int`):
|
| 392 |
+
Effective batch size.
|
| 393 |
+
|
| 394 |
+
Outputs:
|
| 395 |
+
prompt_embeds (`Tensor`):
|
| 396 |
+
Packed conditional encoder_hidden_states (B, total_seq, dim).
|
| 397 |
+
position_ids (`Tensor`):
|
| 398 |
+
Conditional 3-axis MRoPE position ids.
|
| 399 |
+
segment_ids (`Tensor`):
|
| 400 |
+
Conditional block-diagonal segment ids.
|
| 401 |
+
indicator (`Tensor`):
|
| 402 |
+
Conditional per-token text/image/pad role.
|
| 403 |
+
negative_prompt_embeds (`Tensor`):
|
| 404 |
+
Unconditional (zeroed) text features (B, num_image_tokens, dim).
|
| 405 |
+
negative_position_ids (`Tensor`):
|
| 406 |
+
Unconditional position ids (image region).
|
| 407 |
+
negative_segment_ids (`Tensor`):
|
| 408 |
+
Unconditional segment ids (image region).
|
| 409 |
+
negative_indicator (`Tensor`):
|
| 410 |
+
Unconditional indicator (image region).
|
| 411 |
+
"""
|
| 412 |
+
|
| 413 |
+
model_name = "ideogram4"
|
| 414 |
+
|
| 415 |
+
@property
|
| 416 |
+
def description(self) -> str:
|
| 417 |
+
return (
|
| 418 |
+
"Step that prepares the additional denoiser inputs from the packed-sequence layout: the conditional "
|
| 419 |
+
"encoder_hidden_states (text features packed with image padding) and the position_ids/segment_ids/"
|
| 420 |
+
"indicator, plus the unconditional (image-only) counterparts. Place after prepare_latents."
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
@property
|
| 424 |
+
def inputs(self) -> list[InputParam]:
|
| 425 |
+
return [
|
| 426 |
+
InputParam.template("height", required=True),
|
| 427 |
+
InputParam.template("width", required=True),
|
| 428 |
+
InputParam(
|
| 429 |
+
name="text_features",
|
| 430 |
+
required=True,
|
| 431 |
+
type_hint=torch.Tensor,
|
| 432 |
+
description="Batch-expanded text features.",
|
| 433 |
+
),
|
| 434 |
+
InputParam(
|
| 435 |
+
name="text_lengths", required=True, type_hint=list, description="Batch-expanded text-token counts."
|
| 436 |
+
),
|
| 437 |
+
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
|
| 438 |
+
]
|
| 439 |
+
|
| 440 |
+
@property
|
| 441 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 442 |
+
return [
|
| 443 |
+
OutputParam(
|
| 444 |
+
name="prompt_embeds",
|
| 445 |
+
type_hint=torch.Tensor,
|
| 446 |
+
description="Packed conditional encoder_hidden_states (B, total_seq, dim).",
|
| 447 |
+
),
|
| 448 |
+
OutputParam(
|
| 449 |
+
name="position_ids", type_hint=torch.Tensor, description="Conditional 3-axis MRoPE position ids."
|
| 450 |
+
),
|
| 451 |
+
OutputParam(
|
| 452 |
+
name="segment_ids", type_hint=torch.Tensor, description="Conditional block-diagonal segment ids."
|
| 453 |
+
),
|
| 454 |
+
OutputParam(
|
| 455 |
+
name="indicator", type_hint=torch.Tensor, description="Conditional per-token text/image/pad role."
|
| 456 |
+
),
|
| 457 |
+
OutputParam(
|
| 458 |
+
name="negative_prompt_embeds",
|
| 459 |
+
type_hint=torch.Tensor,
|
| 460 |
+
description="Unconditional (zeroed) text features (B, num_image_tokens, dim).",
|
| 461 |
+
),
|
| 462 |
+
OutputParam(
|
| 463 |
+
name="negative_position_ids",
|
| 464 |
+
type_hint=torch.Tensor,
|
| 465 |
+
description="Unconditional position ids (image region).",
|
| 466 |
+
),
|
| 467 |
+
OutputParam(
|
| 468 |
+
name="negative_segment_ids",
|
| 469 |
+
type_hint=torch.Tensor,
|
| 470 |
+
description="Unconditional segment ids (image region).",
|
| 471 |
+
),
|
| 472 |
+
OutputParam(
|
| 473 |
+
name="negative_indicator",
|
| 474 |
+
type_hint=torch.Tensor,
|
| 475 |
+
description="Unconditional indicator (image region).",
|
| 476 |
+
),
|
| 477 |
+
]
|
| 478 |
+
|
| 479 |
+
@staticmethod
|
| 480 |
+
# Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4.Ideogram4Pipeline._prepare_ids
|
| 481 |
+
def _prepare_ids(
|
| 482 |
+
text_lengths: list[int],
|
| 483 |
+
grid_h: int,
|
| 484 |
+
grid_w: int,
|
| 485 |
+
max_text_tokens: int,
|
| 486 |
+
device: torch.device,
|
| 487 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 488 |
+
"""Build the packed `[left-pad][text][image]` layout from the per-prompt text lengths and the image grid.
|
| 489 |
+
|
| 490 |
+
Returns `position_ids` (3-axis MRoPE), `segment_ids` (block-diagonal attention) and `indicator` (per-token
|
| 491 |
+
text/image/pad role).
|
| 492 |
+
"""
|
| 493 |
+
batch_size = len(text_lengths)
|
| 494 |
+
num_image_tokens = grid_h * grid_w
|
| 495 |
+
total_seq_len = max_text_tokens + num_image_tokens
|
| 496 |
+
|
| 497 |
+
# Image position ids (t=0, h, w); offset keeps them disjoint from text positions.
|
| 498 |
+
h_idx = torch.arange(grid_h).view(-1, 1).expand(grid_h, grid_w).reshape(-1)
|
| 499 |
+
w_idx = torch.arange(grid_w).view(1, -1).expand(grid_h, grid_w).reshape(-1)
|
| 500 |
+
t_idx = torch.zeros_like(h_idx)
|
| 501 |
+
image_pos = torch.stack([t_idx, h_idx, w_idx], dim=1) + IMAGE_POSITION_OFFSET
|
| 502 |
+
|
| 503 |
+
position_ids = torch.zeros(batch_size, total_seq_len, 3, dtype=torch.long)
|
| 504 |
+
segment_ids = torch.full((batch_size, total_seq_len), SEQUENCE_PADDING_INDICATOR, dtype=torch.long)
|
| 505 |
+
indicator = torch.zeros(batch_size, total_seq_len, dtype=torch.long)
|
| 506 |
+
|
| 507 |
+
for b, num_text in enumerate(text_lengths):
|
| 508 |
+
offset = max_text_tokens - num_text
|
| 509 |
+
|
| 510 |
+
text_pos = torch.arange(num_text)
|
| 511 |
+
text_pos_3d = torch.stack([text_pos, text_pos, text_pos], dim=1)
|
| 512 |
+
position_ids[b, offset : offset + num_text] = text_pos_3d
|
| 513 |
+
position_ids[b, offset + num_text :] = image_pos
|
| 514 |
+
|
| 515 |
+
indicator[b, offset : offset + num_text] = LLM_TOKEN_INDICATOR
|
| 516 |
+
indicator[b, offset + num_text :] = OUTPUT_IMAGE_INDICATOR
|
| 517 |
+
|
| 518 |
+
segment_ids[b, offset : offset + num_text + num_image_tokens] = 1
|
| 519 |
+
|
| 520 |
+
return position_ids.to(device), segment_ids.to(device), indicator.to(device)
|
| 521 |
+
|
| 522 |
+
@torch.no_grad()
|
| 523 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 524 |
+
block_state = self.get_block_state(state)
|
| 525 |
+
|
| 526 |
+
device = components._execution_device
|
| 527 |
+
patch = components.patch_size
|
| 528 |
+
grid_h = block_state.height // (components.vae_scale_factor * patch)
|
| 529 |
+
grid_w = block_state.width // (components.vae_scale_factor * patch)
|
| 530 |
+
num_image_tokens = grid_h * grid_w
|
| 531 |
+
|
| 532 |
+
text_features = block_state.text_features
|
| 533 |
+
max_text_tokens = text_features.shape[1]
|
| 534 |
+
feature_dim = text_features.shape[-1]
|
| 535 |
+
|
| 536 |
+
position_ids, segment_ids, indicator = self._prepare_ids(
|
| 537 |
+
block_state.text_lengths, grid_h, grid_w, max_text_tokens, device
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
# Pack the text features into the full sequence; image positions carry no text features.
|
| 541 |
+
image_feature_padding = torch.zeros(
|
| 542 |
+
block_state.batch_size, num_image_tokens, feature_dim, dtype=text_features.dtype, device=device
|
| 543 |
+
)
|
| 544 |
+
block_state.prompt_embeds = torch.cat([text_features, image_feature_padding], dim=1)
|
| 545 |
+
|
| 546 |
+
# Unconditional (image-only) branch, derived from the conditioning.
|
| 547 |
+
block_state.negative_prompt_embeds = torch.zeros(
|
| 548 |
+
block_state.batch_size, num_image_tokens, feature_dim, dtype=text_features.dtype, device=device
|
| 549 |
+
)
|
| 550 |
+
block_state.position_ids = position_ids
|
| 551 |
+
block_state.segment_ids = segment_ids
|
| 552 |
+
block_state.indicator = indicator
|
| 553 |
+
block_state.negative_position_ids = position_ids[:, max_text_tokens:]
|
| 554 |
+
block_state.negative_segment_ids = segment_ids[:, max_text_tokens:]
|
| 555 |
+
block_state.negative_indicator = indicator[:, max_text_tokens:]
|
| 556 |
+
|
| 557 |
+
self.set_block_state(state, block_state)
|
| 558 |
+
return components, state
|
diffusers/modular_pipelines/ideogram4/decoders.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...image_processor import VaeImageProcessor
|
| 20 |
+
from ...models import AutoencoderKLFlux2
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 23 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 24 |
+
from .modular_pipeline import Ideogram4ModularPipeline
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# auto_docstring
|
| 31 |
+
class Ideogram4DecodeStep(ModularPipelineBlocks):
|
| 32 |
+
"""
|
| 33 |
+
Step that decodes the unpatchified (B, ae_channels, H, W) latents into images: de-normalizes with the VAE
|
| 34 |
+
batch-norm statistics and decodes through the VAE.
|
| 35 |
+
|
| 36 |
+
Components:
|
| 37 |
+
vae (`AutoencoderKLFlux2`) image_processor (`VaeImageProcessor`)
|
| 38 |
+
|
| 39 |
+
Inputs:
|
| 40 |
+
output_type (`str`, *optional*, defaults to pil):
|
| 41 |
+
Output format: 'pil', 'np', 'pt'.
|
| 42 |
+
latents (`Tensor`):
|
| 43 |
+
The unpatchified (B, ae_channels, H, W) latents to decode, from the after-denoise step.
|
| 44 |
+
|
| 45 |
+
Outputs:
|
| 46 |
+
images (`list`):
|
| 47 |
+
Generated images.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
model_name = "ideogram4"
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def description(self) -> str:
|
| 54 |
+
return (
|
| 55 |
+
"Step that decodes the unpatchified (B, ae_channels, H, W) latents into images: de-normalizes with the "
|
| 56 |
+
"VAE batch-norm statistics and decodes through the VAE."
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 61 |
+
return [
|
| 62 |
+
ComponentSpec("vae", AutoencoderKLFlux2),
|
| 63 |
+
ComponentSpec(
|
| 64 |
+
"image_processor",
|
| 65 |
+
VaeImageProcessor,
|
| 66 |
+
config=FrozenDict({"vae_scale_factor": 16}),
|
| 67 |
+
default_creation_method="from_config",
|
| 68 |
+
),
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def inputs(self) -> list[InputParam]:
|
| 73 |
+
return [
|
| 74 |
+
InputParam.template("output_type", default="pil"),
|
| 75 |
+
InputParam(
|
| 76 |
+
name="latents",
|
| 77 |
+
required=True,
|
| 78 |
+
type_hint=torch.Tensor,
|
| 79 |
+
description="The unpatchified (B, ae_channels, H, W) latents to decode, from the after-denoise step.",
|
| 80 |
+
),
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 85 |
+
return [OutputParam.template("images")]
|
| 86 |
+
|
| 87 |
+
@torch.no_grad()
|
| 88 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 89 |
+
block_state = self.get_block_state(state)
|
| 90 |
+
|
| 91 |
+
z = block_state.latents
|
| 92 |
+
patch = components.patch_size
|
| 93 |
+
ae_channels = z.shape[1]
|
| 94 |
+
grid_h, grid_w = z.shape[2] // patch, z.shape[3] // patch
|
| 95 |
+
|
| 96 |
+
# VAE bn stores per-channel statistics over the packed channels, laid out as (patch_row, patch_col,
|
| 97 |
+
# ae_channel). Reshape them into an (ae_channels, patch, patch) tile and repeat across the grid so the
|
| 98 |
+
# denormalization on the unpatchified latents matches the packed-space statistics.
|
| 99 |
+
bn_mean = components.vae.bn.running_mean.view(patch, patch, ae_channels).permute(2, 0, 1)
|
| 100 |
+
bn_std = torch.sqrt(components.vae.bn.running_var + components.vae.config.batch_norm_eps)
|
| 101 |
+
bn_std = bn_std.view(patch, patch, ae_channels).permute(2, 0, 1)
|
| 102 |
+
bn_mean = bn_mean.repeat(1, grid_h, grid_w).to(device=z.device, dtype=z.dtype)
|
| 103 |
+
bn_std = bn_std.repeat(1, grid_h, grid_w).to(device=z.device, dtype=z.dtype)
|
| 104 |
+
z = z * bn_std + bn_mean
|
| 105 |
+
|
| 106 |
+
decoded = components.vae.decode(z.to(components.vae.dtype), return_dict=False)[0]
|
| 107 |
+
block_state.images = components.image_processor.postprocess(
|
| 108 |
+
decoded.float(), output_type=block_state.output_type
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
self.set_block_state(state, block_state)
|
| 112 |
+
return components, state
|
diffusers/modular_pipelines/ideogram4/denoise.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...models.transformers.transformer_ideogram4 import Ideogram4Transformer2DModel
|
| 19 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 20 |
+
from ...utils import logging
|
| 21 |
+
from ..modular_pipeline import (
|
| 22 |
+
BlockState,
|
| 23 |
+
LoopSequentialPipelineBlocks,
|
| 24 |
+
ModularPipelineBlocks,
|
| 25 |
+
PipelineState,
|
| 26 |
+
)
|
| 27 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 28 |
+
from .modular_pipeline import Ideogram4ModularPipeline
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Ideogram4LoopBeforeDenoiser(ModularPipelineBlocks):
|
| 35 |
+
model_name = "ideogram4"
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def description(self) -> str:
|
| 39 |
+
return (
|
| 40 |
+
"Within the denoising loop: build the conditional packed input `[text-padding][image latents]` and the "
|
| 41 |
+
"model timestep. Compose into the `sub_blocks` of `Ideogram4DenoiseLoopWrapper`."
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 46 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def inputs(self) -> list[InputParam]:
|
| 50 |
+
return [
|
| 51 |
+
InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
|
| 52 |
+
InputParam(
|
| 53 |
+
name="position_ids", required=True, type_hint=torch.Tensor, description="Conditional position ids."
|
| 54 |
+
),
|
| 55 |
+
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
@torch.no_grad()
|
| 59 |
+
def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 60 |
+
# Conditional packed sequence is [text-padding][image latents]; text region length = total - image tokens.
|
| 61 |
+
max_text_tokens = block_state.position_ids.shape[1] - block_state.latents.shape[1]
|
| 62 |
+
text_z_padding = torch.zeros(
|
| 63 |
+
block_state.latents.shape[0],
|
| 64 |
+
max_text_tokens,
|
| 65 |
+
block_state.latents.shape[-1],
|
| 66 |
+
dtype=block_state.latents.dtype,
|
| 67 |
+
device=block_state.latents.device,
|
| 68 |
+
)
|
| 69 |
+
block_state.pos_z = torch.cat([text_z_padding, block_state.latents], dim=1)
|
| 70 |
+
block_state.max_text_tokens = max_text_tokens
|
| 71 |
+
|
| 72 |
+
# Map sigma-domain timestep to model time t in [0, 1] (0 = noise, 1 = clean data).
|
| 73 |
+
num_train_timesteps = components.scheduler.config.num_train_timesteps
|
| 74 |
+
t_model = 1.0 - (t.float() / num_train_timesteps)
|
| 75 |
+
block_state.t_model = t_model.expand(block_state.batch_size)
|
| 76 |
+
return components, block_state
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Ideogram4LoopDenoiser(ModularPipelineBlocks):
|
| 80 |
+
model_name = "ideogram4"
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def description(self) -> str:
|
| 84 |
+
return (
|
| 85 |
+
"Within the denoising loop: run the conditional `transformer` on the full packed sequence and the "
|
| 86 |
+
"`unconditional_transformer` on the image-only sequence, then blend with the per-step guidance weight "
|
| 87 |
+
"(asymmetric CFG, no guider). Compose into `Ideogram4DenoiseLoopWrapper`."
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 92 |
+
return [
|
| 93 |
+
ComponentSpec("transformer", Ideogram4Transformer2DModel),
|
| 94 |
+
ComponentSpec("unconditional_transformer", Ideogram4Transformer2DModel),
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def inputs(self) -> list[InputParam]:
|
| 99 |
+
return [
|
| 100 |
+
InputParam(
|
| 101 |
+
name="prompt_embeds",
|
| 102 |
+
required=True,
|
| 103 |
+
type_hint=torch.Tensor,
|
| 104 |
+
description="Packed conditional encoder_hidden_states.",
|
| 105 |
+
),
|
| 106 |
+
InputParam(
|
| 107 |
+
name="position_ids",
|
| 108 |
+
required=True,
|
| 109 |
+
type_hint=torch.Tensor,
|
| 110 |
+
description="Conditional 3-axis MRoPE position ids.",
|
| 111 |
+
),
|
| 112 |
+
InputParam(
|
| 113 |
+
name="segment_ids",
|
| 114 |
+
required=True,
|
| 115 |
+
type_hint=torch.Tensor,
|
| 116 |
+
description="Conditional block-diagonal segment ids.",
|
| 117 |
+
),
|
| 118 |
+
InputParam(
|
| 119 |
+
name="indicator",
|
| 120 |
+
required=True,
|
| 121 |
+
type_hint=torch.Tensor,
|
| 122 |
+
description="Conditional per-token text/image/pad role.",
|
| 123 |
+
),
|
| 124 |
+
InputParam(
|
| 125 |
+
name="negative_prompt_embeds",
|
| 126 |
+
required=True,
|
| 127 |
+
type_hint=torch.Tensor,
|
| 128 |
+
description="Unconditional (zeroed) text features.",
|
| 129 |
+
),
|
| 130 |
+
InputParam(
|
| 131 |
+
name="negative_position_ids",
|
| 132 |
+
required=True,
|
| 133 |
+
type_hint=torch.Tensor,
|
| 134 |
+
description="Unconditional position ids (image region).",
|
| 135 |
+
),
|
| 136 |
+
InputParam(
|
| 137 |
+
name="negative_segment_ids",
|
| 138 |
+
required=True,
|
| 139 |
+
type_hint=torch.Tensor,
|
| 140 |
+
description="Unconditional segment ids (image region).",
|
| 141 |
+
),
|
| 142 |
+
InputParam(
|
| 143 |
+
name="negative_indicator",
|
| 144 |
+
required=True,
|
| 145 |
+
type_hint=torch.Tensor,
|
| 146 |
+
description="Unconditional indicator (image region).",
|
| 147 |
+
),
|
| 148 |
+
InputParam(name="gw", required=True, type_hint=torch.Tensor, description="Per-step guidance weights."),
|
| 149 |
+
InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
|
| 150 |
+
]
|
| 151 |
+
|
| 152 |
+
@torch.no_grad()
|
| 153 |
+
def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 154 |
+
transformer = components.transformer
|
| 155 |
+
unconditional_transformer = components.unconditional_transformer
|
| 156 |
+
|
| 157 |
+
# Conditional pass operates on the full packed sequence; the velocity is the image-token region.
|
| 158 |
+
pos_out = transformer(
|
| 159 |
+
hidden_states=block_state.pos_z.to(transformer.dtype),
|
| 160 |
+
timestep=block_state.t_model.to(transformer.dtype),
|
| 161 |
+
encoder_hidden_states=block_state.prompt_embeds.to(transformer.dtype),
|
| 162 |
+
position_ids=block_state.position_ids,
|
| 163 |
+
segment_ids=block_state.segment_ids,
|
| 164 |
+
indicator=block_state.indicator,
|
| 165 |
+
return_dict=False,
|
| 166 |
+
)[0]
|
| 167 |
+
pos_v = pos_out[:, block_state.max_text_tokens :].to(torch.float32)
|
| 168 |
+
|
| 169 |
+
# Unconditional pass uses the image-only positions with zeroed text features.
|
| 170 |
+
neg_v = unconditional_transformer(
|
| 171 |
+
hidden_states=block_state.latents.to(unconditional_transformer.dtype),
|
| 172 |
+
timestep=block_state.t_model.to(unconditional_transformer.dtype),
|
| 173 |
+
encoder_hidden_states=block_state.negative_prompt_embeds.to(unconditional_transformer.dtype),
|
| 174 |
+
position_ids=block_state.negative_position_ids,
|
| 175 |
+
segment_ids=block_state.negative_segment_ids,
|
| 176 |
+
indicator=block_state.negative_indicator,
|
| 177 |
+
return_dict=False,
|
| 178 |
+
)[0].to(torch.float32)
|
| 179 |
+
|
| 180 |
+
gw_i = block_state.gw[i]
|
| 181 |
+
v = gw_i * pos_v + (1.0 - gw_i) * neg_v
|
| 182 |
+
# The scheduler integrates `-v` (Ideogram predicts velocity v = x0 - noise).
|
| 183 |
+
block_state.noise_pred = -v
|
| 184 |
+
return components, block_state
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class Ideogram4LoopAfterDenoiser(ModularPipelineBlocks):
|
| 188 |
+
model_name = "ideogram4"
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def description(self) -> str:
|
| 192 |
+
return "Within the denoising loop: scheduler step. Compose into `Ideogram4DenoiseLoopWrapper`."
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 196 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 200 |
+
return [OutputParam(name="latents", type_hint=torch.Tensor, description="The denoised latents.")]
|
| 201 |
+
|
| 202 |
+
@torch.no_grad()
|
| 203 |
+
def __call__(self, components: Ideogram4ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 204 |
+
block_state.latents = components.scheduler.step(
|
| 205 |
+
block_state.noise_pred, t, block_state.latents, return_dict=False
|
| 206 |
+
)[0]
|
| 207 |
+
return components, block_state
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# auto_docstring
|
| 211 |
+
class Ideogram4DenoiseStep(LoopSequentialPipelineBlocks):
|
| 212 |
+
"""
|
| 213 |
+
Denoising loop that iteratively denoises the packed image latents over `timesteps`, running both the conditional
|
| 214 |
+
and unconditional transformers and blending with the per-step guidance schedule.
|
| 215 |
+
|
| 216 |
+
Components:
|
| 217 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Ideogram4Transformer2DModel`)
|
| 218 |
+
unconditional_transformer (`Ideogram4Transformer2DModel`)
|
| 219 |
+
|
| 220 |
+
Inputs:
|
| 221 |
+
timesteps (`Tensor`):
|
| 222 |
+
Denoising timesteps from set_timesteps.
|
| 223 |
+
num_inference_steps (`int`, *optional*, defaults to 48):
|
| 224 |
+
The number of denoising steps.
|
| 225 |
+
latents (`Tensor`):
|
| 226 |
+
Packed image latents.
|
| 227 |
+
position_ids (`Tensor`):
|
| 228 |
+
Conditional position ids.
|
| 229 |
+
batch_size (`int`):
|
| 230 |
+
Effective batch size.
|
| 231 |
+
prompt_embeds (`Tensor`):
|
| 232 |
+
Packed conditional encoder_hidden_states.
|
| 233 |
+
position_ids (`Tensor`):
|
| 234 |
+
Conditional 3-axis MRoPE position ids.
|
| 235 |
+
segment_ids (`Tensor`):
|
| 236 |
+
Conditional block-diagonal segment ids.
|
| 237 |
+
indicator (`Tensor`):
|
| 238 |
+
Conditional per-token text/image/pad role.
|
| 239 |
+
negative_prompt_embeds (`Tensor`):
|
| 240 |
+
Unconditional (zeroed) text features.
|
| 241 |
+
negative_position_ids (`Tensor`):
|
| 242 |
+
Unconditional position ids (image region).
|
| 243 |
+
negative_segment_ids (`Tensor`):
|
| 244 |
+
Unconditional segment ids (image region).
|
| 245 |
+
negative_indicator (`Tensor`):
|
| 246 |
+
Unconditional indicator (image region).
|
| 247 |
+
gw (`Tensor`):
|
| 248 |
+
Per-step guidance weights.
|
| 249 |
+
|
| 250 |
+
Outputs:
|
| 251 |
+
latents (`Tensor`):
|
| 252 |
+
The denoised latents.
|
| 253 |
+
"""
|
| 254 |
+
|
| 255 |
+
model_name = "ideogram4"
|
| 256 |
+
block_classes = [Ideogram4LoopBeforeDenoiser, Ideogram4LoopDenoiser, Ideogram4LoopAfterDenoiser]
|
| 257 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 258 |
+
|
| 259 |
+
@property
|
| 260 |
+
def description(self) -> str:
|
| 261 |
+
return (
|
| 262 |
+
"Denoising loop that iteratively denoises the packed image latents over `timesteps`, running both the "
|
| 263 |
+
"conditional and unconditional transformers and blending with the per-step guidance schedule."
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
@property
|
| 267 |
+
def loop_expected_components(self) -> list[ComponentSpec]:
|
| 268 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 272 |
+
return [
|
| 273 |
+
InputParam(
|
| 274 |
+
name="timesteps",
|
| 275 |
+
required=True,
|
| 276 |
+
type_hint=torch.Tensor,
|
| 277 |
+
description="Denoising timesteps from set_timesteps.",
|
| 278 |
+
),
|
| 279 |
+
InputParam.template("num_inference_steps", default=48),
|
| 280 |
+
]
|
| 281 |
+
|
| 282 |
+
@torch.no_grad()
|
| 283 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 284 |
+
block_state = self.get_block_state(state)
|
| 285 |
+
|
| 286 |
+
with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
|
| 287 |
+
for i, t in enumerate(block_state.timesteps):
|
| 288 |
+
components, block_state = self.loop_step(components, block_state, i=i, t=t)
|
| 289 |
+
progress_bar.update()
|
| 290 |
+
|
| 291 |
+
self.set_block_state(state, block_state)
|
| 292 |
+
return components, state
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
# auto_docstring
|
| 296 |
+
class Ideogram4AfterDenoiseStep(ModularPipelineBlocks):
|
| 297 |
+
"""
|
| 298 |
+
Step that runs after the denoising loop: unpatchifies the packed image latents (B, num_image_tokens, ae_channels *
|
| 299 |
+
patch ** 2) into a (B, ae_channels, H, W) latent for the decoder.
|
| 300 |
+
|
| 301 |
+
Inputs:
|
| 302 |
+
height (`int`):
|
| 303 |
+
The height in pixels of the generated image.
|
| 304 |
+
width (`int`):
|
| 305 |
+
The width in pixels of the generated image.
|
| 306 |
+
latents (`Tensor`):
|
| 307 |
+
The denoised packed image latents (B, num_image_tokens, latent_dim).
|
| 308 |
+
|
| 309 |
+
Outputs:
|
| 310 |
+
latents (`Tensor`):
|
| 311 |
+
Unpatchified latents (B, ae_channels, H, W) ready for the VAE decoder.
|
| 312 |
+
"""
|
| 313 |
+
|
| 314 |
+
model_name = "ideogram4"
|
| 315 |
+
|
| 316 |
+
@property
|
| 317 |
+
def description(self) -> str:
|
| 318 |
+
return (
|
| 319 |
+
"Step that runs after the denoising loop: unpatchifies the packed image latents "
|
| 320 |
+
"(B, num_image_tokens, ae_channels * patch ** 2) into a (B, ae_channels, H, W) latent for the decoder."
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
@property
|
| 324 |
+
def inputs(self) -> list[InputParam]:
|
| 325 |
+
return [
|
| 326 |
+
InputParam.template("height", required=True),
|
| 327 |
+
InputParam.template("width", required=True),
|
| 328 |
+
InputParam(
|
| 329 |
+
name="latents",
|
| 330 |
+
required=True,
|
| 331 |
+
type_hint=torch.Tensor,
|
| 332 |
+
description="The denoised packed image latents (B, num_image_tokens, latent_dim).",
|
| 333 |
+
),
|
| 334 |
+
]
|
| 335 |
+
|
| 336 |
+
@property
|
| 337 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 338 |
+
return [
|
| 339 |
+
OutputParam(
|
| 340 |
+
name="latents",
|
| 341 |
+
type_hint=torch.Tensor,
|
| 342 |
+
description="Unpatchified latents (B, ae_channels, H, W) ready for the VAE decoder.",
|
| 343 |
+
)
|
| 344 |
+
]
|
| 345 |
+
|
| 346 |
+
@torch.no_grad()
|
| 347 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 348 |
+
block_state = self.get_block_state(state)
|
| 349 |
+
|
| 350 |
+
z = block_state.latents
|
| 351 |
+
patch = components.patch_size
|
| 352 |
+
grid_h = block_state.height // (components.vae_scale_factor * patch)
|
| 353 |
+
grid_w = block_state.width // (components.vae_scale_factor * patch)
|
| 354 |
+
|
| 355 |
+
ae_channels = z.shape[-1] // (patch * patch)
|
| 356 |
+
z = z.view(z.shape[0], grid_h, grid_w, patch, patch, ae_channels)
|
| 357 |
+
z = z.permute(0, 5, 1, 3, 2, 4).contiguous()
|
| 358 |
+
z = z.view(z.shape[0], ae_channels, grid_h * patch, grid_w * patch)
|
| 359 |
+
|
| 360 |
+
block_state.latents = z
|
| 361 |
+
|
| 362 |
+
self.set_block_state(state, block_state)
|
| 363 |
+
return components, state
|
diffusers/modular_pipelines/ideogram4/encoders.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from transformers import Qwen2Tokenizer, Qwen3VLModel
|
| 18 |
+
from transformers.masking_utils import create_causal_mask
|
| 19 |
+
|
| 20 |
+
from ...pipelines.ideogram4.prompt_enhancer import (
|
| 21 |
+
PROMPT_UPSAMPLE_TEMPERATURE,
|
| 22 |
+
Ideogram4PromptEnhancerHead,
|
| 23 |
+
build_caption_logits_processor,
|
| 24 |
+
build_prompt_enhancer,
|
| 25 |
+
generate_captions,
|
| 26 |
+
)
|
| 27 |
+
from ...utils import is_outlines_available, logging
|
| 28 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 29 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 30 |
+
from .modular_pipeline import Ideogram4ModularPipeline
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# Hidden states of these Qwen3-VL decoder layers are concatenated to form the per-token
|
| 37 |
+
# text conditioning consumed by the Ideogram4 transformer.
|
| 38 |
+
QWEN3_VL_ACTIVATION_LAYERS = (0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 35)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# auto_docstring
|
| 42 |
+
class Ideogram4PromptUpsampleStep(ModularPipelineBlocks):
|
| 43 |
+
"""
|
| 44 |
+
Optional step that rewrites the prompt(s) into Ideogram4's native structured JSON caption when
|
| 45 |
+
`prompt_upsampling=True` (the format the model is trained on). Requires a generative `text_encoder` (a
|
| 46 |
+
`Qwen3VLForConditionalGeneration`); install `outlines` for schema-constrained captions.
|
| 47 |
+
|
| 48 |
+
Components:
|
| 49 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
|
| 50 |
+
with the text encoder. prompt_enhancer_head (`Ideogram4PromptEnhancerHead`): LM head grafted onto the text
|
| 51 |
+
encoder for prompt upsampling.
|
| 52 |
+
|
| 53 |
+
Inputs:
|
| 54 |
+
prompt (`str`):
|
| 55 |
+
The prompt or prompts to guide image generation.
|
| 56 |
+
prompt_upsampling (`bool`, *optional*, defaults to False):
|
| 57 |
+
If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.
|
| 58 |
+
prompt_upsampling_temperature (`float`, *optional*, defaults to 1.0):
|
| 59 |
+
Sampling temperature for prompt upsampling.
|
| 60 |
+
height (`int`, *optional*):
|
| 61 |
+
The height in pixels of the generated image.
|
| 62 |
+
width (`int`, *optional*):
|
| 63 |
+
The width in pixels of the generated image.
|
| 64 |
+
max_sequence_length (`int`, *optional*, defaults to 2048):
|
| 65 |
+
Maximum sequence length for prompt encoding.
|
| 66 |
+
generator (`Generator`, *optional*):
|
| 67 |
+
Torch generator for deterministic generation.
|
| 68 |
+
|
| 69 |
+
Outputs:
|
| 70 |
+
prompt (`list`):
|
| 71 |
+
The (possibly upsampled) prompt forwarded to the text encoder.
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
model_name = "ideogram4"
|
| 75 |
+
|
| 76 |
+
def __init__(self):
|
| 77 |
+
# Built lazily on first upsample: the head-less encoder body + `prompt_enhancer_head`, combined.
|
| 78 |
+
self._prompt_enhancer = None
|
| 79 |
+
# Outlines logits processor for schema-constrained captions; built lazily on first upsample.
|
| 80 |
+
self._caption_logits_processor = None
|
| 81 |
+
super().__init__()
|
| 82 |
+
|
| 83 |
+
@property
|
| 84 |
+
def description(self) -> str:
|
| 85 |
+
return (
|
| 86 |
+
"Optional step that rewrites the prompt(s) into Ideogram4's native structured JSON caption when "
|
| 87 |
+
"`prompt_upsampling=True` (the format the model is trained on). Requires a generative `text_encoder` "
|
| 88 |
+
"(a `Qwen3VLForConditionalGeneration`); install `outlines` for schema-constrained captions."
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 93 |
+
return [
|
| 94 |
+
ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
|
| 95 |
+
ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer paired with the text encoder."),
|
| 96 |
+
ComponentSpec(
|
| 97 |
+
"prompt_enhancer_head",
|
| 98 |
+
Ideogram4PromptEnhancerHead,
|
| 99 |
+
description="LM head grafted onto the text encoder for prompt upsampling.",
|
| 100 |
+
),
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
@property
|
| 104 |
+
def inputs(self) -> list[InputParam]:
|
| 105 |
+
return [
|
| 106 |
+
InputParam.template("prompt", required=True),
|
| 107 |
+
InputParam(
|
| 108 |
+
name="prompt_upsampling",
|
| 109 |
+
type_hint=bool,
|
| 110 |
+
default=False,
|
| 111 |
+
description="If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.",
|
| 112 |
+
),
|
| 113 |
+
InputParam(
|
| 114 |
+
name="prompt_upsampling_temperature",
|
| 115 |
+
type_hint=float,
|
| 116 |
+
default=PROMPT_UPSAMPLE_TEMPERATURE,
|
| 117 |
+
description="Sampling temperature for prompt upsampling.",
|
| 118 |
+
),
|
| 119 |
+
InputParam.template("height"),
|
| 120 |
+
InputParam.template("width"),
|
| 121 |
+
InputParam.template("max_sequence_length", default=2048),
|
| 122 |
+
InputParam.template("generator"),
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
@property
|
| 126 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 127 |
+
return [
|
| 128 |
+
OutputParam(
|
| 129 |
+
name="prompt",
|
| 130 |
+
type_hint=list,
|
| 131 |
+
description="The (possibly upsampled) prompt forwarded to the text encoder.",
|
| 132 |
+
),
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
@torch.no_grad()
|
| 136 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 137 |
+
block_state = self.get_block_state(state)
|
| 138 |
+
|
| 139 |
+
if block_state.prompt_upsampling:
|
| 140 |
+
if components.prompt_enhancer_head is None:
|
| 141 |
+
raise ValueError(
|
| 142 |
+
"Prompt upsampling requires the `prompt_enhancer_head` component, which is not loaded. Load an "
|
| 143 |
+
"`Ideogram4PromptEnhancerHead` and add it to the pipeline."
|
| 144 |
+
)
|
| 145 |
+
if self._prompt_enhancer is None:
|
| 146 |
+
self._prompt_enhancer = build_prompt_enhancer(components.text_encoder, components.prompt_enhancer_head)
|
| 147 |
+
if self._caption_logits_processor is None and is_outlines_available():
|
| 148 |
+
self._caption_logits_processor = build_caption_logits_processor(
|
| 149 |
+
self._prompt_enhancer, components.tokenizer
|
| 150 |
+
)
|
| 151 |
+
if self._caption_logits_processor is None:
|
| 152 |
+
logger.warning_once(
|
| 153 |
+
"`outlines` is not installed; prompt upsampling runs unconstrained and may not return "
|
| 154 |
+
"schema-valid JSON. Install with `pip install outlines` for structured captions."
|
| 155 |
+
)
|
| 156 |
+
height = block_state.height or components.default_height
|
| 157 |
+
width = block_state.width or components.default_width
|
| 158 |
+
block_state.prompt = generate_captions(
|
| 159 |
+
self._prompt_enhancer,
|
| 160 |
+
components.tokenizer,
|
| 161 |
+
self._caption_logits_processor,
|
| 162 |
+
block_state.prompt,
|
| 163 |
+
height,
|
| 164 |
+
width,
|
| 165 |
+
temperature=block_state.prompt_upsampling_temperature,
|
| 166 |
+
max_new_tokens=block_state.max_sequence_length,
|
| 167 |
+
generator=block_state.generator,
|
| 168 |
+
device=components._execution_device,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
self.set_block_state(state, block_state)
|
| 172 |
+
return components, state
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# auto_docstring
|
| 176 |
+
class Ideogram4TextEncoderStep(ModularPipelineBlocks):
|
| 177 |
+
"""
|
| 178 |
+
Text encoder step that tokenizes the prompt(s) and runs the Qwen3-VL text encoder, returning the per-token text
|
| 179 |
+
features (concatenated from a fixed set of activation layers). Only the text tokens are encoded; the packed image
|
| 180 |
+
tokens are appended later (the encoder is causal with image after text, so they never affect the text features).
|
| 181 |
+
|
| 182 |
+
Components:
|
| 183 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
|
| 184 |
+
with the text encoder.
|
| 185 |
+
|
| 186 |
+
Inputs:
|
| 187 |
+
prompt (`str`):
|
| 188 |
+
The prompt or prompts to guide image generation.
|
| 189 |
+
max_sequence_length (`int`, *optional*, defaults to 2048):
|
| 190 |
+
Maximum sequence length for prompt encoding.
|
| 191 |
+
|
| 192 |
+
Outputs:
|
| 193 |
+
text_features (`Tensor`):
|
| 194 |
+
Per-prompt text features (B, max_sequence_length, llm_features_dim), padding zeroed.
|
| 195 |
+
text_lengths (`list`):
|
| 196 |
+
Per-prompt real text-token counts, used to lay out the packed sequence.
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
model_name = "ideogram4"
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def description(self) -> str:
|
| 203 |
+
return (
|
| 204 |
+
"Text encoder step that tokenizes the prompt(s) and runs the Qwen3-VL text encoder, returning the "
|
| 205 |
+
"per-token text features (concatenated from a fixed set of activation layers). Only the text tokens are "
|
| 206 |
+
"encoded; the packed image tokens are appended later (the encoder is causal with image after text, so "
|
| 207 |
+
"they never affect the text features)."
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 212 |
+
return [
|
| 213 |
+
ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
|
| 214 |
+
ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer paired with the text encoder."),
|
| 215 |
+
]
|
| 216 |
+
|
| 217 |
+
@property
|
| 218 |
+
def inputs(self) -> list[InputParam]:
|
| 219 |
+
return [
|
| 220 |
+
InputParam.template("prompt", required=True),
|
| 221 |
+
InputParam.template("max_sequence_length", default=2048),
|
| 222 |
+
]
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 226 |
+
return [
|
| 227 |
+
OutputParam(
|
| 228 |
+
name="text_features",
|
| 229 |
+
type_hint=torch.Tensor,
|
| 230 |
+
description="Per-prompt text features (B, max_sequence_length, llm_features_dim), padding zeroed.",
|
| 231 |
+
),
|
| 232 |
+
OutputParam(
|
| 233 |
+
name="text_lengths",
|
| 234 |
+
type_hint=list,
|
| 235 |
+
description="Per-prompt real text-token counts, used to lay out the packed sequence.",
|
| 236 |
+
),
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
@staticmethod
|
| 240 |
+
# Copied from diffusers.pipelines.ideogram4.pipeline_ideogram4.Ideogram4Pipeline._get_text_encoder_hidden_states
|
| 241 |
+
def _get_text_encoder_hidden_states(
|
| 242 |
+
text_encoder,
|
| 243 |
+
token_ids: torch.Tensor,
|
| 244 |
+
attention_mask: torch.Tensor,
|
| 245 |
+
pos_2d: torch.Tensor,
|
| 246 |
+
) -> list[torch.Tensor]:
|
| 247 |
+
"""Run the text encoder's decoder layers, returning the hidden states tapped at each activation layer."""
|
| 248 |
+
|
| 249 |
+
language_model = text_encoder.language_model
|
| 250 |
+
|
| 251 |
+
inputs_embeds = language_model.embed_tokens(token_ids)
|
| 252 |
+
|
| 253 |
+
position_ids_4d = pos_2d[None, ...].expand(4, pos_2d.shape[0], -1)
|
| 254 |
+
text_position_ids = position_ids_4d[0]
|
| 255 |
+
mrope_position_ids = position_ids_4d[1:]
|
| 256 |
+
|
| 257 |
+
causal_mask = create_causal_mask(
|
| 258 |
+
config=language_model.config,
|
| 259 |
+
inputs_embeds=inputs_embeds,
|
| 260 |
+
attention_mask=attention_mask,
|
| 261 |
+
past_key_values=None,
|
| 262 |
+
position_ids=text_position_ids,
|
| 263 |
+
)
|
| 264 |
+
position_embeddings = language_model.rotary_emb(inputs_embeds, mrope_position_ids)
|
| 265 |
+
|
| 266 |
+
tap_set = set(QWEN3_VL_ACTIVATION_LAYERS)
|
| 267 |
+
captured: dict[int, torch.Tensor] = {}
|
| 268 |
+
hidden_states = inputs_embeds
|
| 269 |
+
for layer_idx, decoder_layer in enumerate(language_model.layers):
|
| 270 |
+
hidden_states = decoder_layer(
|
| 271 |
+
hidden_states,
|
| 272 |
+
attention_mask=causal_mask,
|
| 273 |
+
position_ids=text_position_ids,
|
| 274 |
+
past_key_values=None,
|
| 275 |
+
position_embeddings=position_embeddings,
|
| 276 |
+
)
|
| 277 |
+
if layer_idx in tap_set:
|
| 278 |
+
captured[layer_idx] = hidden_states
|
| 279 |
+
|
| 280 |
+
return [captured[i] for i in QWEN3_VL_ACTIVATION_LAYERS]
|
| 281 |
+
|
| 282 |
+
@torch.no_grad()
|
| 283 |
+
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
|
| 284 |
+
block_state = self.get_block_state(state)
|
| 285 |
+
|
| 286 |
+
device = components._execution_device
|
| 287 |
+
tokenizer = components.tokenizer
|
| 288 |
+
max_text_tokens = block_state.max_sequence_length
|
| 289 |
+
|
| 290 |
+
prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
|
| 291 |
+
batch_size = len(prompts)
|
| 292 |
+
|
| 293 |
+
# Tokenize each chat-formatted prompt and left-pad to `max_sequence_length`.
|
| 294 |
+
token_ids = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
|
| 295 |
+
attention_mask = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
|
| 296 |
+
text_position_ids = torch.zeros(batch_size, max_text_tokens, dtype=torch.long)
|
| 297 |
+
text_lengths = []
|
| 298 |
+
for b, text_prompt in enumerate(prompts):
|
| 299 |
+
messages = [{"role": "user", "content": [{"type": "text", "text": text_prompt}]}]
|
| 300 |
+
text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
|
| 301 |
+
toks = tokenizer(text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
|
| 302 |
+
n = int(toks.shape[0])
|
| 303 |
+
if n > max_text_tokens:
|
| 304 |
+
raise ValueError(f"prompt has {n} tokens, exceeds max_sequence_length={max_text_tokens}")
|
| 305 |
+
text_lengths.append(n)
|
| 306 |
+
offset = max_text_tokens - n
|
| 307 |
+
token_ids[b, offset:] = toks
|
| 308 |
+
attention_mask[b, offset:] = 1
|
| 309 |
+
text_position_ids[b, offset:] = torch.arange(n)
|
| 310 |
+
|
| 311 |
+
token_ids = token_ids.to(device)
|
| 312 |
+
attention_mask = attention_mask.to(device)
|
| 313 |
+
text_position_ids = text_position_ids.to(device)
|
| 314 |
+
|
| 315 |
+
# Run the text encoder, tapping the activation-layer hidden states, then concatenate them into per-token
|
| 316 |
+
# text features (padding zeroed).
|
| 317 |
+
selected = self._get_text_encoder_hidden_states(
|
| 318 |
+
components.text_encoder, token_ids, attention_mask, text_position_ids
|
| 319 |
+
)
|
| 320 |
+
text_features = torch.stack(selected, dim=0).permute(1, 2, 3, 0).reshape(batch_size, max_text_tokens, -1)
|
| 321 |
+
text_features = (text_features * attention_mask.to(text_features.dtype).unsqueeze(-1)).to(torch.float32)
|
| 322 |
+
|
| 323 |
+
block_state.text_features = text_features
|
| 324 |
+
block_state.text_lengths = text_lengths
|
| 325 |
+
|
| 326 |
+
self.set_block_state(state, block_state)
|
| 327 |
+
return components, state
|
diffusers/modular_pipelines/ideogram4/modular_blocks_ideogram4.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Ideogram4PrepareAdditionalInputsStep,
|
| 21 |
+
Ideogram4PrepareLatentsStep,
|
| 22 |
+
Ideogram4SetTimestepsStep,
|
| 23 |
+
Ideogram4TextInputsStep,
|
| 24 |
+
)
|
| 25 |
+
from .decoders import Ideogram4DecodeStep
|
| 26 |
+
from .denoise import Ideogram4AfterDenoiseStep, Ideogram4DenoiseStep
|
| 27 |
+
from .encoders import Ideogram4PromptUpsampleStep, Ideogram4TextEncoderStep
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Core denoise: consumes the per-prompt text features and produces the unpatchified latents
|
| 34 |
+
# (batch/latents/timesteps/ids inputs -> denoising loop -> unpatchify).
|
| 35 |
+
CORE_DENOISE_BLOCKS = InsertableDict(
|
| 36 |
+
[
|
| 37 |
+
("input", Ideogram4TextInputsStep()),
|
| 38 |
+
("prepare_latents", Ideogram4PrepareLatentsStep()),
|
| 39 |
+
("set_timesteps", Ideogram4SetTimestepsStep()),
|
| 40 |
+
("prepare_additional_inputs", Ideogram4PrepareAdditionalInputsStep()),
|
| 41 |
+
("denoise", Ideogram4DenoiseStep()),
|
| 42 |
+
("after_denoise", Ideogram4AfterDenoiseStep()),
|
| 43 |
+
]
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# auto_docstring
|
| 48 |
+
class Ideogram4CoreDenoiseStep(SequentialPipelineBlocks):
|
| 49 |
+
"""
|
| 50 |
+
Core denoising workflow for Ideogram4 text-to-image: prepares the batch/latents/timesteps and the packed denoiser
|
| 51 |
+
inputs, runs the asymmetric-CFG denoising loop over the conditional and unconditional transformers, and
|
| 52 |
+
unpatchifies the result for the decoder.
|
| 53 |
+
|
| 54 |
+
Components:
|
| 55 |
+
transformer (`Ideogram4Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 56 |
+
unconditional_transformer (`Ideogram4Transformer2DModel`)
|
| 57 |
+
|
| 58 |
+
Inputs:
|
| 59 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 60 |
+
The number of images to generate per prompt.
|
| 61 |
+
text_features (`Tensor`):
|
| 62 |
+
Per-prompt text features from the encoder.
|
| 63 |
+
text_lengths (`list`):
|
| 64 |
+
Per-prompt text-token counts from the encoder.
|
| 65 |
+
latents (`Tensor`, *optional*):
|
| 66 |
+
Pre-generated noisy latents for image generation.
|
| 67 |
+
height (`int`):
|
| 68 |
+
The height in pixels of the generated image.
|
| 69 |
+
width (`int`):
|
| 70 |
+
The width in pixels of the generated image.
|
| 71 |
+
generator (`Generator`, *optional*):
|
| 72 |
+
Torch generator for deterministic generation.
|
| 73 |
+
num_inference_steps (`int`, *optional*, defaults to 48):
|
| 74 |
+
The number of denoising steps.
|
| 75 |
+
mu (`float`, *optional*, defaults to 0.0):
|
| 76 |
+
Base mean of the logit-normal schedule.
|
| 77 |
+
std (`float`, *optional*, defaults to 1.5):
|
| 78 |
+
Std of the logit-normal schedule.
|
| 79 |
+
guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 80 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 81 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
|
| 82 |
+
Per-step guidance scale schedule (length num_inference_steps).
|
| 83 |
+
|
| 84 |
+
Outputs:
|
| 85 |
+
latents (`Tensor`):
|
| 86 |
+
Unpatchified (B, ae_channels, H, W) latents.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
model_name = "ideogram4"
|
| 90 |
+
block_classes = list(CORE_DENOISE_BLOCKS.values())
|
| 91 |
+
block_names = list(CORE_DENOISE_BLOCKS.keys())
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def description(self) -> str:
|
| 95 |
+
return (
|
| 96 |
+
"Core denoising workflow for Ideogram4 text-to-image: prepares the batch/latents/timesteps and the packed "
|
| 97 |
+
"denoiser inputs, runs the asymmetric-CFG denoising loop over the conditional and unconditional "
|
| 98 |
+
"transformers, and unpatchifies the result for the decoder."
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
@property
|
| 102 |
+
def outputs(self) -> list[OutputParam]:
|
| 103 |
+
# The only meaningful product of the core step is the unpatchified latents; the batch/timesteps/packed-sequence
|
| 104 |
+
# inputs prepared along the way are consumed within the loop and are not updated by it.
|
| 105 |
+
return [OutputParam.template("latents", description="Unpatchified (B, ae_channels, H, W) latents.")]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# auto_docstring
|
| 109 |
+
class Ideogram4AutoBlocks(SequentialPipelineBlocks):
|
| 110 |
+
"""
|
| 111 |
+
Auto Modular pipeline for text-to-image generation using Ideogram4: (optional) prompt upsampling -> encode text ->
|
| 112 |
+
core denoise (asymmetric CFG over two transformers) -> decode.
|
| 113 |
+
|
| 114 |
+
Supported workflows:
|
| 115 |
+
- `text2image`: requires `prompt`
|
| 116 |
+
|
| 117 |
+
Components:
|
| 118 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`Qwen2Tokenizer`): The tokenizer paired
|
| 119 |
+
with the text encoder. prompt_enhancer_head (`Ideogram4PromptEnhancerHead`): LM head grafted onto the text
|
| 120 |
+
encoder for prompt upsampling. transformer (`Ideogram4Transformer2DModel`) scheduler
|
| 121 |
+
(`FlowMatchEulerDiscreteScheduler`) unconditional_transformer (`Ideogram4Transformer2DModel`) vae
|
| 122 |
+
(`AutoencoderKLFlux2`) image_processor (`VaeImageProcessor`)
|
| 123 |
+
|
| 124 |
+
Inputs:
|
| 125 |
+
prompt (`str`):
|
| 126 |
+
The prompt or prompts to guide image generation.
|
| 127 |
+
prompt_upsampling (`bool`, *optional*, defaults to False):
|
| 128 |
+
If True, rewrite the prompt into Ideogram4's native JSON caption before encoding.
|
| 129 |
+
prompt_upsampling_temperature (`float`, *optional*, defaults to 1.0):
|
| 130 |
+
Sampling temperature for prompt upsampling.
|
| 131 |
+
height (`int`, *optional*):
|
| 132 |
+
The height in pixels of the generated image.
|
| 133 |
+
width (`int`, *optional*):
|
| 134 |
+
The width in pixels of the generated image.
|
| 135 |
+
max_sequence_length (`int`, *optional*, defaults to 2048):
|
| 136 |
+
Maximum sequence length for prompt encoding.
|
| 137 |
+
generator (`Generator`, *optional*):
|
| 138 |
+
Torch generator for deterministic generation.
|
| 139 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 140 |
+
The number of images to generate per prompt.
|
| 141 |
+
latents (`Tensor`, *optional*):
|
| 142 |
+
Pre-generated noisy latents for image generation.
|
| 143 |
+
num_inference_steps (`int`, *optional*, defaults to 48):
|
| 144 |
+
The number of denoising steps.
|
| 145 |
+
mu (`float`, *optional*, defaults to 0.0):
|
| 146 |
+
Base mean of the logit-normal schedule.
|
| 147 |
+
std (`float`, *optional*, defaults to 1.5):
|
| 148 |
+
Std of the logit-normal schedule.
|
| 149 |
+
guidance_schedule (`list`, *optional*, defaults to (7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 150 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
|
| 151 |
+
7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 3.0, 3.0, 3.0)):
|
| 152 |
+
Per-step guidance scale schedule (length num_inference_steps).
|
| 153 |
+
output_type (`str`, *optional*, defaults to pil):
|
| 154 |
+
Output format: 'pil', 'np', 'pt'.
|
| 155 |
+
|
| 156 |
+
Outputs:
|
| 157 |
+
images (`list`):
|
| 158 |
+
Generated images.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
model_name = "ideogram4"
|
| 162 |
+
block_classes = [
|
| 163 |
+
Ideogram4PromptUpsampleStep(),
|
| 164 |
+
Ideogram4TextEncoderStep(),
|
| 165 |
+
Ideogram4CoreDenoiseStep(),
|
| 166 |
+
Ideogram4DecodeStep(),
|
| 167 |
+
]
|
| 168 |
+
block_names = ["prompt_upsample", "text_encoder", "denoise", "decode"]
|
| 169 |
+
|
| 170 |
+
# Workflow map declaring the trigger conditions for each supported workflow.
|
| 171 |
+
# `True` means the workflow triggers when the input is not None.
|
| 172 |
+
_workflow_map = {
|
| 173 |
+
"text2image": {"prompt": True},
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
@property
|
| 177 |
+
def description(self) -> str:
|
| 178 |
+
return (
|
| 179 |
+
"Auto Modular pipeline for text-to-image generation using Ideogram4: (optional) prompt upsampling -> "
|
| 180 |
+
"encode text -> core denoise (asymmetric CFG over two transformers) -> decode."
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def outputs(self) -> list[OutputParam]:
|
| 185 |
+
return [OutputParam.template("images")]
|
diffusers/modular_pipelines/ideogram4/modular_pipeline.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Ideogram AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ...loaders import Ideogram4LoraLoaderMixin
|
| 16 |
+
from ..modular_pipeline import ModularPipeline
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Ideogram4ModularPipeline(ModularPipeline, Ideogram4LoraLoaderMixin):
|
| 20 |
+
"""
|
| 21 |
+
A ModularPipeline for Ideogram4.
|
| 22 |
+
|
| 23 |
+
> [!WARNING] > This is an experimental feature!
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
default_blocks_name = "Ideogram4AutoBlocks"
|
| 27 |
+
|
| 28 |
+
# Ideogram4 patchifies the VAE output by a factor of 2 before feeding the transformer.
|
| 29 |
+
@property
|
| 30 |
+
def patch_size(self):
|
| 31 |
+
return 2
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def default_height(self):
|
| 35 |
+
return 2048
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def default_width(self):
|
| 39 |
+
return 2048
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def vae_scale_factor(self):
|
| 43 |
+
vae_scale_factor = 8
|
| 44 |
+
if getattr(self, "vae", None) is not None:
|
| 45 |
+
vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
|
| 46 |
+
return vae_scale_factor
|
diffusers/modular_pipelines/krea2/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_krea2"] = ["Krea2AutoBlocks"]
|
| 25 |
+
_import_structure["modular_blocks_krea2_turbo"] = ["Krea2TurboAutoBlocks"]
|
| 26 |
+
_import_structure["modular_pipeline"] = ["Krea2ModularPipeline", "Krea2TurboModularPipeline"]
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 29 |
+
try:
|
| 30 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 31 |
+
raise OptionalDependencyNotAvailable()
|
| 32 |
+
except OptionalDependencyNotAvailable:
|
| 33 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 34 |
+
else:
|
| 35 |
+
from .modular_blocks_krea2 import Krea2AutoBlocks
|
| 36 |
+
from .modular_blocks_krea2_turbo import Krea2TurboAutoBlocks
|
| 37 |
+
from .modular_pipeline import Krea2ModularPipeline, Krea2TurboModularPipeline
|
| 38 |
+
else:
|
| 39 |
+
import sys
|
| 40 |
+
|
| 41 |
+
sys.modules[__name__] = _LazyModule(
|
| 42 |
+
__name__,
|
| 43 |
+
globals()["__file__"],
|
| 44 |
+
_import_structure,
|
| 45 |
+
module_spec=__spec__,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
for name, value in _dummy_objects.items():
|
| 49 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/krea2/before_denoise.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ...models.transformers.transformer_krea2 import Krea2Transformer2DModel
|
| 20 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ...utils.torch_utils import randn_tensor
|
| 23 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 24 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 25 |
+
from .modular_pipeline import Krea2ModularPipeline
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Copied from diffusers.pipelines.krea2.pipeline_krea2.calculate_shift
|
| 32 |
+
def calculate_shift(
|
| 33 |
+
image_seq_len,
|
| 34 |
+
base_seq_len: int = 256,
|
| 35 |
+
max_seq_len: int = 4096,
|
| 36 |
+
base_shift: float = 0.5,
|
| 37 |
+
max_shift: float = 1.15,
|
| 38 |
+
):
|
| 39 |
+
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
| 40 |
+
b = base_shift - m * base_seq_len
|
| 41 |
+
mu = image_seq_len * m + b
|
| 42 |
+
return mu
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# auto_docstring
|
| 46 |
+
class Krea2TextInputsStep(ModularPipelineBlocks):
|
| 47 |
+
"""
|
| 48 |
+
Input step that determines `batch_size`/`dtype` from the per-prompt `prompt_embeds` and replicates the text
|
| 49 |
+
conditioning (and the optional negative branch) to `batch_size * num_images_per_prompt`. Place after the text
|
| 50 |
+
encoder.
|
| 51 |
+
|
| 52 |
+
Inputs:
|
| 53 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 54 |
+
The number of images to generate per prompt.
|
| 55 |
+
prompt_embeds (`Tensor`):
|
| 56 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 57 |
+
prompt_embeds_mask (`Tensor`):
|
| 58 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 59 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 60 |
+
Per-prompt negative text features.
|
| 61 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 62 |
+
Per-prompt negative text mask.
|
| 63 |
+
|
| 64 |
+
Outputs:
|
| 65 |
+
batch_size (`int`):
|
| 66 |
+
Effective batch size (num prompts * num_images_per_prompt).
|
| 67 |
+
dtype (`dtype`):
|
| 68 |
+
The dtype of the text features.
|
| 69 |
+
prompt_embeds (`Tensor`):
|
| 70 |
+
Text features, batch-expanded.
|
| 71 |
+
prompt_embeds_mask (`Tensor`):
|
| 72 |
+
Text mask, batch-expanded.
|
| 73 |
+
negative_prompt_embeds (`Tensor`):
|
| 74 |
+
Negative text features, batch-expanded.
|
| 75 |
+
negative_prompt_embeds_mask (`Tensor`):
|
| 76 |
+
Negative text mask, batch-expanded.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
model_name = "krea2"
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def description(self) -> str:
|
| 83 |
+
return (
|
| 84 |
+
"Input step that determines `batch_size`/`dtype` from the per-prompt `prompt_embeds` and replicates the "
|
| 85 |
+
"text conditioning (and the optional negative branch) to `batch_size * num_images_per_prompt`. Place after "
|
| 86 |
+
"the text encoder."
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def inputs(self) -> list[InputParam]:
|
| 91 |
+
return [
|
| 92 |
+
InputParam.template("num_images_per_prompt", default=1),
|
| 93 |
+
InputParam(
|
| 94 |
+
name="prompt_embeds",
|
| 95 |
+
required=True,
|
| 96 |
+
type_hint=torch.Tensor,
|
| 97 |
+
description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
|
| 98 |
+
),
|
| 99 |
+
InputParam(
|
| 100 |
+
name="prompt_embeds_mask",
|
| 101 |
+
required=True,
|
| 102 |
+
type_hint=torch.Tensor,
|
| 103 |
+
description="Per-prompt boolean text mask (B, text_seq_len).",
|
| 104 |
+
),
|
| 105 |
+
InputParam(
|
| 106 |
+
name="negative_prompt_embeds",
|
| 107 |
+
type_hint=torch.Tensor,
|
| 108 |
+
description="Per-prompt negative text features.",
|
| 109 |
+
),
|
| 110 |
+
InputParam(
|
| 111 |
+
name="negative_prompt_embeds_mask",
|
| 112 |
+
type_hint=torch.Tensor,
|
| 113 |
+
description="Per-prompt negative text mask.",
|
| 114 |
+
),
|
| 115 |
+
]
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 119 |
+
return [
|
| 120 |
+
OutputParam(
|
| 121 |
+
name="batch_size",
|
| 122 |
+
type_hint=int,
|
| 123 |
+
description="Effective batch size (num prompts * num_images_per_prompt).",
|
| 124 |
+
),
|
| 125 |
+
OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
|
| 126 |
+
OutputParam(name="prompt_embeds", type_hint=torch.Tensor, description="Text features, batch-expanded."),
|
| 127 |
+
OutputParam(name="prompt_embeds_mask", type_hint=torch.Tensor, description="Text mask, batch-expanded."),
|
| 128 |
+
OutputParam(
|
| 129 |
+
name="negative_prompt_embeds",
|
| 130 |
+
type_hint=torch.Tensor,
|
| 131 |
+
description="Negative text features, batch-expanded.",
|
| 132 |
+
),
|
| 133 |
+
OutputParam(
|
| 134 |
+
name="negative_prompt_embeds_mask",
|
| 135 |
+
type_hint=torch.Tensor,
|
| 136 |
+
description="Negative text mask, batch-expanded.",
|
| 137 |
+
),
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
@torch.no_grad()
|
| 141 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 142 |
+
block_state = self.get_block_state(state)
|
| 143 |
+
|
| 144 |
+
prompt_batch, seq_len, num_layers, dim = block_state.prompt_embeds.shape
|
| 145 |
+
n = block_state.num_images_per_prompt
|
| 146 |
+
|
| 147 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 148 |
+
block_state.batch_size = prompt_batch * n
|
| 149 |
+
|
| 150 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, n, 1, 1).view(
|
| 151 |
+
prompt_batch * n, seq_len, num_layers, dim
|
| 152 |
+
)
|
| 153 |
+
block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.repeat(1, n).view(prompt_batch * n, seq_len)
|
| 154 |
+
|
| 155 |
+
if block_state.negative_prompt_embeds is not None:
|
| 156 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(1, n, 1, 1).view(
|
| 157 |
+
prompt_batch * n, seq_len, num_layers, dim
|
| 158 |
+
)
|
| 159 |
+
block_state.negative_prompt_embeds_mask = block_state.negative_prompt_embeds_mask.repeat(1, n).view(
|
| 160 |
+
prompt_batch * n, seq_len
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
self.set_block_state(state, block_state)
|
| 164 |
+
return components, state
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
# auto_docstring
|
| 168 |
+
class Krea2TurboTextInputsStep(ModularPipelineBlocks):
|
| 169 |
+
"""
|
| 170 |
+
Input step for the distilled Krea 2 turbo checkpoint that determines `batch_size`/`dtype` from the per-prompt
|
| 171 |
+
`prompt_embeds` and replicates the text conditioning to `batch_size * num_images_per_prompt`. The distilled
|
| 172 |
+
checkpoint runs without classifier-free guidance, so there is no negative branch. Place after the text encoder.
|
| 173 |
+
|
| 174 |
+
Inputs:
|
| 175 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 176 |
+
The number of images to generate per prompt.
|
| 177 |
+
prompt_embeds (`Tensor`):
|
| 178 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 179 |
+
prompt_embeds_mask (`Tensor`):
|
| 180 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 181 |
+
|
| 182 |
+
Outputs:
|
| 183 |
+
batch_size (`int`):
|
| 184 |
+
Effective batch size (num prompts * num_images_per_prompt).
|
| 185 |
+
dtype (`dtype`):
|
| 186 |
+
The dtype of the text features.
|
| 187 |
+
prompt_embeds (`Tensor`):
|
| 188 |
+
Text features, batch-expanded.
|
| 189 |
+
prompt_embeds_mask (`Tensor`):
|
| 190 |
+
Text mask, batch-expanded.
|
| 191 |
+
"""
|
| 192 |
+
|
| 193 |
+
model_name = "krea2"
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def description(self) -> str:
|
| 197 |
+
return (
|
| 198 |
+
"Input step for the distilled Krea 2 turbo checkpoint that determines `batch_size`/`dtype` from the "
|
| 199 |
+
"per-prompt `prompt_embeds` and replicates the text conditioning to `batch_size * num_images_per_prompt`. "
|
| 200 |
+
"The distilled checkpoint runs without classifier-free guidance, so there is no negative branch. Place "
|
| 201 |
+
"after the text encoder."
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
@property
|
| 205 |
+
def inputs(self) -> list[InputParam]:
|
| 206 |
+
return [
|
| 207 |
+
InputParam.template("num_images_per_prompt", default=1),
|
| 208 |
+
InputParam(
|
| 209 |
+
name="prompt_embeds",
|
| 210 |
+
required=True,
|
| 211 |
+
type_hint=torch.Tensor,
|
| 212 |
+
description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
|
| 213 |
+
),
|
| 214 |
+
InputParam(
|
| 215 |
+
name="prompt_embeds_mask",
|
| 216 |
+
required=True,
|
| 217 |
+
type_hint=torch.Tensor,
|
| 218 |
+
description="Per-prompt boolean text mask (B, text_seq_len).",
|
| 219 |
+
),
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 224 |
+
return [
|
| 225 |
+
OutputParam(
|
| 226 |
+
name="batch_size",
|
| 227 |
+
type_hint=int,
|
| 228 |
+
description="Effective batch size (num prompts * num_images_per_prompt).",
|
| 229 |
+
),
|
| 230 |
+
OutputParam(name="dtype", type_hint=torch.dtype, description="The dtype of the text features."),
|
| 231 |
+
OutputParam(name="prompt_embeds", type_hint=torch.Tensor, description="Text features, batch-expanded."),
|
| 232 |
+
OutputParam(name="prompt_embeds_mask", type_hint=torch.Tensor, description="Text mask, batch-expanded."),
|
| 233 |
+
]
|
| 234 |
+
|
| 235 |
+
@torch.no_grad()
|
| 236 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 237 |
+
block_state = self.get_block_state(state)
|
| 238 |
+
|
| 239 |
+
prompt_batch, seq_len, num_layers, dim = block_state.prompt_embeds.shape
|
| 240 |
+
n = block_state.num_images_per_prompt
|
| 241 |
+
|
| 242 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 243 |
+
block_state.batch_size = prompt_batch * n
|
| 244 |
+
|
| 245 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, n, 1, 1).view(
|
| 246 |
+
prompt_batch * n, seq_len, num_layers, dim
|
| 247 |
+
)
|
| 248 |
+
block_state.prompt_embeds_mask = block_state.prompt_embeds_mask.repeat(1, n).view(prompt_batch * n, seq_len)
|
| 249 |
+
|
| 250 |
+
self.set_block_state(state, block_state)
|
| 251 |
+
return components, state
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# auto_docstring
|
| 255 |
+
class Krea2PrepareLatentsStep(ModularPipelineBlocks):
|
| 256 |
+
"""
|
| 257 |
+
Step that samples the spatial image latents and patch-packs them into (B, image_seq_len, in_channels) for the
|
| 258 |
+
denoising loop.
|
| 259 |
+
|
| 260 |
+
Components:
|
| 261 |
+
transformer (`Krea2Transformer2DModel`)
|
| 262 |
+
|
| 263 |
+
Inputs:
|
| 264 |
+
latents (`Tensor`, *optional*):
|
| 265 |
+
Pre-generated noisy latents for image generation.
|
| 266 |
+
height (`int`, *optional*, defaults to 1024):
|
| 267 |
+
The height in pixels of the generated image.
|
| 268 |
+
width (`int`, *optional*, defaults to 1024):
|
| 269 |
+
The width in pixels of the generated image.
|
| 270 |
+
generator (`Generator`, *optional*):
|
| 271 |
+
Torch generator for deterministic generation.
|
| 272 |
+
batch_size (`int`):
|
| 273 |
+
Effective batch size.
|
| 274 |
+
dtype (`dtype`):
|
| 275 |
+
The working dtype.
|
| 276 |
+
|
| 277 |
+
Outputs:
|
| 278 |
+
latents (`Tensor`):
|
| 279 |
+
The initial packed image latents (B, image_seq_len, in_channels).
|
| 280 |
+
image_seq_len (`int`):
|
| 281 |
+
Number of image tokens (grid_h * grid_w).
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
model_name = "krea2"
|
| 285 |
+
|
| 286 |
+
@property
|
| 287 |
+
def description(self) -> str:
|
| 288 |
+
return (
|
| 289 |
+
"Step that samples the spatial image latents and patch-packs them into (B, image_seq_len, in_channels) "
|
| 290 |
+
"for the denoising loop."
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
@property
|
| 294 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 295 |
+
return [ComponentSpec("transformer", Krea2Transformer2DModel)]
|
| 296 |
+
|
| 297 |
+
@property
|
| 298 |
+
def inputs(self) -> list[InputParam]:
|
| 299 |
+
return [
|
| 300 |
+
InputParam.template("latents"),
|
| 301 |
+
InputParam.template("height", default=1024),
|
| 302 |
+
InputParam.template("width", default=1024),
|
| 303 |
+
InputParam.template("generator"),
|
| 304 |
+
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
|
| 305 |
+
InputParam(name="dtype", required=True, type_hint=torch.dtype, description="The working dtype."),
|
| 306 |
+
]
|
| 307 |
+
|
| 308 |
+
@property
|
| 309 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 310 |
+
return [
|
| 311 |
+
OutputParam(
|
| 312 |
+
name="latents",
|
| 313 |
+
type_hint=torch.Tensor,
|
| 314 |
+
description="The initial packed image latents (B, image_seq_len, in_channels).",
|
| 315 |
+
),
|
| 316 |
+
OutputParam(name="image_seq_len", type_hint=int, description="Number of image tokens (grid_h * grid_w)."),
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
@torch.no_grad()
|
| 320 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 321 |
+
block_state = self.get_block_state(state)
|
| 322 |
+
|
| 323 |
+
device = components._execution_device
|
| 324 |
+
p = components.patch_size
|
| 325 |
+
num_channels_latents = components.transformer.config.in_channels // (p**2)
|
| 326 |
+
|
| 327 |
+
multiple = components.vae_scale_factor * components.patch_size
|
| 328 |
+
if block_state.height % multiple != 0 or block_state.width % multiple != 0:
|
| 329 |
+
rounded_height = ((block_state.height + multiple - 1) // multiple) * multiple
|
| 330 |
+
rounded_width = ((block_state.width + multiple - 1) // multiple) * multiple
|
| 331 |
+
logger.warning(
|
| 332 |
+
f"`height` and `width` must be multiples of {multiple}; rounding up from {block_state.height}x{block_state.width} to"
|
| 333 |
+
f" {rounded_height}x{rounded_width}."
|
| 334 |
+
)
|
| 335 |
+
block_state.height, block_state.width = rounded_height, rounded_width
|
| 336 |
+
|
| 337 |
+
latent_height = block_state.height // components.vae_scale_factor
|
| 338 |
+
latent_width = block_state.width // components.vae_scale_factor
|
| 339 |
+
|
| 340 |
+
if block_state.latents is not None:
|
| 341 |
+
block_state.latents = block_state.latents.to(device=device, dtype=block_state.dtype)
|
| 342 |
+
else:
|
| 343 |
+
latents = randn_tensor(
|
| 344 |
+
(block_state.batch_size, num_channels_latents, latent_height, latent_width),
|
| 345 |
+
generator=block_state.generator,
|
| 346 |
+
device=device,
|
| 347 |
+
dtype=block_state.dtype,
|
| 348 |
+
)
|
| 349 |
+
latents = latents.view(
|
| 350 |
+
block_state.batch_size, num_channels_latents, latent_height // p, p, latent_width // p, p
|
| 351 |
+
)
|
| 352 |
+
latents = latents.permute(0, 2, 4, 1, 3, 5)
|
| 353 |
+
block_state.latents = latents.reshape(
|
| 354 |
+
block_state.batch_size, (latent_height // p) * (latent_width // p), num_channels_latents * p * p
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
block_state.image_seq_len = block_state.latents.shape[1]
|
| 358 |
+
|
| 359 |
+
self.set_block_state(state, block_state)
|
| 360 |
+
return components, state
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
# auto_docstring
|
| 364 |
+
class Krea2SetTimestepsStep(ModularPipelineBlocks):
|
| 365 |
+
"""
|
| 366 |
+
Step that sets the Krea 2 flow-matching schedule on the scheduler: a linear sigma schedule with a resolution-aware
|
| 367 |
+
dynamic time shift `mu`.
|
| 368 |
+
|
| 369 |
+
Components:
|
| 370 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 371 |
+
|
| 372 |
+
Inputs:
|
| 373 |
+
num_inference_steps (`int`, *optional*, defaults to 28):
|
| 374 |
+
The number of denoising steps.
|
| 375 |
+
sigmas (`list`, *optional*):
|
| 376 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 377 |
+
image_seq_len (`int`):
|
| 378 |
+
Number of image tokens, used to compute the resolution-aware shift.
|
| 379 |
+
|
| 380 |
+
Outputs:
|
| 381 |
+
timesteps (`Tensor`):
|
| 382 |
+
The denoising timesteps.
|
| 383 |
+
"""
|
| 384 |
+
|
| 385 |
+
model_name = "krea2"
|
| 386 |
+
|
| 387 |
+
@property
|
| 388 |
+
def description(self) -> str:
|
| 389 |
+
return (
|
| 390 |
+
"Step that sets the Krea 2 flow-matching schedule on the scheduler: a linear sigma schedule with a "
|
| 391 |
+
"resolution-aware dynamic time shift `mu`."
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
@property
|
| 395 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 396 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 397 |
+
|
| 398 |
+
@property
|
| 399 |
+
def inputs(self) -> list[InputParam]:
|
| 400 |
+
return [
|
| 401 |
+
InputParam.template("num_inference_steps", default=28),
|
| 402 |
+
InputParam(
|
| 403 |
+
name="sigmas", type_hint=list, description="Custom sigma schedule (defaults to a linear ramp)."
|
| 404 |
+
),
|
| 405 |
+
InputParam(
|
| 406 |
+
name="image_seq_len",
|
| 407 |
+
required=True,
|
| 408 |
+
type_hint=int,
|
| 409 |
+
description="Number of image tokens, used to compute the resolution-aware shift.",
|
| 410 |
+
),
|
| 411 |
+
]
|
| 412 |
+
|
| 413 |
+
@property
|
| 414 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 415 |
+
return [OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps.")]
|
| 416 |
+
|
| 417 |
+
@torch.no_grad()
|
| 418 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 419 |
+
block_state = self.get_block_state(state)
|
| 420 |
+
|
| 421 |
+
device = components._execution_device
|
| 422 |
+
num_inference_steps = block_state.num_inference_steps
|
| 423 |
+
|
| 424 |
+
sigmas = block_state.sigmas
|
| 425 |
+
if sigmas is None:
|
| 426 |
+
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
|
| 427 |
+
else:
|
| 428 |
+
block_state.num_inference_steps = len(sigmas)
|
| 429 |
+
|
| 430 |
+
config = components.scheduler.config
|
| 431 |
+
mu = calculate_shift(
|
| 432 |
+
block_state.image_seq_len,
|
| 433 |
+
config.get("base_image_seq_len", 256),
|
| 434 |
+
config.get("max_image_seq_len", 6400),
|
| 435 |
+
config.get("base_shift", 0.5),
|
| 436 |
+
config.get("max_shift", 1.15),
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
components.scheduler.set_timesteps(sigmas=sigmas, mu=mu, device=device)
|
| 440 |
+
components.scheduler.set_begin_index(0)
|
| 441 |
+
block_state.timesteps = components.scheduler.timesteps
|
| 442 |
+
|
| 443 |
+
self.set_block_state(state, block_state)
|
| 444 |
+
return components, state
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
# auto_docstring
|
| 448 |
+
class Krea2TurboSetTimestepsStep(ModularPipelineBlocks):
|
| 449 |
+
"""
|
| 450 |
+
Step that sets the flow-matching schedule for the distilled Krea 2 turbo checkpoint on the scheduler: a linear
|
| 451 |
+
sigma schedule with the fixed time shift `mu=1.15` the checkpoint was distilled with.
|
| 452 |
+
|
| 453 |
+
Components:
|
| 454 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 455 |
+
|
| 456 |
+
Inputs:
|
| 457 |
+
num_inference_steps (`int`, *optional*, defaults to 8):
|
| 458 |
+
The number of denoising steps.
|
| 459 |
+
sigmas (`list`, *optional*):
|
| 460 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 461 |
+
|
| 462 |
+
Outputs:
|
| 463 |
+
timesteps (`Tensor`):
|
| 464 |
+
The denoising timesteps.
|
| 465 |
+
"""
|
| 466 |
+
|
| 467 |
+
model_name = "krea2"
|
| 468 |
+
|
| 469 |
+
@property
|
| 470 |
+
def description(self) -> str:
|
| 471 |
+
return (
|
| 472 |
+
"Step that sets the flow-matching schedule for the distilled Krea 2 turbo checkpoint on the scheduler: a "
|
| 473 |
+
"linear sigma schedule with the fixed time shift `mu=1.15` the checkpoint was distilled with."
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
@property
|
| 477 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 478 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 479 |
+
|
| 480 |
+
@property
|
| 481 |
+
def inputs(self) -> list[InputParam]:
|
| 482 |
+
return [
|
| 483 |
+
InputParam.template("num_inference_steps", default=8),
|
| 484 |
+
InputParam(
|
| 485 |
+
name="sigmas", type_hint=list, description="Custom sigma schedule (defaults to a linear ramp)."
|
| 486 |
+
),
|
| 487 |
+
]
|
| 488 |
+
|
| 489 |
+
@property
|
| 490 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 491 |
+
return [OutputParam(name="timesteps", type_hint=torch.Tensor, description="The denoising timesteps.")]
|
| 492 |
+
|
| 493 |
+
@torch.no_grad()
|
| 494 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 495 |
+
block_state = self.get_block_state(state)
|
| 496 |
+
|
| 497 |
+
device = components._execution_device
|
| 498 |
+
num_inference_steps = block_state.num_inference_steps
|
| 499 |
+
|
| 500 |
+
sigmas = block_state.sigmas
|
| 501 |
+
if sigmas is None:
|
| 502 |
+
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps)
|
| 503 |
+
else:
|
| 504 |
+
block_state.num_inference_steps = len(sigmas)
|
| 505 |
+
|
| 506 |
+
components.scheduler.set_timesteps(sigmas=sigmas, mu=1.15, device=device)
|
| 507 |
+
components.scheduler.set_begin_index(0)
|
| 508 |
+
block_state.timesteps = components.scheduler.timesteps
|
| 509 |
+
|
| 510 |
+
self.set_block_state(state, block_state)
|
| 511 |
+
return components, state
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
# auto_docstring
|
| 515 |
+
class Krea2PreparePositionIdsStep(ModularPipelineBlocks):
|
| 516 |
+
"""
|
| 517 |
+
Step that builds the shared rotary position ids for the combined [text | image] sequence: text at the origin, image
|
| 518 |
+
tokens at their (0, h, w) latent-grid coordinates. Place after prepare_latents.
|
| 519 |
+
|
| 520 |
+
Inputs:
|
| 521 |
+
height (`int`, *optional*, defaults to 1024):
|
| 522 |
+
The height in pixels of the generated image.
|
| 523 |
+
width (`int`, *optional*, defaults to 1024):
|
| 524 |
+
The width in pixels of the generated image.
|
| 525 |
+
prompt_embeds (`Tensor`):
|
| 526 |
+
Batch-expanded text features (only text_seq_len is used).
|
| 527 |
+
|
| 528 |
+
Outputs:
|
| 529 |
+
position_ids (`Tensor`):
|
| 530 |
+
Shared rotary coordinates (text_seq_len + grid_h * grid_w, 3).
|
| 531 |
+
"""
|
| 532 |
+
|
| 533 |
+
model_name = "krea2"
|
| 534 |
+
|
| 535 |
+
@property
|
| 536 |
+
def description(self) -> str:
|
| 537 |
+
return (
|
| 538 |
+
"Step that builds the shared rotary position ids for the combined [text | image] sequence: text at the "
|
| 539 |
+
"origin, image tokens at their (0, h, w) latent-grid coordinates. Place after prepare_latents."
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
@property
|
| 543 |
+
def inputs(self) -> list[InputParam]:
|
| 544 |
+
return [
|
| 545 |
+
InputParam.template("height", default=1024),
|
| 546 |
+
InputParam.template("width", default=1024),
|
| 547 |
+
InputParam(
|
| 548 |
+
name="prompt_embeds",
|
| 549 |
+
required=True,
|
| 550 |
+
type_hint=torch.Tensor,
|
| 551 |
+
description="Batch-expanded text features (only text_seq_len is used).",
|
| 552 |
+
),
|
| 553 |
+
]
|
| 554 |
+
|
| 555 |
+
@property
|
| 556 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 557 |
+
return [
|
| 558 |
+
OutputParam(
|
| 559 |
+
name="position_ids",
|
| 560 |
+
type_hint=torch.Tensor,
|
| 561 |
+
description="Shared rotary coordinates (text_seq_len + grid_h * grid_w, 3).",
|
| 562 |
+
)
|
| 563 |
+
]
|
| 564 |
+
|
| 565 |
+
@staticmethod
|
| 566 |
+
# Copied from diffusers.pipelines.krea2.pipeline_krea2.Krea2Pipeline.prepare_position_ids
|
| 567 |
+
def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
|
| 568 |
+
"""Build the `(text_seq_len + grid_height * grid_width, 3)` rotary coordinates for the combined sequence:
|
| 569 |
+
text tokens sit at the origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
|
| 570 |
+
text_ids = torch.zeros(text_seq_len, 3, device=device)
|
| 571 |
+
image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
|
| 572 |
+
image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
|
| 573 |
+
image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
|
| 574 |
+
image_ids = image_ids.reshape(grid_height * grid_width, 3)
|
| 575 |
+
return torch.cat([text_ids, image_ids], dim=0)
|
| 576 |
+
|
| 577 |
+
@torch.no_grad()
|
| 578 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 579 |
+
block_state = self.get_block_state(state)
|
| 580 |
+
|
| 581 |
+
device = components._execution_device
|
| 582 |
+
p = components.patch_size
|
| 583 |
+
grid_h = block_state.height // (components.vae_scale_factor * p)
|
| 584 |
+
grid_w = block_state.width // (components.vae_scale_factor * p)
|
| 585 |
+
text_seq_len = block_state.prompt_embeds.shape[1]
|
| 586 |
+
|
| 587 |
+
block_state.position_ids = self.prepare_position_ids(text_seq_len, grid_h, grid_w, device)
|
| 588 |
+
|
| 589 |
+
self.set_block_state(state, block_state)
|
| 590 |
+
return components, state
|
diffusers/modular_pipelines/krea2/decoders.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...image_processor import VaeImageProcessor
|
| 20 |
+
from ...models import AutoencoderKLQwenImage
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 23 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 24 |
+
from .modular_pipeline import Krea2ModularPipeline
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# auto_docstring
|
| 31 |
+
class Krea2DecodeStep(ModularPipelineBlocks):
|
| 32 |
+
"""
|
| 33 |
+
Step that unpacks the denoised packed latents back to the spatial grid, de-normalizes them with the VAE's
|
| 34 |
+
per-channel statistics, and decodes them through the Qwen-Image VAE into images.
|
| 35 |
+
|
| 36 |
+
Components:
|
| 37 |
+
vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
|
| 38 |
+
|
| 39 |
+
Inputs:
|
| 40 |
+
output_type (`str`, *optional*, defaults to pil):
|
| 41 |
+
Output format: 'pil', 'np', 'pt'.
|
| 42 |
+
height (`int`, *optional*, defaults to 1024):
|
| 43 |
+
The height in pixels of the generated image.
|
| 44 |
+
width (`int`, *optional*, defaults to 1024):
|
| 45 |
+
The width in pixels of the generated image.
|
| 46 |
+
latents (`Tensor`):
|
| 47 |
+
The denoised packed latents (B, image_seq_len, in_channels) from the denoising loop.
|
| 48 |
+
|
| 49 |
+
Outputs:
|
| 50 |
+
images (`list`):
|
| 51 |
+
Generated images.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
model_name = "krea2"
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def description(self) -> str:
|
| 58 |
+
return (
|
| 59 |
+
"Step that unpacks the denoised packed latents back to the spatial grid, de-normalizes them with the "
|
| 60 |
+
"VAE's per-channel statistics, and decodes them through the Qwen-Image VAE into images."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 65 |
+
return [
|
| 66 |
+
ComponentSpec("vae", AutoencoderKLQwenImage),
|
| 67 |
+
ComponentSpec(
|
| 68 |
+
"image_processor",
|
| 69 |
+
VaeImageProcessor,
|
| 70 |
+
# Effective pixel-to-token downsampling factor: vae_scale_factor (8) * patch_size (2).
|
| 71 |
+
config=FrozenDict({"vae_scale_factor": 16}),
|
| 72 |
+
default_creation_method="from_config",
|
| 73 |
+
),
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
@property
|
| 77 |
+
def inputs(self) -> list[InputParam]:
|
| 78 |
+
return [
|
| 79 |
+
InputParam.template("output_type", default="pil"),
|
| 80 |
+
InputParam.template("height", default=1024),
|
| 81 |
+
InputParam.template("width", default=1024),
|
| 82 |
+
InputParam(
|
| 83 |
+
name="latents",
|
| 84 |
+
required=True,
|
| 85 |
+
type_hint=torch.Tensor,
|
| 86 |
+
description="The denoised packed latents (B, image_seq_len, in_channels) from the denoising loop.",
|
| 87 |
+
),
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 92 |
+
return [OutputParam.template("images")]
|
| 93 |
+
|
| 94 |
+
@torch.no_grad()
|
| 95 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 96 |
+
block_state = self.get_block_state(state)
|
| 97 |
+
|
| 98 |
+
vae = components.vae
|
| 99 |
+
p = components.patch_size
|
| 100 |
+
latents = block_state.latents
|
| 101 |
+
|
| 102 |
+
batch_size, _, channels = latents.shape
|
| 103 |
+
height = p * (int(block_state.height) // (components.vae_scale_factor * p))
|
| 104 |
+
width = p * (int(block_state.width) // (components.vae_scale_factor * p))
|
| 105 |
+
latents = latents.view(batch_size, height // p, width // p, channels // (p * p), p, p)
|
| 106 |
+
latents = latents.permute(0, 3, 1, 4, 2, 5)
|
| 107 |
+
latents = latents.reshape(batch_size, channels // (p * p), 1, height, width)
|
| 108 |
+
|
| 109 |
+
latents = latents.to(vae.dtype)
|
| 110 |
+
latents_mean = (
|
| 111 |
+
torch.tensor(vae.config.latents_mean).view(1, vae.config.z_dim, 1, 1, 1).to(latents.device, latents.dtype)
|
| 112 |
+
)
|
| 113 |
+
latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
|
| 114 |
+
latents.device, latents.dtype
|
| 115 |
+
)
|
| 116 |
+
latents = latents / latents_std + latents_mean
|
| 117 |
+
image = vae.decode(latents, return_dict=False)[0][:, :, 0]
|
| 118 |
+
block_state.images = components.image_processor.postprocess(image, output_type=block_state.output_type)
|
| 119 |
+
|
| 120 |
+
self.set_block_state(state, block_state)
|
| 121 |
+
return components, state
|
diffusers/modular_pipelines/krea2/denoise.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...guiders import ClassifierFreeGuidance
|
| 20 |
+
from ...models.transformers.transformer_krea2 import Krea2Transformer2DModel
|
| 21 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 22 |
+
from ...utils import logging
|
| 23 |
+
from ..modular_pipeline import (
|
| 24 |
+
BlockState,
|
| 25 |
+
LoopSequentialPipelineBlocks,
|
| 26 |
+
ModularPipelineBlocks,
|
| 27 |
+
PipelineState,
|
| 28 |
+
)
|
| 29 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 30 |
+
from .modular_pipeline import Krea2ModularPipeline
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class Krea2LoopBeforeDenoiser(ModularPipelineBlocks):
|
| 37 |
+
model_name = "krea2"
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def description(self) -> str:
|
| 41 |
+
return (
|
| 42 |
+
"Within the denoising loop: normalize the scheduler timestep into the model's flow time and broadcast it "
|
| 43 |
+
"across the batch. Compose into the `sub_blocks` of a `Krea2DenoiseLoopWrapper`-based step."
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
@property
|
| 47 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 48 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def inputs(self) -> list[InputParam]:
|
| 52 |
+
return [
|
| 53 |
+
InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
|
| 54 |
+
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
@torch.no_grad()
|
| 58 |
+
def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 59 |
+
num_train_timesteps = components.scheduler.config.num_train_timesteps
|
| 60 |
+
block_state.timestep = (t / num_train_timesteps).expand(block_state.batch_size)
|
| 61 |
+
return components, block_state
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class Krea2LoopDenoiser(ModularPipelineBlocks):
|
| 65 |
+
model_name = "krea2"
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def description(self) -> str:
|
| 69 |
+
return (
|
| 70 |
+
"Within the denoising loop: run the `transformer` on the conditional (and, when the guider enables CFG, "
|
| 71 |
+
"the negative) text features and combine them through the `guider`. Compose into `Krea2DenoiseStep`."
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 76 |
+
return [
|
| 77 |
+
ComponentSpec(
|
| 78 |
+
"guider",
|
| 79 |
+
ClassifierFreeGuidance,
|
| 80 |
+
# Krea 2 uses cond-anchored CFG (`cond + scale * (cond - uncond)`), which is the
|
| 81 |
+
# `use_original_formulation` branch of ClassifierFreeGuidance; scale 0 disables it (distilled TDM).
|
| 82 |
+
config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
|
| 83 |
+
default_creation_method="from_config",
|
| 84 |
+
),
|
| 85 |
+
ComponentSpec("transformer", Krea2Transformer2DModel),
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def inputs(self) -> list[InputParam]:
|
| 90 |
+
return [
|
| 91 |
+
InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
|
| 92 |
+
InputParam.template("num_inference_steps", required=True),
|
| 93 |
+
InputParam(
|
| 94 |
+
name="prompt_embeds",
|
| 95 |
+
required=True,
|
| 96 |
+
type_hint=torch.Tensor,
|
| 97 |
+
description="Conditional stacked text features.",
|
| 98 |
+
),
|
| 99 |
+
InputParam(
|
| 100 |
+
name="prompt_embeds_mask", required=True, type_hint=torch.Tensor, description="Conditional text mask."
|
| 101 |
+
),
|
| 102 |
+
InputParam(
|
| 103 |
+
name="position_ids",
|
| 104 |
+
required=True,
|
| 105 |
+
type_hint=torch.Tensor,
|
| 106 |
+
description="Shared rotary coordinates for the [text | image] sequence.",
|
| 107 |
+
),
|
| 108 |
+
InputParam(
|
| 109 |
+
name="negative_prompt_embeds", type_hint=torch.Tensor, description="Negative stacked text features."
|
| 110 |
+
),
|
| 111 |
+
InputParam(name="negative_prompt_embeds_mask", type_hint=torch.Tensor, description="Negative text mask."),
|
| 112 |
+
InputParam.template("attention_kwargs"),
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
@torch.no_grad()
|
| 116 |
+
def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 117 |
+
transformer = components.transformer
|
| 118 |
+
|
| 119 |
+
latents = block_state.latents.to(transformer.dtype)
|
| 120 |
+
timestep = block_state.timestep.to(transformer.dtype)
|
| 121 |
+
|
| 122 |
+
guider_inputs = {
|
| 123 |
+
"encoder_hidden_states": (
|
| 124 |
+
block_state.prompt_embeds.to(transformer.dtype),
|
| 125 |
+
block_state.negative_prompt_embeds.to(transformer.dtype)
|
| 126 |
+
if block_state.negative_prompt_embeds is not None
|
| 127 |
+
else None,
|
| 128 |
+
),
|
| 129 |
+
"encoder_attention_mask": (
|
| 130 |
+
block_state.prompt_embeds_mask,
|
| 131 |
+
block_state.negative_prompt_embeds_mask,
|
| 132 |
+
),
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 136 |
+
guider_state = components.guider.prepare_inputs(guider_inputs)
|
| 137 |
+
|
| 138 |
+
for guider_state_batch in guider_state:
|
| 139 |
+
components.guider.prepare_models(components.transformer)
|
| 140 |
+
cond_kwargs = {name: getattr(guider_state_batch, name) for name in guider_inputs}
|
| 141 |
+
guider_state_batch.noise_pred = transformer(
|
| 142 |
+
hidden_states=latents,
|
| 143 |
+
timestep=timestep,
|
| 144 |
+
position_ids=block_state.position_ids,
|
| 145 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 146 |
+
return_dict=False,
|
| 147 |
+
**cond_kwargs,
|
| 148 |
+
)[0]
|
| 149 |
+
components.guider.cleanup_models(components.transformer)
|
| 150 |
+
|
| 151 |
+
block_state.noise_pred = components.guider(guider_state).pred
|
| 152 |
+
return components, block_state
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class Krea2TurboLoopDenoiser(ModularPipelineBlocks):
|
| 156 |
+
model_name = "krea2"
|
| 157 |
+
|
| 158 |
+
@property
|
| 159 |
+
def description(self) -> str:
|
| 160 |
+
return (
|
| 161 |
+
"Within the denoising loop: run the `transformer` on the conditional text features. The distilled Krea 2 "
|
| 162 |
+
"turbo checkpoint runs without classifier-free guidance, so there is no negative branch or guider. Compose "
|
| 163 |
+
"into the `sub_blocks` of `Krea2TurboDenoiseStep`."
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 168 |
+
return [ComponentSpec("transformer", Krea2Transformer2DModel)]
|
| 169 |
+
|
| 170 |
+
@property
|
| 171 |
+
def inputs(self) -> list[InputParam]:
|
| 172 |
+
return [
|
| 173 |
+
InputParam(name="latents", required=True, type_hint=torch.Tensor, description="Packed image latents."),
|
| 174 |
+
InputParam(
|
| 175 |
+
name="prompt_embeds",
|
| 176 |
+
required=True,
|
| 177 |
+
type_hint=torch.Tensor,
|
| 178 |
+
description="Conditional stacked text features.",
|
| 179 |
+
),
|
| 180 |
+
InputParam(
|
| 181 |
+
name="prompt_embeds_mask", required=True, type_hint=torch.Tensor, description="Conditional text mask."
|
| 182 |
+
),
|
| 183 |
+
InputParam(
|
| 184 |
+
name="position_ids",
|
| 185 |
+
required=True,
|
| 186 |
+
type_hint=torch.Tensor,
|
| 187 |
+
description="Shared rotary coordinates for the [text | image] sequence.",
|
| 188 |
+
),
|
| 189 |
+
InputParam.template("attention_kwargs"),
|
| 190 |
+
]
|
| 191 |
+
|
| 192 |
+
@torch.no_grad()
|
| 193 |
+
def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 194 |
+
transformer = components.transformer
|
| 195 |
+
|
| 196 |
+
latents = block_state.latents.to(transformer.dtype)
|
| 197 |
+
timestep = block_state.timestep.to(transformer.dtype)
|
| 198 |
+
|
| 199 |
+
block_state.noise_pred = transformer(
|
| 200 |
+
hidden_states=latents,
|
| 201 |
+
timestep=timestep,
|
| 202 |
+
position_ids=block_state.position_ids,
|
| 203 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 204 |
+
encoder_hidden_states=block_state.prompt_embeds.to(transformer.dtype),
|
| 205 |
+
encoder_attention_mask=block_state.prompt_embeds_mask,
|
| 206 |
+
return_dict=False,
|
| 207 |
+
)[0]
|
| 208 |
+
return components, block_state
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
class Krea2LoopAfterDenoiser(ModularPipelineBlocks):
|
| 212 |
+
model_name = "krea2"
|
| 213 |
+
|
| 214 |
+
@property
|
| 215 |
+
def description(self) -> str:
|
| 216 |
+
return "Within the denoising loop: scheduler step. Compose into a `Krea2DenoiseLoopWrapper`-based step."
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 220 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 224 |
+
return [OutputParam(name="latents", type_hint=torch.Tensor, description="The denoised latents.")]
|
| 225 |
+
|
| 226 |
+
@torch.no_grad()
|
| 227 |
+
def __call__(self, components: Krea2ModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 228 |
+
latents_dtype = block_state.latents.dtype
|
| 229 |
+
block_state.latents = components.scheduler.step(
|
| 230 |
+
block_state.noise_pred, t, block_state.latents, return_dict=False
|
| 231 |
+
)[0]
|
| 232 |
+
block_state.latents = block_state.latents.to(latents_dtype)
|
| 233 |
+
return components, block_state
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
class Krea2DenoiseLoopWrapper(LoopSequentialPipelineBlocks):
|
| 237 |
+
model_name = "krea2"
|
| 238 |
+
|
| 239 |
+
@property
|
| 240 |
+
def description(self) -> str:
|
| 241 |
+
return (
|
| 242 |
+
"Pipeline block that iteratively denoises the packed image latents over `timesteps`. "
|
| 243 |
+
"The specific steps within each iteration can be customized with the `sub_blocks` attribute."
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
@property
|
| 247 |
+
def loop_expected_components(self) -> list[ComponentSpec]:
|
| 248 |
+
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 252 |
+
return [
|
| 253 |
+
InputParam(
|
| 254 |
+
name="timesteps",
|
| 255 |
+
required=True,
|
| 256 |
+
type_hint=torch.Tensor,
|
| 257 |
+
description="Denoising timesteps from set_timesteps.",
|
| 258 |
+
),
|
| 259 |
+
InputParam.template("num_inference_steps", required=True),
|
| 260 |
+
InputParam.template("attention_kwargs"),
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
@torch.no_grad()
|
| 264 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 265 |
+
block_state = self.get_block_state(state)
|
| 266 |
+
|
| 267 |
+
with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
|
| 268 |
+
for i, t in enumerate(block_state.timesteps):
|
| 269 |
+
components, block_state = self.loop_step(components, block_state, i=i, t=t)
|
| 270 |
+
progress_bar.update()
|
| 271 |
+
|
| 272 |
+
self.set_block_state(state, block_state)
|
| 273 |
+
return components, state
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# auto_docstring
|
| 277 |
+
class Krea2DenoiseStep(Krea2DenoiseLoopWrapper):
|
| 278 |
+
"""
|
| 279 |
+
Denoising loop that iteratively denoises the packed image latents over `timesteps`, running the transformer on the
|
| 280 |
+
conditional (and, when the guider enables CFG, the negative) text features and combining them through the `guider`.
|
| 281 |
+
|
| 282 |
+
Components:
|
| 283 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) guider (`ClassifierFreeGuidance`) transformer
|
| 284 |
+
(`Krea2Transformer2DModel`)
|
| 285 |
+
|
| 286 |
+
Inputs:
|
| 287 |
+
timesteps (`Tensor`):
|
| 288 |
+
Denoising timesteps from set_timesteps.
|
| 289 |
+
num_inference_steps (`int`):
|
| 290 |
+
The number of denoising steps.
|
| 291 |
+
attention_kwargs (`dict`, *optional*):
|
| 292 |
+
Additional kwargs for attention processors.
|
| 293 |
+
latents (`Tensor`):
|
| 294 |
+
Packed image latents.
|
| 295 |
+
batch_size (`int`):
|
| 296 |
+
Effective batch size.
|
| 297 |
+
prompt_embeds (`Tensor`):
|
| 298 |
+
Conditional stacked text features.
|
| 299 |
+
prompt_embeds_mask (`Tensor`):
|
| 300 |
+
Conditional text mask.
|
| 301 |
+
position_ids (`Tensor`):
|
| 302 |
+
Shared rotary coordinates for the [text | image] sequence.
|
| 303 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 304 |
+
Negative stacked text features.
|
| 305 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 306 |
+
Negative text mask.
|
| 307 |
+
|
| 308 |
+
Outputs:
|
| 309 |
+
latents (`Tensor`):
|
| 310 |
+
The denoised latents.
|
| 311 |
+
"""
|
| 312 |
+
|
| 313 |
+
model_name = "krea2"
|
| 314 |
+
block_classes = [Krea2LoopBeforeDenoiser, Krea2LoopDenoiser, Krea2LoopAfterDenoiser]
|
| 315 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 316 |
+
|
| 317 |
+
@property
|
| 318 |
+
def description(self) -> str:
|
| 319 |
+
return (
|
| 320 |
+
"Denoising loop that iteratively denoises the packed image latents over `timesteps`, running the "
|
| 321 |
+
"transformer on the conditional (and, when the guider enables CFG, the negative) text features and "
|
| 322 |
+
"combining them through the `guider`."
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# auto_docstring
|
| 327 |
+
class Krea2TurboDenoiseStep(Krea2DenoiseLoopWrapper):
|
| 328 |
+
"""
|
| 329 |
+
Denoising loop for the distilled Krea 2 turbo checkpoint that iteratively denoises the packed image latents over
|
| 330 |
+
`timesteps`, running the transformer on the conditional text features. The distilled checkpoint runs without
|
| 331 |
+
classifier-free guidance.
|
| 332 |
+
|
| 333 |
+
Components:
|
| 334 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) transformer (`Krea2Transformer2DModel`)
|
| 335 |
+
|
| 336 |
+
Inputs:
|
| 337 |
+
timesteps (`Tensor`):
|
| 338 |
+
Denoising timesteps from set_timesteps.
|
| 339 |
+
num_inference_steps (`int`):
|
| 340 |
+
The number of denoising steps.
|
| 341 |
+
attention_kwargs (`dict`, *optional*):
|
| 342 |
+
Additional kwargs for attention processors.
|
| 343 |
+
latents (`Tensor`):
|
| 344 |
+
Packed image latents.
|
| 345 |
+
batch_size (`int`):
|
| 346 |
+
Effective batch size.
|
| 347 |
+
prompt_embeds (`Tensor`):
|
| 348 |
+
Conditional stacked text features.
|
| 349 |
+
prompt_embeds_mask (`Tensor`):
|
| 350 |
+
Conditional text mask.
|
| 351 |
+
position_ids (`Tensor`):
|
| 352 |
+
Shared rotary coordinates for the [text | image] sequence.
|
| 353 |
+
|
| 354 |
+
Outputs:
|
| 355 |
+
latents (`Tensor`):
|
| 356 |
+
The denoised latents.
|
| 357 |
+
"""
|
| 358 |
+
|
| 359 |
+
model_name = "krea2"
|
| 360 |
+
block_classes = [Krea2LoopBeforeDenoiser, Krea2TurboLoopDenoiser, Krea2LoopAfterDenoiser]
|
| 361 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 362 |
+
|
| 363 |
+
@property
|
| 364 |
+
def description(self) -> str:
|
| 365 |
+
return (
|
| 366 |
+
"Denoising loop for the distilled Krea 2 turbo checkpoint that iteratively denoises the packed image "
|
| 367 |
+
"latents over `timesteps`, running the transformer on the conditional text features. The distilled "
|
| 368 |
+
"checkpoint runs without classifier-free guidance."
|
| 369 |
+
)
|
diffusers/modular_pipelines/krea2/encoders.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from transformers import AutoTokenizer, Qwen3VLModel
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...guiders import ClassifierFreeGuidance
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 23 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 24 |
+
from .modular_pipeline import Krea2ModularPipeline
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Indices into the Qwen3-VL `hidden_states` tuple (0 is the embedding output) whose states are stacked per token as the
|
| 31 |
+
# transformer's text conditioning. Must have `transformer.config.num_text_layers` entries.
|
| 32 |
+
KREA2_TEXT_ENCODER_SELECT_LAYERS = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
|
| 33 |
+
|
| 34 |
+
# Krea 2 wraps the prompt in this Qwen-Image chat template before encoding. The prompt is padded to a fixed length
|
| 35 |
+
# first and the assistant suffix is appended *after* the padding (matching how the model was sampled at training time);
|
| 36 |
+
# the first `_PROMPT_TEMPLATE_ENCODE_START_IDX` (system prefix) tokens are dropped from the encoder outputs.
|
| 37 |
+
_PROMPT_TEMPLATE_ENCODE_PREFIX = (
|
| 38 |
+
"<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
|
| 39 |
+
"spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
|
| 40 |
+
)
|
| 41 |
+
_PROMPT_TEMPLATE_ENCODE_SUFFIX = "<|im_end|>\n<|im_start|>assistant\n"
|
| 42 |
+
_PROMPT_TEMPLATE_ENCODE_START_IDX = 34
|
| 43 |
+
_PROMPT_TEMPLATE_ENCODE_NUM_SUFFIX_TOKENS = 5
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# auto_docstring
|
| 47 |
+
class Krea2TextEncoderStep(ModularPipelineBlocks):
|
| 48 |
+
"""
|
| 49 |
+
Text encoder step that tokenizes the prompt(s) with the Krea 2 chat template, runs the Qwen3-VL text encoder, and
|
| 50 |
+
stacks a fixed set of decoder-layer hidden states per token as the transformer's text conditioning. The negative
|
| 51 |
+
prompt is encoded the same way when the guider enables CFG.
|
| 52 |
+
|
| 53 |
+
Components:
|
| 54 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
|
| 55 |
+
with the text encoder. guider (`ClassifierFreeGuidance`)
|
| 56 |
+
|
| 57 |
+
Inputs:
|
| 58 |
+
prompt (`str`):
|
| 59 |
+
The prompt or prompts to guide image generation.
|
| 60 |
+
negative_prompt (`str`, *optional*):
|
| 61 |
+
The negative prompt(s) for CFG.
|
| 62 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 63 |
+
Maximum sequence length for prompt encoding.
|
| 64 |
+
|
| 65 |
+
Outputs:
|
| 66 |
+
prompt_embeds (`Tensor`):
|
| 67 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 68 |
+
prompt_embeds_mask (`Tensor`):
|
| 69 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 70 |
+
negative_prompt_embeds (`Tensor`):
|
| 71 |
+
Per-prompt negative text features (only when guidance is enabled).
|
| 72 |
+
negative_prompt_embeds_mask (`Tensor`):
|
| 73 |
+
Per-prompt negative text mask (only when guidance is enabled).
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
model_name = "krea2"
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def description(self) -> str:
|
| 80 |
+
return (
|
| 81 |
+
"Text encoder step that tokenizes the prompt(s) with the Krea 2 chat template, runs the Qwen3-VL text "
|
| 82 |
+
"encoder, and stacks a fixed set of decoder-layer hidden states per token as the transformer's text "
|
| 83 |
+
"conditioning. The negative prompt is encoded the same way when the guider enables CFG."
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 88 |
+
return [
|
| 89 |
+
ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
|
| 90 |
+
ComponentSpec("tokenizer", AutoTokenizer, description="The tokenizer paired with the text encoder."),
|
| 91 |
+
ComponentSpec(
|
| 92 |
+
"guider",
|
| 93 |
+
ClassifierFreeGuidance,
|
| 94 |
+
config=FrozenDict({"guidance_scale": 4.5, "use_original_formulation": True}),
|
| 95 |
+
default_creation_method="from_config",
|
| 96 |
+
),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def inputs(self) -> list[InputParam]:
|
| 101 |
+
return [
|
| 102 |
+
InputParam.template("prompt", required=True),
|
| 103 |
+
InputParam(name="negative_prompt", type_hint=str, description="The negative prompt(s) for CFG."),
|
| 104 |
+
InputParam.template("max_sequence_length", default=512),
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 109 |
+
return [
|
| 110 |
+
OutputParam(
|
| 111 |
+
name="prompt_embeds",
|
| 112 |
+
type_hint=torch.Tensor,
|
| 113 |
+
description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
|
| 114 |
+
),
|
| 115 |
+
OutputParam(
|
| 116 |
+
name="prompt_embeds_mask",
|
| 117 |
+
type_hint=torch.Tensor,
|
| 118 |
+
description="Per-prompt boolean text mask (B, text_seq_len).",
|
| 119 |
+
),
|
| 120 |
+
OutputParam(
|
| 121 |
+
name="negative_prompt_embeds",
|
| 122 |
+
type_hint=torch.Tensor,
|
| 123 |
+
description="Per-prompt negative text features (only when guidance is enabled).",
|
| 124 |
+
),
|
| 125 |
+
OutputParam(
|
| 126 |
+
name="negative_prompt_embeds_mask",
|
| 127 |
+
type_hint=torch.Tensor,
|
| 128 |
+
description="Per-prompt negative text mask (only when guidance is enabled).",
|
| 129 |
+
),
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
def _encode_prompt(self, components, prompt, max_sequence_length, device):
|
| 133 |
+
"""Tokenize `prompt` into the fixed-length Krea 2 layout and tap the selected encoder hidden states.
|
| 134 |
+
|
| 135 |
+
Mirrors `Krea2Pipeline.get_text_hidden_states`. Returns a `(hidden_states, attention_mask)` tuple of shapes
|
| 136 |
+
`(batch_size, text_seq_len, num_text_layers, text_hidden_dim)` and `(batch_size, text_seq_len)` (bool).
|
| 137 |
+
"""
|
| 138 |
+
tokenizer = components.tokenizer
|
| 139 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 140 |
+
prefix_idx = _PROMPT_TEMPLATE_ENCODE_START_IDX
|
| 141 |
+
text = [_PROMPT_TEMPLATE_ENCODE_PREFIX + e for e in prompt]
|
| 142 |
+
text_tokens = tokenizer(
|
| 143 |
+
text,
|
| 144 |
+
truncation=True,
|
| 145 |
+
padding="max_length",
|
| 146 |
+
max_length=max_sequence_length + prefix_idx - _PROMPT_TEMPLATE_ENCODE_NUM_SUFFIX_TOKENS,
|
| 147 |
+
return_tensors="pt",
|
| 148 |
+
).to(device)
|
| 149 |
+
suffix_tokens = tokenizer([_PROMPT_TEMPLATE_ENCODE_SUFFIX] * len(text), return_tensors="pt").to(device)
|
| 150 |
+
|
| 151 |
+
input_ids = torch.cat([text_tokens.input_ids, suffix_tokens.input_ids], dim=1)
|
| 152 |
+
attention_mask = torch.cat([text_tokens.attention_mask, suffix_tokens.attention_mask], dim=1).bool()
|
| 153 |
+
|
| 154 |
+
# Krea 2 pads in the middle of the template (`[prefix | prompt | PAD | suffix]`), so the suffix tokens sit
|
| 155 |
+
# downstream of the padding. The text features must use positions that count only real tokens (padding does
|
| 156 |
+
# not consume a position) to match how the model was trained; otherwise the suffix gets a shifted mRoPE phase.
|
| 157 |
+
position_ids = (attention_mask.long().cumsum(dim=-1) - 1).clamp(min=0)
|
| 158 |
+
position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
|
| 159 |
+
|
| 160 |
+
outputs = components.text_encoder(
|
| 161 |
+
input_ids=input_ids,
|
| 162 |
+
attention_mask=attention_mask,
|
| 163 |
+
position_ids=position_ids,
|
| 164 |
+
output_hidden_states=True,
|
| 165 |
+
)
|
| 166 |
+
hidden_states = torch.stack([outputs.hidden_states[i] for i in KREA2_TEXT_ENCODER_SELECT_LAYERS], dim=2)
|
| 167 |
+
|
| 168 |
+
hidden_states = hidden_states[:, prefix_idx:]
|
| 169 |
+
attention_mask = attention_mask[:, prefix_idx:]
|
| 170 |
+
return hidden_states, attention_mask
|
| 171 |
+
|
| 172 |
+
@torch.no_grad()
|
| 173 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 174 |
+
block_state = self.get_block_state(state)
|
| 175 |
+
|
| 176 |
+
device = components._execution_device
|
| 177 |
+
prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
|
| 178 |
+
|
| 179 |
+
block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
|
| 180 |
+
components, prompts, block_state.max_sequence_length, device
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
block_state.negative_prompt_embeds = None
|
| 184 |
+
block_state.negative_prompt_embeds_mask = None
|
| 185 |
+
if components.requires_unconditional_embeds:
|
| 186 |
+
negative_prompt = block_state.negative_prompt
|
| 187 |
+
if negative_prompt is None:
|
| 188 |
+
negative_prompt = ""
|
| 189 |
+
if isinstance(negative_prompt, str):
|
| 190 |
+
negative_prompt = [negative_prompt] * len(prompts)
|
| 191 |
+
block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = self._encode_prompt(
|
| 192 |
+
components, negative_prompt, block_state.max_sequence_length, device
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
self.set_block_state(state, block_state)
|
| 196 |
+
return components, state
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# auto_docstring
|
| 200 |
+
class Krea2TurboTextEncoderStep(Krea2TextEncoderStep):
|
| 201 |
+
"""
|
| 202 |
+
Text encoder step for the distilled Krea 2 turbo checkpoint that tokenizes the prompt(s) with the Krea 2 chat
|
| 203 |
+
template, runs the Qwen3-VL text encoder, and stacks a fixed set of decoder-layer hidden states per token as the
|
| 204 |
+
transformer's text conditioning. The distilled checkpoint runs without classifier-free guidance, so it takes no
|
| 205 |
+
negative prompt and has no guider.
|
| 206 |
+
|
| 207 |
+
Components:
|
| 208 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
|
| 209 |
+
with the text encoder.
|
| 210 |
+
|
| 211 |
+
Inputs:
|
| 212 |
+
prompt (`str`):
|
| 213 |
+
The prompt or prompts to guide image generation.
|
| 214 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 215 |
+
Maximum sequence length for prompt encoding.
|
| 216 |
+
|
| 217 |
+
Outputs:
|
| 218 |
+
prompt_embeds (`Tensor`):
|
| 219 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 220 |
+
prompt_embeds_mask (`Tensor`):
|
| 221 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
model_name = "krea2"
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def description(self) -> str:
|
| 228 |
+
return (
|
| 229 |
+
"Text encoder step for the distilled Krea 2 turbo checkpoint that tokenizes the prompt(s) with the Krea 2 "
|
| 230 |
+
"chat template, runs the Qwen3-VL text encoder, and stacks a fixed set of decoder-layer hidden states per "
|
| 231 |
+
"token as the transformer's text conditioning. The distilled checkpoint runs without classifier-free "
|
| 232 |
+
"guidance, so it takes no negative prompt and has no guider."
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
@property
|
| 236 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 237 |
+
return [
|
| 238 |
+
ComponentSpec("text_encoder", Qwen3VLModel, description="The Qwen3-VL text encoder."),
|
| 239 |
+
ComponentSpec("tokenizer", AutoTokenizer, description="The tokenizer paired with the text encoder."),
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
@property
|
| 243 |
+
def inputs(self) -> list[InputParam]:
|
| 244 |
+
return [
|
| 245 |
+
InputParam.template("prompt", required=True),
|
| 246 |
+
InputParam.template("max_sequence_length", default=512),
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
@property
|
| 250 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 251 |
+
return [
|
| 252 |
+
OutputParam(
|
| 253 |
+
name="prompt_embeds",
|
| 254 |
+
type_hint=torch.Tensor,
|
| 255 |
+
description="Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).",
|
| 256 |
+
),
|
| 257 |
+
OutputParam(
|
| 258 |
+
name="prompt_embeds_mask",
|
| 259 |
+
type_hint=torch.Tensor,
|
| 260 |
+
description="Per-prompt boolean text mask (B, text_seq_len).",
|
| 261 |
+
),
|
| 262 |
+
]
|
| 263 |
+
|
| 264 |
+
@torch.no_grad()
|
| 265 |
+
def __call__(self, components: Krea2ModularPipeline, state: PipelineState) -> PipelineState:
|
| 266 |
+
block_state = self.get_block_state(state)
|
| 267 |
+
|
| 268 |
+
device = components._execution_device
|
| 269 |
+
prompts = [block_state.prompt] if isinstance(block_state.prompt, str) else list(block_state.prompt)
|
| 270 |
+
|
| 271 |
+
block_state.prompt_embeds, block_state.prompt_embeds_mask = self._encode_prompt(
|
| 272 |
+
components, prompts, block_state.max_sequence_length, device
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
self.set_block_state(state, block_state)
|
| 276 |
+
return components, state
|
diffusers/modular_pipelines/krea2/modular_blocks_krea2.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Krea2PrepareLatentsStep,
|
| 21 |
+
Krea2PreparePositionIdsStep,
|
| 22 |
+
Krea2SetTimestepsStep,
|
| 23 |
+
Krea2TextInputsStep,
|
| 24 |
+
)
|
| 25 |
+
from .decoders import Krea2DecodeStep
|
| 26 |
+
from .denoise import Krea2DenoiseStep
|
| 27 |
+
from .encoders import Krea2TextEncoderStep
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
CORE_DENOISE_BLOCKS = InsertableDict(
|
| 34 |
+
[
|
| 35 |
+
("input", Krea2TextInputsStep()),
|
| 36 |
+
("prepare_latents", Krea2PrepareLatentsStep()),
|
| 37 |
+
("set_timesteps", Krea2SetTimestepsStep()),
|
| 38 |
+
("prepare_position_ids", Krea2PreparePositionIdsStep()),
|
| 39 |
+
("denoise", Krea2DenoiseStep()),
|
| 40 |
+
]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# auto_docstring
|
| 45 |
+
class Krea2CoreDenoiseStep(SequentialPipelineBlocks):
|
| 46 |
+
"""
|
| 47 |
+
Core denoising workflow for Krea 2 text-to-image: prepares the batch/latents/timesteps and the shared position ids,
|
| 48 |
+
then runs the symmetric-CFG denoising loop, producing the denoised packed latents for the decoder.
|
| 49 |
+
|
| 50 |
+
Components:
|
| 51 |
+
transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`) guider
|
| 52 |
+
(`ClassifierFreeGuidance`)
|
| 53 |
+
|
| 54 |
+
Inputs:
|
| 55 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 56 |
+
The number of images to generate per prompt.
|
| 57 |
+
prompt_embeds (`Tensor`):
|
| 58 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 59 |
+
prompt_embeds_mask (`Tensor`):
|
| 60 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 61 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 62 |
+
Per-prompt negative text features.
|
| 63 |
+
negative_prompt_embeds_mask (`Tensor`, *optional*):
|
| 64 |
+
Per-prompt negative text mask.
|
| 65 |
+
latents (`Tensor`, *optional*):
|
| 66 |
+
Pre-generated noisy latents for image generation.
|
| 67 |
+
height (`int`, *optional*, defaults to 1024):
|
| 68 |
+
The height in pixels of the generated image.
|
| 69 |
+
width (`int`, *optional*, defaults to 1024):
|
| 70 |
+
The width in pixels of the generated image.
|
| 71 |
+
generator (`Generator`, *optional*):
|
| 72 |
+
Torch generator for deterministic generation.
|
| 73 |
+
num_inference_steps (`int`, *optional*, defaults to 28):
|
| 74 |
+
The number of denoising steps.
|
| 75 |
+
sigmas (`list`, *optional*):
|
| 76 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 77 |
+
attention_kwargs (`dict`, *optional*):
|
| 78 |
+
Additional kwargs for attention processors.
|
| 79 |
+
|
| 80 |
+
Outputs:
|
| 81 |
+
latents (`Tensor`):
|
| 82 |
+
The denoised packed latents (B, image_seq_len, in_channels).
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
model_name = "krea2"
|
| 86 |
+
block_classes = list(CORE_DENOISE_BLOCKS.values())
|
| 87 |
+
block_names = list(CORE_DENOISE_BLOCKS.keys())
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def description(self) -> str:
|
| 91 |
+
return (
|
| 92 |
+
"Core denoising workflow for Krea 2 text-to-image: prepares the batch/latents/timesteps and the shared "
|
| 93 |
+
"position ids, then runs the symmetric-CFG denoising loop, producing the denoised packed latents for the "
|
| 94 |
+
"decoder."
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
@property
|
| 98 |
+
def outputs(self) -> list[OutputParam]:
|
| 99 |
+
return [
|
| 100 |
+
OutputParam.template("latents", description="The denoised packed latents (B, image_seq_len, in_channels).")
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# auto_docstring
|
| 105 |
+
class Krea2AutoBlocks(SequentialPipelineBlocks):
|
| 106 |
+
"""
|
| 107 |
+
Auto Modular pipeline for text-to-image generation using Krea 2: encode text -> core denoise (symmetric CFG) ->
|
| 108 |
+
decode.
|
| 109 |
+
|
| 110 |
+
Supported workflows:
|
| 111 |
+
- `text2image`: requires `prompt`
|
| 112 |
+
|
| 113 |
+
Components:
|
| 114 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
|
| 115 |
+
with the text encoder. guider (`ClassifierFreeGuidance`) transformer (`Krea2Transformer2DModel`) scheduler
|
| 116 |
+
(`FlowMatchEulerDiscreteScheduler`) vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
|
| 117 |
+
|
| 118 |
+
Inputs:
|
| 119 |
+
prompt (`str`):
|
| 120 |
+
The prompt or prompts to guide image generation.
|
| 121 |
+
negative_prompt (`str`, *optional*):
|
| 122 |
+
The negative prompt(s) for CFG.
|
| 123 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 124 |
+
Maximum sequence length for prompt encoding.
|
| 125 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 126 |
+
The number of images to generate per prompt.
|
| 127 |
+
latents (`Tensor`, *optional*):
|
| 128 |
+
Pre-generated noisy latents for image generation.
|
| 129 |
+
height (`int`, *optional*, defaults to 1024):
|
| 130 |
+
The height in pixels of the generated image.
|
| 131 |
+
width (`int`, *optional*, defaults to 1024):
|
| 132 |
+
The width in pixels of the generated image.
|
| 133 |
+
generator (`Generator`, *optional*):
|
| 134 |
+
Torch generator for deterministic generation.
|
| 135 |
+
num_inference_steps (`int`, *optional*, defaults to 28):
|
| 136 |
+
The number of denoising steps.
|
| 137 |
+
sigmas (`list`, *optional*):
|
| 138 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 139 |
+
attention_kwargs (`dict`, *optional*):
|
| 140 |
+
Additional kwargs for attention processors.
|
| 141 |
+
output_type (`str`, *optional*, defaults to pil):
|
| 142 |
+
Output format: 'pil', 'np', 'pt'.
|
| 143 |
+
|
| 144 |
+
Outputs:
|
| 145 |
+
images (`list`):
|
| 146 |
+
Generated images.
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
model_name = "krea2"
|
| 150 |
+
block_classes = [
|
| 151 |
+
Krea2TextEncoderStep,
|
| 152 |
+
Krea2CoreDenoiseStep,
|
| 153 |
+
Krea2DecodeStep,
|
| 154 |
+
]
|
| 155 |
+
block_names = ["text_encoder", "denoise", "decode"]
|
| 156 |
+
|
| 157 |
+
_workflow_map = {
|
| 158 |
+
"text2image": {"prompt": True},
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def description(self) -> str:
|
| 163 |
+
return (
|
| 164 |
+
"Auto Modular pipeline for text-to-image generation using Krea 2: encode text -> core denoise "
|
| 165 |
+
"(symmetric CFG) -> decode."
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
@property
|
| 169 |
+
def outputs(self) -> list[OutputParam]:
|
| 170 |
+
return [OutputParam.template("images")]
|
diffusers/modular_pipelines/krea2/modular_blocks_krea2_turbo.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
from ...utils import logging
|
| 17 |
+
from ..modular_pipeline import SequentialPipelineBlocks
|
| 18 |
+
from ..modular_pipeline_utils import InsertableDict, OutputParam
|
| 19 |
+
from .before_denoise import (
|
| 20 |
+
Krea2PrepareLatentsStep,
|
| 21 |
+
Krea2PreparePositionIdsStep,
|
| 22 |
+
Krea2TurboSetTimestepsStep,
|
| 23 |
+
Krea2TurboTextInputsStep,
|
| 24 |
+
)
|
| 25 |
+
from .decoders import Krea2DecodeStep
|
| 26 |
+
from .denoise import Krea2TurboDenoiseStep
|
| 27 |
+
from .encoders import Krea2TurboTextEncoderStep
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
CORE_DENOISE_BLOCKS = InsertableDict(
|
| 34 |
+
[
|
| 35 |
+
("input", Krea2TurboTextInputsStep()),
|
| 36 |
+
("prepare_latents", Krea2PrepareLatentsStep()),
|
| 37 |
+
("set_timesteps", Krea2TurboSetTimestepsStep()),
|
| 38 |
+
("prepare_position_ids", Krea2PreparePositionIdsStep()),
|
| 39 |
+
("denoise", Krea2TurboDenoiseStep()),
|
| 40 |
+
]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# auto_docstring
|
| 45 |
+
class Krea2TurboCoreDenoiseStep(SequentialPipelineBlocks):
|
| 46 |
+
"""
|
| 47 |
+
Core denoising workflow for the distilled Krea 2 turbo text-to-image checkpoint: prepares the
|
| 48 |
+
batch/latents/timesteps and the shared position ids, then runs the guidance-free denoising loop, producing the
|
| 49 |
+
denoised packed latents for the decoder.
|
| 50 |
+
|
| 51 |
+
Components:
|
| 52 |
+
transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 53 |
+
|
| 54 |
+
Inputs:
|
| 55 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 56 |
+
The number of images to generate per prompt.
|
| 57 |
+
prompt_embeds (`Tensor`):
|
| 58 |
+
Per-prompt stacked text features (B, text_seq_len, num_text_layers, text_hidden_dim).
|
| 59 |
+
prompt_embeds_mask (`Tensor`):
|
| 60 |
+
Per-prompt boolean text mask (B, text_seq_len).
|
| 61 |
+
latents (`Tensor`, *optional*):
|
| 62 |
+
Pre-generated noisy latents for image generation.
|
| 63 |
+
height (`int`, *optional*, defaults to 1024):
|
| 64 |
+
The height in pixels of the generated image.
|
| 65 |
+
width (`int`, *optional*, defaults to 1024):
|
| 66 |
+
The width in pixels of the generated image.
|
| 67 |
+
generator (`Generator`, *optional*):
|
| 68 |
+
Torch generator for deterministic generation.
|
| 69 |
+
num_inference_steps (`int`, *optional*, defaults to 8):
|
| 70 |
+
The number of denoising steps.
|
| 71 |
+
sigmas (`list`, *optional*):
|
| 72 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 73 |
+
attention_kwargs (`dict`, *optional*):
|
| 74 |
+
Additional kwargs for attention processors.
|
| 75 |
+
|
| 76 |
+
Outputs:
|
| 77 |
+
latents (`Tensor`):
|
| 78 |
+
The denoised packed latents (B, image_seq_len, in_channels).
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
model_name = "krea2"
|
| 82 |
+
block_classes = list(CORE_DENOISE_BLOCKS.values())
|
| 83 |
+
block_names = list(CORE_DENOISE_BLOCKS.keys())
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def description(self) -> str:
|
| 87 |
+
return (
|
| 88 |
+
"Core denoising workflow for the distilled Krea 2 turbo text-to-image checkpoint: prepares the "
|
| 89 |
+
"batch/latents/timesteps and the shared position ids, then runs the guidance-free denoising loop, "
|
| 90 |
+
"producing the denoised packed latents for the decoder."
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def outputs(self) -> list[OutputParam]:
|
| 95 |
+
return [
|
| 96 |
+
OutputParam.template("latents", description="The denoised packed latents (B, image_seq_len, in_channels).")
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# auto_docstring
|
| 101 |
+
class Krea2TurboAutoBlocks(SequentialPipelineBlocks):
|
| 102 |
+
"""
|
| 103 |
+
Auto Modular pipeline for text-to-image generation using the distilled Krea 2 turbo checkpoint: encode text -> core
|
| 104 |
+
denoise (guidance-free) -> decode.
|
| 105 |
+
|
| 106 |
+
Supported workflows:
|
| 107 |
+
- `text2image`: requires `prompt`
|
| 108 |
+
|
| 109 |
+
Components:
|
| 110 |
+
text_encoder (`Qwen3VLModel`): The Qwen3-VL text encoder. tokenizer (`AutoTokenizer`): The tokenizer paired
|
| 111 |
+
with the text encoder. transformer (`Krea2Transformer2DModel`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 112 |
+
vae (`AutoencoderKLQwenImage`) image_processor (`VaeImageProcessor`)
|
| 113 |
+
|
| 114 |
+
Inputs:
|
| 115 |
+
prompt (`str`):
|
| 116 |
+
The prompt or prompts to guide image generation.
|
| 117 |
+
max_sequence_length (`int`, *optional*, defaults to 512):
|
| 118 |
+
Maximum sequence length for prompt encoding.
|
| 119 |
+
num_images_per_prompt (`int`, *optional*, defaults to 1):
|
| 120 |
+
The number of images to generate per prompt.
|
| 121 |
+
latents (`Tensor`, *optional*):
|
| 122 |
+
Pre-generated noisy latents for image generation.
|
| 123 |
+
height (`int`, *optional*, defaults to 1024):
|
| 124 |
+
The height in pixels of the generated image.
|
| 125 |
+
width (`int`, *optional*, defaults to 1024):
|
| 126 |
+
The width in pixels of the generated image.
|
| 127 |
+
generator (`Generator`, *optional*):
|
| 128 |
+
Torch generator for deterministic generation.
|
| 129 |
+
num_inference_steps (`int`, *optional*, defaults to 8):
|
| 130 |
+
The number of denoising steps.
|
| 131 |
+
sigmas (`list`, *optional*):
|
| 132 |
+
Custom sigma schedule (defaults to a linear ramp).
|
| 133 |
+
attention_kwargs (`dict`, *optional*):
|
| 134 |
+
Additional kwargs for attention processors.
|
| 135 |
+
output_type (`str`, *optional*, defaults to pil):
|
| 136 |
+
Output format: 'pil', 'np', 'pt'.
|
| 137 |
+
|
| 138 |
+
Outputs:
|
| 139 |
+
images (`list`):
|
| 140 |
+
Generated images.
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
model_name = "krea2"
|
| 144 |
+
block_classes = [
|
| 145 |
+
Krea2TurboTextEncoderStep,
|
| 146 |
+
Krea2TurboCoreDenoiseStep,
|
| 147 |
+
Krea2DecodeStep,
|
| 148 |
+
]
|
| 149 |
+
block_names = ["text_encoder", "denoise", "decode"]
|
| 150 |
+
|
| 151 |
+
_workflow_map = {
|
| 152 |
+
"text2image": {"prompt": True},
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
@property
|
| 156 |
+
def description(self) -> str:
|
| 157 |
+
return (
|
| 158 |
+
"Auto Modular pipeline for text-to-image generation using the distilled Krea 2 turbo checkpoint: encode "
|
| 159 |
+
"text -> core denoise (guidance-free) -> decode."
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
@property
|
| 163 |
+
def outputs(self) -> list[OutputParam]:
|
| 164 |
+
return [OutputParam.template("images")]
|
diffusers/modular_pipelines/krea2/modular_pipeline.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 Krea AI and The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ...loaders import Krea2LoraLoaderMixin
|
| 16 |
+
from ..modular_pipeline import ModularPipeline
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Krea2ModularPipeline(ModularPipeline, Krea2LoraLoaderMixin):
|
| 20 |
+
"""
|
| 21 |
+
A ModularPipeline for Krea 2.
|
| 22 |
+
|
| 23 |
+
> [!WARNING] > This is an experimental feature!
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
default_blocks_name = "Krea2AutoBlocks"
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def patch_size(self):
|
| 30 |
+
return 2
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def default_height(self):
|
| 34 |
+
return 1024
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def default_width(self):
|
| 38 |
+
return 1024
|
| 39 |
+
|
| 40 |
+
@property
|
| 41 |
+
def vae_scale_factor(self):
|
| 42 |
+
vae_scale_factor = 8
|
| 43 |
+
if getattr(self, "vae", None) is not None:
|
| 44 |
+
vae_scale_factor = 2 ** len(self.vae.temperal_downsample)
|
| 45 |
+
return vae_scale_factor
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def requires_unconditional_embeds(self):
|
| 49 |
+
requires_unconditional_embeds = False
|
| 50 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 51 |
+
requires_unconditional_embeds = self.guider._enabled and self.guider.num_conditions > 1
|
| 52 |
+
return requires_unconditional_embeds
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Krea2TurboModularPipeline(Krea2ModularPipeline):
|
| 56 |
+
"""
|
| 57 |
+
A ModularPipeline for the distilled Krea 2 turbo (TDM) checkpoint. It runs without classifier-free guidance, so it
|
| 58 |
+
takes no negative prompt and has no guider.
|
| 59 |
+
|
| 60 |
+
> [!WARNING] > This is an experimental feature!
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
default_blocks_name = "Krea2TurboAutoBlocks"
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def requires_unconditional_embeds(self):
|
| 67 |
+
return False
|
diffusers/modular_pipelines/ltx/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_ltx"] = ["LTXAutoBlocks", "LTXBlocks", "LTXImage2VideoBlocks"]
|
| 25 |
+
_import_structure["modular_pipeline"] = ["LTXModularPipeline"]
|
| 26 |
+
|
| 27 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 28 |
+
try:
|
| 29 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 30 |
+
raise OptionalDependencyNotAvailable()
|
| 31 |
+
except OptionalDependencyNotAvailable:
|
| 32 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 33 |
+
else:
|
| 34 |
+
from .modular_blocks_ltx import LTXAutoBlocks, LTXBlocks, LTXImage2VideoBlocks
|
| 35 |
+
from .modular_pipeline import LTXModularPipeline
|
| 36 |
+
else:
|
| 37 |
+
import sys
|
| 38 |
+
|
| 39 |
+
sys.modules[__name__] = _LazyModule(
|
| 40 |
+
__name__,
|
| 41 |
+
globals()["__file__"],
|
| 42 |
+
_import_structure,
|
| 43 |
+
module_spec=__spec__,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
for name, value in _dummy_objects.items():
|
| 47 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/ltx/before_denoise.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import inspect
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
from ...configuration_utils import FrozenDict
|
| 21 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 22 |
+
from ...utils import logging
|
| 23 |
+
from ...utils.torch_utils import randn_tensor
|
| 24 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 25 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 26 |
+
from .modular_pipeline import LTXModularPipeline, LTXVideoPachifier
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = logging.get_logger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def calculate_shift(
|
| 33 |
+
image_seq_len,
|
| 34 |
+
base_seq_len: int = 256,
|
| 35 |
+
max_seq_len: int = 4096,
|
| 36 |
+
base_shift: float = 0.5,
|
| 37 |
+
max_shift: float = 1.15,
|
| 38 |
+
):
|
| 39 |
+
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
| 40 |
+
b = base_shift - m * base_seq_len
|
| 41 |
+
mu = image_seq_len * m + b
|
| 42 |
+
return mu
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
|
| 46 |
+
def retrieve_timesteps(
|
| 47 |
+
scheduler,
|
| 48 |
+
num_inference_steps: int | None = None,
|
| 49 |
+
device: str | torch.device | None = None,
|
| 50 |
+
timesteps: list[int] | None = None,
|
| 51 |
+
sigmas: list[float] | None = None,
|
| 52 |
+
**kwargs,
|
| 53 |
+
):
|
| 54 |
+
r"""
|
| 55 |
+
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
|
| 56 |
+
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
scheduler (`SchedulerMixin`):
|
| 60 |
+
The scheduler to get timesteps from.
|
| 61 |
+
num_inference_steps (`int`):
|
| 62 |
+
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
|
| 63 |
+
must be `None`.
|
| 64 |
+
device (`str` or `torch.device`, *optional*):
|
| 65 |
+
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
| 66 |
+
timesteps (`list[int]`, *optional*):
|
| 67 |
+
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
|
| 68 |
+
`num_inference_steps` and `sigmas` must be `None`.
|
| 69 |
+
sigmas (`list[float]`, *optional*):
|
| 70 |
+
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
|
| 71 |
+
`num_inference_steps` and `timesteps` must be `None`.
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
`tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
|
| 75 |
+
second element is the number of inference steps.
|
| 76 |
+
"""
|
| 77 |
+
if timesteps is not None and sigmas is not None:
|
| 78 |
+
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
|
| 79 |
+
if timesteps is not None:
|
| 80 |
+
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
| 81 |
+
if not accepts_timesteps:
|
| 82 |
+
raise ValueError(
|
| 83 |
+
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
| 84 |
+
f" timestep schedules. Please check whether you are using the correct scheduler."
|
| 85 |
+
)
|
| 86 |
+
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
| 87 |
+
timesteps = scheduler.timesteps
|
| 88 |
+
num_inference_steps = len(timesteps)
|
| 89 |
+
elif sigmas is not None:
|
| 90 |
+
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
| 91 |
+
if not accept_sigmas:
|
| 92 |
+
raise ValueError(
|
| 93 |
+
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
| 94 |
+
f" sigmas schedules. Please check whether you are using the correct scheduler."
|
| 95 |
+
)
|
| 96 |
+
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
| 97 |
+
timesteps = scheduler.timesteps
|
| 98 |
+
num_inference_steps = len(timesteps)
|
| 99 |
+
else:
|
| 100 |
+
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
| 101 |
+
timesteps = scheduler.timesteps
|
| 102 |
+
return timesteps, num_inference_steps
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class LTXTextInputStep(ModularPipelineBlocks):
|
| 106 |
+
model_name = "ltx"
|
| 107 |
+
|
| 108 |
+
@property
|
| 109 |
+
def description(self) -> str:
|
| 110 |
+
return (
|
| 111 |
+
"Input processing step that:\n"
|
| 112 |
+
" 1. Determines `batch_size` and `dtype` based on `prompt_embeds`\n"
|
| 113 |
+
" 2. Adjusts input tensor shapes based on `batch_size` and `num_videos_per_prompt`"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def inputs(self) -> list[InputParam]:
|
| 118 |
+
return [
|
| 119 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 120 |
+
InputParam.template("prompt_embeds", required=True),
|
| 121 |
+
InputParam.template("prompt_embeds_mask", name="prompt_attention_mask"),
|
| 122 |
+
InputParam.template("negative_prompt_embeds"),
|
| 123 |
+
InputParam.template("negative_prompt_embeds_mask", name="negative_prompt_attention_mask"),
|
| 124 |
+
]
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 128 |
+
return [
|
| 129 |
+
OutputParam("batch_size", type_hint=int),
|
| 130 |
+
OutputParam("dtype", type_hint=torch.dtype),
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
@torch.no_grad()
|
| 134 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 135 |
+
block_state = self.get_block_state(state)
|
| 136 |
+
|
| 137 |
+
block_state.batch_size = block_state.prompt_embeds.shape[0]
|
| 138 |
+
block_state.dtype = block_state.prompt_embeds.dtype
|
| 139 |
+
num_videos = block_state.num_videos_per_prompt
|
| 140 |
+
|
| 141 |
+
# Repeat prompt_embeds for num_videos_per_prompt
|
| 142 |
+
_, seq_len, _ = block_state.prompt_embeds.shape
|
| 143 |
+
block_state.prompt_embeds = block_state.prompt_embeds.repeat(1, num_videos, 1)
|
| 144 |
+
block_state.prompt_embeds = block_state.prompt_embeds.view(block_state.batch_size * num_videos, seq_len, -1)
|
| 145 |
+
|
| 146 |
+
if block_state.prompt_attention_mask is not None:
|
| 147 |
+
block_state.prompt_attention_mask = block_state.prompt_attention_mask.repeat(num_videos, 1)
|
| 148 |
+
|
| 149 |
+
if block_state.negative_prompt_embeds is not None:
|
| 150 |
+
_, seq_len, _ = block_state.negative_prompt_embeds.shape
|
| 151 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.repeat(1, num_videos, 1)
|
| 152 |
+
block_state.negative_prompt_embeds = block_state.negative_prompt_embeds.view(
|
| 153 |
+
block_state.batch_size * num_videos, seq_len, -1
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
if block_state.negative_prompt_attention_mask is not None:
|
| 157 |
+
block_state.negative_prompt_attention_mask = block_state.negative_prompt_attention_mask.repeat(
|
| 158 |
+
num_videos, 1
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
self.set_block_state(state, block_state)
|
| 162 |
+
return components, state
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class LTXSetTimestepsStep(ModularPipelineBlocks):
|
| 166 |
+
model_name = "ltx"
|
| 167 |
+
|
| 168 |
+
@property
|
| 169 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 170 |
+
return [
|
| 171 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
@property
|
| 175 |
+
def description(self) -> str:
|
| 176 |
+
return "Step that sets the scheduler's timesteps for inference"
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def inputs(self) -> list[InputParam]:
|
| 180 |
+
return [
|
| 181 |
+
InputParam.template("num_inference_steps"),
|
| 182 |
+
InputParam.template("timesteps"),
|
| 183 |
+
InputParam.template("sigmas"),
|
| 184 |
+
InputParam.template("height", default=512),
|
| 185 |
+
InputParam.template("width", default=704),
|
| 186 |
+
InputParam("num_frames", type_hint=int, default=161),
|
| 187 |
+
InputParam("frame_rate", type_hint=int, default=25),
|
| 188 |
+
]
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 192 |
+
return [
|
| 193 |
+
OutputParam("timesteps", type_hint=torch.Tensor),
|
| 194 |
+
OutputParam("num_inference_steps", type_hint=int),
|
| 195 |
+
OutputParam("rope_interpolation_scale", type_hint=tuple),
|
| 196 |
+
]
|
| 197 |
+
|
| 198 |
+
@torch.no_grad()
|
| 199 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 200 |
+
block_state = self.get_block_state(state)
|
| 201 |
+
device = components._execution_device
|
| 202 |
+
|
| 203 |
+
height = block_state.height
|
| 204 |
+
width = block_state.width
|
| 205 |
+
num_frames = block_state.num_frames
|
| 206 |
+
frame_rate = block_state.frame_rate
|
| 207 |
+
|
| 208 |
+
latent_num_frames = (num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 209 |
+
latent_height = height // components.vae_spatial_compression_ratio
|
| 210 |
+
latent_width = width // components.vae_spatial_compression_ratio
|
| 211 |
+
video_sequence_length = latent_num_frames * latent_height * latent_width
|
| 212 |
+
|
| 213 |
+
custom_timesteps = block_state.timesteps
|
| 214 |
+
sigmas = block_state.sigmas
|
| 215 |
+
|
| 216 |
+
if custom_timesteps is not None:
|
| 217 |
+
# User provided custom timesteps, don't compute sigmas
|
| 218 |
+
block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
|
| 219 |
+
components.scheduler,
|
| 220 |
+
block_state.num_inference_steps,
|
| 221 |
+
device,
|
| 222 |
+
custom_timesteps,
|
| 223 |
+
)
|
| 224 |
+
else:
|
| 225 |
+
if sigmas is None:
|
| 226 |
+
sigmas = np.linspace(1.0, 1 / block_state.num_inference_steps, block_state.num_inference_steps)
|
| 227 |
+
|
| 228 |
+
mu = calculate_shift(
|
| 229 |
+
video_sequence_length,
|
| 230 |
+
components.scheduler.config.get("base_image_seq_len", 256),
|
| 231 |
+
components.scheduler.config.get("max_image_seq_len", 4096),
|
| 232 |
+
components.scheduler.config.get("base_shift", 0.5),
|
| 233 |
+
components.scheduler.config.get("max_shift", 1.15),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
block_state.timesteps, block_state.num_inference_steps = retrieve_timesteps(
|
| 237 |
+
components.scheduler,
|
| 238 |
+
block_state.num_inference_steps,
|
| 239 |
+
device,
|
| 240 |
+
sigmas=sigmas,
|
| 241 |
+
mu=mu,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
block_state.rope_interpolation_scale = (
|
| 245 |
+
components.vae_temporal_compression_ratio / frame_rate,
|
| 246 |
+
components.vae_spatial_compression_ratio,
|
| 247 |
+
components.vae_spatial_compression_ratio,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
self.set_block_state(state, block_state)
|
| 251 |
+
return components, state
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class LTXPrepareLatentsStep(ModularPipelineBlocks):
|
| 255 |
+
model_name = "ltx"
|
| 256 |
+
|
| 257 |
+
@property
|
| 258 |
+
def description(self) -> str:
|
| 259 |
+
return "Prepare latents step that prepares the latents for the text-to-video generation process"
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 263 |
+
return [
|
| 264 |
+
ComponentSpec(
|
| 265 |
+
"pachifier",
|
| 266 |
+
LTXVideoPachifier,
|
| 267 |
+
config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
|
| 268 |
+
default_creation_method="from_config",
|
| 269 |
+
),
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
@property
|
| 273 |
+
def inputs(self) -> list[InputParam]:
|
| 274 |
+
return [
|
| 275 |
+
InputParam.template("height", default=512),
|
| 276 |
+
InputParam.template("width", default=704),
|
| 277 |
+
InputParam("num_frames", type_hint=int, default=161),
|
| 278 |
+
InputParam.template("latents"),
|
| 279 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 280 |
+
InputParam.template("generator"),
|
| 281 |
+
InputParam.template("batch_size", required=True),
|
| 282 |
+
]
|
| 283 |
+
|
| 284 |
+
@property
|
| 285 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 286 |
+
return [
|
| 287 |
+
OutputParam("latents", type_hint=torch.Tensor),
|
| 288 |
+
]
|
| 289 |
+
|
| 290 |
+
@torch.no_grad()
|
| 291 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 292 |
+
block_state = self.get_block_state(state)
|
| 293 |
+
device = components._execution_device
|
| 294 |
+
|
| 295 |
+
batch_size = block_state.batch_size * block_state.num_videos_per_prompt
|
| 296 |
+
num_channels_latents = components.transformer.config.in_channels
|
| 297 |
+
|
| 298 |
+
if block_state.latents is not None:
|
| 299 |
+
block_state.latents = block_state.latents.to(device=device, dtype=torch.float32)
|
| 300 |
+
else:
|
| 301 |
+
height = block_state.height // components.vae_spatial_compression_ratio
|
| 302 |
+
width = block_state.width // components.vae_spatial_compression_ratio
|
| 303 |
+
num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 304 |
+
|
| 305 |
+
shape = (batch_size, num_channels_latents, num_frames, height, width)
|
| 306 |
+
block_state.latents = randn_tensor(
|
| 307 |
+
shape, generator=block_state.generator, device=device, dtype=torch.float32
|
| 308 |
+
)
|
| 309 |
+
block_state.latents = components.pachifier.pack_latents(block_state.latents)
|
| 310 |
+
|
| 311 |
+
self.set_block_state(state, block_state)
|
| 312 |
+
return components, state
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
class LTXImage2VideoPrepareLatentsStep(ModularPipelineBlocks):
|
| 316 |
+
model_name = "ltx"
|
| 317 |
+
|
| 318 |
+
@property
|
| 319 |
+
def description(self) -> str:
|
| 320 |
+
return (
|
| 321 |
+
"Prepare image-to-video latents: adds noise to pre-encoded image latents and creates a conditioning mask. "
|
| 322 |
+
"Expects pure noise `latents` from LTXPrepareLatentsStep."
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
@property
|
| 326 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 327 |
+
return [
|
| 328 |
+
ComponentSpec(
|
| 329 |
+
"pachifier",
|
| 330 |
+
LTXVideoPachifier,
|
| 331 |
+
config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
|
| 332 |
+
default_creation_method="from_config",
|
| 333 |
+
),
|
| 334 |
+
]
|
| 335 |
+
|
| 336 |
+
@property
|
| 337 |
+
def inputs(self) -> list[InputParam]:
|
| 338 |
+
return [
|
| 339 |
+
InputParam("image_latents", type_hint=torch.Tensor, required=True),
|
| 340 |
+
InputParam.template("latents", required=True),
|
| 341 |
+
InputParam.template("height", default=512),
|
| 342 |
+
InputParam.template("width", default=704),
|
| 343 |
+
InputParam("num_frames", type_hint=int, default=161),
|
| 344 |
+
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
|
| 345 |
+
InputParam.template("batch_size", required=True),
|
| 346 |
+
]
|
| 347 |
+
|
| 348 |
+
@property
|
| 349 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 350 |
+
return [
|
| 351 |
+
OutputParam("latents", type_hint=torch.Tensor),
|
| 352 |
+
OutputParam("conditioning_mask", type_hint=torch.Tensor),
|
| 353 |
+
]
|
| 354 |
+
|
| 355 |
+
@torch.no_grad()
|
| 356 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 357 |
+
block_state = self.get_block_state(state)
|
| 358 |
+
device = components._execution_device
|
| 359 |
+
|
| 360 |
+
batch_size = block_state.batch_size * block_state.num_videos_per_prompt
|
| 361 |
+
|
| 362 |
+
height = block_state.height // components.vae_spatial_compression_ratio
|
| 363 |
+
width = block_state.width // components.vae_spatial_compression_ratio
|
| 364 |
+
num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 365 |
+
|
| 366 |
+
init_latents = block_state.image_latents.to(device=device, dtype=torch.float32)
|
| 367 |
+
if init_latents.shape[0] < batch_size:
|
| 368 |
+
init_latents = init_latents.repeat_interleave(batch_size // init_latents.shape[0], dim=0)
|
| 369 |
+
init_latents = init_latents.repeat(1, 1, num_frames, 1, 1)
|
| 370 |
+
|
| 371 |
+
conditioning_mask = torch.zeros(
|
| 372 |
+
init_latents.shape[0],
|
| 373 |
+
1,
|
| 374 |
+
init_latents.shape[2],
|
| 375 |
+
init_latents.shape[3],
|
| 376 |
+
init_latents.shape[4],
|
| 377 |
+
device=device,
|
| 378 |
+
dtype=torch.float32,
|
| 379 |
+
)
|
| 380 |
+
conditioning_mask[:, :, 0] = 1.0
|
| 381 |
+
|
| 382 |
+
noise = components.pachifier.unpack_latents(block_state.latents, num_frames, height, width)
|
| 383 |
+
latents = init_latents * conditioning_mask + noise * (1 - conditioning_mask)
|
| 384 |
+
|
| 385 |
+
conditioning_mask = components.pachifier.pack_latents(conditioning_mask).squeeze(-1)
|
| 386 |
+
latents = components.pachifier.pack_latents(latents)
|
| 387 |
+
|
| 388 |
+
block_state.latents = latents
|
| 389 |
+
block_state.conditioning_mask = conditioning_mask
|
| 390 |
+
|
| 391 |
+
self.set_block_state(state, block_state)
|
| 392 |
+
return components, state
|
diffusers/modular_pipelines/ltx/decoders.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...models import AutoencoderKLLTXVideo
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ...utils.torch_utils import randn_tensor
|
| 23 |
+
from ...video_processor import VideoProcessor
|
| 24 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 25 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 26 |
+
from .modular_pipeline import LTXVideoPachifier
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = logging.get_logger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _denormalize_latents(
|
| 33 |
+
latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
# Denormalize latents across the channel dimension [B, C, F, H, W]
|
| 36 |
+
latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
|
| 37 |
+
latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
|
| 38 |
+
latents = latents * latents_std / scaling_factor + latents_mean
|
| 39 |
+
return latents
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class LTXVaeDecoderStep(ModularPipelineBlocks):
|
| 43 |
+
model_name = "ltx"
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 47 |
+
return [
|
| 48 |
+
ComponentSpec("vae", AutoencoderKLLTXVideo),
|
| 49 |
+
ComponentSpec(
|
| 50 |
+
"video_processor",
|
| 51 |
+
VideoProcessor,
|
| 52 |
+
config=FrozenDict({"vae_scale_factor": 32}),
|
| 53 |
+
default_creation_method="from_config",
|
| 54 |
+
),
|
| 55 |
+
ComponentSpec(
|
| 56 |
+
"pachifier",
|
| 57 |
+
LTXVideoPachifier,
|
| 58 |
+
config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
|
| 59 |
+
default_creation_method="from_config",
|
| 60 |
+
),
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def description(self) -> str:
|
| 65 |
+
return "Step that decodes the denoised latents into videos"
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 69 |
+
return [
|
| 70 |
+
InputParam.template("latents", required=True),
|
| 71 |
+
InputParam.template("output_type", default="np"),
|
| 72 |
+
InputParam.template("height", default=512),
|
| 73 |
+
InputParam.template("width", default=704),
|
| 74 |
+
InputParam("num_frames", type_hint=int, default=161),
|
| 75 |
+
InputParam("decode_timestep", default=0.0),
|
| 76 |
+
InputParam("decode_noise_scale", default=None),
|
| 77 |
+
InputParam.template("generator"),
|
| 78 |
+
InputParam.template("batch_size"),
|
| 79 |
+
InputParam.template("dtype", required=True),
|
| 80 |
+
]
|
| 81 |
+
|
| 82 |
+
@property
|
| 83 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 84 |
+
return [OutputParam.template("videos")]
|
| 85 |
+
|
| 86 |
+
@torch.no_grad()
|
| 87 |
+
def __call__(self, components, state: PipelineState) -> PipelineState:
|
| 88 |
+
block_state = self.get_block_state(state)
|
| 89 |
+
vae = components.vae
|
| 90 |
+
|
| 91 |
+
latents = block_state.latents
|
| 92 |
+
|
| 93 |
+
height = block_state.height
|
| 94 |
+
width = block_state.width
|
| 95 |
+
num_frames = block_state.num_frames
|
| 96 |
+
|
| 97 |
+
latent_num_frames = (num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 98 |
+
latent_height = height // components.vae_spatial_compression_ratio
|
| 99 |
+
latent_width = width // components.vae_spatial_compression_ratio
|
| 100 |
+
|
| 101 |
+
latents = components.pachifier.unpack_latents(latents, latent_num_frames, latent_height, latent_width)
|
| 102 |
+
latents = _denormalize_latents(latents, vae.latents_mean, vae.latents_std, vae.config.scaling_factor)
|
| 103 |
+
latents = latents.to(block_state.dtype)
|
| 104 |
+
|
| 105 |
+
if not vae.config.timestep_conditioning:
|
| 106 |
+
timestep = None
|
| 107 |
+
else:
|
| 108 |
+
device = latents.device
|
| 109 |
+
batch_size = block_state.batch_size
|
| 110 |
+
decode_timestep = block_state.decode_timestep
|
| 111 |
+
decode_noise_scale = block_state.decode_noise_scale
|
| 112 |
+
|
| 113 |
+
noise = randn_tensor(latents.shape, generator=block_state.generator, device=device, dtype=latents.dtype)
|
| 114 |
+
if not isinstance(decode_timestep, list):
|
| 115 |
+
decode_timestep = [decode_timestep] * batch_size
|
| 116 |
+
if decode_noise_scale is None:
|
| 117 |
+
decode_noise_scale = decode_timestep
|
| 118 |
+
elif not isinstance(decode_noise_scale, list):
|
| 119 |
+
decode_noise_scale = [decode_noise_scale] * batch_size
|
| 120 |
+
|
| 121 |
+
timestep = torch.tensor(decode_timestep, device=device, dtype=latents.dtype)
|
| 122 |
+
decode_noise_scale = torch.tensor(decode_noise_scale, device=device, dtype=latents.dtype)[
|
| 123 |
+
:, None, None, None, None
|
| 124 |
+
]
|
| 125 |
+
latents = (1 - decode_noise_scale) * latents + decode_noise_scale * noise
|
| 126 |
+
|
| 127 |
+
latents = latents.to(vae.dtype)
|
| 128 |
+
video = vae.decode(latents, timestep, return_dict=False)[0]
|
| 129 |
+
block_state.videos = components.video_processor.postprocess_video(video, output_type=block_state.output_type)
|
| 130 |
+
|
| 131 |
+
self.set_block_state(state, block_state)
|
| 132 |
+
return components, state
|
diffusers/modular_pipelines/ltx/denoise.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
from ...configuration_utils import FrozenDict
|
| 20 |
+
from ...guiders import ClassifierFreeGuidance
|
| 21 |
+
from ...models import LTXVideoTransformer3DModel
|
| 22 |
+
from ...schedulers import FlowMatchEulerDiscreteScheduler
|
| 23 |
+
from ..modular_pipeline import (
|
| 24 |
+
BlockState,
|
| 25 |
+
LoopSequentialPipelineBlocks,
|
| 26 |
+
ModularPipelineBlocks,
|
| 27 |
+
PipelineState,
|
| 28 |
+
)
|
| 29 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam
|
| 30 |
+
from .modular_pipeline import LTXModularPipeline, LTXVideoPachifier
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class LTXLoopBeforeDenoiser(ModularPipelineBlocks):
|
| 34 |
+
model_name = "ltx"
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def description(self) -> str:
|
| 38 |
+
return (
|
| 39 |
+
"Step within the denoising loop that prepares the latent input for the denoiser. "
|
| 40 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 41 |
+
"object (e.g. `LTXDenoiseLoopWrapper`)"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def inputs(self) -> list[InputParam]:
|
| 46 |
+
return [
|
| 47 |
+
InputParam.template("latents", required=True),
|
| 48 |
+
InputParam.template("dtype", required=True),
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
@torch.no_grad()
|
| 52 |
+
def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 53 |
+
block_state.latent_model_input = block_state.latents.to(block_state.dtype)
|
| 54 |
+
return components, block_state
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class LTXLoopDenoiser(ModularPipelineBlocks):
|
| 58 |
+
model_name = "ltx"
|
| 59 |
+
|
| 60 |
+
def __init__(
|
| 61 |
+
self,
|
| 62 |
+
guider_input_fields: dict[str, Any] | None = None,
|
| 63 |
+
):
|
| 64 |
+
if guider_input_fields is None:
|
| 65 |
+
guider_input_fields = {
|
| 66 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 67 |
+
"encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
|
| 68 |
+
}
|
| 69 |
+
if not isinstance(guider_input_fields, dict):
|
| 70 |
+
raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
|
| 71 |
+
self._guider_input_fields = guider_input_fields
|
| 72 |
+
super().__init__()
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 76 |
+
return [
|
| 77 |
+
ComponentSpec(
|
| 78 |
+
"guider",
|
| 79 |
+
ClassifierFreeGuidance,
|
| 80 |
+
config=FrozenDict({"guidance_scale": 3.0}),
|
| 81 |
+
default_creation_method="from_config",
|
| 82 |
+
),
|
| 83 |
+
ComponentSpec("transformer", LTXVideoTransformer3DModel),
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def description(self) -> str:
|
| 88 |
+
return (
|
| 89 |
+
"Step within the denoising loop that denoises the latents with guidance. "
|
| 90 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 91 |
+
"object (e.g. `LTXDenoiseLoopWrapper`)"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 96 |
+
inputs = [
|
| 97 |
+
InputParam.template("attention_kwargs"),
|
| 98 |
+
InputParam.template("num_inference_steps", required=True),
|
| 99 |
+
InputParam("rope_interpolation_scale", type_hint=tuple),
|
| 100 |
+
InputParam.template("height"),
|
| 101 |
+
InputParam.template("width"),
|
| 102 |
+
InputParam("num_frames", type_hint=int),
|
| 103 |
+
]
|
| 104 |
+
guider_input_names = []
|
| 105 |
+
for value in self._guider_input_fields.values():
|
| 106 |
+
if isinstance(value, tuple):
|
| 107 |
+
guider_input_names.extend(value)
|
| 108 |
+
else:
|
| 109 |
+
guider_input_names.append(value)
|
| 110 |
+
|
| 111 |
+
for name in guider_input_names:
|
| 112 |
+
inputs.append(InputParam(name=name, required=True, type_hint=torch.Tensor))
|
| 113 |
+
return inputs
|
| 114 |
+
|
| 115 |
+
@torch.no_grad()
|
| 116 |
+
def __call__(
|
| 117 |
+
self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 118 |
+
) -> PipelineState:
|
| 119 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 120 |
+
|
| 121 |
+
latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 122 |
+
latent_height = block_state.height // components.vae_spatial_compression_ratio
|
| 123 |
+
latent_width = block_state.width // components.vae_spatial_compression_ratio
|
| 124 |
+
|
| 125 |
+
guider_state = components.guider.prepare_inputs_from_block_state(block_state, self._guider_input_fields)
|
| 126 |
+
|
| 127 |
+
for guider_state_batch in guider_state:
|
| 128 |
+
components.guider.prepare_models(components.transformer)
|
| 129 |
+
cond_kwargs = guider_state_batch.as_dict()
|
| 130 |
+
cond_kwargs = {
|
| 131 |
+
k: v.to(block_state.dtype) if isinstance(v, torch.Tensor) else v
|
| 132 |
+
for k, v in cond_kwargs.items()
|
| 133 |
+
if k in self._guider_input_fields.keys()
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key, None)
|
| 137 |
+
with components.transformer.cache_context(context_name):
|
| 138 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 139 |
+
hidden_states=block_state.latent_model_input,
|
| 140 |
+
timestep=t.expand(block_state.latent_model_input.shape[0]).to(block_state.dtype),
|
| 141 |
+
num_frames=latent_num_frames,
|
| 142 |
+
height=latent_height,
|
| 143 |
+
width=latent_width,
|
| 144 |
+
rope_interpolation_scale=block_state.rope_interpolation_scale,
|
| 145 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 146 |
+
return_dict=False,
|
| 147 |
+
**cond_kwargs,
|
| 148 |
+
)[0]
|
| 149 |
+
components.guider.cleanup_models(components.transformer)
|
| 150 |
+
|
| 151 |
+
block_state.noise_pred = components.guider(guider_state)[0]
|
| 152 |
+
|
| 153 |
+
return components, block_state
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class LTXLoopAfterDenoiser(ModularPipelineBlocks):
|
| 157 |
+
model_name = "ltx"
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 161 |
+
return [
|
| 162 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
@property
|
| 166 |
+
def description(self) -> str:
|
| 167 |
+
return (
|
| 168 |
+
"Step within the denoising loop that updates the latents. "
|
| 169 |
+
"This block should be used to compose the `sub_blocks` attribute of a `LoopSequentialPipelineBlocks` "
|
| 170 |
+
"object (e.g. `LTXDenoiseLoopWrapper`)"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
@torch.no_grad()
|
| 174 |
+
def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 175 |
+
latents_dtype = block_state.latents.dtype
|
| 176 |
+
block_state.latents = components.scheduler.step(
|
| 177 |
+
block_state.noise_pred,
|
| 178 |
+
t,
|
| 179 |
+
block_state.latents,
|
| 180 |
+
return_dict=False,
|
| 181 |
+
)[0]
|
| 182 |
+
|
| 183 |
+
if block_state.latents.dtype != latents_dtype:
|
| 184 |
+
block_state.latents = block_state.latents.to(latents_dtype)
|
| 185 |
+
|
| 186 |
+
return components, block_state
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class LTXDenoiseLoopWrapper(LoopSequentialPipelineBlocks):
|
| 190 |
+
model_name = "ltx"
|
| 191 |
+
|
| 192 |
+
@property
|
| 193 |
+
def description(self) -> str:
|
| 194 |
+
return (
|
| 195 |
+
"Pipeline block that iteratively denoises the latents over `timesteps`. "
|
| 196 |
+
"The specific steps within each iteration can be customized with `sub_blocks` attributes"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
@property
|
| 200 |
+
def loop_expected_components(self) -> list[ComponentSpec]:
|
| 201 |
+
return [
|
| 202 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 203 |
+
ComponentSpec("transformer", LTXVideoTransformer3DModel),
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def loop_inputs(self) -> list[InputParam]:
|
| 208 |
+
return [
|
| 209 |
+
InputParam.template("timesteps", required=True),
|
| 210 |
+
InputParam.template("num_inference_steps", required=True),
|
| 211 |
+
]
|
| 212 |
+
|
| 213 |
+
@torch.no_grad()
|
| 214 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 215 |
+
block_state = self.get_block_state(state)
|
| 216 |
+
|
| 217 |
+
block_state.num_warmup_steps = max(
|
| 218 |
+
len(block_state.timesteps) - block_state.num_inference_steps * components.scheduler.order, 0
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
with self.progress_bar(total=block_state.num_inference_steps) as progress_bar:
|
| 222 |
+
for i, t in enumerate(block_state.timesteps):
|
| 223 |
+
components, block_state = self.loop_step(components, block_state, i=i, t=t)
|
| 224 |
+
if i == len(block_state.timesteps) - 1 or (
|
| 225 |
+
(i + 1) > block_state.num_warmup_steps and (i + 1) % components.scheduler.order == 0
|
| 226 |
+
):
|
| 227 |
+
progress_bar.update()
|
| 228 |
+
|
| 229 |
+
self.set_block_state(state, block_state)
|
| 230 |
+
return components, state
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class LTXDenoiseStep(LTXDenoiseLoopWrapper):
|
| 234 |
+
block_classes = [
|
| 235 |
+
LTXLoopBeforeDenoiser,
|
| 236 |
+
LTXLoopDenoiser(
|
| 237 |
+
guider_input_fields={
|
| 238 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 239 |
+
"encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
|
| 240 |
+
}
|
| 241 |
+
),
|
| 242 |
+
LTXLoopAfterDenoiser,
|
| 243 |
+
]
|
| 244 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 245 |
+
|
| 246 |
+
@property
|
| 247 |
+
def description(self) -> str:
|
| 248 |
+
return (
|
| 249 |
+
"Denoise step that iteratively denoises the latents.\n"
|
| 250 |
+
"Its loop logic is defined in `LTXDenoiseLoopWrapper.__call__` method.\n"
|
| 251 |
+
"At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
|
| 252 |
+
" - `LTXLoopBeforeDenoiser`\n"
|
| 253 |
+
" - `LTXLoopDenoiser`\n"
|
| 254 |
+
" - `LTXLoopAfterDenoiser`\n"
|
| 255 |
+
"This block supports text-to-video tasks."
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class LTXImage2VideoLoopBeforeDenoiser(ModularPipelineBlocks):
|
| 260 |
+
model_name = "ltx"
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def description(self) -> str:
|
| 264 |
+
return (
|
| 265 |
+
"Step within the i2v denoising loop that prepares the latent input and modulates "
|
| 266 |
+
"the timestep with the conditioning mask."
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
@property
|
| 270 |
+
def inputs(self) -> list[InputParam]:
|
| 271 |
+
return [
|
| 272 |
+
InputParam.template("latents", required=True),
|
| 273 |
+
InputParam("conditioning_mask", required=True, type_hint=torch.Tensor),
|
| 274 |
+
InputParam.template("dtype", required=True),
|
| 275 |
+
]
|
| 276 |
+
|
| 277 |
+
@torch.no_grad()
|
| 278 |
+
def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 279 |
+
block_state.latent_model_input = block_state.latents.to(block_state.dtype)
|
| 280 |
+
block_state.timestep_adjusted = t.expand(block_state.latent_model_input.shape[0]).unsqueeze(-1) * (
|
| 281 |
+
1 - block_state.conditioning_mask
|
| 282 |
+
)
|
| 283 |
+
return components, block_state
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class LTXImage2VideoLoopDenoiser(ModularPipelineBlocks):
|
| 287 |
+
model_name = "ltx"
|
| 288 |
+
|
| 289 |
+
def __init__(
|
| 290 |
+
self,
|
| 291 |
+
guider_input_fields: dict[str, Any] | None = None,
|
| 292 |
+
):
|
| 293 |
+
if guider_input_fields is None:
|
| 294 |
+
guider_input_fields = {
|
| 295 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 296 |
+
"encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
|
| 297 |
+
}
|
| 298 |
+
if not isinstance(guider_input_fields, dict):
|
| 299 |
+
raise ValueError(f"guider_input_fields must be a dictionary but is {type(guider_input_fields)}")
|
| 300 |
+
self._guider_input_fields = guider_input_fields
|
| 301 |
+
super().__init__()
|
| 302 |
+
|
| 303 |
+
@property
|
| 304 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 305 |
+
return [
|
| 306 |
+
ComponentSpec(
|
| 307 |
+
"guider",
|
| 308 |
+
ClassifierFreeGuidance,
|
| 309 |
+
config=FrozenDict({"guidance_scale": 3.0}),
|
| 310 |
+
default_creation_method="from_config",
|
| 311 |
+
),
|
| 312 |
+
ComponentSpec("transformer", LTXVideoTransformer3DModel),
|
| 313 |
+
]
|
| 314 |
+
|
| 315 |
+
@property
|
| 316 |
+
def description(self) -> str:
|
| 317 |
+
return (
|
| 318 |
+
"Step within the i2v denoising loop that denoises the latents with guidance "
|
| 319 |
+
"using timestep modulated by the conditioning mask."
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
@property
|
| 323 |
+
def inputs(self) -> list[tuple[str, Any]]:
|
| 324 |
+
inputs = [
|
| 325 |
+
InputParam.template("attention_kwargs"),
|
| 326 |
+
InputParam.template("num_inference_steps", required=True),
|
| 327 |
+
InputParam("rope_interpolation_scale", type_hint=tuple),
|
| 328 |
+
InputParam.template("height"),
|
| 329 |
+
InputParam.template("width"),
|
| 330 |
+
InputParam("num_frames", type_hint=int),
|
| 331 |
+
]
|
| 332 |
+
guider_input_names = []
|
| 333 |
+
for value in self._guider_input_fields.values():
|
| 334 |
+
if isinstance(value, tuple):
|
| 335 |
+
guider_input_names.extend(value)
|
| 336 |
+
else:
|
| 337 |
+
guider_input_names.append(value)
|
| 338 |
+
for name in guider_input_names:
|
| 339 |
+
inputs.append(InputParam(name=name, required=True, type_hint=torch.Tensor))
|
| 340 |
+
return inputs
|
| 341 |
+
|
| 342 |
+
@torch.no_grad()
|
| 343 |
+
def __call__(
|
| 344 |
+
self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor
|
| 345 |
+
) -> PipelineState:
|
| 346 |
+
components.guider.set_state(step=i, num_inference_steps=block_state.num_inference_steps, timestep=t)
|
| 347 |
+
|
| 348 |
+
latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 349 |
+
latent_height = block_state.height // components.vae_spatial_compression_ratio
|
| 350 |
+
latent_width = block_state.width // components.vae_spatial_compression_ratio
|
| 351 |
+
|
| 352 |
+
guider_state = components.guider.prepare_inputs_from_block_state(block_state, self._guider_input_fields)
|
| 353 |
+
|
| 354 |
+
for guider_state_batch in guider_state:
|
| 355 |
+
components.guider.prepare_models(components.transformer)
|
| 356 |
+
cond_kwargs = guider_state_batch.as_dict()
|
| 357 |
+
cond_kwargs = {
|
| 358 |
+
k: v.to(block_state.dtype) if isinstance(v, torch.Tensor) else v
|
| 359 |
+
for k, v in cond_kwargs.items()
|
| 360 |
+
if k in self._guider_input_fields.keys()
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
context_name = getattr(guider_state_batch, components.guider._identifier_key, None)
|
| 364 |
+
with components.transformer.cache_context(context_name):
|
| 365 |
+
guider_state_batch.noise_pred = components.transformer(
|
| 366 |
+
hidden_states=block_state.latent_model_input,
|
| 367 |
+
timestep=block_state.timestep_adjusted,
|
| 368 |
+
num_frames=latent_num_frames,
|
| 369 |
+
height=latent_height,
|
| 370 |
+
width=latent_width,
|
| 371 |
+
rope_interpolation_scale=block_state.rope_interpolation_scale,
|
| 372 |
+
attention_kwargs=block_state.attention_kwargs,
|
| 373 |
+
return_dict=False,
|
| 374 |
+
**cond_kwargs,
|
| 375 |
+
)[0]
|
| 376 |
+
components.guider.cleanup_models(components.transformer)
|
| 377 |
+
|
| 378 |
+
block_state.noise_pred = components.guider(guider_state)[0]
|
| 379 |
+
|
| 380 |
+
return components, block_state
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
class LTXImage2VideoLoopAfterDenoiser(ModularPipelineBlocks):
|
| 384 |
+
model_name = "ltx"
|
| 385 |
+
|
| 386 |
+
@property
|
| 387 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 388 |
+
return [
|
| 389 |
+
ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler),
|
| 390 |
+
ComponentSpec(
|
| 391 |
+
"pachifier",
|
| 392 |
+
LTXVideoPachifier,
|
| 393 |
+
config=FrozenDict({"patch_size": 1, "patch_size_t": 1}),
|
| 394 |
+
default_creation_method="from_config",
|
| 395 |
+
),
|
| 396 |
+
]
|
| 397 |
+
|
| 398 |
+
@property
|
| 399 |
+
def description(self) -> str:
|
| 400 |
+
return (
|
| 401 |
+
"Step within the i2v denoising loop that updates the latents, "
|
| 402 |
+
"applying the scheduler step only to frames after the first (conditioned) frame."
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def inputs(self) -> list[InputParam]:
|
| 407 |
+
return [
|
| 408 |
+
InputParam.template("height"),
|
| 409 |
+
InputParam.template("width"),
|
| 410 |
+
InputParam("num_frames", type_hint=int),
|
| 411 |
+
]
|
| 412 |
+
|
| 413 |
+
@torch.no_grad()
|
| 414 |
+
def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
|
| 415 |
+
latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
|
| 416 |
+
latent_height = block_state.height // components.vae_spatial_compression_ratio
|
| 417 |
+
latent_width = block_state.width // components.vae_spatial_compression_ratio
|
| 418 |
+
|
| 419 |
+
noise_pred = components.pachifier.unpack_latents(
|
| 420 |
+
block_state.noise_pred, latent_num_frames, latent_height, latent_width
|
| 421 |
+
)
|
| 422 |
+
latents = components.pachifier.unpack_latents(
|
| 423 |
+
block_state.latents, latent_num_frames, latent_height, latent_width
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
noise_pred = noise_pred[:, :, 1:]
|
| 427 |
+
noise_latents = latents[:, :, 1:]
|
| 428 |
+
pred_latents = components.scheduler.step(noise_pred, t, noise_latents, return_dict=False)[0]
|
| 429 |
+
|
| 430 |
+
latents = torch.cat([latents[:, :, :1], pred_latents], dim=2)
|
| 431 |
+
block_state.latents = components.pachifier.pack_latents(latents)
|
| 432 |
+
|
| 433 |
+
return components, block_state
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
class LTXImage2VideoDenoiseStep(LTXDenoiseLoopWrapper):
|
| 437 |
+
block_classes = [
|
| 438 |
+
LTXImage2VideoLoopBeforeDenoiser,
|
| 439 |
+
LTXImage2VideoLoopDenoiser(
|
| 440 |
+
guider_input_fields={
|
| 441 |
+
"encoder_hidden_states": ("prompt_embeds", "negative_prompt_embeds"),
|
| 442 |
+
"encoder_attention_mask": ("prompt_attention_mask", "negative_prompt_attention_mask"),
|
| 443 |
+
}
|
| 444 |
+
),
|
| 445 |
+
LTXImage2VideoLoopAfterDenoiser,
|
| 446 |
+
]
|
| 447 |
+
block_names = ["before_denoiser", "denoiser", "after_denoiser"]
|
| 448 |
+
|
| 449 |
+
@property
|
| 450 |
+
def description(self) -> str:
|
| 451 |
+
return (
|
| 452 |
+
"Denoise step for image-to-video that iteratively denoises the latents.\n"
|
| 453 |
+
"The first frame is kept fixed via a conditioning mask.\n"
|
| 454 |
+
"At each iteration, it runs blocks defined in `sub_blocks` sequentially:\n"
|
| 455 |
+
" - `LTXImage2VideoLoopBeforeDenoiser`\n"
|
| 456 |
+
" - `LTXImage2VideoLoopDenoiser`\n"
|
| 457 |
+
" - `LTXImage2VideoLoopAfterDenoiser`"
|
| 458 |
+
)
|
diffusers/modular_pipelines/ltx/encoders.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
from transformers import T5EncoderModel, T5TokenizerFast
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import FrozenDict
|
| 19 |
+
from ...guiders import ClassifierFreeGuidance
|
| 20 |
+
from ...models import AutoencoderKLLTXVideo
|
| 21 |
+
from ...utils import logging
|
| 22 |
+
from ...video_processor import VideoProcessor
|
| 23 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 24 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 25 |
+
from .modular_pipeline import LTXModularPipeline
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
logger = logging.get_logger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _get_t5_prompt_embeds(
|
| 32 |
+
components,
|
| 33 |
+
prompt: str | list[str],
|
| 34 |
+
max_sequence_length: int,
|
| 35 |
+
device: torch.device,
|
| 36 |
+
dtype: torch.dtype,
|
| 37 |
+
):
|
| 38 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 39 |
+
|
| 40 |
+
text_inputs = components.tokenizer(
|
| 41 |
+
prompt,
|
| 42 |
+
padding="max_length",
|
| 43 |
+
max_length=max_sequence_length,
|
| 44 |
+
truncation=True,
|
| 45 |
+
add_special_tokens=True,
|
| 46 |
+
return_tensors="pt",
|
| 47 |
+
)
|
| 48 |
+
text_input_ids = text_inputs.input_ids
|
| 49 |
+
prompt_attention_mask = text_inputs.attention_mask
|
| 50 |
+
prompt_attention_mask = prompt_attention_mask.bool().to(device)
|
| 51 |
+
|
| 52 |
+
prompt_embeds = components.text_encoder(text_input_ids.to(device))[0]
|
| 53 |
+
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
|
| 54 |
+
|
| 55 |
+
return prompt_embeds, prompt_attention_mask
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class LTXTextEncoderStep(ModularPipelineBlocks):
|
| 59 |
+
model_name = "ltx"
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def description(self) -> str:
|
| 63 |
+
return "Text Encoder step that generates text embeddings to guide the video generation"
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 67 |
+
return [
|
| 68 |
+
ComponentSpec("text_encoder", T5EncoderModel),
|
| 69 |
+
ComponentSpec("tokenizer", T5TokenizerFast),
|
| 70 |
+
ComponentSpec(
|
| 71 |
+
"guider",
|
| 72 |
+
ClassifierFreeGuidance,
|
| 73 |
+
config=FrozenDict({"guidance_scale": 3.0}),
|
| 74 |
+
default_creation_method="from_config",
|
| 75 |
+
),
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def inputs(self) -> list[InputParam]:
|
| 80 |
+
return [
|
| 81 |
+
InputParam.template("prompt"),
|
| 82 |
+
InputParam.template("negative_prompt"),
|
| 83 |
+
InputParam.template("max_sequence_length", default=128),
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
@property
|
| 87 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 88 |
+
return [
|
| 89 |
+
OutputParam.template("prompt_embeds"),
|
| 90 |
+
OutputParam.template("prompt_embeds_mask", name="prompt_attention_mask"),
|
| 91 |
+
OutputParam.template("negative_prompt_embeds"),
|
| 92 |
+
OutputParam.template("negative_prompt_embeds_mask", name="negative_prompt_attention_mask"),
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
@staticmethod
|
| 96 |
+
def check_inputs(block_state):
|
| 97 |
+
if block_state.prompt is not None and (
|
| 98 |
+
not isinstance(block_state.prompt, str) and not isinstance(block_state.prompt, list)
|
| 99 |
+
):
|
| 100 |
+
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(block_state.prompt)}")
|
| 101 |
+
|
| 102 |
+
@staticmethod
|
| 103 |
+
def encode_prompt(
|
| 104 |
+
components,
|
| 105 |
+
prompt: str,
|
| 106 |
+
device: torch.device | None = None,
|
| 107 |
+
prepare_unconditional_embeds: bool = True,
|
| 108 |
+
negative_prompt: str | None = None,
|
| 109 |
+
max_sequence_length: int = 128,
|
| 110 |
+
):
|
| 111 |
+
device = device or components._execution_device
|
| 112 |
+
dtype = components.text_encoder.dtype
|
| 113 |
+
|
| 114 |
+
if not isinstance(prompt, list):
|
| 115 |
+
prompt = [prompt]
|
| 116 |
+
batch_size = len(prompt)
|
| 117 |
+
|
| 118 |
+
prompt_embeds, prompt_attention_mask = _get_t5_prompt_embeds(
|
| 119 |
+
components=components,
|
| 120 |
+
prompt=prompt,
|
| 121 |
+
max_sequence_length=max_sequence_length,
|
| 122 |
+
device=device,
|
| 123 |
+
dtype=dtype,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
negative_prompt_embeds = None
|
| 127 |
+
negative_prompt_attention_mask = None
|
| 128 |
+
|
| 129 |
+
if prepare_unconditional_embeds:
|
| 130 |
+
negative_prompt = negative_prompt or ""
|
| 131 |
+
negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
|
| 132 |
+
|
| 133 |
+
if batch_size != len(negative_prompt):
|
| 134 |
+
raise ValueError(
|
| 135 |
+
f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
|
| 136 |
+
f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
|
| 137 |
+
" the batch size of `prompt`."
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
negative_prompt_embeds, negative_prompt_attention_mask = _get_t5_prompt_embeds(
|
| 141 |
+
components=components,
|
| 142 |
+
prompt=negative_prompt,
|
| 143 |
+
max_sequence_length=max_sequence_length,
|
| 144 |
+
device=device,
|
| 145 |
+
dtype=dtype,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
return prompt_embeds, prompt_attention_mask, negative_prompt_embeds, negative_prompt_attention_mask
|
| 149 |
+
|
| 150 |
+
@torch.no_grad()
|
| 151 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 152 |
+
block_state = self.get_block_state(state)
|
| 153 |
+
self.check_inputs(block_state)
|
| 154 |
+
|
| 155 |
+
block_state.device = components._execution_device
|
| 156 |
+
|
| 157 |
+
(
|
| 158 |
+
block_state.prompt_embeds,
|
| 159 |
+
block_state.prompt_attention_mask,
|
| 160 |
+
block_state.negative_prompt_embeds,
|
| 161 |
+
block_state.negative_prompt_attention_mask,
|
| 162 |
+
) = self.encode_prompt(
|
| 163 |
+
components=components,
|
| 164 |
+
prompt=block_state.prompt,
|
| 165 |
+
device=block_state.device,
|
| 166 |
+
prepare_unconditional_embeds=components.requires_unconditional_embeds,
|
| 167 |
+
negative_prompt=block_state.negative_prompt,
|
| 168 |
+
max_sequence_length=block_state.max_sequence_length,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
self.set_block_state(state, block_state)
|
| 172 |
+
return components, state
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
|
| 176 |
+
def retrieve_latents(
|
| 177 |
+
encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
|
| 178 |
+
):
|
| 179 |
+
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
|
| 180 |
+
return encoder_output.latent_dist.sample(generator)
|
| 181 |
+
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
|
| 182 |
+
return encoder_output.latent_dist.mode()
|
| 183 |
+
elif hasattr(encoder_output, "latents"):
|
| 184 |
+
return encoder_output.latents
|
| 185 |
+
else:
|
| 186 |
+
raise AttributeError("Could not access latents of provided encoder_output")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _normalize_latents(
|
| 190 |
+
latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
|
| 191 |
+
) -> torch.Tensor:
|
| 192 |
+
# Normalize latents across the channel dimension [B, C, F, H, W]
|
| 193 |
+
latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
|
| 194 |
+
latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
|
| 195 |
+
latents = (latents - latents_mean) * scaling_factor / latents_std
|
| 196 |
+
return latents
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class LTXVaeEncoderStep(ModularPipelineBlocks):
|
| 200 |
+
model_name = "ltx"
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def description(self) -> str:
|
| 204 |
+
return "VAE Encoder step that encodes an input image into latent space for image-to-video generation"
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 208 |
+
return [
|
| 209 |
+
ComponentSpec("vae", AutoencoderKLLTXVideo),
|
| 210 |
+
ComponentSpec(
|
| 211 |
+
"video_processor",
|
| 212 |
+
VideoProcessor,
|
| 213 |
+
config=FrozenDict({"vae_scale_factor": 32}),
|
| 214 |
+
default_creation_method="from_config",
|
| 215 |
+
),
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def inputs(self) -> list[InputParam]:
|
| 220 |
+
return [
|
| 221 |
+
InputParam.template("image", required=True),
|
| 222 |
+
InputParam.template("height", default=512),
|
| 223 |
+
InputParam.template("width", default=704),
|
| 224 |
+
InputParam.template("generator"),
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 229 |
+
return [
|
| 230 |
+
OutputParam(
|
| 231 |
+
"image_latents",
|
| 232 |
+
type_hint=torch.Tensor,
|
| 233 |
+
description="Encoded image latents from the VAE encoder",
|
| 234 |
+
),
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
@torch.no_grad()
|
| 238 |
+
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
|
| 239 |
+
block_state = self.get_block_state(state)
|
| 240 |
+
device = components._execution_device
|
| 241 |
+
|
| 242 |
+
image = block_state.image
|
| 243 |
+
if not isinstance(image, torch.Tensor):
|
| 244 |
+
image = components.video_processor.preprocess(image, height=block_state.height, width=block_state.width)
|
| 245 |
+
image = image.to(device=device, dtype=torch.float32)
|
| 246 |
+
|
| 247 |
+
vae_dtype = components.vae.dtype
|
| 248 |
+
|
| 249 |
+
num_images = image.shape[0]
|
| 250 |
+
if isinstance(block_state.generator, list):
|
| 251 |
+
init_latents = [
|
| 252 |
+
retrieve_latents(
|
| 253 |
+
components.vae.encode(image[i].unsqueeze(0).unsqueeze(2).to(vae_dtype)),
|
| 254 |
+
block_state.generator[i],
|
| 255 |
+
)
|
| 256 |
+
for i in range(num_images)
|
| 257 |
+
]
|
| 258 |
+
else:
|
| 259 |
+
init_latents = [
|
| 260 |
+
retrieve_latents(
|
| 261 |
+
components.vae.encode(img.unsqueeze(0).unsqueeze(2).to(vae_dtype)),
|
| 262 |
+
block_state.generator,
|
| 263 |
+
)
|
| 264 |
+
for img in image
|
| 265 |
+
]
|
| 266 |
+
|
| 267 |
+
init_latents = torch.cat(init_latents, dim=0).to(torch.float32)
|
| 268 |
+
block_state.image_latents = _normalize_latents(
|
| 269 |
+
init_latents, components.vae.latents_mean, components.vae.latents_std
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
self.set_block_state(state, block_state)
|
| 273 |
+
return components, state
|
diffusers/modular_pipelines/ltx/modular_blocks_ltx.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
from ...utils import logging
|
| 16 |
+
from ..modular_pipeline import AutoPipelineBlocks, SequentialPipelineBlocks
|
| 17 |
+
from ..modular_pipeline_utils import OutputParam
|
| 18 |
+
from .before_denoise import (
|
| 19 |
+
LTXImage2VideoPrepareLatentsStep,
|
| 20 |
+
LTXPrepareLatentsStep,
|
| 21 |
+
LTXSetTimestepsStep,
|
| 22 |
+
LTXTextInputStep,
|
| 23 |
+
)
|
| 24 |
+
from .decoders import LTXVaeDecoderStep
|
| 25 |
+
from .denoise import LTXDenoiseStep, LTXImage2VideoDenoiseStep
|
| 26 |
+
from .encoders import LTXTextEncoderStep, LTXVaeEncoderStep
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = logging.get_logger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# auto_docstring
|
| 33 |
+
class LTXCoreDenoiseStep(SequentialPipelineBlocks):
|
| 34 |
+
"""
|
| 35 |
+
Denoise block that takes encoded conditions and runs the denoising process.
|
| 36 |
+
|
| 37 |
+
Components:
|
| 38 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
|
| 39 |
+
(`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
|
| 40 |
+
|
| 41 |
+
Inputs:
|
| 42 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 43 |
+
The number of images to generate per prompt.
|
| 44 |
+
prompt_embeds (`Tensor`):
|
| 45 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 46 |
+
prompt_attention_mask (`Tensor`):
|
| 47 |
+
mask for the text embeddings. Can be generated from text_encoder step.
|
| 48 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 49 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 50 |
+
negative_prompt_attention_mask (`Tensor`, *optional*):
|
| 51 |
+
mask for the negative text embeddings. Can be generated from text_encoder step.
|
| 52 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 53 |
+
The number of denoising steps.
|
| 54 |
+
timesteps (`Tensor`, *optional*):
|
| 55 |
+
Timesteps for the denoising process.
|
| 56 |
+
sigmas (`list`, *optional*):
|
| 57 |
+
Custom sigmas for the denoising process.
|
| 58 |
+
height (`int`, *optional*, defaults to 512):
|
| 59 |
+
The height in pixels of the generated image.
|
| 60 |
+
width (`int`, *optional*, defaults to 704):
|
| 61 |
+
The width in pixels of the generated image.
|
| 62 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 63 |
+
TODO: Add description.
|
| 64 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 65 |
+
TODO: Add description.
|
| 66 |
+
latents (`Tensor`, *optional*):
|
| 67 |
+
Pre-generated noisy latents for image generation.
|
| 68 |
+
generator (`Generator`, *optional*):
|
| 69 |
+
Torch generator for deterministic generation.
|
| 70 |
+
attention_kwargs (`dict`, *optional*):
|
| 71 |
+
Additional kwargs for attention processors.
|
| 72 |
+
|
| 73 |
+
Outputs:
|
| 74 |
+
latents (`Tensor`):
|
| 75 |
+
Denoised latents.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
model_name = "ltx"
|
| 79 |
+
block_classes = [
|
| 80 |
+
LTXTextInputStep,
|
| 81 |
+
LTXSetTimestepsStep,
|
| 82 |
+
LTXPrepareLatentsStep,
|
| 83 |
+
LTXDenoiseStep,
|
| 84 |
+
]
|
| 85 |
+
block_names = ["input", "set_timesteps", "prepare_latents", "denoise"]
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def description(self):
|
| 89 |
+
return "Denoise block that takes encoded conditions and runs the denoising process."
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def outputs(self):
|
| 93 |
+
return [OutputParam.template("latents")]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# auto_docstring
|
| 97 |
+
class LTXImage2VideoCoreDenoiseStep(SequentialPipelineBlocks):
|
| 98 |
+
"""
|
| 99 |
+
Denoise block for image-to-video that takes encoded conditions and image latents, and runs the denoising process.
|
| 100 |
+
|
| 101 |
+
Components:
|
| 102 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
|
| 103 |
+
(`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
|
| 104 |
+
|
| 105 |
+
Inputs:
|
| 106 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 107 |
+
The number of images to generate per prompt.
|
| 108 |
+
prompt_embeds (`Tensor`):
|
| 109 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 110 |
+
prompt_attention_mask (`Tensor`):
|
| 111 |
+
mask for the text embeddings. Can be generated from text_encoder step.
|
| 112 |
+
negative_prompt_embeds (`Tensor`, *optional*):
|
| 113 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 114 |
+
negative_prompt_attention_mask (`Tensor`, *optional*):
|
| 115 |
+
mask for the negative text embeddings. Can be generated from text_encoder step.
|
| 116 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 117 |
+
The number of denoising steps.
|
| 118 |
+
timesteps (`Tensor`, *optional*):
|
| 119 |
+
Timesteps for the denoising process.
|
| 120 |
+
sigmas (`list`, *optional*):
|
| 121 |
+
Custom sigmas for the denoising process.
|
| 122 |
+
height (`int`, *optional*, defaults to 512):
|
| 123 |
+
The height in pixels of the generated image.
|
| 124 |
+
width (`int`, *optional*, defaults to 704):
|
| 125 |
+
The width in pixels of the generated image.
|
| 126 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 127 |
+
TODO: Add description.
|
| 128 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 129 |
+
TODO: Add description.
|
| 130 |
+
latents (`Tensor`, *optional*):
|
| 131 |
+
Pre-generated noisy latents for image generation.
|
| 132 |
+
generator (`Generator`, *optional*):
|
| 133 |
+
Torch generator for deterministic generation.
|
| 134 |
+
image_latents (`Tensor`):
|
| 135 |
+
TODO: Add description.
|
| 136 |
+
attention_kwargs (`dict`, *optional*):
|
| 137 |
+
Additional kwargs for attention processors.
|
| 138 |
+
|
| 139 |
+
Outputs:
|
| 140 |
+
latents (`Tensor`):
|
| 141 |
+
Denoised latents.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
model_name = "ltx"
|
| 145 |
+
block_classes = [
|
| 146 |
+
LTXTextInputStep,
|
| 147 |
+
LTXSetTimestepsStep,
|
| 148 |
+
LTXPrepareLatentsStep,
|
| 149 |
+
LTXImage2VideoPrepareLatentsStep,
|
| 150 |
+
LTXImage2VideoDenoiseStep,
|
| 151 |
+
]
|
| 152 |
+
block_names = ["input", "set_timesteps", "prepare_latents", "prepare_i2v_latents", "denoise"]
|
| 153 |
+
|
| 154 |
+
@property
|
| 155 |
+
def description(self):
|
| 156 |
+
return "Denoise block for image-to-video that takes encoded conditions and image latents, and runs the denoising process."
|
| 157 |
+
|
| 158 |
+
@property
|
| 159 |
+
def outputs(self):
|
| 160 |
+
return [OutputParam.template("latents")]
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# auto_docstring
|
| 164 |
+
class LTXBlocks(SequentialPipelineBlocks):
|
| 165 |
+
"""
|
| 166 |
+
Modular pipeline blocks for LTX Video text-to-video.
|
| 167 |
+
|
| 168 |
+
Components:
|
| 169 |
+
text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) scheduler
|
| 170 |
+
(`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) transformer
|
| 171 |
+
(`LTXVideoTransformer3DModel`) vae (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`)
|
| 172 |
+
|
| 173 |
+
Inputs:
|
| 174 |
+
prompt (`str`):
|
| 175 |
+
The prompt or prompts to guide image generation.
|
| 176 |
+
negative_prompt (`str`, *optional*):
|
| 177 |
+
The prompt or prompts not to guide the image generation.
|
| 178 |
+
max_sequence_length (`int`, *optional*, defaults to 128):
|
| 179 |
+
Maximum sequence length for prompt encoding.
|
| 180 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 181 |
+
The number of images to generate per prompt.
|
| 182 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 183 |
+
The number of denoising steps.
|
| 184 |
+
timesteps (`Tensor`, *optional*):
|
| 185 |
+
Timesteps for the denoising process.
|
| 186 |
+
sigmas (`list`, *optional*):
|
| 187 |
+
Custom sigmas for the denoising process.
|
| 188 |
+
height (`int`, *optional*, defaults to 512):
|
| 189 |
+
The height in pixels of the generated image.
|
| 190 |
+
width (`int`, *optional*, defaults to 704):
|
| 191 |
+
The width in pixels of the generated image.
|
| 192 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 193 |
+
TODO: Add description.
|
| 194 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 195 |
+
TODO: Add description.
|
| 196 |
+
latents (`Tensor`, *optional*):
|
| 197 |
+
Pre-generated noisy latents for image generation.
|
| 198 |
+
generator (`Generator`, *optional*):
|
| 199 |
+
Torch generator for deterministic generation.
|
| 200 |
+
attention_kwargs (`dict`, *optional*):
|
| 201 |
+
Additional kwargs for attention processors.
|
| 202 |
+
output_type (`str`, *optional*, defaults to np):
|
| 203 |
+
Output format: 'pil', 'np', 'pt'.
|
| 204 |
+
decode_timestep (`None`, *optional*, defaults to 0.0):
|
| 205 |
+
TODO: Add description.
|
| 206 |
+
decode_noise_scale (`None`, *optional*):
|
| 207 |
+
TODO: Add description.
|
| 208 |
+
|
| 209 |
+
Outputs:
|
| 210 |
+
videos (`list`):
|
| 211 |
+
The generated videos.
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
model_name = "ltx"
|
| 215 |
+
block_classes = [
|
| 216 |
+
LTXTextEncoderStep,
|
| 217 |
+
LTXCoreDenoiseStep,
|
| 218 |
+
LTXVaeDecoderStep,
|
| 219 |
+
]
|
| 220 |
+
block_names = ["text_encoder", "denoise", "decode"]
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def description(self):
|
| 224 |
+
return "Modular pipeline blocks for LTX Video text-to-video."
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def outputs(self):
|
| 228 |
+
return [OutputParam.template("videos")]
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# auto_docstring
|
| 232 |
+
class LTXAutoVaeEncoderStep(AutoPipelineBlocks):
|
| 233 |
+
"""
|
| 234 |
+
VAE encoder step that encodes the image input into its latent representation.
|
| 235 |
+
This is an auto pipeline block that works for image-to-video tasks.
|
| 236 |
+
- `LTXVaeEncoderStep` is used when `image` is provided.
|
| 237 |
+
- If `image` is not provided, step will be skipped.
|
| 238 |
+
|
| 239 |
+
Components:
|
| 240 |
+
vae (`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`)
|
| 241 |
+
|
| 242 |
+
Inputs:
|
| 243 |
+
image (`Image | list`, *optional*):
|
| 244 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 245 |
+
height (`int`, *optional*, defaults to 512):
|
| 246 |
+
The height in pixels of the generated image.
|
| 247 |
+
width (`int`, *optional*, defaults to 704):
|
| 248 |
+
The width in pixels of the generated image.
|
| 249 |
+
generator (`Generator`, *optional*):
|
| 250 |
+
Torch generator for deterministic generation.
|
| 251 |
+
|
| 252 |
+
Outputs:
|
| 253 |
+
image_latents (`Tensor`):
|
| 254 |
+
Encoded image latents from the VAE encoder
|
| 255 |
+
"""
|
| 256 |
+
|
| 257 |
+
model_name = "ltx"
|
| 258 |
+
block_classes = [LTXVaeEncoderStep]
|
| 259 |
+
block_names = ["vae_encoder"]
|
| 260 |
+
block_trigger_inputs = ["image"]
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def description(self):
|
| 264 |
+
return (
|
| 265 |
+
"VAE encoder step that encodes the image input into its latent representation.\n"
|
| 266 |
+
"This is an auto pipeline block that works for image-to-video tasks.\n"
|
| 267 |
+
" - `LTXVaeEncoderStep` is used when `image` is provided.\n"
|
| 268 |
+
" - If `image` is not provided, step will be skipped."
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# auto_docstring
|
| 273 |
+
class LTXAutoCoreDenoiseStep(AutoPipelineBlocks):
|
| 274 |
+
"""
|
| 275 |
+
Auto denoise block that selects the appropriate denoise pipeline based on inputs.
|
| 276 |
+
- `LTXImage2VideoCoreDenoiseStep` is used when `image_latents` is provided.
|
| 277 |
+
- `LTXCoreDenoiseStep` is used otherwise (text-to-video).
|
| 278 |
+
|
| 279 |
+
Components:
|
| 280 |
+
scheduler (`FlowMatchEulerDiscreteScheduler`) pachifier (`LTXVideoPachifier`) guider
|
| 281 |
+
(`ClassifierFreeGuidance`) transformer (`LTXVideoTransformer3DModel`)
|
| 282 |
+
|
| 283 |
+
Inputs:
|
| 284 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 285 |
+
The number of images to generate per prompt.
|
| 286 |
+
prompt_embeds (`Tensor`):
|
| 287 |
+
text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 288 |
+
prompt_attention_mask (`Tensor`):
|
| 289 |
+
mask for the text embeddings. Can be generated from text_encoder step.
|
| 290 |
+
negative_prompt_embeds (`Tensor`):
|
| 291 |
+
negative text embeddings used to guide the image generation. Can be generated from text_encoder step.
|
| 292 |
+
negative_prompt_attention_mask (`Tensor`):
|
| 293 |
+
mask for the negative text embeddings. Can be generated from text_encoder step.
|
| 294 |
+
num_inference_steps (`int`):
|
| 295 |
+
The number of denoising steps.
|
| 296 |
+
timesteps (`Tensor`):
|
| 297 |
+
Timesteps for the denoising process.
|
| 298 |
+
sigmas (`list`, *optional*):
|
| 299 |
+
Custom sigmas for the denoising process.
|
| 300 |
+
height (`int`, *optional*, defaults to 512):
|
| 301 |
+
The height in pixels of the generated image.
|
| 302 |
+
width (`int`, *optional*, defaults to 704):
|
| 303 |
+
The width in pixels of the generated image.
|
| 304 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 305 |
+
TODO: Add description.
|
| 306 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 307 |
+
TODO: Add description.
|
| 308 |
+
latents (`Tensor`):
|
| 309 |
+
Pre-generated noisy latents for image generation.
|
| 310 |
+
generator (`Generator`, *optional*):
|
| 311 |
+
Torch generator for deterministic generation.
|
| 312 |
+
image_latents (`Tensor`, *optional*):
|
| 313 |
+
TODO: Add description.
|
| 314 |
+
attention_kwargs (`dict`, *optional*):
|
| 315 |
+
Additional kwargs for attention processors.
|
| 316 |
+
|
| 317 |
+
Outputs:
|
| 318 |
+
latents (`Tensor`):
|
| 319 |
+
Denoised latents.
|
| 320 |
+
"""
|
| 321 |
+
|
| 322 |
+
model_name = "ltx"
|
| 323 |
+
block_classes = [LTXImage2VideoCoreDenoiseStep, LTXCoreDenoiseStep]
|
| 324 |
+
block_names = ["image2video", "text2video"]
|
| 325 |
+
block_trigger_inputs = ["image_latents", None]
|
| 326 |
+
|
| 327 |
+
@property
|
| 328 |
+
def description(self):
|
| 329 |
+
return (
|
| 330 |
+
"Auto denoise block that selects the appropriate denoise pipeline based on inputs.\n"
|
| 331 |
+
" - `LTXImage2VideoCoreDenoiseStep` is used when `image_latents` is provided.\n"
|
| 332 |
+
" - `LTXCoreDenoiseStep` is used otherwise (text-to-video)."
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# auto_docstring
|
| 337 |
+
class LTXAutoBlocks(SequentialPipelineBlocks):
|
| 338 |
+
"""
|
| 339 |
+
Auto blocks for LTX Video that support both text-to-video and image-to-video workflows.
|
| 340 |
+
|
| 341 |
+
Supported workflows:
|
| 342 |
+
- `text2video`: requires `prompt`
|
| 343 |
+
- `image2video`: requires `image`, `prompt`
|
| 344 |
+
|
| 345 |
+
Components:
|
| 346 |
+
text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) vae
|
| 347 |
+
(`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 348 |
+
pachifier (`LTXVideoPachifier`) transformer (`LTXVideoTransformer3DModel`)
|
| 349 |
+
|
| 350 |
+
Inputs:
|
| 351 |
+
prompt (`str`):
|
| 352 |
+
The prompt or prompts to guide image generation.
|
| 353 |
+
negative_prompt (`str`, *optional*):
|
| 354 |
+
The prompt or prompts not to guide the image generation.
|
| 355 |
+
max_sequence_length (`int`, *optional*, defaults to 128):
|
| 356 |
+
Maximum sequence length for prompt encoding.
|
| 357 |
+
image (`Image | list`, *optional*):
|
| 358 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 359 |
+
height (`int`, *optional*, defaults to 512):
|
| 360 |
+
The height in pixels of the generated image.
|
| 361 |
+
width (`int`, *optional*, defaults to 704):
|
| 362 |
+
The width in pixels of the generated image.
|
| 363 |
+
generator (`Generator`, *optional*):
|
| 364 |
+
Torch generator for deterministic generation.
|
| 365 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 366 |
+
The number of images to generate per prompt.
|
| 367 |
+
num_inference_steps (`int`):
|
| 368 |
+
The number of denoising steps.
|
| 369 |
+
timesteps (`Tensor`):
|
| 370 |
+
Timesteps for the denoising process.
|
| 371 |
+
sigmas (`list`, *optional*):
|
| 372 |
+
Custom sigmas for the denoising process.
|
| 373 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 374 |
+
TODO: Add description.
|
| 375 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 376 |
+
TODO: Add description.
|
| 377 |
+
latents (`Tensor`):
|
| 378 |
+
Pre-generated noisy latents for image generation.
|
| 379 |
+
image_latents (`Tensor`, *optional*):
|
| 380 |
+
TODO: Add description.
|
| 381 |
+
attention_kwargs (`dict`, *optional*):
|
| 382 |
+
Additional kwargs for attention processors.
|
| 383 |
+
output_type (`str`, *optional*, defaults to np):
|
| 384 |
+
Output format: 'pil', 'np', 'pt'.
|
| 385 |
+
decode_timestep (`None`, *optional*, defaults to 0.0):
|
| 386 |
+
TODO: Add description.
|
| 387 |
+
decode_noise_scale (`None`, *optional*):
|
| 388 |
+
TODO: Add description.
|
| 389 |
+
|
| 390 |
+
Outputs:
|
| 391 |
+
videos (`list`):
|
| 392 |
+
The generated videos.
|
| 393 |
+
"""
|
| 394 |
+
|
| 395 |
+
model_name = "ltx"
|
| 396 |
+
block_classes = [
|
| 397 |
+
LTXTextEncoderStep,
|
| 398 |
+
LTXAutoVaeEncoderStep,
|
| 399 |
+
LTXAutoCoreDenoiseStep,
|
| 400 |
+
LTXVaeDecoderStep,
|
| 401 |
+
]
|
| 402 |
+
block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
|
| 403 |
+
_workflow_map = {
|
| 404 |
+
"text2video": {"prompt": True},
|
| 405 |
+
"image2video": {"image": True, "prompt": True},
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
@property
|
| 409 |
+
def description(self):
|
| 410 |
+
return "Auto blocks for LTX Video that support both text-to-video and image-to-video workflows."
|
| 411 |
+
|
| 412 |
+
@property
|
| 413 |
+
def outputs(self):
|
| 414 |
+
return [OutputParam.template("videos")]
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
# auto_docstring
|
| 418 |
+
class LTXImage2VideoBlocks(SequentialPipelineBlocks):
|
| 419 |
+
"""
|
| 420 |
+
Modular pipeline blocks for LTX Video image-to-video.
|
| 421 |
+
|
| 422 |
+
Components:
|
| 423 |
+
text_encoder (`T5EncoderModel`) tokenizer (`T5Tokenizer`) guider (`ClassifierFreeGuidance`) vae
|
| 424 |
+
(`AutoencoderKLLTXVideo`) video_processor (`VideoProcessor`) scheduler (`FlowMatchEulerDiscreteScheduler`)
|
| 425 |
+
pachifier (`LTXVideoPachifier`) transformer (`LTXVideoTransformer3DModel`)
|
| 426 |
+
|
| 427 |
+
Inputs:
|
| 428 |
+
prompt (`str`):
|
| 429 |
+
The prompt or prompts to guide image generation.
|
| 430 |
+
negative_prompt (`str`, *optional*):
|
| 431 |
+
The prompt or prompts not to guide the image generation.
|
| 432 |
+
max_sequence_length (`int`, *optional*, defaults to 128):
|
| 433 |
+
Maximum sequence length for prompt encoding.
|
| 434 |
+
image (`Image | list`, *optional*):
|
| 435 |
+
Reference image(s) for denoising. Can be a single image or list of images.
|
| 436 |
+
height (`int`, *optional*, defaults to 512):
|
| 437 |
+
The height in pixels of the generated image.
|
| 438 |
+
width (`int`, *optional*, defaults to 704):
|
| 439 |
+
The width in pixels of the generated image.
|
| 440 |
+
generator (`Generator`, *optional*):
|
| 441 |
+
Torch generator for deterministic generation.
|
| 442 |
+
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
| 443 |
+
The number of images to generate per prompt.
|
| 444 |
+
num_inference_steps (`int`, *optional*, defaults to 50):
|
| 445 |
+
The number of denoising steps.
|
| 446 |
+
timesteps (`Tensor`, *optional*):
|
| 447 |
+
Timesteps for the denoising process.
|
| 448 |
+
sigmas (`list`, *optional*):
|
| 449 |
+
Custom sigmas for the denoising process.
|
| 450 |
+
num_frames (`int`, *optional*, defaults to 161):
|
| 451 |
+
TODO: Add description.
|
| 452 |
+
frame_rate (`int`, *optional*, defaults to 25):
|
| 453 |
+
TODO: Add description.
|
| 454 |
+
latents (`Tensor`, *optional*):
|
| 455 |
+
Pre-generated noisy latents for image generation.
|
| 456 |
+
image_latents (`Tensor`):
|
| 457 |
+
TODO: Add description.
|
| 458 |
+
attention_kwargs (`dict`, *optional*):
|
| 459 |
+
Additional kwargs for attention processors.
|
| 460 |
+
output_type (`str`, *optional*, defaults to np):
|
| 461 |
+
Output format: 'pil', 'np', 'pt'.
|
| 462 |
+
decode_timestep (`None`, *optional*, defaults to 0.0):
|
| 463 |
+
TODO: Add description.
|
| 464 |
+
decode_noise_scale (`None`, *optional*):
|
| 465 |
+
TODO: Add description.
|
| 466 |
+
|
| 467 |
+
Outputs:
|
| 468 |
+
videos (`list`):
|
| 469 |
+
The generated videos.
|
| 470 |
+
"""
|
| 471 |
+
|
| 472 |
+
model_name = "ltx"
|
| 473 |
+
block_classes = [
|
| 474 |
+
LTXTextEncoderStep,
|
| 475 |
+
LTXAutoVaeEncoderStep,
|
| 476 |
+
LTXImage2VideoCoreDenoiseStep,
|
| 477 |
+
LTXVaeDecoderStep,
|
| 478 |
+
]
|
| 479 |
+
block_names = ["text_encoder", "vae_encoder", "denoise", "decode"]
|
| 480 |
+
|
| 481 |
+
@property
|
| 482 |
+
def description(self):
|
| 483 |
+
return "Modular pipeline blocks for LTX Video image-to-video."
|
| 484 |
+
|
| 485 |
+
@property
|
| 486 |
+
def outputs(self):
|
| 487 |
+
return [OutputParam.template("videos")]
|
diffusers/modular_pipelines/ltx/modular_pipeline.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from ...configuration_utils import ConfigMixin, register_to_config
|
| 19 |
+
from ...loaders import LTXVideoLoraLoaderMixin
|
| 20 |
+
from ...utils import logging
|
| 21 |
+
from ..modular_pipeline import ModularPipeline
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
logger = logging.get_logger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class LTXVideoPachifier(ConfigMixin):
|
| 28 |
+
"""
|
| 29 |
+
A class to pack and unpack latents for LTX Video.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
config_name = "config.json"
|
| 33 |
+
|
| 34 |
+
@register_to_config
|
| 35 |
+
def __init__(self, patch_size: int = 1, patch_size_t: int = 1):
|
| 36 |
+
super().__init__()
|
| 37 |
+
|
| 38 |
+
def pack_latents(self, latents: torch.Tensor) -> torch.Tensor:
|
| 39 |
+
batch_size, _, num_frames, height, width = latents.shape
|
| 40 |
+
patch_size = self.config.patch_size
|
| 41 |
+
patch_size_t = self.config.patch_size_t
|
| 42 |
+
post_patch_num_frames = num_frames // patch_size_t
|
| 43 |
+
post_patch_height = height // patch_size
|
| 44 |
+
post_patch_width = width // patch_size
|
| 45 |
+
latents = latents.reshape(
|
| 46 |
+
batch_size,
|
| 47 |
+
-1,
|
| 48 |
+
post_patch_num_frames,
|
| 49 |
+
patch_size_t,
|
| 50 |
+
post_patch_height,
|
| 51 |
+
patch_size,
|
| 52 |
+
post_patch_width,
|
| 53 |
+
patch_size,
|
| 54 |
+
)
|
| 55 |
+
latents = latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
|
| 56 |
+
return latents
|
| 57 |
+
|
| 58 |
+
def unpack_latents(self, latents: torch.Tensor, num_frames: int, height: int, width: int) -> torch.Tensor:
|
| 59 |
+
batch_size = latents.size(0)
|
| 60 |
+
patch_size = self.config.patch_size
|
| 61 |
+
patch_size_t = self.config.patch_size_t
|
| 62 |
+
latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size)
|
| 63 |
+
latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
|
| 64 |
+
return latents
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class LTXModularPipeline(
|
| 68 |
+
ModularPipeline,
|
| 69 |
+
LTXVideoLoraLoaderMixin,
|
| 70 |
+
):
|
| 71 |
+
"""
|
| 72 |
+
A ModularPipeline for LTX Video.
|
| 73 |
+
|
| 74 |
+
> [!WARNING] > This is an experimental feature and is likely to change in the future.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
default_blocks_name = "LTXAutoBlocks"
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def vae_spatial_compression_ratio(self):
|
| 81 |
+
if getattr(self, "vae", None) is not None:
|
| 82 |
+
return self.vae.spatial_compression_ratio
|
| 83 |
+
return 32
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def vae_temporal_compression_ratio(self):
|
| 87 |
+
if getattr(self, "vae", None) is not None:
|
| 88 |
+
return self.vae.temporal_compression_ratio
|
| 89 |
+
return 8
|
| 90 |
+
|
| 91 |
+
@property
|
| 92 |
+
def requires_unconditional_embeds(self):
|
| 93 |
+
if hasattr(self, "guider") and self.guider is not None:
|
| 94 |
+
return self.guider._enabled and self.guider.num_conditions > 1
|
| 95 |
+
return False
|
diffusers/modular_pipelines/mellon_node_utils.py
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
# Simple typed wrapper for parameter overrides
|
| 7 |
+
from dataclasses import asdict, dataclass
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from huggingface_hub import create_repo, hf_hub_download, upload_file
|
| 11 |
+
from huggingface_hub.utils import (
|
| 12 |
+
EntryNotFoundError,
|
| 13 |
+
HfHubHTTPError,
|
| 14 |
+
RepositoryNotFoundError,
|
| 15 |
+
RevisionNotFoundError,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
from ..utils import HUGGINGFACE_CO_RESOLVE_ENDPOINT
|
| 19 |
+
from .modular_pipeline_utils import InputParam, OutputParam
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _name_to_label(name: str) -> str:
|
| 26 |
+
"""Convert snake_case name to Title Case label."""
|
| 27 |
+
return name.replace("_", " ").title()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Template definitions for standard diffuser pipeline parameters
|
| 31 |
+
MELLON_PARAM_TEMPLATES = {
|
| 32 |
+
# Image I/O
|
| 33 |
+
"image": {"label": "Image", "type": "image", "display": "input", "required_block_params": ["image"]},
|
| 34 |
+
"images": {"label": "Images", "type": "image", "display": "output", "required_block_params": ["images"]},
|
| 35 |
+
"control_image": {
|
| 36 |
+
"label": "Control Image",
|
| 37 |
+
"type": "image",
|
| 38 |
+
"display": "input",
|
| 39 |
+
"required_block_params": ["control_image"],
|
| 40 |
+
},
|
| 41 |
+
# Latents
|
| 42 |
+
"latents": {"label": "Latents", "type": "latents", "display": "input", "required_block_params": ["latents"]},
|
| 43 |
+
"image_latents": {
|
| 44 |
+
"label": "Image Latents",
|
| 45 |
+
"type": "latents",
|
| 46 |
+
"display": "input",
|
| 47 |
+
"required_block_params": ["image_latents"],
|
| 48 |
+
},
|
| 49 |
+
"first_frame_latents": {
|
| 50 |
+
"label": "First Frame Latents",
|
| 51 |
+
"type": "latents",
|
| 52 |
+
"display": "input",
|
| 53 |
+
"required_block_params": ["first_frame_latents"],
|
| 54 |
+
},
|
| 55 |
+
"latents_preview": {"label": "Latents Preview", "type": "latent", "display": "output"},
|
| 56 |
+
# Image Latents with Strength
|
| 57 |
+
"image_latents_with_strength": {
|
| 58 |
+
"name": "image_latents", # name is not same as template key
|
| 59 |
+
"label": "Image Latents",
|
| 60 |
+
"type": "latents",
|
| 61 |
+
"display": "input",
|
| 62 |
+
"onChange": {"false": ["height", "width"], "true": ["strength"]},
|
| 63 |
+
"required_block_params": ["image_latents", "strength"],
|
| 64 |
+
},
|
| 65 |
+
# Embeddings
|
| 66 |
+
"embeddings": {"label": "Text Embeddings", "type": "embeddings", "display": "output"},
|
| 67 |
+
"image_embeds": {
|
| 68 |
+
"label": "Image Embeddings",
|
| 69 |
+
"type": "image_embeds",
|
| 70 |
+
"display": "output",
|
| 71 |
+
"required_block_params": ["image_embeds"],
|
| 72 |
+
},
|
| 73 |
+
# Text inputs
|
| 74 |
+
"prompt": {
|
| 75 |
+
"label": "Prompt",
|
| 76 |
+
"type": "string",
|
| 77 |
+
"display": "textarea",
|
| 78 |
+
"default": "",
|
| 79 |
+
"required_block_params": ["prompt"],
|
| 80 |
+
},
|
| 81 |
+
"negative_prompt": {
|
| 82 |
+
"label": "Negative Prompt",
|
| 83 |
+
"type": "string",
|
| 84 |
+
"display": "textarea",
|
| 85 |
+
"default": "",
|
| 86 |
+
"required_block_params": ["negative_prompt"],
|
| 87 |
+
},
|
| 88 |
+
# Numeric params
|
| 89 |
+
"guidance_scale": {
|
| 90 |
+
"label": "Guidance Scale",
|
| 91 |
+
"type": "float",
|
| 92 |
+
"display": "slider",
|
| 93 |
+
"default": 5.0,
|
| 94 |
+
"min": 1.0,
|
| 95 |
+
"max": 30.0,
|
| 96 |
+
"step": 0.1,
|
| 97 |
+
},
|
| 98 |
+
"strength": {
|
| 99 |
+
"label": "Strength",
|
| 100 |
+
"type": "float",
|
| 101 |
+
"default": 0.5,
|
| 102 |
+
"min": 0.0,
|
| 103 |
+
"max": 1.0,
|
| 104 |
+
"step": 0.01,
|
| 105 |
+
"required_block_params": ["strength"],
|
| 106 |
+
},
|
| 107 |
+
"height": {
|
| 108 |
+
"label": "Height",
|
| 109 |
+
"type": "int",
|
| 110 |
+
"default": 1024,
|
| 111 |
+
"min": 64,
|
| 112 |
+
"step": 8,
|
| 113 |
+
"required_block_params": ["height"],
|
| 114 |
+
},
|
| 115 |
+
"width": {
|
| 116 |
+
"label": "Width",
|
| 117 |
+
"type": "int",
|
| 118 |
+
"default": 1024,
|
| 119 |
+
"min": 64,
|
| 120 |
+
"step": 8,
|
| 121 |
+
"required_block_params": ["width"],
|
| 122 |
+
},
|
| 123 |
+
"seed": {
|
| 124 |
+
"label": "Seed",
|
| 125 |
+
"type": "int",
|
| 126 |
+
"default": 0,
|
| 127 |
+
"min": 0,
|
| 128 |
+
"max": 4294967295,
|
| 129 |
+
"display": "random",
|
| 130 |
+
"required_block_params": ["generator"],
|
| 131 |
+
},
|
| 132 |
+
"num_inference_steps": {
|
| 133 |
+
"label": "Steps",
|
| 134 |
+
"type": "int",
|
| 135 |
+
"default": 25,
|
| 136 |
+
"min": 1,
|
| 137 |
+
"max": 100,
|
| 138 |
+
"display": "slider",
|
| 139 |
+
"required_block_params": ["num_inference_steps"],
|
| 140 |
+
},
|
| 141 |
+
"num_frames": {
|
| 142 |
+
"label": "Frames",
|
| 143 |
+
"type": "int",
|
| 144 |
+
"default": 81,
|
| 145 |
+
"min": 1,
|
| 146 |
+
"max": 480,
|
| 147 |
+
"display": "slider",
|
| 148 |
+
"required_block_params": ["num_frames"],
|
| 149 |
+
},
|
| 150 |
+
"layers": {
|
| 151 |
+
"label": "Layers",
|
| 152 |
+
"type": "int",
|
| 153 |
+
"default": 4,
|
| 154 |
+
"min": 1,
|
| 155 |
+
"max": 10,
|
| 156 |
+
"display": "slider",
|
| 157 |
+
"required_block_params": ["layers"],
|
| 158 |
+
},
|
| 159 |
+
"output_type": {
|
| 160 |
+
"label": "Output Type",
|
| 161 |
+
"type": "dropdown",
|
| 162 |
+
"default": "np",
|
| 163 |
+
"options": ["np", "pil", "pt"],
|
| 164 |
+
},
|
| 165 |
+
# ControlNet
|
| 166 |
+
"controlnet_conditioning_scale": {
|
| 167 |
+
"label": "Controlnet Conditioning Scale",
|
| 168 |
+
"type": "float",
|
| 169 |
+
"default": 0.5,
|
| 170 |
+
"min": 0.0,
|
| 171 |
+
"max": 1.0,
|
| 172 |
+
"step": 0.01,
|
| 173 |
+
"required_block_params": ["controlnet_conditioning_scale"],
|
| 174 |
+
},
|
| 175 |
+
"control_guidance_start": {
|
| 176 |
+
"label": "Control Guidance Start",
|
| 177 |
+
"type": "float",
|
| 178 |
+
"default": 0.0,
|
| 179 |
+
"min": 0.0,
|
| 180 |
+
"max": 1.0,
|
| 181 |
+
"step": 0.01,
|
| 182 |
+
"required_block_params": ["control_guidance_start"],
|
| 183 |
+
},
|
| 184 |
+
"control_guidance_end": {
|
| 185 |
+
"label": "Control Guidance End",
|
| 186 |
+
"type": "float",
|
| 187 |
+
"default": 1.0,
|
| 188 |
+
"min": 0.0,
|
| 189 |
+
"max": 1.0,
|
| 190 |
+
"step": 0.01,
|
| 191 |
+
"required_block_params": ["control_guidance_end"],
|
| 192 |
+
},
|
| 193 |
+
# Video
|
| 194 |
+
"videos": {"label": "Videos", "type": "video", "display": "output", "required_block_params": ["videos"]},
|
| 195 |
+
# Models
|
| 196 |
+
"vae": {"label": "VAE", "type": "diffusers_auto_model", "display": "input", "required_block_params": ["vae"]},
|
| 197 |
+
"image_encoder": {
|
| 198 |
+
"label": "Image Encoder",
|
| 199 |
+
"type": "diffusers_auto_model",
|
| 200 |
+
"display": "input",
|
| 201 |
+
"required_block_params": ["image_encoder"],
|
| 202 |
+
},
|
| 203 |
+
"unet": {"label": "Denoise Model", "type": "diffusers_auto_model", "display": "input"},
|
| 204 |
+
"scheduler": {"label": "Scheduler", "type": "diffusers_auto_model", "display": "input"},
|
| 205 |
+
"controlnet": {
|
| 206 |
+
"label": "ControlNet Model",
|
| 207 |
+
"type": "diffusers_auto_model",
|
| 208 |
+
"display": "input",
|
| 209 |
+
"required_block_params": ["controlnet"],
|
| 210 |
+
},
|
| 211 |
+
"text_encoders": {
|
| 212 |
+
"label": "Text Encoders",
|
| 213 |
+
"type": "diffusers_auto_models",
|
| 214 |
+
"display": "input",
|
| 215 |
+
"required_block_params": ["text_encoder"],
|
| 216 |
+
},
|
| 217 |
+
# Bundles/Custom
|
| 218 |
+
"controlnet_bundle": {
|
| 219 |
+
"label": "ControlNet",
|
| 220 |
+
"type": "custom_controlnet",
|
| 221 |
+
"display": "input",
|
| 222 |
+
"required_block_params": "controlnet_image",
|
| 223 |
+
},
|
| 224 |
+
"ip_adapter": {"label": "IP Adapter", "type": "custom_ip_adapter", "display": "input"},
|
| 225 |
+
"guider": {
|
| 226 |
+
"label": "Guider",
|
| 227 |
+
"type": "custom_guider",
|
| 228 |
+
"display": "input",
|
| 229 |
+
"onChange": {False: ["guidance_scale"], True: []},
|
| 230 |
+
},
|
| 231 |
+
"doc": {"label": "Doc", "type": "string", "display": "output"},
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class MellonParamMeta(type):
|
| 236 |
+
"""Metaclass that enables MellonParam.template_name(**overrides) syntax."""
|
| 237 |
+
|
| 238 |
+
def __getattr__(cls, name: str):
|
| 239 |
+
if name in MELLON_PARAM_TEMPLATES:
|
| 240 |
+
|
| 241 |
+
def factory(default=None, **overrides):
|
| 242 |
+
template = MELLON_PARAM_TEMPLATES[name]
|
| 243 |
+
# Use template's name if specified, otherwise use the key
|
| 244 |
+
params = {"name": template.get("name", name), **template, **overrides}
|
| 245 |
+
if default is not None:
|
| 246 |
+
params["default"] = default
|
| 247 |
+
return cls(**params)
|
| 248 |
+
|
| 249 |
+
return factory
|
| 250 |
+
|
| 251 |
+
raise AttributeError(f"type object 'MellonParam' has no attribute '{name}'")
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
@dataclass(frozen=True)
|
| 255 |
+
class MellonParam(metaclass=MellonParamMeta):
|
| 256 |
+
"""
|
| 257 |
+
Parameter definition for Mellon nodes.
|
| 258 |
+
|
| 259 |
+
Usage:
|
| 260 |
+
```python
|
| 261 |
+
# From template (standard diffuser params)
|
| 262 |
+
MellonParam.seed()
|
| 263 |
+
MellonParam.prompt(default="a cat")
|
| 264 |
+
MellonParam.latents(display="output")
|
| 265 |
+
|
| 266 |
+
# Generic inputs (for custom blocks)
|
| 267 |
+
MellonParam.Input.slider("my_scale", default=1.0, min=0.0, max=2.0)
|
| 268 |
+
MellonParam.Input.dropdown("mode", options=["fast", "slow"])
|
| 269 |
+
|
| 270 |
+
# Generic outputs
|
| 271 |
+
MellonParam.Output.image("result_images")
|
| 272 |
+
|
| 273 |
+
# Fully custom
|
| 274 |
+
MellonParam(name="custom", label="Custom", type="float", default=0.5)
|
| 275 |
+
```
|
| 276 |
+
"""
|
| 277 |
+
|
| 278 |
+
name: str
|
| 279 |
+
label: str
|
| 280 |
+
type: str
|
| 281 |
+
display: str | None = None
|
| 282 |
+
default: Any = None
|
| 283 |
+
min: float | None = None
|
| 284 |
+
max: float | None = None
|
| 285 |
+
step: float | None = None
|
| 286 |
+
options: Any = None
|
| 287 |
+
value: Any = None
|
| 288 |
+
fieldOptions: dict[str, Any] | None = None
|
| 289 |
+
onChange: Any = None
|
| 290 |
+
onSignal: Any = None
|
| 291 |
+
required_block_params: str | list[str] | None = None
|
| 292 |
+
|
| 293 |
+
def to_dict(self) -> dict[str, Any]:
|
| 294 |
+
"""Convert to dict for Mellon schema, excluding None values and internal fields."""
|
| 295 |
+
data = asdict(self)
|
| 296 |
+
return {k: v for k, v in data.items() if v is not None and k not in ("name", "required_block_params")}
|
| 297 |
+
|
| 298 |
+
# =========================================================================
|
| 299 |
+
# Input: Generic input parameter factories (for custom blocks)
|
| 300 |
+
# =========================================================================
|
| 301 |
+
class Input:
|
| 302 |
+
"""input UI elements for custom blocks."""
|
| 303 |
+
|
| 304 |
+
@classmethod
|
| 305 |
+
def image(cls, name: str) -> "MellonParam":
|
| 306 |
+
"""image input."""
|
| 307 |
+
return MellonParam(name=name, label=_name_to_label(name), type="image", display="input")
|
| 308 |
+
|
| 309 |
+
@classmethod
|
| 310 |
+
def textbox(cls, name: str, default: str = "") -> "MellonParam":
|
| 311 |
+
"""text input as textarea."""
|
| 312 |
+
return MellonParam(
|
| 313 |
+
name=name, label=_name_to_label(name), type="string", display="textarea", default=default
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
@classmethod
|
| 317 |
+
def dropdown(cls, name: str, options: list[str] = None, default: str = None) -> "MellonParam":
|
| 318 |
+
"""dropdown selection."""
|
| 319 |
+
if options and not default:
|
| 320 |
+
default = options[0]
|
| 321 |
+
if not default:
|
| 322 |
+
default = ""
|
| 323 |
+
if not options:
|
| 324 |
+
options = [default]
|
| 325 |
+
return MellonParam(name=name, label=_name_to_label(name), type="string", options=options, value=default)
|
| 326 |
+
|
| 327 |
+
@classmethod
|
| 328 |
+
def slider(
|
| 329 |
+
cls, name: str, default: float = 0, min: float = None, max: float = None, step: float = None
|
| 330 |
+
) -> "MellonParam":
|
| 331 |
+
"""slider input."""
|
| 332 |
+
is_float = isinstance(default, float) or (step is not None and isinstance(step, float))
|
| 333 |
+
param_type = "float" if is_float else "int"
|
| 334 |
+
if min is None:
|
| 335 |
+
min = default
|
| 336 |
+
if max is None:
|
| 337 |
+
max = default
|
| 338 |
+
if step is None:
|
| 339 |
+
step = 0.01 if is_float else 1
|
| 340 |
+
return MellonParam(
|
| 341 |
+
name=name,
|
| 342 |
+
label=_name_to_label(name),
|
| 343 |
+
type=param_type,
|
| 344 |
+
display="slider",
|
| 345 |
+
default=default,
|
| 346 |
+
min=min,
|
| 347 |
+
max=max,
|
| 348 |
+
step=step,
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
@classmethod
|
| 352 |
+
def number(
|
| 353 |
+
cls, name: str, default: float = 0, min: float = None, max: float = None, step: float = None
|
| 354 |
+
) -> "MellonParam":
|
| 355 |
+
"""number input (no slider)."""
|
| 356 |
+
is_float = isinstance(default, float) or (step is not None and isinstance(step, float))
|
| 357 |
+
param_type = "float" if is_float else "int"
|
| 358 |
+
return MellonParam(
|
| 359 |
+
name=name, label=_name_to_label(name), type=param_type, default=default, min=min, max=max, step=step
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
@classmethod
|
| 363 |
+
def seed(cls, name: str = "seed", default: int = 0) -> "MellonParam":
|
| 364 |
+
"""seed input with randomize button."""
|
| 365 |
+
return MellonParam(
|
| 366 |
+
name=name,
|
| 367 |
+
label=_name_to_label(name),
|
| 368 |
+
type="int",
|
| 369 |
+
display="random",
|
| 370 |
+
default=default,
|
| 371 |
+
min=0,
|
| 372 |
+
max=4294967295,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
@classmethod
|
| 376 |
+
def checkbox(cls, name: str, default: bool = False) -> "MellonParam":
|
| 377 |
+
"""boolean checkbox."""
|
| 378 |
+
return MellonParam(name=name, label=_name_to_label(name), type="boolean", value=default)
|
| 379 |
+
|
| 380 |
+
@classmethod
|
| 381 |
+
def custom_type(cls, name: str, type: str) -> "MellonParam":
|
| 382 |
+
"""custom type input for node connections."""
|
| 383 |
+
return MellonParam(name=name, label=_name_to_label(name), type=type, display="input")
|
| 384 |
+
|
| 385 |
+
@classmethod
|
| 386 |
+
def model(cls, name: str) -> "MellonParam":
|
| 387 |
+
"""model input for diffusers components."""
|
| 388 |
+
return MellonParam(name=name, label=_name_to_label(name), type="diffusers_auto_model", display="input")
|
| 389 |
+
|
| 390 |
+
# =========================================================================
|
| 391 |
+
# Output: Generic output parameter factories (for custom blocks)
|
| 392 |
+
# =========================================================================
|
| 393 |
+
class Output:
|
| 394 |
+
"""output UI elements for custom blocks."""
|
| 395 |
+
|
| 396 |
+
@classmethod
|
| 397 |
+
def image(cls, name: str) -> "MellonParam":
|
| 398 |
+
"""image output."""
|
| 399 |
+
return MellonParam(name=name, label=_name_to_label(name), type="image", display="output")
|
| 400 |
+
|
| 401 |
+
@classmethod
|
| 402 |
+
def video(cls, name: str) -> "MellonParam":
|
| 403 |
+
"""video output."""
|
| 404 |
+
return MellonParam(name=name, label=_name_to_label(name), type="video", display="output")
|
| 405 |
+
|
| 406 |
+
@classmethod
|
| 407 |
+
def text(cls, name: str) -> "MellonParam":
|
| 408 |
+
"""text output."""
|
| 409 |
+
return MellonParam(name=name, label=_name_to_label(name), type="string", display="output")
|
| 410 |
+
|
| 411 |
+
@classmethod
|
| 412 |
+
def custom_type(cls, name: str, type: str) -> "MellonParam":
|
| 413 |
+
"""custom type output for node connections."""
|
| 414 |
+
return MellonParam(name=name, label=_name_to_label(name), type=type, display="output")
|
| 415 |
+
|
| 416 |
+
@classmethod
|
| 417 |
+
def model(cls, name: str) -> "MellonParam":
|
| 418 |
+
"""model output for diffusers components."""
|
| 419 |
+
return MellonParam(name=name, label=_name_to_label(name), type="diffusers_auto_model", display="output")
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def input_param_to_mellon_param(input_param: "InputParam") -> MellonParam:
|
| 423 |
+
"""
|
| 424 |
+
Convert an InputParam to a MellonParam using metadata.
|
| 425 |
+
|
| 426 |
+
Args:
|
| 427 |
+
input_param: An InputParam with optional metadata containing either:
|
| 428 |
+
- {"mellon": "<type>"} for simple types (image, textbox, slider, etc.)
|
| 429 |
+
- {"mellon": MellonParam(...)} for full control over UI configuration
|
| 430 |
+
|
| 431 |
+
Returns:
|
| 432 |
+
MellonParam instance
|
| 433 |
+
"""
|
| 434 |
+
name = input_param.name
|
| 435 |
+
metadata = input_param.metadata
|
| 436 |
+
mellon_value = metadata.get("mellon") if metadata else None
|
| 437 |
+
default = input_param.default
|
| 438 |
+
|
| 439 |
+
# If it's already a MellonParam, return it directly
|
| 440 |
+
if isinstance(mellon_value, MellonParam):
|
| 441 |
+
return mellon_value
|
| 442 |
+
|
| 443 |
+
mellon_type = mellon_value
|
| 444 |
+
|
| 445 |
+
if mellon_type == "image":
|
| 446 |
+
return MellonParam.Input.image(name)
|
| 447 |
+
elif mellon_type == "textbox":
|
| 448 |
+
return MellonParam.Input.textbox(name, default=default or "")
|
| 449 |
+
elif mellon_type == "dropdown":
|
| 450 |
+
return MellonParam.Input.dropdown(name, default=default or "")
|
| 451 |
+
elif mellon_type == "slider":
|
| 452 |
+
return MellonParam.Input.slider(name, default=default or 0)
|
| 453 |
+
elif mellon_type == "number":
|
| 454 |
+
return MellonParam.Input.number(name, default=default or 0)
|
| 455 |
+
elif mellon_type == "seed":
|
| 456 |
+
return MellonParam.Input.seed(name, default=default or 0)
|
| 457 |
+
elif mellon_type == "checkbox":
|
| 458 |
+
return MellonParam.Input.checkbox(name, default=default or False)
|
| 459 |
+
elif mellon_type == "model":
|
| 460 |
+
return MellonParam.Input.model(name)
|
| 461 |
+
else:
|
| 462 |
+
# None or unknown -> custom
|
| 463 |
+
return MellonParam.Input.custom_type(name, type="custom")
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def output_param_to_mellon_param(output_param: "OutputParam") -> MellonParam:
|
| 467 |
+
"""
|
| 468 |
+
Convert an OutputParam to a MellonParam using metadata.
|
| 469 |
+
|
| 470 |
+
Args:
|
| 471 |
+
output_param: An OutputParam with optional metadata={"mellon": "<type>"} where type is one of:
|
| 472 |
+
image, video, text, model. If metadata is None or unknown, maps to "custom".
|
| 473 |
+
|
| 474 |
+
Returns:
|
| 475 |
+
MellonParam instance
|
| 476 |
+
"""
|
| 477 |
+
name = output_param.name
|
| 478 |
+
metadata = output_param.metadata
|
| 479 |
+
mellon_type = metadata.get("mellon") if metadata else None
|
| 480 |
+
|
| 481 |
+
if mellon_type == "image":
|
| 482 |
+
return MellonParam.Output.image(name)
|
| 483 |
+
elif mellon_type == "video":
|
| 484 |
+
return MellonParam.Output.video(name)
|
| 485 |
+
elif mellon_type == "text":
|
| 486 |
+
return MellonParam.Output.text(name)
|
| 487 |
+
elif mellon_type == "model":
|
| 488 |
+
return MellonParam.Output.model(name)
|
| 489 |
+
else:
|
| 490 |
+
# None or unknown -> custom
|
| 491 |
+
return MellonParam.Output.custom_type(name, type="custom")
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
DEFAULT_NODE_SPECS = {
|
| 495 |
+
"controlnet": None,
|
| 496 |
+
"denoise": {
|
| 497 |
+
"inputs": [
|
| 498 |
+
MellonParam.embeddings(display="input"),
|
| 499 |
+
MellonParam.width(),
|
| 500 |
+
MellonParam.height(),
|
| 501 |
+
MellonParam.seed(),
|
| 502 |
+
MellonParam.num_inference_steps(),
|
| 503 |
+
MellonParam.num_frames(),
|
| 504 |
+
MellonParam.guidance_scale(),
|
| 505 |
+
MellonParam.strength(),
|
| 506 |
+
MellonParam.image_latents_with_strength(),
|
| 507 |
+
MellonParam.image_latents(),
|
| 508 |
+
MellonParam.first_frame_latents(),
|
| 509 |
+
MellonParam.controlnet_bundle(display="input"),
|
| 510 |
+
],
|
| 511 |
+
"model_inputs": [
|
| 512 |
+
MellonParam.unet(),
|
| 513 |
+
MellonParam.guider(),
|
| 514 |
+
MellonParam.scheduler(),
|
| 515 |
+
],
|
| 516 |
+
"outputs": [
|
| 517 |
+
MellonParam.latents(display="output"),
|
| 518 |
+
MellonParam.latents_preview(),
|
| 519 |
+
MellonParam.doc(),
|
| 520 |
+
],
|
| 521 |
+
"required_inputs": ["embeddings"],
|
| 522 |
+
"required_model_inputs": ["unet", "scheduler"],
|
| 523 |
+
"block_name": "denoise",
|
| 524 |
+
},
|
| 525 |
+
"vae_encoder": {
|
| 526 |
+
"inputs": [
|
| 527 |
+
MellonParam.image(),
|
| 528 |
+
],
|
| 529 |
+
"model_inputs": [
|
| 530 |
+
MellonParam.vae(),
|
| 531 |
+
],
|
| 532 |
+
"outputs": [
|
| 533 |
+
MellonParam.image_latents(display="output"),
|
| 534 |
+
MellonParam.doc(),
|
| 535 |
+
],
|
| 536 |
+
"required_inputs": ["image"],
|
| 537 |
+
"required_model_inputs": ["vae"],
|
| 538 |
+
"block_name": "vae_encoder",
|
| 539 |
+
},
|
| 540 |
+
"text_encoder": {
|
| 541 |
+
"inputs": [
|
| 542 |
+
MellonParam.prompt(),
|
| 543 |
+
MellonParam.negative_prompt(),
|
| 544 |
+
],
|
| 545 |
+
"model_inputs": [
|
| 546 |
+
MellonParam.text_encoders(),
|
| 547 |
+
],
|
| 548 |
+
"outputs": [
|
| 549 |
+
MellonParam.embeddings(display="output"),
|
| 550 |
+
MellonParam.doc(),
|
| 551 |
+
],
|
| 552 |
+
"required_inputs": ["prompt"],
|
| 553 |
+
"required_model_inputs": ["text_encoders"],
|
| 554 |
+
"block_name": "text_encoder",
|
| 555 |
+
},
|
| 556 |
+
"decoder": {
|
| 557 |
+
"inputs": [
|
| 558 |
+
MellonParam.latents(display="input"),
|
| 559 |
+
],
|
| 560 |
+
"model_inputs": [
|
| 561 |
+
MellonParam.vae(),
|
| 562 |
+
],
|
| 563 |
+
"outputs": [
|
| 564 |
+
MellonParam.images(),
|
| 565 |
+
MellonParam.videos(),
|
| 566 |
+
MellonParam.doc(),
|
| 567 |
+
],
|
| 568 |
+
"required_inputs": ["latents"],
|
| 569 |
+
"required_model_inputs": ["vae"],
|
| 570 |
+
"block_name": "decode",
|
| 571 |
+
},
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def mark_required(label: str, marker: str = " *") -> str:
|
| 576 |
+
"""Add required marker to label if not already present."""
|
| 577 |
+
if label.endswith(marker):
|
| 578 |
+
return label
|
| 579 |
+
return f"{label}{marker}"
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
def node_spec_to_mellon_dict(node_spec: dict[str, Any], node_type: str) -> dict[str, Any]:
|
| 583 |
+
"""
|
| 584 |
+
Convert a node spec dict into Mellon format.
|
| 585 |
+
|
| 586 |
+
A node spec is how we define a Mellon diffusers node in code. This function converts it into the `params` map
|
| 587 |
+
format that Mellon UI expects.
|
| 588 |
+
|
| 589 |
+
The `params` map is a dict where keys are parameter names and values are UI configuration:
|
| 590 |
+
```python
|
| 591 |
+
{"seed": {"label": "Seed", "type": "int", "default": 0}}
|
| 592 |
+
```
|
| 593 |
+
|
| 594 |
+
For Modular Mellon nodes, we need to distinguish:
|
| 595 |
+
- `inputs`: Pipeline inputs (e.g., seed, prompt, image)
|
| 596 |
+
- `model_inputs`: Model components (e.g., unet, vae, scheduler)
|
| 597 |
+
- `outputs`: Node outputs (e.g., latents, images)
|
| 598 |
+
|
| 599 |
+
The node spec also includes:
|
| 600 |
+
- `required_inputs` / `required_model_inputs`: Which params are required (marked with *)
|
| 601 |
+
- `block_name`: The modular pipeline block this node corresponds to on backend
|
| 602 |
+
|
| 603 |
+
We provide factory methods for common parameters (e.g., `MellonParam.seed()`, `MellonParam.unet()`) so you don't
|
| 604 |
+
have to manually specify all the UI configuration.
|
| 605 |
+
|
| 606 |
+
Args:
|
| 607 |
+
node_spec: Dict with `inputs`, `model_inputs`, `outputs` (lists of MellonParam),
|
| 608 |
+
plus `required_inputs`, `required_model_inputs`, `block_name`.
|
| 609 |
+
node_type: The node type string (e.g., "denoise", "controlnet")
|
| 610 |
+
|
| 611 |
+
Returns:
|
| 612 |
+
Dict with:
|
| 613 |
+
- `params`: Flat dict of all params in Mellon UI format
|
| 614 |
+
- `input_names`: List of input parameter names
|
| 615 |
+
- `model_input_names`: List of model input parameter names
|
| 616 |
+
- `output_names`: List of output parameter names
|
| 617 |
+
- `block_name`: The backend block name
|
| 618 |
+
- `node_type`: The node type
|
| 619 |
+
|
| 620 |
+
Example:
|
| 621 |
+
```python
|
| 622 |
+
node_spec = {
|
| 623 |
+
"inputs": [MellonParam.seed(), MellonParam.prompt()],
|
| 624 |
+
"model_inputs": [MellonParam.unet()],
|
| 625 |
+
"outputs": [MellonParam.latents(display="output")],
|
| 626 |
+
"required_inputs": ["prompt"],
|
| 627 |
+
"required_model_inputs": ["unet"],
|
| 628 |
+
"block_name": "denoise",
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
result = node_spec_to_mellon_dict(node_spec, "denoise")
|
| 632 |
+
# Returns:
|
| 633 |
+
# {
|
| 634 |
+
# "params": {
|
| 635 |
+
# "seed": {"label": "Seed", "type": "int", "default": 0},
|
| 636 |
+
# "prompt": {"label": "Prompt *", "type": "string", "default": ""}, # * marks required
|
| 637 |
+
# "unet": {"label": "Denoise Model *", "type": "diffusers_auto_model", "display": "input"},
|
| 638 |
+
# "latents": {"label": "Latents", "type": "latents", "display": "output"},
|
| 639 |
+
# },
|
| 640 |
+
# "input_names": ["seed", "prompt"],
|
| 641 |
+
# "model_input_names": ["unet"],
|
| 642 |
+
# "output_names": ["latents"],
|
| 643 |
+
# "block_name": "denoise",
|
| 644 |
+
# "node_type": "denoise",
|
| 645 |
+
# }
|
| 646 |
+
```
|
| 647 |
+
"""
|
| 648 |
+
params = {}
|
| 649 |
+
input_names = []
|
| 650 |
+
model_input_names = []
|
| 651 |
+
output_names = []
|
| 652 |
+
|
| 653 |
+
required_inputs = node_spec.get("required_inputs", [])
|
| 654 |
+
required_model_inputs = node_spec.get("required_model_inputs", [])
|
| 655 |
+
|
| 656 |
+
# Process inputs
|
| 657 |
+
for p in node_spec.get("inputs", []):
|
| 658 |
+
param_dict = p.to_dict()
|
| 659 |
+
if p.name in required_inputs:
|
| 660 |
+
param_dict["label"] = mark_required(param_dict["label"])
|
| 661 |
+
params[p.name] = param_dict
|
| 662 |
+
input_names.append(p.name)
|
| 663 |
+
|
| 664 |
+
# Process model_inputs
|
| 665 |
+
for p in node_spec.get("model_inputs", []):
|
| 666 |
+
param_dict = p.to_dict()
|
| 667 |
+
if p.name in required_model_inputs:
|
| 668 |
+
param_dict["label"] = mark_required(param_dict["label"])
|
| 669 |
+
params[p.name] = param_dict
|
| 670 |
+
model_input_names.append(p.name)
|
| 671 |
+
|
| 672 |
+
# Process outputs: add a prefix to the output name if it already exists as an input
|
| 673 |
+
for p in node_spec.get("outputs", []):
|
| 674 |
+
if p.name in input_names:
|
| 675 |
+
# rename to out_<name>
|
| 676 |
+
output_name = f"out_{p.name}"
|
| 677 |
+
else:
|
| 678 |
+
output_name = p.name
|
| 679 |
+
params[output_name] = p.to_dict()
|
| 680 |
+
output_names.append(output_name)
|
| 681 |
+
|
| 682 |
+
return {
|
| 683 |
+
"params": params,
|
| 684 |
+
"input_names": input_names,
|
| 685 |
+
"model_input_names": model_input_names,
|
| 686 |
+
"output_names": output_names,
|
| 687 |
+
"block_name": node_spec.get("block_name"),
|
| 688 |
+
"node_type": node_type,
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
class MellonPipelineConfig:
|
| 693 |
+
"""
|
| 694 |
+
Configuration for an entire Mellon pipeline containing multiple nodes.
|
| 695 |
+
|
| 696 |
+
Accepts node specs as dicts with inputs/model_inputs/outputs lists of MellonParam, converts them to Mellon-ready
|
| 697 |
+
format, and handles save/load to Hub.
|
| 698 |
+
|
| 699 |
+
Example:
|
| 700 |
+
```python
|
| 701 |
+
config = MellonPipelineConfig(
|
| 702 |
+
node_specs={
|
| 703 |
+
"denoise": {
|
| 704 |
+
"inputs": [MellonParam.seed(), MellonParam.prompt()],
|
| 705 |
+
"model_inputs": [MellonParam.unet()],
|
| 706 |
+
"outputs": [MellonParam.latents(display="output")],
|
| 707 |
+
"required_inputs": ["prompt"],
|
| 708 |
+
"required_model_inputs": ["unet"],
|
| 709 |
+
"block_name": "denoise",
|
| 710 |
+
},
|
| 711 |
+
"decoder": {
|
| 712 |
+
"inputs": [MellonParam.latents(display="input")],
|
| 713 |
+
"outputs": [MellonParam.images()],
|
| 714 |
+
"block_name": "decoder",
|
| 715 |
+
},
|
| 716 |
+
},
|
| 717 |
+
label="My Pipeline",
|
| 718 |
+
default_repo="user/my-pipeline",
|
| 719 |
+
default_dtype="float16",
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
# Access Mellon format dict
|
| 723 |
+
denoise = config.node_params["denoise"]
|
| 724 |
+
input_names = denoise["input_names"]
|
| 725 |
+
params = denoise["params"]
|
| 726 |
+
|
| 727 |
+
# Save to Hub
|
| 728 |
+
config.save("./my_config", push_to_hub=True, repo_id="user/my-pipeline")
|
| 729 |
+
|
| 730 |
+
# Load from Hub
|
| 731 |
+
loaded = MellonPipelineConfig.load("user/my-pipeline")
|
| 732 |
+
```
|
| 733 |
+
"""
|
| 734 |
+
|
| 735 |
+
config_name = "mellon_pipeline_config.json"
|
| 736 |
+
|
| 737 |
+
def __init__(
|
| 738 |
+
self,
|
| 739 |
+
node_specs: dict[str, dict[str, Any] | None],
|
| 740 |
+
label: str = "",
|
| 741 |
+
default_repo: str = "",
|
| 742 |
+
default_dtype: str = "",
|
| 743 |
+
):
|
| 744 |
+
"""
|
| 745 |
+
Args:
|
| 746 |
+
node_specs: Dict mapping node_type to node spec or None.
|
| 747 |
+
Node spec has: inputs, model_inputs, outputs, required_inputs, required_model_inputs,
|
| 748 |
+
block_name (all optional)
|
| 749 |
+
label: Human-readable label for the pipeline
|
| 750 |
+
default_repo: Default HuggingFace repo for this pipeline
|
| 751 |
+
default_dtype: Default dtype (e.g., "float16", "bfloat16")
|
| 752 |
+
"""
|
| 753 |
+
# Convert all node specs to Mellon format immediately
|
| 754 |
+
self.node_specs = node_specs
|
| 755 |
+
|
| 756 |
+
self.label = label
|
| 757 |
+
self.default_repo = default_repo
|
| 758 |
+
self.default_dtype = default_dtype
|
| 759 |
+
|
| 760 |
+
@property
|
| 761 |
+
def node_params(self) -> dict[str, Any]:
|
| 762 |
+
"""Lazily compute node_params from node_specs."""
|
| 763 |
+
if self.node_specs is None:
|
| 764 |
+
return self._node_params
|
| 765 |
+
|
| 766 |
+
params = {}
|
| 767 |
+
for node_type, spec in self.node_specs.items():
|
| 768 |
+
if spec is None:
|
| 769 |
+
params[node_type] = None
|
| 770 |
+
else:
|
| 771 |
+
params[node_type] = node_spec_to_mellon_dict(spec, node_type)
|
| 772 |
+
return params
|
| 773 |
+
|
| 774 |
+
def __repr__(self) -> str:
|
| 775 |
+
lines = [
|
| 776 |
+
f"MellonPipelineConfig(label={self.label!r}, default_repo={self.default_repo!r}, default_dtype={self.default_dtype!r})"
|
| 777 |
+
]
|
| 778 |
+
for node_type, spec in self.node_specs.items():
|
| 779 |
+
if spec is None:
|
| 780 |
+
lines.append(f" {node_type}: None")
|
| 781 |
+
else:
|
| 782 |
+
inputs = [p.name for p in spec.get("inputs", [])]
|
| 783 |
+
model_inputs = [p.name for p in spec.get("model_inputs", [])]
|
| 784 |
+
outputs = [p.name for p in spec.get("outputs", [])]
|
| 785 |
+
lines.append(f" {node_type}:")
|
| 786 |
+
lines.append(f" inputs: {inputs}")
|
| 787 |
+
lines.append(f" model_inputs: {model_inputs}")
|
| 788 |
+
lines.append(f" outputs: {outputs}")
|
| 789 |
+
return "\n".join(lines)
|
| 790 |
+
|
| 791 |
+
def to_dict(self) -> dict[str, Any]:
|
| 792 |
+
"""Convert to a JSON-serializable dictionary."""
|
| 793 |
+
return {
|
| 794 |
+
"label": self.label,
|
| 795 |
+
"default_repo": self.default_repo,
|
| 796 |
+
"default_dtype": self.default_dtype,
|
| 797 |
+
"node_params": self.node_params,
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
@classmethod
|
| 801 |
+
def from_dict(cls, data: dict[str, Any]) -> "MellonPipelineConfig":
|
| 802 |
+
"""
|
| 803 |
+
Create from a dictionary (loaded from JSON).
|
| 804 |
+
|
| 805 |
+
Note: The mellon_params are already in Mellon format when loading from JSON.
|
| 806 |
+
"""
|
| 807 |
+
instance = cls.__new__(cls)
|
| 808 |
+
instance.node_specs = None
|
| 809 |
+
instance._node_params = data.get("node_params", {})
|
| 810 |
+
instance.label = data.get("label", "")
|
| 811 |
+
instance.default_repo = data.get("default_repo", "")
|
| 812 |
+
instance.default_dtype = data.get("default_dtype", "")
|
| 813 |
+
return instance
|
| 814 |
+
|
| 815 |
+
def to_json_string(self) -> str:
|
| 816 |
+
"""Serialize to JSON string."""
|
| 817 |
+
return json.dumps(self.to_dict(), indent=2, sort_keys=False) + "\n"
|
| 818 |
+
|
| 819 |
+
def to_json_file(self, json_file_path: str | os.PathLike):
|
| 820 |
+
"""Save to a JSON file."""
|
| 821 |
+
with open(json_file_path, "w", encoding="utf-8") as writer:
|
| 822 |
+
writer.write(self.to_json_string())
|
| 823 |
+
|
| 824 |
+
@classmethod
|
| 825 |
+
def from_json_file(cls, json_file_path: str | os.PathLike) -> "MellonPipelineConfig":
|
| 826 |
+
"""Load from a JSON file."""
|
| 827 |
+
with open(json_file_path, "r", encoding="utf-8") as reader:
|
| 828 |
+
data = json.load(reader)
|
| 829 |
+
return cls.from_dict(data)
|
| 830 |
+
|
| 831 |
+
def save(self, save_directory: str | os.PathLike, push_to_hub: bool = False, **kwargs):
|
| 832 |
+
"""Save the mellon pipeline config to a directory."""
|
| 833 |
+
if os.path.isfile(save_directory):
|
| 834 |
+
raise AssertionError(f"Provided path ({save_directory}) should be a directory, not a file")
|
| 835 |
+
|
| 836 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 837 |
+
output_path = os.path.join(save_directory, self.config_name)
|
| 838 |
+
self.to_json_file(output_path)
|
| 839 |
+
logger.info(f"Pipeline config saved to {output_path}")
|
| 840 |
+
|
| 841 |
+
if push_to_hub:
|
| 842 |
+
commit_message = kwargs.pop("commit_message", None)
|
| 843 |
+
private = kwargs.pop("private", None)
|
| 844 |
+
create_pr = kwargs.pop("create_pr", False)
|
| 845 |
+
token = kwargs.pop("token", None)
|
| 846 |
+
repo_id = kwargs.pop("repo_id", save_directory.split(os.path.sep)[-1])
|
| 847 |
+
repo_id = create_repo(repo_id, exist_ok=True, private=private, token=token).repo_id
|
| 848 |
+
|
| 849 |
+
upload_file(
|
| 850 |
+
path_or_fileobj=output_path,
|
| 851 |
+
path_in_repo=self.config_name,
|
| 852 |
+
repo_id=repo_id,
|
| 853 |
+
token=token,
|
| 854 |
+
commit_message=commit_message or "Upload MellonPipelineConfig",
|
| 855 |
+
create_pr=create_pr,
|
| 856 |
+
)
|
| 857 |
+
logger.info(f"Pipeline config pushed to hub: {repo_id}")
|
| 858 |
+
|
| 859 |
+
@classmethod
|
| 860 |
+
def load(
|
| 861 |
+
cls,
|
| 862 |
+
pretrained_model_name_or_path: str | os.PathLike,
|
| 863 |
+
**kwargs,
|
| 864 |
+
) -> "MellonPipelineConfig":
|
| 865 |
+
"""Load a pipeline config from a local path or Hugging Face Hub."""
|
| 866 |
+
cache_dir = kwargs.pop("cache_dir", None)
|
| 867 |
+
local_dir = kwargs.pop("local_dir", None)
|
| 868 |
+
local_dir_use_symlinks = kwargs.pop("local_dir_use_symlinks", "auto")
|
| 869 |
+
force_download = kwargs.pop("force_download", False)
|
| 870 |
+
proxies = kwargs.pop("proxies", None)
|
| 871 |
+
token = kwargs.pop("token", None)
|
| 872 |
+
local_files_only = kwargs.pop("local_files_only", False)
|
| 873 |
+
revision = kwargs.pop("revision", None)
|
| 874 |
+
subfolder = kwargs.pop("subfolder", None)
|
| 875 |
+
|
| 876 |
+
pretrained_model_name_or_path = str(pretrained_model_name_or_path)
|
| 877 |
+
|
| 878 |
+
if os.path.isfile(pretrained_model_name_or_path):
|
| 879 |
+
config_file = pretrained_model_name_or_path
|
| 880 |
+
elif os.path.isdir(pretrained_model_name_or_path):
|
| 881 |
+
config_file = os.path.join(pretrained_model_name_or_path, cls.config_name)
|
| 882 |
+
if not os.path.isfile(config_file):
|
| 883 |
+
raise EnvironmentError(f"No file named {cls.config_name} found in {pretrained_model_name_or_path}")
|
| 884 |
+
else:
|
| 885 |
+
try:
|
| 886 |
+
config_file = hf_hub_download(
|
| 887 |
+
pretrained_model_name_or_path,
|
| 888 |
+
filename=cls.config_name,
|
| 889 |
+
cache_dir=cache_dir,
|
| 890 |
+
force_download=force_download,
|
| 891 |
+
proxies=proxies,
|
| 892 |
+
local_files_only=local_files_only,
|
| 893 |
+
token=token,
|
| 894 |
+
revision=revision,
|
| 895 |
+
subfolder=subfolder,
|
| 896 |
+
local_dir=local_dir,
|
| 897 |
+
local_dir_use_symlinks=local_dir_use_symlinks,
|
| 898 |
+
)
|
| 899 |
+
except RepositoryNotFoundError:
|
| 900 |
+
raise EnvironmentError(
|
| 901 |
+
f"{pretrained_model_name_or_path} is not a local folder and is not a valid model identifier"
|
| 902 |
+
" listed on 'https://huggingface.co/models'\nIf this is a private repository, make sure to pass a"
|
| 903 |
+
" token having permission to this repo with `token` or log in with `hf auth login`."
|
| 904 |
+
)
|
| 905 |
+
except RevisionNotFoundError:
|
| 906 |
+
raise EnvironmentError(
|
| 907 |
+
f"{revision} is not a valid git identifier (branch name, tag name or commit id) that exists for"
|
| 908 |
+
" this model name. Check the model page at"
|
| 909 |
+
f" 'https://huggingface.co/{pretrained_model_name_or_path}' for available revisions."
|
| 910 |
+
)
|
| 911 |
+
except EntryNotFoundError:
|
| 912 |
+
raise EnvironmentError(
|
| 913 |
+
f"{pretrained_model_name_or_path} does not appear to have a file named {cls.config_name}."
|
| 914 |
+
)
|
| 915 |
+
except HfHubHTTPError as err:
|
| 916 |
+
raise EnvironmentError(
|
| 917 |
+
"There was a specific connection error when trying to load"
|
| 918 |
+
f" {pretrained_model_name_or_path}:\n{err}"
|
| 919 |
+
)
|
| 920 |
+
except ValueError:
|
| 921 |
+
raise EnvironmentError(
|
| 922 |
+
f"We couldn't connect to '{HUGGINGFACE_CO_RESOLVE_ENDPOINT}' to load this model, couldn't find it"
|
| 923 |
+
f" in the cached files and it looks like {pretrained_model_name_or_path} is not the path to a"
|
| 924 |
+
f" directory containing a {cls.config_name} file.\nCheckout your internet connection or see how to"
|
| 925 |
+
" run the library in offline mode at"
|
| 926 |
+
" 'https://huggingface.co/docs/diffusers/installation#offline-mode'."
|
| 927 |
+
)
|
| 928 |
+
except EnvironmentError:
|
| 929 |
+
raise EnvironmentError(
|
| 930 |
+
f"Can't load config for '{pretrained_model_name_or_path}'. If you were trying to load it from "
|
| 931 |
+
"'https://huggingface.co/models', make sure you don't have a local directory with the same name. "
|
| 932 |
+
f"Otherwise, make sure '{pretrained_model_name_or_path}' is the correct path to a directory "
|
| 933 |
+
f"containing a {cls.config_name} file"
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
+
try:
|
| 937 |
+
return cls.from_json_file(config_file)
|
| 938 |
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
| 939 |
+
raise EnvironmentError(f"The config file at '{config_file}' is not a valid JSON file.")
|
| 940 |
+
|
| 941 |
+
@classmethod
|
| 942 |
+
def from_blocks(
|
| 943 |
+
cls,
|
| 944 |
+
blocks,
|
| 945 |
+
template: dict[str, dict[str, Any]] | None = None,
|
| 946 |
+
label: str = "",
|
| 947 |
+
default_repo: str = "",
|
| 948 |
+
default_dtype: str = "bfloat16",
|
| 949 |
+
) -> "MellonPipelineConfig":
|
| 950 |
+
"""
|
| 951 |
+
Create MellonPipelineConfig by matching template against actual pipeline blocks.
|
| 952 |
+
"""
|
| 953 |
+
if template is None:
|
| 954 |
+
template = DEFAULT_NODE_SPECS
|
| 955 |
+
|
| 956 |
+
sub_block_map = dict(blocks.sub_blocks)
|
| 957 |
+
|
| 958 |
+
def filter_spec_for_block(template_spec: dict[str, Any], block) -> dict[str, Any] | None:
|
| 959 |
+
"""Filter template spec params based on what the block actually supports."""
|
| 960 |
+
block_input_names = set(block.input_names)
|
| 961 |
+
block_output_names = set(block.intermediate_output_names)
|
| 962 |
+
block_component_names = set(block.component_names)
|
| 963 |
+
|
| 964 |
+
filtered_inputs = [
|
| 965 |
+
p
|
| 966 |
+
for p in template_spec.get("inputs", [])
|
| 967 |
+
if p.required_block_params is None
|
| 968 |
+
or all(name in block_input_names for name in p.required_block_params)
|
| 969 |
+
]
|
| 970 |
+
filtered_model_inputs = [
|
| 971 |
+
p
|
| 972 |
+
for p in template_spec.get("model_inputs", [])
|
| 973 |
+
if p.required_block_params is None
|
| 974 |
+
or all(name in block_component_names for name in p.required_block_params)
|
| 975 |
+
]
|
| 976 |
+
filtered_outputs = [
|
| 977 |
+
p
|
| 978 |
+
for p in template_spec.get("outputs", [])
|
| 979 |
+
if p.required_block_params is None
|
| 980 |
+
or all(name in block_output_names for name in p.required_block_params)
|
| 981 |
+
]
|
| 982 |
+
|
| 983 |
+
filtered_input_names = {p.name for p in filtered_inputs}
|
| 984 |
+
filtered_model_input_names = {p.name for p in filtered_model_inputs}
|
| 985 |
+
|
| 986 |
+
filtered_required_inputs = [
|
| 987 |
+
r for r in template_spec.get("required_inputs", []) if r in filtered_input_names
|
| 988 |
+
]
|
| 989 |
+
filtered_required_model_inputs = [
|
| 990 |
+
r for r in template_spec.get("required_model_inputs", []) if r in filtered_model_input_names
|
| 991 |
+
]
|
| 992 |
+
|
| 993 |
+
return {
|
| 994 |
+
"inputs": filtered_inputs,
|
| 995 |
+
"model_inputs": filtered_model_inputs,
|
| 996 |
+
"outputs": filtered_outputs,
|
| 997 |
+
"required_inputs": filtered_required_inputs,
|
| 998 |
+
"required_model_inputs": filtered_required_model_inputs,
|
| 999 |
+
"block_name": template_spec.get("block_name"),
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
# Build node specs
|
| 1003 |
+
node_specs = {}
|
| 1004 |
+
for node_type, template_spec in template.items():
|
| 1005 |
+
if template_spec is None:
|
| 1006 |
+
node_specs[node_type] = None
|
| 1007 |
+
continue
|
| 1008 |
+
|
| 1009 |
+
block_name = template_spec.get("block_name")
|
| 1010 |
+
if block_name is None or block_name not in sub_block_map:
|
| 1011 |
+
node_specs[node_type] = None
|
| 1012 |
+
continue
|
| 1013 |
+
|
| 1014 |
+
node_specs[node_type] = filter_spec_for_block(template_spec, sub_block_map[block_name])
|
| 1015 |
+
|
| 1016 |
+
return cls(
|
| 1017 |
+
node_specs=node_specs,
|
| 1018 |
+
label=label or getattr(blocks, "model_name", ""),
|
| 1019 |
+
default_repo=default_repo,
|
| 1020 |
+
default_dtype=default_dtype,
|
| 1021 |
+
)
|
| 1022 |
+
|
| 1023 |
+
@classmethod
|
| 1024 |
+
def from_custom_block(
|
| 1025 |
+
cls,
|
| 1026 |
+
block,
|
| 1027 |
+
node_label: str = None,
|
| 1028 |
+
input_types: dict[str, Any] | None = None,
|
| 1029 |
+
output_types: dict[str, Any] | None = None,
|
| 1030 |
+
) -> "MellonPipelineConfig":
|
| 1031 |
+
"""
|
| 1032 |
+
Create a MellonPipelineConfig from a custom block.
|
| 1033 |
+
|
| 1034 |
+
Args:
|
| 1035 |
+
block: A block instance with `inputs`, `outputs`, and `expected_components`/`component_names` properties.
|
| 1036 |
+
Each InputParam/OutputParam should have metadata={"mellon": "<type>"} where type is one of: image,
|
| 1037 |
+
video, text, checkbox, number, slider, dropdown, model. If metadata is None, maps to "custom".
|
| 1038 |
+
node_label: The display label for the node. Defaults to block class name with spaces.
|
| 1039 |
+
input_types:
|
| 1040 |
+
Optional dict mapping input param names to mellon types. Overrides the block's metadata if provided.
|
| 1041 |
+
Example: {"prompt": "textbox", "image": "image"}
|
| 1042 |
+
output_types:
|
| 1043 |
+
Optional dict mapping output param names to mellon types. Overrides the block's metadata if provided.
|
| 1044 |
+
Example: {"prompt": "text", "images": "image"}
|
| 1045 |
+
|
| 1046 |
+
Returns:
|
| 1047 |
+
MellonPipelineConfig instance
|
| 1048 |
+
"""
|
| 1049 |
+
if node_label is None:
|
| 1050 |
+
class_name = block.__class__.__name__
|
| 1051 |
+
node_label = "".join([" " + c if c.isupper() else c for c in class_name]).strip()
|
| 1052 |
+
|
| 1053 |
+
if input_types is None:
|
| 1054 |
+
input_types = {}
|
| 1055 |
+
if output_types is None:
|
| 1056 |
+
output_types = {}
|
| 1057 |
+
|
| 1058 |
+
inputs = []
|
| 1059 |
+
model_inputs = []
|
| 1060 |
+
outputs = []
|
| 1061 |
+
|
| 1062 |
+
# Process block inputs
|
| 1063 |
+
for input_param in block.inputs:
|
| 1064 |
+
if input_param.name is None:
|
| 1065 |
+
continue
|
| 1066 |
+
if input_param.name in input_types:
|
| 1067 |
+
input_param = copy.copy(input_param)
|
| 1068 |
+
input_param.metadata = {"mellon": input_types[input_param.name]}
|
| 1069 |
+
print(f" processing input: {input_param.name}, metadata: {input_param.metadata}")
|
| 1070 |
+
inputs.append(input_param_to_mellon_param(input_param))
|
| 1071 |
+
|
| 1072 |
+
# Process block outputs
|
| 1073 |
+
for output_param in block.outputs:
|
| 1074 |
+
if output_param.name is None:
|
| 1075 |
+
continue
|
| 1076 |
+
if output_param.name in output_types:
|
| 1077 |
+
output_param = copy.copy(output_param)
|
| 1078 |
+
output_param.metadata = {"mellon": output_types[output_param.name]}
|
| 1079 |
+
outputs.append(output_param_to_mellon_param(output_param))
|
| 1080 |
+
|
| 1081 |
+
# Process expected components (all map to model inputs)
|
| 1082 |
+
component_names = block.component_names
|
| 1083 |
+
for component_name in component_names:
|
| 1084 |
+
model_inputs.append(MellonParam.Input.model(component_name))
|
| 1085 |
+
|
| 1086 |
+
# Always add doc output
|
| 1087 |
+
outputs.append(MellonParam.doc())
|
| 1088 |
+
|
| 1089 |
+
node_spec = {
|
| 1090 |
+
"inputs": inputs,
|
| 1091 |
+
"model_inputs": model_inputs,
|
| 1092 |
+
"outputs": outputs,
|
| 1093 |
+
"required_inputs": [],
|
| 1094 |
+
"required_model_inputs": [],
|
| 1095 |
+
"block_name": "custom",
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
return cls(
|
| 1099 |
+
node_specs={"custom": node_spec},
|
| 1100 |
+
label=node_label,
|
| 1101 |
+
)
|
diffusers/modular_pipelines/minimax_h3/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import TYPE_CHECKING
|
| 2 |
+
|
| 3 |
+
from ...utils import (
|
| 4 |
+
DIFFUSERS_SLOW_IMPORT,
|
| 5 |
+
OptionalDependencyNotAvailable,
|
| 6 |
+
_LazyModule,
|
| 7 |
+
get_objects_from_module,
|
| 8 |
+
is_torch_available,
|
| 9 |
+
is_transformers_available,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
_dummy_objects = {}
|
| 14 |
+
_import_structure = {}
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 18 |
+
raise OptionalDependencyNotAvailable()
|
| 19 |
+
except OptionalDependencyNotAvailable:
|
| 20 |
+
from ...utils import dummy_torch_and_transformers_objects # noqa F403
|
| 21 |
+
|
| 22 |
+
_dummy_objects.update(get_objects_from_module(dummy_torch_and_transformers_objects))
|
| 23 |
+
else:
|
| 24 |
+
_import_structure["modular_blocks_minimax_h3"] = ["MiniMaxH3Blocks", "MiniMaxH3Ref2VABlocks"]
|
| 25 |
+
_import_structure["modular_pipeline"] = ["MiniMaxH3ModularPipeline", "MiniMaxH3Ref2VAModularPipeline"]
|
| 26 |
+
_import_structure["packing_ref2va"] = ["MiniMaxH3Reference"]
|
| 27 |
+
|
| 28 |
+
if TYPE_CHECKING or DIFFUSERS_SLOW_IMPORT:
|
| 29 |
+
try:
|
| 30 |
+
if not (is_transformers_available() and is_torch_available()):
|
| 31 |
+
raise OptionalDependencyNotAvailable()
|
| 32 |
+
except OptionalDependencyNotAvailable:
|
| 33 |
+
from ...utils.dummy_torch_and_transformers_objects import * # noqa F403
|
| 34 |
+
else:
|
| 35 |
+
from .modular_blocks_minimax_h3 import MiniMaxH3Blocks, MiniMaxH3Ref2VABlocks
|
| 36 |
+
from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
|
| 37 |
+
from .packing_ref2va import MiniMaxH3Reference
|
| 38 |
+
else:
|
| 39 |
+
import sys
|
| 40 |
+
|
| 41 |
+
sys.modules[__name__] = _LazyModule(
|
| 42 |
+
__name__,
|
| 43 |
+
globals()["__file__"],
|
| 44 |
+
_import_structure,
|
| 45 |
+
module_spec=__spec__,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
for name, value in _dummy_objects.items():
|
| 49 |
+
setattr(sys.modules[__name__], name, value)
|
diffusers/modular_pipelines/minimax_h3/before_denoise.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The MiniMax and HuggingFace Teams. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
|
| 17 |
+
from ...schedulers import MiniMaxH3Scheduler
|
| 18 |
+
from ...utils import logging
|
| 19 |
+
from ...utils.torch_utils import randn_tensor
|
| 20 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 21 |
+
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
|
| 22 |
+
from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
|
| 23 |
+
from .packing import (
|
| 24 |
+
MINIMAX_H3_AUDIO_CHANNELS,
|
| 25 |
+
MINIMAX_H3_KEYFRAME_NOISE_AUG,
|
| 26 |
+
MiniMaxH3PackedSequence,
|
| 27 |
+
build_packed_sequence,
|
| 28 |
+
build_row_timesteps,
|
| 29 |
+
patchify_video_latents,
|
| 30 |
+
)
|
| 31 |
+
from .packing_ref2va import MiniMaxH3PreparedReference, build_ref2va_packed_sequence
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _layout_inputs() -> list[InputParam]:
|
| 38 |
+
r"""What both packed layouts are built from, beyond the conditioning of the task itself."""
|
| 39 |
+
return [
|
| 40 |
+
InputParam(
|
| 41 |
+
name="text_token_tags",
|
| 42 |
+
type_hint=torch.Tensor,
|
| 43 |
+
required=True,
|
| 44 |
+
description="The per-row modality tag of every row of `prompt_embeds`.",
|
| 45 |
+
),
|
| 46 |
+
InputParam(
|
| 47 |
+
name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames."
|
| 48 |
+
),
|
| 49 |
+
InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."),
|
| 50 |
+
InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."),
|
| 51 |
+
InputParam(
|
| 52 |
+
name="num_audio_latents",
|
| 53 |
+
type_hint=int,
|
| 54 |
+
required=True,
|
| 55 |
+
description="Number of audio latents per channel.",
|
| 56 |
+
),
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _layout_outputs() -> list[OutputParam]:
|
| 61 |
+
r"""The row layout of the packed sequence, shared by the two tasks."""
|
| 62 |
+
return [
|
| 63 |
+
OutputParam(
|
| 64 |
+
"layout",
|
| 65 |
+
type_hint=MiniMaxH3PackedSequence,
|
| 66 |
+
description="The structural description of the packed sequence.",
|
| 67 |
+
),
|
| 68 |
+
OutputParam(
|
| 69 |
+
"position_ids",
|
| 70 |
+
type_hint=torch.Tensor,
|
| 71 |
+
description="The `(t, h, w)` rotary coordinate of every row, in float64.",
|
| 72 |
+
),
|
| 73 |
+
OutputParam("token_tags", type_hint=torch.Tensor, description="The modality tag of every row."),
|
| 74 |
+
OutputParam(
|
| 75 |
+
"video_indices",
|
| 76 |
+
type_hint=torch.Tensor,
|
| 77 |
+
description="Sequence positions of the video rows, conditioning rows first.",
|
| 78 |
+
),
|
| 79 |
+
OutputParam(
|
| 80 |
+
"audio_indices",
|
| 81 |
+
type_hint=torch.Tensor,
|
| 82 |
+
description="Sequence positions of the audio rows, reference rows first.",
|
| 83 |
+
),
|
| 84 |
+
OutputParam("text_indices", type_hint=torch.Tensor, description="Sequence positions of the text rows."),
|
| 85 |
+
OutputParam(
|
| 86 |
+
"num_condition_video_rows",
|
| 87 |
+
type_hint=int,
|
| 88 |
+
description="How many leading video rows are conditioning rows rather than generated rows.",
|
| 89 |
+
),
|
| 90 |
+
OutputParam(
|
| 91 |
+
"num_condition_audio_rows",
|
| 92 |
+
type_hint=int,
|
| 93 |
+
description="How many leading audio rows are reference rows rather than generated rows.",
|
| 94 |
+
),
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _set_layout_state(block_state, layout: MiniMaxH3PackedSequence, device: torch.device) -> None:
|
| 99 |
+
block_state.layout = layout
|
| 100 |
+
block_state.position_ids = layout.position_ids.to(device)
|
| 101 |
+
block_state.token_tags = layout.token_tags.to(device)
|
| 102 |
+
block_state.video_indices = layout.video_indices.to(device)
|
| 103 |
+
block_state.audio_indices = layout.audio_indices.to(device)
|
| 104 |
+
block_state.text_indices = layout.text_indices.to(device)
|
| 105 |
+
block_state.num_condition_video_rows = layout.num_condition_video_rows
|
| 106 |
+
block_state.num_condition_audio_rows = layout.num_condition_audio_rows
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class MiniMaxH3PrepareLayoutStep(ModularPipelineBlocks):
|
| 110 |
+
model_name = "minimax-h3"
|
| 111 |
+
|
| 112 |
+
@property
|
| 113 |
+
def description(self) -> str:
|
| 114 |
+
return (
|
| 115 |
+
"Builds the packed layout of a `t2va` / `fl2va` request — `[text | keyframe conditions | target audio | "
|
| 116 |
+
"target video]` — and its fp64 rotary grid. MiniMax-H3 runs full self-attention over this one sequence, "
|
| 117 |
+
"so the layout is what every later block addresses rows through."
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
@property
|
| 121 |
+
def inputs(self) -> list[InputParam]:
|
| 122 |
+
return [
|
| 123 |
+
*_layout_inputs(),
|
| 124 |
+
InputParam(
|
| 125 |
+
name="keyframe_anchors",
|
| 126 |
+
type_hint=tuple,
|
| 127 |
+
default=(),
|
| 128 |
+
description="Which end of the video every keyframe is anchored to, in packed order.",
|
| 129 |
+
),
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 134 |
+
return _layout_outputs()
|
| 135 |
+
|
| 136 |
+
@torch.no_grad()
|
| 137 |
+
def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
|
| 138 |
+
block_state = self.get_block_state(state)
|
| 139 |
+
|
| 140 |
+
layout = build_packed_sequence(
|
| 141 |
+
block_state.text_token_tags,
|
| 142 |
+
block_state.num_latent_frames,
|
| 143 |
+
block_state.latent_height,
|
| 144 |
+
block_state.latent_width,
|
| 145 |
+
block_state.num_audio_latents,
|
| 146 |
+
components.patch_size,
|
| 147 |
+
block_state.keyframe_anchors,
|
| 148 |
+
)
|
| 149 |
+
_set_layout_state(block_state, layout, components._execution_device)
|
| 150 |
+
|
| 151 |
+
self.set_block_state(state, block_state)
|
| 152 |
+
return components, state
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class MiniMaxH3Ref2VAPrepareLayoutStep(ModularPipelineBlocks):
|
| 156 |
+
model_name = "minimax-h3-ref2va"
|
| 157 |
+
|
| 158 |
+
@property
|
| 159 |
+
def description(self) -> str:
|
| 160 |
+
return (
|
| 161 |
+
"Builds the packed layout of a `ref2va` request — `[text | reference blocks | target audio | target "
|
| 162 |
+
"video]` — and its fp64 rotary grid. The reference order advances the shared audio/video rotary clock, so "
|
| 163 |
+
"it is part of the layout rather than a detail of the presentation."
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def inputs(self) -> list[InputParam]:
|
| 168 |
+
return [
|
| 169 |
+
*_layout_inputs(),
|
| 170 |
+
InputParam(
|
| 171 |
+
name="prepared_references",
|
| 172 |
+
type_hint=list[MiniMaxH3PreparedReference],
|
| 173 |
+
required=True,
|
| 174 |
+
description="The prepared references, in packed order, with their latent geometry filled in.",
|
| 175 |
+
),
|
| 176 |
+
]
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 180 |
+
return _layout_outputs()
|
| 181 |
+
|
| 182 |
+
@torch.no_grad()
|
| 183 |
+
def __call__(self, components: MiniMaxH3Ref2VAModularPipeline, state: PipelineState) -> PipelineState:
|
| 184 |
+
block_state = self.get_block_state(state)
|
| 185 |
+
|
| 186 |
+
layout = build_ref2va_packed_sequence(
|
| 187 |
+
block_state.text_token_tags,
|
| 188 |
+
block_state.prepared_references,
|
| 189 |
+
block_state.num_latent_frames,
|
| 190 |
+
block_state.latent_height,
|
| 191 |
+
block_state.latent_width,
|
| 192 |
+
block_state.num_audio_latents,
|
| 193 |
+
components.patch_size,
|
| 194 |
+
)
|
| 195 |
+
_set_layout_state(block_state, layout, components._execution_device)
|
| 196 |
+
|
| 197 |
+
self.set_block_state(state, block_state)
|
| 198 |
+
return components, state
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class MiniMaxH3PrepareLatentsStep(ModularPipelineBlocks):
|
| 202 |
+
model_name = "minimax-h3"
|
| 203 |
+
|
| 204 |
+
@property
|
| 205 |
+
def description(self) -> str:
|
| 206 |
+
return (
|
| 207 |
+
"Draws the initial noise of the generated rows and prepends the conditioning rows. MiniMax-H3 draws the "
|
| 208 |
+
"video noise as a latent tensor and patchifies it afterwards, then the audio noise directly in row "
|
| 209 |
+
"layout — both off the request's generator, after the conditioning noise of the encoder step."
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
@property
|
| 213 |
+
def inputs(self) -> list[InputParam]:
|
| 214 |
+
return [
|
| 215 |
+
InputParam(
|
| 216 |
+
name="num_latent_frames", type_hint=int, required=True, description="Number of video latent frames."
|
| 217 |
+
),
|
| 218 |
+
InputParam(name="latent_height", type_hint=int, required=True, description="Height of the video latents."),
|
| 219 |
+
InputParam(name="latent_width", type_hint=int, required=True, description="Width of the video latents."),
|
| 220 |
+
InputParam(
|
| 221 |
+
name="num_audio_latents",
|
| 222 |
+
type_hint=int,
|
| 223 |
+
required=True,
|
| 224 |
+
description="Number of audio latents per channel.",
|
| 225 |
+
),
|
| 226 |
+
InputParam.template(
|
| 227 |
+
"generator",
|
| 228 |
+
description=(
|
| 229 |
+
"The generator of the request. The video noise is drawn from it first, then the audio noise."
|
| 230 |
+
),
|
| 231 |
+
),
|
| 232 |
+
InputParam(
|
| 233 |
+
name="latents",
|
| 234 |
+
type_hint=torch.Tensor,
|
| 235 |
+
description=(
|
| 236 |
+
"Pre-generated video noise of shape `(1, 24, num_latent_frames, latent_height, latent_width)`, "
|
| 237 |
+
"used instead of the draw."
|
| 238 |
+
),
|
| 239 |
+
),
|
| 240 |
+
InputParam(
|
| 241 |
+
name="audio_latents",
|
| 242 |
+
type_hint=torch.Tensor,
|
| 243 |
+
description="Pre-generated audio noise of shape `(2, 32, num_audio_latents)`.",
|
| 244 |
+
),
|
| 245 |
+
InputParam(
|
| 246 |
+
name="condition_latents",
|
| 247 |
+
type_hint=torch.Tensor,
|
| 248 |
+
description="The video conditioning rows to prepend, or None for a request that has none.",
|
| 249 |
+
),
|
| 250 |
+
InputParam(
|
| 251 |
+
name="audio_condition_latents",
|
| 252 |
+
type_hint=torch.Tensor,
|
| 253 |
+
description="The audio conditioning rows to prepend, or None for a request that has none.",
|
| 254 |
+
),
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
@property
|
| 258 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 259 |
+
return [
|
| 260 |
+
OutputParam(
|
| 261 |
+
"latents",
|
| 262 |
+
type_hint=torch.Tensor,
|
| 263 |
+
description="The video rows of the packed sequence, conditioning rows first.",
|
| 264 |
+
),
|
| 265 |
+
OutputParam(
|
| 266 |
+
"audio_latents",
|
| 267 |
+
type_hint=torch.Tensor,
|
| 268 |
+
description="The channel-major audio rows of the packed sequence, reference rows first.",
|
| 269 |
+
),
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
@staticmethod
|
| 273 |
+
def prepare_latents(
|
| 274 |
+
components,
|
| 275 |
+
num_latent_frames: int,
|
| 276 |
+
latent_height: int,
|
| 277 |
+
latent_width: int,
|
| 278 |
+
num_audio_latents: int,
|
| 279 |
+
device: torch.device,
|
| 280 |
+
generator: torch.Generator | list[torch.Generator] | None = None,
|
| 281 |
+
latents: torch.Tensor | None = None,
|
| 282 |
+
audio_latents: torch.Tensor | None = None,
|
| 283 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 284 |
+
r"""
|
| 285 |
+
Draw the initial noise of both modalities and pack it into transformer rows.
|
| 286 |
+
|
| 287 |
+
A request draws every stream from the one generator it is given, and the order is part of what that generator
|
| 288 |
+
reproduces: the conditioning noise of the keyframes or references first (one draw per condition, in
|
| 289 |
+
[`~modular_pipelines.minimax_h3.packing.keyframe_condition_noise`]), then the video noise here, as a latent tensor
|
| 290 |
+
that is patchified afterwards, then the audio noise, directly in row layout. Passing `latents` or
|
| 291 |
+
`audio_latents` skips its draw and shifts the ones after it.
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
num_latent_frames (`int`): Number of video latent frames.
|
| 295 |
+
latent_height (`int`): Latent height.
|
| 296 |
+
latent_width (`int`): Latent width.
|
| 297 |
+
num_audio_latents (`int`): Number of audio latents per channel.
|
| 298 |
+
device (`torch.device`): The device the rows are drawn on.
|
| 299 |
+
generator (`torch.Generator`, *optional*): The generator of the request.
|
| 300 |
+
latents (`torch.Tensor`, *optional*):
|
| 301 |
+
Pre-generated video noise of shape `(1, latent_channels, num_latent_frames, latent_height,
|
| 302 |
+
latent_width)`, used instead of the draw.
|
| 303 |
+
audio_latents (`torch.Tensor`, *optional*):
|
| 304 |
+
Pre-generated audio noise of shape `(2, audio_latent_channels, num_audio_latents)`.
|
| 305 |
+
|
| 306 |
+
Returns:
|
| 307 |
+
`tuple[torch.Tensor, torch.Tensor]`: the video rows and the channel-major audio rows.
|
| 308 |
+
"""
|
| 309 |
+
if latents is None:
|
| 310 |
+
latents = randn_tensor(
|
| 311 |
+
(1, components.vae_latent_channels, num_latent_frames, latent_height, latent_width),
|
| 312 |
+
generator=generator,
|
| 313 |
+
device=device,
|
| 314 |
+
dtype=torch.float32,
|
| 315 |
+
)
|
| 316 |
+
video_rows = patchify_video_latents(latents.to(torch.float32), components.patch_size)
|
| 317 |
+
|
| 318 |
+
if audio_latents is None:
|
| 319 |
+
audio_rows = randn_tensor(
|
| 320 |
+
(num_audio_latents * MINIMAX_H3_AUDIO_CHANNELS, components.audio_latent_channels),
|
| 321 |
+
generator=generator,
|
| 322 |
+
device=device,
|
| 323 |
+
dtype=torch.float32,
|
| 324 |
+
)
|
| 325 |
+
else:
|
| 326 |
+
audio_rows = audio_latents.to(torch.float32).permute(0, 2, 1).reshape(-1, components.audio_latent_channels)
|
| 327 |
+
return video_rows.to(device), audio_rows.to(device)
|
| 328 |
+
|
| 329 |
+
@torch.no_grad()
|
| 330 |
+
def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
|
| 331 |
+
block_state = self.get_block_state(state)
|
| 332 |
+
|
| 333 |
+
latents, audio_latents = self.prepare_latents(
|
| 334 |
+
components,
|
| 335 |
+
block_state.num_latent_frames,
|
| 336 |
+
block_state.latent_height,
|
| 337 |
+
block_state.latent_width,
|
| 338 |
+
block_state.num_audio_latents,
|
| 339 |
+
components._execution_device,
|
| 340 |
+
block_state.generator,
|
| 341 |
+
block_state.latents,
|
| 342 |
+
block_state.audio_latents,
|
| 343 |
+
)
|
| 344 |
+
if block_state.condition_latents is not None:
|
| 345 |
+
latents = torch.cat([block_state.condition_latents, latents])
|
| 346 |
+
if block_state.audio_condition_latents is not None:
|
| 347 |
+
audio_latents = torch.cat([block_state.audio_condition_latents, audio_latents])
|
| 348 |
+
block_state.latents, block_state.audio_latents = latents, audio_latents
|
| 349 |
+
|
| 350 |
+
self.set_block_state(state, block_state)
|
| 351 |
+
return components, state
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class MiniMaxH3SetTimestepsStep(ModularPipelineBlocks):
|
| 355 |
+
model_name = "minimax-h3"
|
| 356 |
+
|
| 357 |
+
@property
|
| 358 |
+
def description(self) -> str:
|
| 359 |
+
return (
|
| 360 |
+
"Initializes the two schedules — `shift = 12.0` for video, `shift = 3.0` for audio — and stages the "
|
| 361 |
+
"row-to-timestep plan of every step. One forward serves every modality and every noise level at once: "
|
| 362 |
+
"the generated rows step down their own schedule while the conditioning rows stay pinned at their "
|
| 363 |
+
"noise-augmentation level, and that assignment is static per step."
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
@property
|
| 367 |
+
def expected_components(self) -> list[ComponentSpec]:
|
| 368 |
+
return [
|
| 369 |
+
ComponentSpec("scheduler", MiniMaxH3Scheduler),
|
| 370 |
+
ComponentSpec("audio_scheduler", MiniMaxH3Scheduler),
|
| 371 |
+
]
|
| 372 |
+
|
| 373 |
+
@property
|
| 374 |
+
def inputs(self) -> list[InputParam]:
|
| 375 |
+
return [
|
| 376 |
+
InputParam.template("num_inference_steps", required=True),
|
| 377 |
+
InputParam(
|
| 378 |
+
name="layout",
|
| 379 |
+
type_hint=MiniMaxH3PackedSequence,
|
| 380 |
+
required=True,
|
| 381 |
+
description="The structural description of the packed sequence.",
|
| 382 |
+
),
|
| 383 |
+
]
|
| 384 |
+
|
| 385 |
+
@property
|
| 386 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 387 |
+
return [
|
| 388 |
+
OutputParam("timesteps", type_hint=torch.Tensor, description="Timesteps of the video schedule."),
|
| 389 |
+
OutputParam("audio_timesteps", type_hint=torch.Tensor, description="Timesteps of the audio schedule."),
|
| 390 |
+
OutputParam(
|
| 391 |
+
"row_timestep_plan",
|
| 392 |
+
type_hint=list,
|
| 393 |
+
description=(
|
| 394 |
+
"One `(timestep, timestep_indices)` pair per step: the distinct timesteps of the sequence and the "
|
| 395 |
+
"index of every row into them."
|
| 396 |
+
),
|
| 397 |
+
),
|
| 398 |
+
]
|
| 399 |
+
|
| 400 |
+
@torch.no_grad()
|
| 401 |
+
def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
|
| 402 |
+
block_state = self.get_block_state(state)
|
| 403 |
+
device = components._execution_device
|
| 404 |
+
|
| 405 |
+
components.scheduler.set_timesteps(block_state.num_inference_steps, device=device)
|
| 406 |
+
components.audio_scheduler.set_timesteps(block_state.num_inference_steps, device=device)
|
| 407 |
+
block_state.timesteps = components.scheduler.timesteps
|
| 408 |
+
block_state.audio_timesteps = components.audio_scheduler.timesteps
|
| 409 |
+
|
| 410 |
+
block_state.row_timestep_plan = [
|
| 411 |
+
tuple(
|
| 412 |
+
tensor.to(device)
|
| 413 |
+
for tensor in build_row_timesteps(
|
| 414 |
+
block_state.layout,
|
| 415 |
+
float(timestep),
|
| 416 |
+
float(audio_timestep),
|
| 417 |
+
max(float(timestep), MINIMAX_H3_KEYFRAME_NOISE_AUG),
|
| 418 |
+
1.0,
|
| 419 |
+
)
|
| 420 |
+
)
|
| 421 |
+
for timestep, audio_timestep in zip(block_state.timesteps, block_state.audio_timesteps)
|
| 422 |
+
]
|
| 423 |
+
|
| 424 |
+
self.set_block_state(state, block_state)
|
| 425 |
+
return components, state
|
diffusers/modular_pipelines/minimax_h3/before_encoder.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The MiniMax and HuggingFace Teams. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import PIL
|
| 16 |
+
import torch
|
| 17 |
+
from PIL import Image, ImageOps
|
| 18 |
+
|
| 19 |
+
from ...utils import logging
|
| 20 |
+
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
|
| 21 |
+
from ..modular_pipeline_utils import InputParam, OutputParam
|
| 22 |
+
from .modular_pipeline import MiniMaxH3ModularPipeline, MiniMaxH3Ref2VAModularPipeline
|
| 23 |
+
from .packing import (
|
| 24 |
+
MINIMAX_H3_CANVAS_MULTIPLE,
|
| 25 |
+
MINIMAX_H3_FPS,
|
| 26 |
+
MINIMAX_H3_MAX_DURATION,
|
| 27 |
+
MINIMAX_H3_MIN_DURATION,
|
| 28 |
+
align_num_frames,
|
| 29 |
+
audio_latent_num_frames,
|
| 30 |
+
prepare_keyframe_image,
|
| 31 |
+
resolve_canvas_size,
|
| 32 |
+
video_latent_num_frames,
|
| 33 |
+
)
|
| 34 |
+
from .packing_ref2va import (
|
| 35 |
+
MINIMAX_H3_MAX_REFERENCE_AUDIOS,
|
| 36 |
+
MINIMAX_H3_MAX_REFERENCE_IMAGES,
|
| 37 |
+
MINIMAX_H3_MAX_REFERENCE_VIDEOS,
|
| 38 |
+
MINIMAX_H3_MAX_REFERENCES,
|
| 39 |
+
MiniMaxH3PreparedReference,
|
| 40 |
+
MiniMaxH3Reference,
|
| 41 |
+
prepare_reference_frames,
|
| 42 |
+
prepare_reference_image,
|
| 43 |
+
prepare_reference_waveform,
|
| 44 |
+
reference_kind,
|
| 45 |
+
reference_media_to_uint8,
|
| 46 |
+
resample_reference_frames,
|
| 47 |
+
resolve_reference_image_size,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _latent_geometry(components, height: int, width: int, num_frames: int) -> tuple[int, int, int, int]:
|
| 55 |
+
r"""The latent geometry the packed layout, the noise draws and the decoders all key off."""
|
| 56 |
+
ratio = components.vae_spatial_compression_ratio
|
| 57 |
+
return video_latent_num_frames(num_frames), height // ratio, width // ratio, audio_latent_num_frames(num_frames)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _latent_geometry_outputs() -> list[OutputParam]:
|
| 61 |
+
r"""The declaration of what [`_latent_geometry`] resolves, shared by the two setup blocks."""
|
| 62 |
+
return [
|
| 63 |
+
OutputParam("num_latent_frames", type_hint=int, description="Number of generated video latent frames."),
|
| 64 |
+
OutputParam("latent_height", type_hint=int, description="Height of the generated video latents."),
|
| 65 |
+
OutputParam("latent_width", type_hint=int, description="Width of the generated video latents."),
|
| 66 |
+
OutputParam("num_audio_latents", type_hint=int, description="Number of generated audio latents per channel."),
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class MiniMaxH3SetupStep(ModularPipelineBlocks):
|
| 71 |
+
model_name = "minimax-h3"
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def description(self) -> str:
|
| 75 |
+
return (
|
| 76 |
+
"Resolves the plan shared by the `t2va` and `fl2va` tasks: the canvas (MiniMax-H3's own 768-short-edge "
|
| 77 |
+
"geometry for the aspect ratio of the first keyframe, or 16:9 without keyframes), the `17 * n + 5` frame "
|
| 78 |
+
"count the video VAE can decode, the latent geometry every later block keys off, and the keyframes put "
|
| 79 |
+
"onto that canvas."
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
@staticmethod
|
| 83 |
+
def _check_inputs(block_state) -> None:
|
| 84 |
+
if (block_state.height is None) != (block_state.width is None):
|
| 85 |
+
raise ValueError("`height` and `width` have to be passed together, or neither of them.")
|
| 86 |
+
if block_state.height is not None and (
|
| 87 |
+
block_state.height % MINIMAX_H3_CANVAS_MULTIPLE or block_state.width % MINIMAX_H3_CANVAS_MULTIPLE
|
| 88 |
+
):
|
| 89 |
+
raise ValueError(
|
| 90 |
+
f"`height` and `width` must be multiples of {MINIMAX_H3_CANVAS_MULTIPLE}, got "
|
| 91 |
+
f"{block_state.height}x{block_state.width}."
|
| 92 |
+
)
|
| 93 |
+
# The duration the request generates is the one of the *aligned* frame count, so that is what the ceiling has
|
| 94 |
+
# to hold for: 346 frames would otherwise pass the check and then be rounded up to 362, i.e. 15.083 seconds.
|
| 95 |
+
aligned_num_frames = align_num_frames(block_state.num_frames)
|
| 96 |
+
duration = aligned_num_frames / MINIMAX_H3_FPS
|
| 97 |
+
if not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
|
| 98 |
+
raise ValueError(
|
| 99 |
+
f"MiniMax-H3 generates between {MINIMAX_H3_MIN_DURATION} and {MINIMAX_H3_MAX_DURATION} seconds at "
|
| 100 |
+
f"{MINIMAX_H3_FPS} fps, so `num_frames`, rounded up to the next `17 * n + 5` the video VAE can "
|
| 101 |
+
f"encode, must be between {int(MINIMAX_H3_MIN_DURATION * MINIMAX_H3_FPS)} and "
|
| 102 |
+
f"{int(MINIMAX_H3_MAX_DURATION * MINIMAX_H3_FPS)}, got {block_state.num_frames} (rounded up to "
|
| 103 |
+
f"{aligned_num_frames})."
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def inputs(self) -> list[InputParam]:
|
| 108 |
+
return [
|
| 109 |
+
InputParam(
|
| 110 |
+
name="image",
|
| 111 |
+
type_hint=PIL.Image.Image,
|
| 112 |
+
description=(
|
| 113 |
+
"Keyframe the video starts from. It is *stretched* onto the target canvas, which by default is "
|
| 114 |
+
"derived from its own aspect ratio."
|
| 115 |
+
),
|
| 116 |
+
),
|
| 117 |
+
InputParam(
|
| 118 |
+
name="last_image",
|
| 119 |
+
type_hint=PIL.Image.Image,
|
| 120 |
+
description=(
|
| 121 |
+
"Keyframe the video ends on. Can be passed on its own to generate *up to* a frame. Combined with "
|
| 122 |
+
"`image` it is the follower of the two and is cover-cropped onto the canvas."
|
| 123 |
+
),
|
| 124 |
+
),
|
| 125 |
+
InputParam.template("height", description="Height of the generated video in pixels, a multiple of 32."),
|
| 126 |
+
InputParam.template("width", description="Width of the generated video in pixels, a multiple of 32."),
|
| 127 |
+
InputParam(
|
| 128 |
+
name="num_frames",
|
| 129 |
+
type_hint=int,
|
| 130 |
+
default=124,
|
| 131 |
+
description=(
|
| 132 |
+
"Number of frames to generate, at the fixed 24 fps. Snapped up to the next `17 * n + 5` the video "
|
| 133 |
+
"VAE can decode; the resulting duration must stay between 5 and 15 seconds."
|
| 134 |
+
),
|
| 135 |
+
),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
@property
|
| 139 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 140 |
+
return [
|
| 141 |
+
OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
|
| 142 |
+
OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
|
| 143 |
+
OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5."),
|
| 144 |
+
*_latent_geometry_outputs(),
|
| 145 |
+
OutputParam(
|
| 146 |
+
"keyframes",
|
| 147 |
+
type_hint=list,
|
| 148 |
+
description="The keyframes put onto the target canvas, in packed order (empty for `t2va`).",
|
| 149 |
+
),
|
| 150 |
+
OutputParam(
|
| 151 |
+
"keyframe_anchors",
|
| 152 |
+
type_hint=tuple,
|
| 153 |
+
description="Which end of the video every keyframe is anchored to, in packed order.",
|
| 154 |
+
),
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
@torch.no_grad()
|
| 158 |
+
def __call__(self, components: MiniMaxH3ModularPipeline, state: PipelineState) -> PipelineState:
|
| 159 |
+
block_state = self.get_block_state(state)
|
| 160 |
+
self._check_inputs(block_state)
|
| 161 |
+
|
| 162 |
+
keyframes = [
|
| 163 |
+
ImageOps.exif_transpose(keyframe).convert("RGB")
|
| 164 |
+
for keyframe in (block_state.image, block_state.last_image)
|
| 165 |
+
if keyframe is not None
|
| 166 |
+
]
|
| 167 |
+
block_state.keyframe_anchors = tuple(
|
| 168 |
+
anchor
|
| 169 |
+
for anchor, keyframe in (("first", block_state.image), ("last", block_state.last_image))
|
| 170 |
+
if keyframe is not None
|
| 171 |
+
)
|
| 172 |
+
if block_state.height is None:
|
| 173 |
+
block_state.height, block_state.width = resolve_canvas_size(*(keyframes[0].size if keyframes else (16, 9)))
|
| 174 |
+
|
| 175 |
+
aligned_num_frames = align_num_frames(block_state.num_frames)
|
| 176 |
+
if aligned_num_frames != block_state.num_frames:
|
| 177 |
+
logger.warning(
|
| 178 |
+
f"`num_frames` has to be of the form 17 * n + 5 for the video VAE; rounding {block_state.num_frames} "
|
| 179 |
+
f"up to {aligned_num_frames}."
|
| 180 |
+
)
|
| 181 |
+
block_state.num_frames = aligned_num_frames
|
| 182 |
+
|
| 183 |
+
(
|
| 184 |
+
block_state.num_latent_frames,
|
| 185 |
+
block_state.latent_height,
|
| 186 |
+
block_state.latent_width,
|
| 187 |
+
block_state.num_audio_latents,
|
| 188 |
+
) = _latent_geometry(components, block_state.height, block_state.width, block_state.num_frames)
|
| 189 |
+
|
| 190 |
+
block_state.keyframes = [
|
| 191 |
+
prepare_keyframe_image(keyframe, block_state.height, block_state.width, stretch=index == 0)
|
| 192 |
+
for index, keyframe in enumerate(keyframes)
|
| 193 |
+
]
|
| 194 |
+
self.set_block_state(state, block_state)
|
| 195 |
+
return components, state
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class MiniMaxH3Ref2VASetupStep(ModularPipelineBlocks):
|
| 199 |
+
model_name = "minimax-h3-ref2va"
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def description(self) -> str:
|
| 203 |
+
return (
|
| 204 |
+
"Resolves the `ref2va` plan: the canvas (MiniMax-H3's own 16:9 unless asked otherwise — references never "
|
| 205 |
+
"bind the generated geometry), the references prepared at their own resolutions, the frame count they "
|
| 206 |
+
"imply when it was left open, and the latent geometry every later block keys off."
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
@staticmethod
|
| 210 |
+
def _check_inputs(components, block_state) -> None:
|
| 211 |
+
if (block_state.height is None) != (block_state.width is None):
|
| 212 |
+
raise ValueError("`height` and `width` have to be passed together, or neither of them.")
|
| 213 |
+
if block_state.height is not None and (
|
| 214 |
+
block_state.height % MINIMAX_H3_CANVAS_MULTIPLE or block_state.width % MINIMAX_H3_CANVAS_MULTIPLE
|
| 215 |
+
):
|
| 216 |
+
raise ValueError(
|
| 217 |
+
f"`height` and `width` must be multiples of {MINIMAX_H3_CANVAS_MULTIPLE}, got "
|
| 218 |
+
f"{block_state.height}x{block_state.width}."
|
| 219 |
+
)
|
| 220 |
+
# The duration the request generates is the one of the *aligned* frame count, so that is what the ceiling has
|
| 221 |
+
# to hold for: 346 frames would otherwise pass the check and then be rounded up to 362, i.e. 15.083 seconds.
|
| 222 |
+
aligned_num_frames = None if block_state.num_frames is None else align_num_frames(block_state.num_frames)
|
| 223 |
+
duration = None if aligned_num_frames is None else aligned_num_frames / MINIMAX_H3_FPS
|
| 224 |
+
if duration is not None and not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
|
| 225 |
+
raise ValueError(
|
| 226 |
+
f"MiniMax-H3 generates between {MINIMAX_H3_MIN_DURATION} and {MINIMAX_H3_MAX_DURATION} seconds at "
|
| 227 |
+
f"{MINIMAX_H3_FPS} fps, so `num_frames`, rounded up to the next `17 * n + 5` the video VAE can "
|
| 228 |
+
f"encode, must be between {int(MINIMAX_H3_MIN_DURATION * MINIMAX_H3_FPS)} and "
|
| 229 |
+
f"{int(MINIMAX_H3_MAX_DURATION * MINIMAX_H3_FPS)}, got {block_state.num_frames} (rounded up to "
|
| 230 |
+
f"{aligned_num_frames})."
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
if not block_state.references:
|
| 234 |
+
raise ValueError(
|
| 235 |
+
"`ref2va` needs at least one reference; use `MiniMaxH3ModularPipeline` for text-only requests."
|
| 236 |
+
)
|
| 237 |
+
kinds = [reference_kind(index, entry) for index, entry in enumerate(block_state.references)]
|
| 238 |
+
for kind, limit in (
|
| 239 |
+
("image", MINIMAX_H3_MAX_REFERENCE_IMAGES),
|
| 240 |
+
("video", MINIMAX_H3_MAX_REFERENCE_VIDEOS),
|
| 241 |
+
("audio", MINIMAX_H3_MAX_REFERENCE_AUDIOS),
|
| 242 |
+
):
|
| 243 |
+
if kinds.count(kind) > limit:
|
| 244 |
+
raise ValueError(f"MiniMax-H3 accepts at most {limit} {kind} references, got {kinds.count(kind)}.")
|
| 245 |
+
if len(kinds) > MINIMAX_H3_MAX_REFERENCES:
|
| 246 |
+
raise ValueError(
|
| 247 |
+
f"MiniMax-H3 accepts at most {MINIMAX_H3_MAX_REFERENCES} references in total, got {len(kinds)}."
|
| 248 |
+
)
|
| 249 |
+
if set(kinds) == {"audio"}:
|
| 250 |
+
raise ValueError(
|
| 251 |
+
"An audio reference has to be paired with at least one image or video reference and cannot be used "
|
| 252 |
+
"on its own."
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
@property
|
| 256 |
+
def inputs(self) -> list[InputParam]:
|
| 257 |
+
return [
|
| 258 |
+
InputParam(
|
| 259 |
+
name="references",
|
| 260 |
+
type_hint=list[MiniMaxH3Reference],
|
| 261 |
+
required=True,
|
| 262 |
+
description=(
|
| 263 |
+
"The references to condition on, **in the order the model should read them**: the order labels "
|
| 264 |
+
"them in the prompt presentation and lays them out on the shared rotary clock, so a different "
|
| 265 |
+
"order is a different request. Every [`MiniMaxH3Reference`] carries exactly one medium, a path or "
|
| 266 |
+
"in-memory media — `image` (at most 9), `video` at its own `fps` (at most 3, whose `audio` "
|
| 267 |
+
"soundtrack is conditioned on as well), or `audio` at its own `sample_rate` (at most 3) — for at "
|
| 268 |
+
"most 12 references in total, and audio references cannot be the only ones. A path is decoded "
|
| 269 |
+
"when the reference is built, so these blocks only ever see pixels and samples."
|
| 270 |
+
),
|
| 271 |
+
),
|
| 272 |
+
InputParam.template("height", description="Height of the generated video in pixels, a multiple of 32."),
|
| 273 |
+
InputParam.template("width", description="Width of the generated video in pixels, a multiple of 32."),
|
| 274 |
+
InputParam(
|
| 275 |
+
name="num_frames",
|
| 276 |
+
type_hint=int,
|
| 277 |
+
description=(
|
| 278 |
+
"Number of frames to generate, at the fixed 24 fps. Snapped up to the next `17 * n + 5` the video "
|
| 279 |
+
"VAE can decode. May be left out, but only when exactly one reference carries audio, in which "
|
| 280 |
+
"case the duration is that soundtrack's."
|
| 281 |
+
),
|
| 282 |
+
),
|
| 283 |
+
]
|
| 284 |
+
|
| 285 |
+
@property
|
| 286 |
+
def intermediate_outputs(self) -> list[OutputParam]:
|
| 287 |
+
return [
|
| 288 |
+
OutputParam("height", type_hint=int, description="Resolved height of the generated video in pixels."),
|
| 289 |
+
OutputParam("width", type_hint=int, description="Resolved width of the generated video in pixels."),
|
| 290 |
+
OutputParam("num_frames", type_hint=int, description="Resolved number of frames, of the form 17 * n + 5."),
|
| 291 |
+
*_latent_geometry_outputs(),
|
| 292 |
+
OutputParam(
|
| 293 |
+
"prepared_references",
|
| 294 |
+
type_hint=list[MiniMaxH3PreparedReference],
|
| 295 |
+
description="The references prepared at their own resolutions, in packed order.",
|
| 296 |
+
),
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
@staticmethod
|
| 300 |
+
def prepare_references(
|
| 301 |
+
components, references: list[MiniMaxH3Reference], num_frames: int | None
|
| 302 |
+
) -> tuple[list[MiniMaxH3PreparedReference], int]:
|
| 303 |
+
r"""
|
| 304 |
+
Resolve the references and, if it was left open, the duration they imply.
|
| 305 |
+
|
| 306 |
+
Every reference is prepared at its own resolution: an image is resized to a 2048 pixel short edge, a video is
|
| 307 |
+
resampled onto MiniMax-H3's own 24 fps, rescaled onto the 768 pixel canvas of *its own* aspect ratio and
|
| 308 |
+
truncated to the generated frame count, and a soundtrack is put on the audio VAE's sample rate and truncated to
|
| 309 |
+
the generated duration. None of this touches the target canvas.
|
| 310 |
+
|
| 311 |
+
A reference that left its `fps` or its `sample_rate` out is taken to already be at MiniMax-H3's own rate, and
|
| 312 |
+
its frames or its samples then flow through untouched.
|
| 313 |
+
|
| 314 |
+
A video reference goes through the two passes the reference implementation's `ffmpeg` decode applied, in the
|
| 315 |
+
same order: the constant frame rate resample of `resample_reference_frames` and the LANCZOS rescale of
|
| 316 |
+
`prepare_reference_frames`. Frames handed over at 24 fps and already at the canvas their own aspect ratio
|
| 317 |
+
resolves to therefore reach the VAE untouched, which is the parity-exact route.
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
references (`list[MiniMaxH3Reference]`):
|
| 321 |
+
The `references` input of a [`MiniMaxH3Ref2VABlocks`] request.
|
| 322 |
+
num_frames (`int`, *optional*):
|
| 323 |
+
The requested frame count, or `None` to derive it from the single audio-bearing reference.
|
| 324 |
+
|
| 325 |
+
Returns:
|
| 326 |
+
`tuple[list[MiniMaxH3PreparedReference], int]`: the prepared references, in packed order, and the frame
|
| 327 |
+
count.
|
| 328 |
+
"""
|
| 329 |
+
resolved = [
|
| 330 |
+
MiniMaxH3PreparedReference(kind=reference_kind(index, entry), has_audio=entry.has_audio)
|
| 331 |
+
for index, entry in enumerate(references)
|
| 332 |
+
]
|
| 333 |
+
|
| 334 |
+
# The duration may be left open, but then exactly one reference may carry audio, or the request is ambiguous.
|
| 335 |
+
if num_frames is None:
|
| 336 |
+
audio_bearing = [index for index, reference in enumerate(resolved) if reference.has_audio]
|
| 337 |
+
if len(audio_bearing) != 1:
|
| 338 |
+
raise ValueError(
|
| 339 |
+
"`num_frames` may only be left to the references when exactly one of them carries audio, got "
|
| 340 |
+
f"{len(audio_bearing)}."
|
| 341 |
+
)
|
| 342 |
+
index = audio_bearing[0]
|
| 343 |
+
sample_rate = references[index].sample_rate or components.audio_sampling_rate
|
| 344 |
+
duration = references[index].audio.shape[-1] / sample_rate
|
| 345 |
+
if not MINIMAX_H3_MIN_DURATION <= duration <= MINIMAX_H3_MAX_DURATION:
|
| 346 |
+
raise ValueError(
|
| 347 |
+
f"`references[{index}]` is {duration:g} seconds long, outside the "
|
| 348 |
+
f"{MINIMAX_H3_MIN_DURATION} to {MINIMAX_H3_MAX_DURATION} seconds MiniMax-H3 generates."
|
| 349 |
+
)
|
| 350 |
+
num_frames = align_num_frames(round(duration * MINIMAX_H3_FPS))
|
| 351 |
+
# The duration the request generates is the one of the *aligned* frame count, so that is what the
|
| 352 |
+
# ceiling has to hold for: a 14.99 second soundtrack rounds up to 362 frames, i.e. 15.083 seconds.
|
| 353 |
+
if num_frames / MINIMAX_H3_FPS > MINIMAX_H3_MAX_DURATION:
|
| 354 |
+
raise ValueError(
|
| 355 |
+
f"`references[{index}]` is {duration:g} seconds long, which rounds up to {num_frames} frames "
|
| 356 |
+
f"(`17 * n + 5`), i.e. {num_frames / MINIMAX_H3_FPS:g} seconds — past the "
|
| 357 |
+
f"{MINIMAX_H3_MAX_DURATION} seconds MiniMax-H3 generates. Pass `num_frames` to generate a "
|
| 358 |
+
"shorter video from this soundtrack."
|
| 359 |
+
)
|
| 360 |
+
num_frames = align_num_frames(num_frames)
|
| 361 |
+
|
| 362 |
+
for reference, entry in zip(resolved, references):
|
| 363 |
+
if reference.kind == "image":
|
| 364 |
+
image = entry.image
|
| 365 |
+
if not isinstance(image, Image.Image):
|
| 366 |
+
image = Image.fromarray(reference_media_to_uint8(image))
|
| 367 |
+
image = ImageOps.exif_transpose(image).convert("RGB")
|
| 368 |
+
height, width = resolve_reference_image_size(*image.size)
|
| 369 |
+
reference.image = prepare_reference_image(image, height, width)
|
| 370 |
+
elif reference.kind == "video":
|
| 371 |
+
frames = resample_reference_frames(reference_media_to_uint8(entry.video), float(entry.fps))
|
| 372 |
+
reference.frames = prepare_reference_frames(frames, num_frames)
|
| 373 |
+
if reference.has_audio:
|
| 374 |
+
reference.waveform = prepare_reference_waveform(
|
| 375 |
+
entry.audio,
|
| 376 |
+
entry.sample_rate or components.audio_sampling_rate,
|
| 377 |
+
components.audio_sampling_rate,
|
| 378 |
+
max_duration=num_frames / MINIMAX_H3_FPS,
|
| 379 |
+
)
|
| 380 |
+
return resolved, num_frames
|
| 381 |
+
|
| 382 |
+
@torch.no_grad()
|
| 383 |
+
def __call__(self, components: MiniMaxH3Ref2VAModularPipeline, state: PipelineState) -> PipelineState:
|
| 384 |
+
block_state = self.get_block_state(state)
|
| 385 |
+
self._check_inputs(components, block_state)
|
| 386 |
+
|
| 387 |
+
if block_state.height is None:
|
| 388 |
+
block_state.height, block_state.width = resolve_canvas_size(16, 9)
|
| 389 |
+
|
| 390 |
+
requested_num_frames = block_state.num_frames
|
| 391 |
+
block_state.prepared_references, block_state.num_frames = self.prepare_references(
|
| 392 |
+
components, block_state.references, block_state.num_frames
|
| 393 |
+
)
|
| 394 |
+
if requested_num_frames is not None and requested_num_frames != block_state.num_frames:
|
| 395 |
+
logger.warning(
|
| 396 |
+
f"`num_frames` has to be of the form 17 * n + 5 for the video VAE; rounding {requested_num_frames} up "
|
| 397 |
+
f"to {block_state.num_frames}."
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
(
|
| 401 |
+
block_state.num_latent_frames,
|
| 402 |
+
block_state.latent_height,
|
| 403 |
+
block_state.latent_width,
|
| 404 |
+
block_state.num_audio_latents,
|
| 405 |
+
) = _latent_geometry(components, block_state.height, block_state.width, block_state.num_frames)
|
| 406 |
+
|
| 407 |
+
self.set_block_state(state, block_state)
|
| 408 |
+
return components, state
|