ideogram4_custom_blocks / ideogram4_img2img.py
OzzyGT's picture
OzzyGT HF Staff
Upload 7 files
0a66847 verified
Raw
History Blame Contribute Delete
19.9 kB
# Copyright 2026 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Custom modular-diffusers blocks adding image-to-image support to Ideogram4.
Reuses the in-repo text2image blocks and adds the img2img-specific steps (VAE encode, strength-trimmed
timesteps, noise scaling). Assembled into a single AutoBlocks that serves both text2image and image2image,
selected by the presence of `image` — the canonical modular-pipeline shape.
"""
import torch
from diffusers.configuration_utils import FrozenDict
from diffusers.image_processor import VaeImageProcessor
from diffusers.models import AutoencoderKLFlux2
from diffusers.modular_pipelines.ideogram4.before_denoise import (
DEFAULT_GUIDANCE_SCHEDULE,
Ideogram4PrepareAdditionalInputsStep,
Ideogram4PrepareLatentsStep,
Ideogram4TextInputsStep,
_expand_tensor_to_effective_batch,
_logit_normal_sigmas,
_resolution_aware_mu,
)
from diffusers.modular_pipelines.ideogram4.decoders import Ideogram4DecodeStep
from diffusers.modular_pipelines.ideogram4.denoise import Ideogram4AfterDenoiseStep, Ideogram4DenoiseStep
from diffusers.modular_pipelines.ideogram4.encoders import Ideogram4PromptUpsampleStep, Ideogram4TextEncoderStep
from diffusers.modular_pipelines.ideogram4.modular_blocks_ideogram4 import Ideogram4CoreDenoiseStep
from diffusers.modular_pipelines.ideogram4.modular_pipeline import Ideogram4ModularPipeline
from diffusers.modular_pipelines.modular_pipeline import (
AutoPipelineBlocks,
ConditionalPipelineBlocks,
ModularPipelineBlocks,
PipelineState,
SequentialPipelineBlocks,
)
from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, InsertableDict, OutputParam
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
from diffusers.utils import logging
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
# The Ideogram4 VAE downscales by 8 and the transformer patchifies by 2, so the pixel grid is downscaled by 16
# in each axis; images are resized to a multiple of that before encoding.
IMAGE_PROCESSOR_SPEC = ComponentSpec(
"image_processor",
VaeImageProcessor,
config=FrozenDict({"vae_scale_factor": 16}),
default_creation_method="from_config",
)
# Modified from diffusers.pipelines.stable_diffusion_3.pipeline_stable_diffusion_3_img2img.StableDiffusion3Img2ImgPipeline.get_timesteps
def get_timesteps(scheduler, num_inference_steps, strength):
"""Trim the schedule to the last `strength` fraction of steps (img2img skips the high-noise steps)."""
init_timestep = min(num_inference_steps * strength, num_inference_steps)
t_start = int(max(num_inference_steps - init_timestep, 0))
timesteps = scheduler.timesteps[t_start * scheduler.order :]
if hasattr(scheduler, "set_begin_index"):
scheduler.set_begin_index(t_start * scheduler.order)
return timesteps, num_inference_steps - t_start, t_start
# auto_docstring
class Ideogram4VaeEncoderStep(ModularPipelineBlocks):
"""
Image-to-image VAE encoder step: resize/preprocess the reference `image`, encode it through the VAE, patchify to
the transformer's packed layout and normalize with the VAE batch-norm statistics (the exact inverse of the
decoder). Resolves `height`/`width` from the image when not provided.
"""
model_name = "ideogram4"
@property
def description(self) -> str:
return (
"Image-to-image VAE encoder step: preprocess the reference `image`, encode through the VAE, patchify to "
"the packed transformer layout and normalize with the VAE batch-norm statistics (inverse of the decoder)."
)
@property
def expected_components(self) -> list[ComponentSpec]:
return [ComponentSpec("vae", AutoencoderKLFlux2), IMAGE_PROCESSOR_SPEC]
@property
def inputs(self) -> list[InputParam]:
return [
InputParam.template("image", required=True),
InputParam.template("height"),
InputParam.template("width"),
InputParam.template("generator"),
]
@property
def intermediate_outputs(self) -> list[OutputParam]:
return [
OutputParam(
name="image_latents",
type_hint=torch.Tensor,
description="Packed, bn-normalized latents of the reference image (B, num_image_tokens, latent_dim).",
),
OutputParam(name="height", type_hint=int, description="Target height, resolved from the image if unset."),
OutputParam(name="width", type_hint=int, description="Target width, resolved from the image if unset."),
]
@torch.no_grad()
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
block_state = self.get_block_state(state)
device = components._execution_device
patch = components.patch_size
# Resolve target size (multiple of vae_scale_factor * patch) and preprocess to it.
height, width = components.image_processor.get_default_height_width(
block_state.image, block_state.height, block_state.width
)
image = components.image_processor.preprocess(block_state.image, height=height, width=width)
image = image.to(device=device, dtype=components.vae.dtype)
# Encode, sample the posterior, then patchify to the packed layout.
z = components.vae.encode(image).latent_dist.sample(generator=block_state.generator)
z = z.to(torch.float32)
ae_channels = z.shape[1]
grid_h = z.shape[2] // patch
grid_w = z.shape[3] // patch
z = z.view(z.shape[0], ae_channels, grid_h, patch, grid_w, patch)
z = z.permute(0, 2, 4, 3, 5, 1).contiguous()
z = z.reshape(z.shape[0], grid_h * grid_w, patch * patch * ae_channels)
# Normalize with the packed-channel batch-norm statistics (inverse of the decoder's denormalization).
bn_mean = components.vae.bn.running_mean.view(1, 1, -1).to(device=z.device, dtype=z.dtype)
bn_std = torch.sqrt(components.vae.bn.running_var + components.vae.config.batch_norm_eps).view(1, 1, -1)
bn_std = bn_std.to(device=z.device, dtype=z.dtype)
block_state.image_latents = (z - bn_mean) / bn_std
block_state.height = height
block_state.width = width
self.set_block_state(state, block_state)
return components, state
# auto_docstring
class Ideogram4ExpandImageLatentsStep(ModularPipelineBlocks):
"""
Replicate `image_latents` from the per-image batch to the effective `batch_size` (num prompts *
num_images_per_prompt). Place after the text-input step, which produces `batch_size`.
"""
model_name = "ideogram4"
@property
def description(self) -> str:
return "Replicate `image_latents` to the effective `batch_size` (num prompts * num_images_per_prompt)."
@property
def inputs(self) -> list[InputParam]:
return [
InputParam.template("image_latents", required=True),
InputParam(name="batch_size", required=True, type_hint=int, description="Effective batch size."),
]
@property
def intermediate_outputs(self) -> list[OutputParam]:
return [
OutputParam(name="image_latents", type_hint=torch.Tensor, description="Image latents, batch-expanded.")
]
@torch.no_grad()
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
block_state = self.get_block_state(state)
image_batch = block_state.image_latents.shape[0]
num_per_prompt = block_state.batch_size // image_batch
block_state.image_latents = _expand_tensor_to_effective_batch(
block_state.image_latents, image_batch, num_per_prompt, "image_latents"
)
self.set_block_state(state, block_state)
return components, state
# auto_docstring
class Ideogram4SetTimestepsWithStrengthStep(ModularPipelineBlocks):
"""
Set the resolution-aware logit-normal sigma schedule, then trim it to the last `strength` fraction of steps for
image-to-image. The per-step guidance weights are trimmed to stay aligned with the retained timesteps.
"""
model_name = "ideogram4"
@property
def description(self) -> str:
return (
"Set the resolution-aware logit-normal sigma schedule and trim it (and the guidance weights) to the last "
"`strength` fraction of steps for image-to-image."
)
@property
def expected_components(self) -> list[ComponentSpec]:
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
@property
def inputs(self) -> list[InputParam]:
return [
InputParam.template("num_inference_steps", default=48),
InputParam.template("height", required=True),
InputParam.template("width", required=True),
InputParam(name="mu", default=0.0, type_hint=float, description="Base mean of the logit-normal schedule."),
InputParam(name="std", default=1.5, type_hint=float, description="Std of the logit-normal schedule."),
InputParam(
name="guidance_schedule",
default=DEFAULT_GUIDANCE_SCHEDULE,
type_hint=list,
description="Per-step guidance scale schedule (length num_inference_steps).",
),
InputParam(
name="strength",
default=0.6,
type_hint=float,
description="How much to transform the reference image; 1.0 ignores it, lower keeps more structure.",
),
]
@property
def intermediate_outputs(self) -> list[OutputParam]:
return [
OutputParam(name="timesteps", type_hint=torch.Tensor, description="The (trimmed) denoising timesteps."),
OutputParam(
name="num_inference_steps",
type_hint=int,
description="Number of denoising steps after strength trimming.",
),
OutputParam(name="gw", type_hint=torch.Tensor, description="Per-step guidance weights (trimmed)."),
]
@torch.no_grad()
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
block_state = self.get_block_state(state)
device = components._execution_device
if len(block_state.guidance_schedule) != block_state.num_inference_steps:
raise ValueError(
f"`guidance_schedule` must have length `num_inference_steps` ({block_state.num_inference_steps}), "
f"got {len(block_state.guidance_schedule)}."
)
schedule_mu = _resolution_aware_mu(height=block_state.height, width=block_state.width, base_mu=block_state.mu)
sigmas = _logit_normal_sigmas(block_state.num_inference_steps, schedule_mu, std=block_state.std, device=device)
components.scheduler.set_timesteps(sigmas=sigmas.tolist(), device=device)
gw_full = torch.as_tensor(block_state.guidance_schedule, dtype=torch.float32, device=device)
timesteps, num_inference_steps, t_start = get_timesteps(
components.scheduler, block_state.num_inference_steps, block_state.strength
)
block_state.timesteps = timesteps
block_state.num_inference_steps = num_inference_steps
block_state.gw = gw_full[t_start:]
self.set_block_state(state, block_state)
return components, state
# auto_docstring
class Ideogram4PrepareLatentsWithStrengthStep(ModularPipelineBlocks):
"""
Add noise to the reference `image_latents` at the first (trimmed) timestep to build the starting latents for
image-to-image denoising. Run after prepare_latents (random noise) and set_timesteps.
"""
model_name = "ideogram4"
@property
def description(self) -> str:
return (
"Add noise to the reference `image_latents` at the first (trimmed) timestep to build the starting latents "
"for image-to-image denoising."
)
@property
def expected_components(self) -> list[ComponentSpec]:
return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)]
@property
def inputs(self) -> list[InputParam]:
return [
InputParam(
name="latents",
required=True,
type_hint=torch.Tensor,
description="Initial random noise from the prepare-latents step.",
),
InputParam.template("image_latents", required=True),
InputParam(
name="timesteps",
required=True,
type_hint=torch.Tensor,
description="The (trimmed) denoising timesteps from set_timesteps.",
),
]
@property
def intermediate_outputs(self) -> list[OutputParam]:
return [
OutputParam(
name="latents",
type_hint=torch.Tensor,
description="Noised image latents to start image-to-image denoising.",
)
]
@staticmethod
def check_inputs(image_latents, latents):
if image_latents.shape != latents.shape:
raise ValueError(
f"`image_latents` {tuple(image_latents.shape)} must match `latents` {tuple(latents.shape)}."
)
@torch.no_grad()
def __call__(self, components: Ideogram4ModularPipeline, state: PipelineState) -> PipelineState:
block_state = self.get_block_state(state)
self.check_inputs(block_state.image_latents, block_state.latents)
latent_timestep = block_state.timesteps[:1].repeat(block_state.latents.shape[0])
image_latents = block_state.image_latents.to(block_state.latents)
block_state.latents = components.scheduler.scale_noise(image_latents, latent_timestep, block_state.latents)
self.set_block_state(state, block_state)
return components, state
# Img2img input: reuse the text-input step, then batch-expand the image latents to the effective batch size.
IMG2IMG_INPUT_BLOCKS = InsertableDict(
[
("text_inputs", Ideogram4TextInputsStep()),
("expand_image_latents", Ideogram4ExpandImageLatentsStep()),
]
)
class Ideogram4Img2ImgInputStep(SequentialPipelineBlocks):
model_name = "ideogram4"
block_classes = list(IMG2IMG_INPUT_BLOCKS.values())
block_names = list(IMG2IMG_INPUT_BLOCKS.keys())
@property
def description(self) -> str:
return (
"Input step for image-to-image: batch-expand the text features and the reference `image_latents` to the "
"effective batch size."
)
# Img2img core denoise: same shape as the text2image core, but with strength-trimmed timesteps and an extra
# noise-scaling step that seeds the loop from the reference image latents.
IMG2IMG_CORE_DENOISE_BLOCKS = InsertableDict(
[
("input", Ideogram4Img2ImgInputStep()),
("prepare_latents", Ideogram4PrepareLatentsStep()),
("set_timesteps", Ideogram4SetTimestepsWithStrengthStep()),
("prepare_additional_inputs", Ideogram4PrepareAdditionalInputsStep()),
("add_noise", Ideogram4PrepareLatentsWithStrengthStep()),
("denoise", Ideogram4DenoiseStep()),
("after_denoise", Ideogram4AfterDenoiseStep()),
]
)
# auto_docstring
class Ideogram4Img2ImgCoreDenoiseStep(SequentialPipelineBlocks):
"""
Core denoising workflow for Ideogram4 image-to-image: prepares the batch/latents, trims the schedule by
`strength`, seeds the loop from the reference image latents, runs the asymmetric-CFG denoising loop and
unpatchifies the result for the decoder.
"""
model_name = "ideogram4"
block_classes = list(IMG2IMG_CORE_DENOISE_BLOCKS.values())
block_names = list(IMG2IMG_CORE_DENOISE_BLOCKS.keys())
@property
def description(self) -> str:
return (
"Core denoising workflow for Ideogram4 image-to-image: prepares the batch/latents, trims the schedule by "
"`strength`, seeds the loop from the reference image latents, runs the asymmetric-CFG denoising loop and "
"unpatchifies the result for the decoder."
)
@property
def outputs(self) -> list[OutputParam]:
return [OutputParam.template("latents", description="Unpatchified (B, ae_channels, H, W) latents.")]
class Ideogram4AutoVaeEncoderStep(AutoPipelineBlocks):
block_classes = [Ideogram4VaeEncoderStep()]
block_names = ["img2img"]
block_trigger_inputs = ["image"]
@property
def description(self) -> str:
return (
"VAE encoder step that encodes the reference `image` into `image_latents`. This is an auto pipeline "
"block: it runs when `image` is provided and is skipped otherwise (text2image)."
)
class Ideogram4AutoCoreDenoiseStep(ConditionalPipelineBlocks):
block_classes = [Ideogram4CoreDenoiseStep, Ideogram4Img2ImgCoreDenoiseStep]
block_names = ["text2image", "img2img"]
block_trigger_inputs = ["image_latents"]
default_block_name = "text2image"
def select_block(self, image_latents=None):
return "img2img" if image_latents is not None else "text2image"
@property
def description(self) -> str:
return (
"Core denoising step. \n"
" - `Ideogram4Img2ImgCoreDenoiseStep` (img2img) is used when `image_latents` is provided.\n"
" - `Ideogram4CoreDenoiseStep` (text2image) is used otherwise."
)
@property
def outputs(self) -> list[OutputParam]:
return [OutputParam.template("latents", description="Unpatchified (B, ae_channels, H, W) latents.")]
AUTO_BLOCKS = InsertableDict(
[
("prompt_upsample", Ideogram4PromptUpsampleStep()),
("text_encoder", Ideogram4TextEncoderStep()),
("vae_encoder", Ideogram4AutoVaeEncoderStep()),
("denoise", Ideogram4AutoCoreDenoiseStep()),
("decode", Ideogram4DecodeStep()),
]
)
# auto_docstring
class Ideogram4Img2ImgAutoBlocks(SequentialPipelineBlocks):
"""
Auto Modular pipeline for Ideogram4 supporting text-to-image and image-to-image, selected by the presence of
`image`: (optional) prompt upsampling -> encode text -> (img2img) VAE-encode reference -> core denoise
(asymmetric CFG over two transformers) -> decode.
Supported workflows:
- `text2image`: requires `prompt`
- `image2image`: requires `prompt`, `image`
"""
model_name = "ideogram4"
block_classes = list(AUTO_BLOCKS.values())
block_names = list(AUTO_BLOCKS.keys())
_workflow_map = {
"text2image": {"prompt": True},
"image2image": {"prompt": True, "image": True},
}
@property
def description(self) -> str:
return (
"Auto Modular pipeline for Ideogram4 text-to-image and image-to-image: (optional) prompt upsampling -> "
"encode text -> (img2img) VAE-encode the reference image -> core denoise (asymmetric CFG over two "
"transformers) -> decode. The workflow is selected by the presence of `image`."
)
@property
def outputs(self) -> list[OutputParam]:
return [OutputParam.template("images")]