Instructions to use OzzyGT/ideogram4_custom_blocks with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use OzzyGT/ideogram4_custom_blocks with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("OzzyGT/ideogram4_custom_blocks", torch_dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
| # 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" | |
| 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)." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ComponentSpec("vae", AutoencoderKLFlux2), IMAGE_PROCESSOR_SPEC] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam.template("image", required=True), | |
| InputParam.template("height"), | |
| InputParam.template("width"), | |
| InputParam.template("generator"), | |
| ] | |
| 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."), | |
| ] | |
| 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" | |
| def description(self) -> str: | |
| return "Replicate `image_latents` to the effective `batch_size` (num prompts * num_images_per_prompt)." | |
| 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."), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam(name="image_latents", type_hint=torch.Tensor, description="Image latents, batch-expanded.") | |
| ] | |
| 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" | |
| 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." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)] | |
| 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.", | |
| ), | |
| ] | |
| 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)."), | |
| ] | |
| 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" | |
| 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." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ComponentSpec("scheduler", FlowMatchEulerDiscreteScheduler)] | |
| 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.", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam( | |
| name="latents", | |
| type_hint=torch.Tensor, | |
| description="Noised image latents to start image-to-image denoising.", | |
| ) | |
| ] | |
| 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)}." | |
| ) | |
| 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()) | |
| 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()) | |
| 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." | |
| ) | |
| 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"] | |
| 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" | |
| def description(self) -> str: | |
| return ( | |
| "Core denoising step. \n" | |
| " - `Ideogram4Img2ImgCoreDenoiseStep` (img2img) is used when `image_latents` is provided.\n" | |
| " - `Ideogram4CoreDenoiseStep` (text2image) is used otherwise." | |
| ) | |
| 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}, | |
| } | |
| 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`." | |
| ) | |
| def outputs(self) -> list[OutputParam]: | |
| return [OutputParam.template("images")] | |