diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9875100a..5d0980c83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,9 +26,8 @@ Plus continued work on modernization of codebase: UI is now fully TypeScript bas
*note* Lens comes with its own prompt-refiner, enable in settings -> model options (disabled by default)
*note* original Lens implements only text-2-image, SD.Next adds image-2-image and inpaint workflows as well
- [Ideogram 4](https://huggingface.co/ideogram-ai/ideogram-4) open-weight 9.3B flow-matching single-stream DiT
- Qwen3-VL-8B text encoder (shared and deduped) and Flux2 VAE, with dual-transformer asymmetric CFG
- converted to a bf16-Diffusers repo with SDNQ-at-load
- *note* requires structured JSON-caption prompts, a plain-text prompt returns the model's built-in safety placeholder
+ with dual-transformer architecture (9.5b) and qwen3 (8b) text encoder
+ too many notes to add here, so check out the dedicated [Ideogram-4 wiki page](wiki/Ideogram.md) for all details!
- **Features**
- **SDNQ** new quantization algorithm: *Hadamard Rotations*
much higher quality than base SDNQ, but runs slightly slower
diff --git a/modules/prompt_parser.py b/modules/prompt_parser.py
index 308f28741..72a88dbbb 100644
--- a/modules/prompt_parser.py
+++ b/modules/prompt_parser.py
@@ -80,7 +80,6 @@ re_attention_v1 = re.compile(r"""
debug_output = os.environ.get('SD_PROMPT_DEBUG', None)
debug = log.trace if debug_output is not None else lambda *args, **kwargs: None
-debug('Trace: PROMPT')
def get_learned_conditioning_prompt_schedules(prompts, steps):
diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py
index 8c7dd13ba..5e8431085 100644
--- a/modules/prompt_parser_diffusers.py
+++ b/modules/prompt_parser_diffusers.py
@@ -11,7 +11,6 @@ from modules.prompt_parser_xhinker import get_weighted_text_embeddings_sd15, get
debug_enabled = os.environ.get('SD_PROMPT_DEBUG', None)
debug = log.trace if debug_enabled else lambda *args, **kwargs: None
-debug('Trace: PROMPT')
orig_encode_token_ids_to_embeddings = EmbeddingsProvider._encode_token_ids_to_embeddings # pylint: disable=protected-access
token_dict = None # used by helper get_tokens
token_type = None # used by helper get_tokens
diff --git a/modules/shared_items.py b/modules/shared_items.py
index 613908ea7..32e6a8e37 100644
--- a/modules/shared_items.py
+++ b/modules/shared_items.py
@@ -160,8 +160,6 @@ def get_pipelines():
stats_builtin += 1
if stats_custom > 0:
log.debug(f'Pipelines init: diffusers={stats_builtin} custom={stats_custom}')
- else:
- log.debug(f'Pipelines init: verified={stats_builtin}')
return pipelines
diff --git a/modules/styles.py b/modules/styles.py
index 79e70a9bc..4f99ffc3d 100644
--- a/modules/styles.py
+++ b/modules/styles.py
@@ -97,6 +97,12 @@ def apply_curly_braces_to_prompt(prompt, seed=-1):
if seed > 0:
old_state = random.getstate()
random.seed(seed)
+ prompt = prompt.strip()
+ try:
+ json.loads(prompt)
+ return prompt # this is already a json, do not process
+ except Exception:
+ pass
try:
pattern = re.compile(r'\{([^{}]*)\}', re.DOTALL) # innermost braces
while True:
diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py
index bb968ab75..e7ba1ac26 100644
--- a/modules/ui_definitions.py
+++ b/modules/ui_definitions.py
@@ -115,6 +115,10 @@ def create_settings(cmd_opts):
"model_ernie_enable_pe": OptionInfo(False, "Enable prompt-enhance"),
"model_lens_sep": OptionInfo("
Lens
", "", gr.HTML),
"model_lens_enable_pe": OptionInfo(False, "Enable prompt-enhance"),
+ "model_ideogram4_sep": OptionInfo("Ideogram 4
", "", gr.HTML),
+ "model_ideogram4_enable_pe": OptionInfo(True, "Enable prompt-enhance"),
+ "model_ideogram4_enable_cg": OptionInfo(True, "Enable conditioning guidance"),
+ "model_ideogram4_pin": OptionInfo(False, "Pin transformers to VRAM"),
}))
# --- Model Offloading ---
diff --git a/pipelines/ideogram/ideogram4.py b/pipelines/ideogram/ideogram4.py
new file mode 100644
index 000000000..4fcdc3d69
--- /dev/null
+++ b/pipelines/ideogram/ideogram4.py
@@ -0,0 +1,750 @@
+# Copyright 2026 Ideogram AI and 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.
+
+import os
+import math
+from typing import Any, Callable
+
+import torch
+from transformers import AutoTokenizer, PreTrainedModel
+from transformers.masking_utils import create_causal_mask
+
+from diffusers.image_processor import VaeImageProcessor
+from diffusers.models.autoencoders import AutoencoderKLFlux2
+from diffusers.models.transformers.transformer_ideogram4 import (
+ IMAGE_POSITION_OFFSET,
+ LLM_TOKEN_INDICATOR,
+ OUTPUT_IMAGE_INDICATOR,
+ SEQUENCE_PADDING_INDICATOR,
+ Ideogram4Transformer2DModel,
+)
+from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
+from diffusers.utils.torch_utils import randn_tensor
+from diffusers.pipelines.pipeline_utils import DiffusionPipeline
+from diffusers.pipelines.ideogram4.pipeline_output import Ideogram4PipelineOutput
+from diffusers.pipelines.ideogram4.prompt_enhancer import (
+ PROMPT_UPSAMPLE_TEMPERATURE,
+ Ideogram4PromptEnhancerHead,
+ build_caption_logits_processor,
+ build_prompt_enhancer,
+ generate_captions,
+)
+from modules.logger import log
+
+
+debug_prompt = log.trace if os.environ.get('SD_PROMPT_DEBUG', None) is not None else lambda *args, **kwargs: None
+
+
+# Hidden states of these Qwen3-VL decoder layers are concatenated to form the per-token
+# text conditioning consumed by the Ideogram4 transformer.
+QWEN3_VL_ACTIVATION_LAYERS = (0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 35)
+
+
+EXAMPLE_DOC_STRING = """
+ Examples:
+ ```py
+ >>> import torch
+ >>> from diffusers import Ideogram4Pipeline
+
+ >>> pipe = Ideogram4Pipeline.from_pretrained("ideogram-ai/ideogram-v4", torch_dtype=torch.bfloat16)
+ >>> pipe.to("cuda")
+
+ >>> prompt = "A photo of a cat holding a sign that says hello world"
+ >>> # The defaults are the recommended settings for best quality.
+ >>> image = pipe(prompt, height=2048, width=2048, generator=torch.Generator("cuda").manual_seed(0)).images[0]
+ >>> image.save("ideogram4.png")
+ ```
+"""
+
+def prompt_to_json(prompt):
+ """Normalize a JSON caption to the compact form Ideogram 4 trained on, or wrap plain text.
+ Ideogram 4 expects a structured JSON caption serialized compactly. A valid JSON prompt is
+ re-serialized to that form; a plain-text prompt is wrapped in a minimal caption so it stays
+ in distribution instead of tripping the weight-baked "blocked by safety filter" placeholder.
+ """
+ import json
+ if isinstance(prompt, list):
+ return [prompt_to_json(p) for p in prompt]
+ if not isinstance(prompt, str) or len(prompt) == 0:
+ return prompt
+ try:
+ return json.dumps(json.loads(prompt), ensure_ascii=False, separators=(',', ':'))
+ except ValueError:
+ caption = {
+ 'high_level_description': prompt,
+ 'compositional_deconstruction': {
+ 'background': prompt,
+ 'elements': [],
+ },
+ }
+ return json.dumps(caption, ensure_ascii=False, separators=(',', ':'))
+
+
+def _logit_normal_sigmas(
+ num_inference_steps: int,
+ mu: float,
+ std: float = 1.0,
+ logsnr_min: float = -15.0,
+ logsnr_max: float = 18.0,
+ device: torch.device | None = None,
+) -> torch.Tensor:
+ r"""
+ Build a length-`num_inference_steps` sigma schedule using the Ideogram4 logit-normal flow-matching schedule.
+
+ Sigmas are returned in `[0, 1]` in decreasing order (sigma close to 1 corresponds to pure noise, sigma close to 0
+ to clean data), matching diffusers conventions.
+
+ The Ideogram4 schedule applies `sigma(s) = 1 - logit_normal_cdf_inverse(1 - s)` to `s = linspace(0, 1, N + 1)` and
+ keeps the first `N` entries; a terminal zero is appended downstream by the scheduler.
+ """
+ intervals = torch.linspace(0.0, 1.0, num_inference_steps + 1, dtype=torch.float64)
+ # Apply the inverse CDF of a normal then push through the logistic to obtain a logit-normal CDF inverse.
+ z = torch.special.ndtri(intervals)
+ y = mu + std * z
+ t = 1.0 - torch.special.expit(y)
+ t_min = 1.0 / (1.0 + math.exp(0.5 * logsnr_max))
+ t_max = 1.0 / (1.0 + math.exp(0.5 * logsnr_min))
+ t = t.clamp(t_min, t_max)
+ # Convert from model time (0 = noise, 1 = data) to diffusers sigma (1 = noise, 0 = data) and reverse.
+ sigmas = (1.0 - t).flip(0)
+ # Drop the trailing 0; FlowMatchEulerDiscreteScheduler.set_timesteps appends one back internally.
+ sigmas = sigmas[:-1].to(dtype=torch.float32, device=device)
+ return sigmas
+
+
+def _resolution_aware_mu(
+ height: int,
+ width: int,
+ base_mu: float,
+ base_resolution: tuple[int, int] = (512, 512),
+) -> float:
+ """Shift the schedule mean as a function of image resolution."""
+ num_pixels = height * width
+ base_pixels = base_resolution[0] * base_resolution[1]
+ return base_mu + 0.5 * math.log(num_pixels / base_pixels)
+
+
+def _expand_tensor_to_effective_batch(
+ tensor: torch.Tensor,
+ batch_size: int,
+ num_per_prompt: int,
+ tensor_name: str | None = None,
+) -> torch.Tensor:
+ """Replicate `tensor` along dim 0 from `batch_size` (or 1) to `batch_size * num_per_prompt`."""
+ target_batch_size = batch_size * num_per_prompt
+
+ if tensor.shape[0] == target_batch_size:
+ return tensor
+
+ if tensor.shape[0] == 1:
+ repeat_by = target_batch_size
+ elif tensor.shape[0] == batch_size:
+ repeat_by = num_per_prompt
+ else:
+ tensor_name = f"`{tensor_name}`" if tensor_name is not None else "Tensor"
+ raise ValueError(
+ f"{tensor_name} batch size must be 1, `batch_size` ({batch_size}), or "
+ f"`batch_size * num_*_per_prompt` ({target_batch_size}), but got {tensor.shape[0]}."
+ )
+
+ return torch.repeat_interleave(tensor, repeats=repeat_by, dim=0, output_size=tensor.shape[0] * repeat_by)
+
+
+class Ideogram4Pipeline(DiffusionPipeline):
+ r"""
+ Text-to-image pipeline for Ideogram4.
+
+ Ideogram4 is a flow-matching model trained with asymmetric classifier-free guidance: a `transformer` consumes
+ text-conditioned features alongside the image latents, while a separate `unconditional_transformer` denoises with
+ zeroed text features. The two velocity predictions are linearly blended each step.
+
+ Args:
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
+ Flow-matching scheduler. The pipeline overrides the default sigma schedule with a resolution-aware
+ logit-normal schedule.
+ vae ([`AutoencoderKLFlux2`]):
+ Variational auto-encoder used to decode latents back into images.
+ text_encoder ([`PreTrainedModel`]):
+ Multimodal text encoder. The pipeline consumes hidden states from a fixed set of intermediate decoder
+ layers (see `QWEN3_VL_ACTIVATION_LAYERS`).
+ tokenizer ([`AutoTokenizer`]):
+ Tokenizer paired with `text_encoder`.
+ transformer ([`Ideogram4Transformer2DModel`]):
+ Conditional flow-matching transformer.
+ unconditional_transformer ([`Ideogram4Transformer2DModel`]):
+ Unconditional (asymmetric-CFG) flow-matching transformer.
+ """
+
+ model_cpu_offload_seq = "prompt_enhancer_head->text_encoder->transformer->unconditional_transformer->vae"
+ _optional_components = ["prompt_enhancer_head"]
+ _callback_tensor_inputs = ["latents"]
+
+ def __init__(
+ self,
+ scheduler: FlowMatchEulerDiscreteScheduler,
+ vae: AutoencoderKLFlux2,
+ text_encoder: PreTrainedModel,
+ tokenizer: AutoTokenizer,
+ transformer: Ideogram4Transformer2DModel,
+ unconditional_transformer: Ideogram4Transformer2DModel,
+ prompt_enhancer_head: Ideogram4PromptEnhancerHead | None = None,
+ ) -> None:
+ super().__init__()
+
+ self.register_modules(
+ scheduler=scheduler,
+ vae=vae,
+ text_encoder=text_encoder,
+ tokenizer=tokenizer,
+ transformer=transformer,
+ unconditional_transformer=unconditional_transformer,
+ prompt_enhancer_head=prompt_enhancer_head,
+ )
+
+ self.vae_scale_factor = (
+ 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) is not None else 8
+ )
+ # Ideogram4 patchifies the VAE output by a factor of 2 before feeding into the transformer.
+ self.patch_size = 2
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * self.patch_size)
+
+ # Built lazily on first upsample: the head-less encoder body + `prompt_enhancer_head`, combined.
+ self.prompt_enhancer = None
+ # Outlines logits processor for schema-constrained captions; built lazily on first upsample.
+ self.caption_logits_processor = None
+
+ def upsample_prompt(self, prompt, height=1024, width=1024, temperature=1.0, max_new_tokens=1024, generator=None, device=None) -> list[str]:
+ """Rewrite each prompt into Ideogram4's native structured JSON caption.
+ Requires the optional `prompt_enhancer_head` component, which is grafted onto the shared `text_encoder` body to
+ make it generative. Generation is schema-constrained when `outlines` is installed, otherwise it runs unconstrained.
+ """
+ if self.prompt_enhancer_head is None:
+ return prompt
+ from installer import install
+ install('outlines')
+ if self.prompt_enhancer is None:
+ self.prompt_enhancer = build_prompt_enhancer(self.text_encoder, self.prompt_enhancer_head)
+ if self.caption_logits_processor is None:
+ self.caption_logits_processor = build_caption_logits_processor(self.prompt_enhancer, self.tokenizer)
+
+ log.debug(f'Encode: enhancer={self.prompt_enhancer.__class__.__name__} processor={self.caption_logits_processor.__class__.__name__} max={max_new_tokens} device={device}')
+ self.prompt_enhancer.to(device)
+ caption = generate_captions(
+ self.prompt_enhancer,
+ self.tokenizer,
+ self.caption_logits_processor,
+ prompt,
+ height,
+ width,
+ temperature=temperature,
+ max_new_tokens=max_new_tokens,
+ generator=generator,
+ device=device,
+ )
+ self.prompt_enhancer.to('cpu')
+ debug_prompt(f'Prompt: input="{prompt}"')
+ debug_prompt(f'Prompt: enhanced="{caption}"')
+ return caption
+
+ @staticmethod
+ def _prepare_ids(
+ text_lengths: list[int],
+ grid_h: int,
+ grid_w: int,
+ max_text_tokens: int,
+ device: torch.device,
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Build the packed `[left-pad][text][image]` layout from the per-prompt text lengths and the image grid.
+
+ Returns `position_ids` (3-axis MRoPE), `segment_ids` (block-diagonal attention) and `indicator` (per-token
+ text/image/pad role).
+ """
+ batch_size = len(text_lengths)
+ num_image_tokens = grid_h * grid_w
+ total_seq_len = max_text_tokens + num_image_tokens
+
+ # Image position ids (t=0, h, w); offset keeps them disjoint from text positions.
+ h_idx = torch.arange(grid_h).view(-1, 1).expand(grid_h, grid_w).reshape(-1)
+ w_idx = torch.arange(grid_w).view(1, -1).expand(grid_h, grid_w).reshape(-1)
+ t_idx = torch.zeros_like(h_idx)
+ image_pos = torch.stack([t_idx, h_idx, w_idx], dim=1) + IMAGE_POSITION_OFFSET
+
+ position_ids = torch.zeros(batch_size, total_seq_len, 3, dtype=torch.long)
+ segment_ids = torch.full((batch_size, total_seq_len), SEQUENCE_PADDING_INDICATOR, dtype=torch.long)
+ indicator = torch.zeros(batch_size, total_seq_len, dtype=torch.long)
+
+ for b, num_text in enumerate(text_lengths):
+ offset = max_text_tokens - num_text
+
+ text_pos = torch.arange(num_text)
+ text_pos_3d = torch.stack([text_pos, text_pos, text_pos], dim=1)
+ position_ids[b, offset : offset + num_text] = text_pos_3d
+ position_ids[b, offset + num_text :] = image_pos
+
+ indicator[b, offset : offset + num_text] = LLM_TOKEN_INDICATOR
+ indicator[b, offset + num_text :] = OUTPUT_IMAGE_INDICATOR
+
+ segment_ids[b, offset : offset + num_text + num_image_tokens] = 1
+
+ return position_ids.to(device), segment_ids.to(device), indicator.to(device)
+
+ @staticmethod
+ def _get_text_encoder_hidden_states(
+ text_encoder,
+ token_ids: torch.Tensor,
+ attention_mask: torch.Tensor,
+ pos_2d: torch.Tensor,
+ ) -> list[torch.Tensor]:
+ """Run the text encoder's decoder layers, returning the hidden states tapped at each activation layer."""
+
+ language_model = text_encoder.language_model
+
+ inputs_embeds = language_model.embed_tokens(token_ids)
+
+ position_ids_4d = pos_2d[None, ...].expand(4, pos_2d.shape[0], -1)
+ text_position_ids = position_ids_4d[0]
+ mrope_position_ids = position_ids_4d[1:]
+
+ causal_mask = create_causal_mask(
+ config=language_model.config,
+ inputs_embeds=inputs_embeds,
+ attention_mask=attention_mask,
+ past_key_values=None,
+ position_ids=text_position_ids,
+ )
+ position_embeddings = language_model.rotary_emb(inputs_embeds, mrope_position_ids)
+
+ tap_set = set(QWEN3_VL_ACTIVATION_LAYERS)
+ captured: dict[int, torch.Tensor] = {}
+ hidden_states = inputs_embeds
+ for layer_idx, decoder_layer in enumerate(language_model.layers):
+ hidden_states = decoder_layer(
+ hidden_states,
+ attention_mask=causal_mask,
+ position_ids=text_position_ids,
+ past_key_values=None,
+ position_embeddings=position_embeddings,
+ )
+ if layer_idx in tap_set:
+ captured[layer_idx] = hidden_states
+
+ return [captured[i] for i in QWEN3_VL_ACTIVATION_LAYERS]
+
+ def _encode_prompt(
+ self,
+ prompt: str | list[str],
+ grid_h: int,
+ grid_w: int,
+ max_sequence_length: int,
+ device: torch.device,
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Prepare the conditioning for the packed text+image sequence (one entry per prompt).
+
+ Returns a flat tuple `(prompt_embeds, position_ids, segment_ids, indicator)`. The unconditional branch carries
+ no text, so the pipeline builds its (zeroed) inputs directly rather than encoding a negative prompt.
+ """
+ prompts = [prompt] if isinstance(prompt, str) else list(prompt)
+ batch_size = len(prompts)
+ num_image_tokens = grid_h * grid_w
+
+ # Tokenize each chat-formatted prompt and left-pad to `max_sequence_length`. Only the text region is fed to
+ # the encoder: the packed image tokens come after the text and the encoder is causal, so they never affect it.
+ token_ids = torch.zeros(batch_size, max_sequence_length, dtype=torch.long)
+ attention_mask = torch.zeros(batch_size, max_sequence_length, dtype=torch.long)
+ text_position_ids = torch.zeros(batch_size, max_sequence_length, dtype=torch.long)
+ text_lengths = []
+ for b, text_prompt in enumerate(prompts):
+ messages = [{"role": "user", "content": [{"type": "text", "text": text_prompt}]}]
+ text = self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
+ toks = self.tokenizer(text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
+ n = int(toks.shape[0])
+ if n > max_sequence_length:
+ raise ValueError(f"prompt has {n} tokens, exceeds max_sequence_length={max_sequence_length}")
+ text_lengths.append(n)
+ offset = max_sequence_length - n
+ token_ids[b, offset:] = toks
+ attention_mask[b, offset:] = 1
+ text_position_ids[b, offset:] = torch.arange(n)
+
+ token_ids = token_ids.to(device)
+ attention_mask = attention_mask.to(device)
+ text_position_ids = text_position_ids.to(device)
+
+ # Concatenate the tapped activation-layer hidden states into per-token text features, zeroing padding.
+ selected = self._get_text_encoder_hidden_states(
+ self.text_encoder, token_ids, attention_mask, text_position_ids
+ )
+ text_features = torch.stack(selected, dim=0).permute(1, 2, 3, 0).reshape(batch_size, max_sequence_length, -1)
+ text_features = (text_features * attention_mask.to(text_features.dtype).unsqueeze(-1)).to(torch.float32)
+
+ position_ids, segment_ids, indicator = self._prepare_ids(
+ text_lengths, grid_h, grid_w, max_sequence_length, device
+ )
+
+ # Pack the text features into the full sequence; image positions carry no text features.
+ image_feature_padding = torch.zeros(
+ batch_size, num_image_tokens, text_features.shape[-1], dtype=text_features.dtype, device=device
+ )
+ prompt_embeds = torch.cat([text_features, image_feature_padding], dim=1)
+ return prompt_embeds, position_ids, segment_ids, indicator
+
+ def encode_prompt(
+ self,
+ prompt: str | list[str],
+ grid_h: int,
+ grid_w: int,
+ max_sequence_length: int,
+ device: torch.device,
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+ from modules import shared, devices
+ if not shared.opts.model_ideogram4_enable_pe:
+ prompt = prompt_to_json(prompt)
+ self.text_encoder.to(self._execution_device)
+ try:
+ prompt = self._encode_prompt(prompt, grid_h, grid_w, max_sequence_length, device)
+ finally:
+ if shared.opts.diffusers_offload_mode != 'none':
+ self.text_encoder.to(devices.cpu)
+ return prompt
+
+ def prepare_latents(
+ self,
+ batch_size: int,
+ num_image_tokens: int,
+ latent_dim: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ generator: torch.Generator | list[torch.Generator] | None,
+ latents: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ shape = (batch_size, num_image_tokens, latent_dim)
+ if latents is None:
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
+ else:
+ if latents.shape != shape:
+ raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}")
+ latents = latents.to(device=device, dtype=dtype)
+ return latents
+
+ @property
+ def guidance_scale(self) -> float | None:
+ return self._guidance_scale
+
+ @property
+ def num_timesteps(self) -> int:
+ return self._num_timesteps
+
+ @property
+ def interrupt(self) -> bool:
+ return self._interrupt
+
+ def check_inputs(
+ self,
+ prompt,
+ height,
+ width,
+ num_inference_steps,
+ guidance_scale,
+ guidance_schedule,
+ callback_on_step_end_tensor_inputs=None,
+ ):
+ if prompt is None:
+ raise ValueError("`prompt` must be provided.")
+ if not isinstance(prompt, (str, list)):
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
+
+ if (
+ height % (self.vae_scale_factor * self.patch_size) != 0
+ or width % (self.vae_scale_factor * self.patch_size) != 0
+ ):
+ raise ValueError(
+ f"`height` ({height}) and `width` ({width}) must both be divisible by {self.vae_scale_factor * self.patch_size} "
+ f"(vae_scale_factor * patch_size)."
+ )
+
+ # Guidance is controlled by either a constant `guidance_scale` or a per-step `guidance_schedule`; exactly
+ # one must be set (the `guidance_schedule` default makes the no-arg call use the recommended schedule).
+ if guidance_scale is not None:
+ guidance_schedule = [guidance_scale] * num_inference_steps
+ if guidance_scale is None and guidance_schedule is None:
+ guidance_schedule = [7.0] * int(num_inference_steps * 0.9) + [3.0] * (num_inference_steps - int(num_inference_steps * 0.9)) # 90% of steps with 7.0, last 10% with 3.0
+
+ if callback_on_step_end_tensor_inputs is not None and not all(k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs):
+ raise ValueError(
+ f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found "
+ f"{[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
+ )
+
+ @torch.no_grad()
+ def __call__(
+ self,
+ prompt: str | list[str] | None = None,
+ height: int = 2048,
+ width: int = 2048,
+ num_inference_steps: int = 48,
+ guidance_scale: float | None = None,
+ guidance_schedule: list[float] | torch.Tensor | None = (7.0,) * 45 + (3.0,) * 3,
+ mu: float = 0.0,
+ std: float = 1.5,
+ prompt_upsampling: bool = False,
+ prompt_upsampling_temperature: float = PROMPT_UPSAMPLE_TEMPERATURE,
+ max_sequence_length: int = 2048,
+ num_images_per_prompt: int = 1,
+ generator: torch.Generator | list[torch.Generator] | None = None,
+ latents: torch.Tensor | None = None,
+ output_type: str = "pil",
+ return_dict: bool = True,
+ callback_on_step_end: Callable[["Ideogram4Pipeline", int, int, dict[str, Any]], dict[str, Any]] | None = None,
+ callback_on_step_end_tensor_inputs: list[str] = ["latents"],
+ ) -> Ideogram4PipelineOutput | tuple[Any]:
+ r"""
+ Run text-to-image generation.
+
+ Args:
+ prompt (`str` or `list[str]`):
+ Prompt(s) to guide image generation.
+ height (`int`, *optional*, defaults to 2048):
+ Output image height in pixels; must be a multiple of `vae_scale_factor * patch_size`.
+ width (`int`, *optional*, defaults to 2048):
+ Output image width in pixels; must be a multiple of `vae_scale_factor * patch_size`.
+ num_inference_steps (`int`, *optional*, defaults to 48):
+ Number of flow-matching steps. The default is the recommended setting for best quality.
+ guidance_scale (`float`, *optional*):
+ Constant classifier-free guidance scale applied at every step. The conditional and unconditional
+ velocity predictions are blended as `v = guidance_scale * v_pos + (1 - guidance_scale) * v_neg`.
+ Mutually exclusive with `guidance_schedule` (setting both raises). Defaults to `None`.
+ guidance_schedule (`list[float]` or `torch.Tensor`, *optional*):
+ Per-step guidance scale schedule; must have length `num_inference_steps`. The first entry corresponds
+ to the first step (largest noise level). Mutually exclusive with `guidance_scale`; exactly one must be
+ set. Defaults to the recommended schedule (7.0 for the main steps, dropping to 3.0 for the final 3
+ "polish" steps). To use a constant scale instead, pass `guidance_scale` and `guidance_schedule=None`.
+ mu (`float`, *optional*, defaults to 0.0):
+ Base mean of the logit-normal flow-matching schedule. The schedule mean is shifted by half the log of
+ the resolution ratio relative to 512x512.
+ std (`float`, *optional*, defaults to 1.5):
+ Standard deviation of the logit-normal flow-matching schedule.
+ prompt_upsampling (`bool`, *optional*, defaults to `False`):
+ If `True`, rewrite `prompt` into Ideogram4's native structured JSON caption via
+ [`~Ideogram4Pipeline.upsample_prompt`] before encoding. Requires the optional `prompt_enhancer_head`
+ component; install `outlines` for schema-constrained captions. `generator` is reused to make the
+ upsampling reproducible.
+ prompt_upsampling_temperature (`float`, *optional*, defaults to 1.0):
+ Sampling temperature for prompt upsampling when `prompt_upsampling=True`.
+ max_sequence_length (`int`, *optional*, defaults to 2048):
+ Maximum number of text tokens per prompt.
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
+ Number of images to generate per prompt.
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
+ Generator(s) used to make sampling deterministic.
+ latents (`torch.Tensor`, *optional*):
+ Pre-generated noise of shape `(batch_size, num_image_tokens, latent_dim)`.
+ output_type (`str`, *optional*, defaults to `"pil"`):
+ One of `"pil"`, `"np"`, `"pt"`, or `"latent"`.
+ return_dict (`bool`, *optional*, defaults to `True`):
+ Whether to return an [`~pipelines.ideogram4.Ideogram4PipelineOutput`].
+ callback_on_step_end (`Callable`, *optional*):
+ Callback invoked at the end of every denoising step.
+ callback_on_step_end_tensor_inputs (`list[str]`, *optional*):
+ Names of tensors to expose to the callback via `callback_kwargs`.
+
+ Examples:
+
+ Returns:
+ [`~pipelines.ideogram4.Ideogram4PipelineOutput`] or `tuple`.
+ """
+ self.check_inputs(
+ prompt=prompt,
+ height=height,
+ width=width,
+ num_inference_steps=num_inference_steps,
+ guidance_scale=guidance_scale,
+ guidance_schedule=guidance_schedule,
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
+ )
+
+ batch_size = 1
+ if isinstance(prompt, list):
+ batch_size = len(prompt)
+ else:
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
+
+ device = self._execution_device
+ self._guidance_scale = guidance_scale # pylint: disable=attribute-defined-outside-init
+ self._interrupt = False # pylint: disable=attribute-defined-outside-init
+
+ # 0. Optionally rewrite the prompt(s) into Ideogram4's native structured JSON caption.
+ if prompt_upsampling:
+ prompt = self.upsample_prompt(
+ prompt,
+ height=height,
+ width=width,
+ temperature=prompt_upsampling_temperature,
+ max_new_tokens=max_sequence_length,
+ generator=generator,
+ device=device,
+ )
+
+ # 1. Image grid (drives both the packed layout and the latent shape).
+ grid_h, grid_w = (
+ height // (self.vae_scale_factor * self.patch_size),
+ width // (self.vae_scale_factor * self.patch_size),
+ )
+ num_image_tokens = grid_h * grid_w
+
+ # 2. Encode prompts into the packed conditioning (one entry per prompt).
+ llm_features, position_ids, segment_ids, indicator = self.encode_prompt(
+ prompt=prompt,
+ grid_h=grid_h,
+ grid_w=grid_w,
+ max_sequence_length=max_sequence_length,
+ device=device,
+ )
+
+ # 3. Replicate the conditioning for num_images_per_prompt.
+ llm_features = _expand_tensor_to_effective_batch(llm_features, batch_size, num_images_per_prompt)
+ position_ids = _expand_tensor_to_effective_batch(position_ids, batch_size, num_images_per_prompt)
+ segment_ids = _expand_tensor_to_effective_batch(segment_ids, batch_size, num_images_per_prompt)
+ indicator = _expand_tensor_to_effective_batch(indicator, batch_size, num_images_per_prompt)
+
+ # 4. Unconditional (image-only) branch, derived from the conditioning: zeroed text features and the
+ # image-region slices of the layout.
+ neg_llm_features = torch.zeros(
+ batch_size * num_images_per_prompt,
+ num_image_tokens,
+ llm_features.shape[-1],
+ dtype=llm_features.dtype,
+ device=device,
+ )
+ neg_position_ids = position_ids[:, max_sequence_length:]
+ neg_segment_ids = segment_ids[:, max_sequence_length:]
+ neg_indicator = indicator[:, max_sequence_length:]
+
+ # 4. Set up the resolution-aware logit-normal schedule on the scheduler.
+ schedule_mu = _resolution_aware_mu(height=height, width=width, base_mu=mu)
+ sigmas = _logit_normal_sigmas(num_inference_steps, schedule_mu, std=std, device=device)
+ self.scheduler.set_timesteps(sigmas=sigmas.tolist(), device=device)
+ timesteps = self.scheduler.timesteps
+ self._num_timesteps = len(timesteps) # pylint: disable=attribute-defined-outside-init
+
+ # 5. Resolve the per-step guidance schedule (a constant `guidance_scale` broadcasts to every step, otherwise
+ # use the provided `guidance_schedule`, validated by `check_inputs`) and the tensor of per-step weights `gw`.
+ if guidance_scale is not None:
+ guidance_schedule = [float(guidance_scale)] * num_inference_steps
+ gw = torch.as_tensor(guidance_schedule, dtype=torch.float32, device=device)
+
+ # 6. Prepare latents in the packed (B, num_image_tokens, latent_dim) layout.
+ latent_dim = self.transformer.config.in_channels
+ latents = self.prepare_latents(
+ batch_size=batch_size * num_images_per_prompt,
+ num_image_tokens=num_image_tokens,
+ latent_dim=latent_dim,
+ dtype=torch.float32,
+ device=device,
+ generator=generator,
+ latents=latents,
+ )
+
+ # 7. Padding for the text region of the conditional packed sequence (image latents are appended after it).
+ max_text_tokens = max_sequence_length
+ text_z_padding = torch.zeros(
+ batch_size * num_images_per_prompt,
+ max_text_tokens,
+ latent_dim,
+ dtype=torch.float32,
+ device=device,
+ )
+
+ # The transformers run in their loaded compute dtype; cast the (otherwise float32) text features to match.
+ # `latents` stay float32 for scheduler precision and are cast per-step at the transformer call below.
+ llm_features = llm_features.to(self.transformer.dtype)
+ neg_llm_features = neg_llm_features.to(self.unconditional_transformer.dtype if self.unconditional_transformer else self.transformer.dtype)
+
+ # 8. Denoising loop. The scheduler stores `num_train_timesteps`-scaled timesteps; convert back to model time.
+ num_train_timesteps = self.scheduler.config.num_train_timesteps # pylint: disable=no-member
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
+ for i, t in enumerate(timesteps):
+ if self.interrupt:
+ continue
+
+ # Map sigma-domain timestep to model time `t` in [0, 1] (0 = noise, 1 = clean data).
+ t_model = 1.0 - (t.float() / num_train_timesteps)
+ t_model = t_model.expand(batch_size * num_images_per_prompt).to(self.transformer.dtype)
+
+ # Conditional pass operates on the full packed sequence.
+ pos_z = torch.cat([text_z_padding, latents], dim=1).to(self.transformer.dtype)
+ pos_out = self.transformer(
+ hidden_states=pos_z,
+ timestep=t_model,
+ encoder_hidden_states=llm_features,
+ position_ids=position_ids,
+ segment_ids=segment_ids,
+ indicator=indicator,
+ return_dict=False,
+ )[0]
+ # Velocity (and guidance) is computed in float32 for scheduler precision; the transformers
+ # return their compute dtype, so cast the predicted velocities up here.
+ pos_v = pos_out[:, max_text_tokens:].to(torch.float32)
+
+ # Unconditional pass uses image-only positions with zeroed text features.
+ self._guidance_scale = guidance_schedule[i] # pylint: disable=attribute-defined-outside-init
+ gw_i = gw[i]
+ if gw[i] > 1.0:
+ uncond_transformer = self.unconditional_transformer if self.unconditional_transformer is not None else self.transformer
+ neg_v = uncond_transformer(
+ hidden_states=latents.to(uncond_transformer.dtype),
+ timestep=t_model,
+ encoder_hidden_states=neg_llm_features,
+ position_ids=neg_position_ids,
+ segment_ids=neg_segment_ids,
+ indicator=neg_indicator,
+ return_dict=False,
+ )[0].to(torch.float32)
+ v = gw_i * pos_v + (1.0 - gw_i) * neg_v
+ else:
+ v = gw_i * pos_v
+
+ latents = self.scheduler.step(-v, t, latents, return_dict=False)[0]
+
+ if callback_on_step_end is not None:
+ callback_kwargs = {k: locals()[k] for k in callback_on_step_end_tensor_inputs}
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
+ latents = callback_outputs.pop("latents", latents)
+
+ progress_bar.update()
+
+ # 9. Decode: unpatch the latents, denormalize with the VAE batch-norm stats, and decode through the VAE.
+ if output_type == "latent":
+ image = latents
+ else:
+ z = latents
+ # VAE bn stores per-channel statistics on the packed-channel latent space (ae_channels * patch ** 2).
+ bn_mean = self.vae.bn.running_mean.view(1, 1, -1).to(device=z.device, dtype=z.dtype)
+ bn_std = torch.sqrt(self.vae.bn.running_var + self.vae.config.batch_norm_eps).view(1, 1, -1)
+ bn_std = bn_std.to(device=z.device, dtype=z.dtype)
+ z = z * bn_std + bn_mean
+
+ patch = self.patch_size
+ ae_channels = z.shape[-1] // (patch * patch)
+ z = z.view(batch_size * num_images_per_prompt, grid_h, grid_w, patch, patch, ae_channels)
+ z = z.permute(0, 5, 1, 3, 2, 4).contiguous()
+ z = z.view(batch_size * num_images_per_prompt, ae_channels, grid_h * patch, grid_w * patch)
+
+ decoded = self.vae.decode(z.to(self.vae.dtype), return_dict=False)[0]
+ image = self.image_processor.postprocess(decoded.float(), output_type=output_type)
+
+ self.maybe_free_model_hooks()
+
+ if not return_dict:
+ return (image,)
+ return Ideogram4PipelineOutput(images=image)
diff --git a/pipelines/ideogram/patch_qwen.py b/pipelines/ideogram/patch_qwen.py
new file mode 100644
index 000000000..3df4f34b3
--- /dev/null
+++ b/pipelines/ideogram/patch_qwen.py
@@ -0,0 +1,63 @@
+from transformers.utils.generic import can_return_tuple
+from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLForConditionalGeneration, Qwen3VLCausalLMOutputWithPast
+
+
+_original_qwen3vl_forward = None
+
+
+@can_return_tuple
+def _qwen3vl_forward(
+ self,
+ input_ids=None,
+ attention_mask=None,
+ position_ids=None,
+ past_key_values=None,
+ inputs_embeds=None,
+ labels=None,
+ pixel_values=None,
+ pixel_values_videos=None,
+ image_grid_thw=None,
+ video_grid_thw=None,
+ mm_token_type_ids=None,
+ logits_to_keep=0,
+ **kwargs,
+):
+ outputs = self.model(
+ input_ids=input_ids,
+ pixel_values=pixel_values,
+ pixel_values_videos=pixel_values_videos,
+ image_grid_thw=image_grid_thw,
+ video_grid_thw=video_grid_thw,
+ position_ids=position_ids,
+ attention_mask=attention_mask,
+ past_key_values=past_key_values,
+ inputs_embeds=inputs_embeds,
+ mm_token_type_ids=mm_token_type_ids,
+ **kwargs,
+ )
+ hidden_states = outputs[0]
+ if hidden_states is not None and hasattr(self, "lm_head"):
+ target_device = hidden_states.device
+ if self.lm_head.weight.device != target_device:
+ self.lm_head = self.lm_head.to(target_device)
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
+ loss = None
+ if labels is not None:
+ loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size)
+ return Qwen3VLCausalLMOutputWithPast(
+ loss=loss,
+ logits=logits,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ rope_deltas=outputs.rope_deltas,
+ )
+
+
+def hijack_qwen3vl():
+ # qwen forward makes a mess of .to() calls when model is manually assembled with lm_head attached
+ global _original_qwen3vl_forward # pylint: disable=global-statement
+ if _original_qwen3vl_forward is None:
+ _original_qwen3vl_forward = Qwen3VLForConditionalGeneration.forward
+ Qwen3VLForConditionalGeneration.forward = _qwen3vl_forward
diff --git a/pipelines/model_ideogram4.py b/pipelines/model_ideogram4.py
index 1482b3668..cb9154a3f 100644
--- a/pipelines/model_ideogram4.py
+++ b/pipelines/model_ideogram4.py
@@ -1,56 +1,18 @@
-import json
import diffusers
-from transformers.models.qwen3_vl import Qwen3VLModel
+import transformers
from modules import shared, devices, sd_models, model_quant
from modules.logger import log
from pipelines import generic
-def prompt_to_json(prompt):
- """Normalize a JSON caption to the compact form Ideogram 4 trained on, or wrap plain text.
-
- Ideogram 4 expects a structured JSON caption serialized compactly. A valid JSON prompt is
- re-serialized to that form; a plain-text prompt is wrapped in a minimal caption so it stays
- in distribution instead of tripping the weight-baked "blocked by safety filter" placeholder.
- """
- if isinstance(prompt, list):
- return [prompt_to_json(p) for p in prompt]
- if not isinstance(prompt, str) or len(prompt) == 0:
- return prompt
- try:
- return json.dumps(json.loads(prompt), ensure_ascii=False, separators=(',', ':'))
- except ValueError:
- caption = {'high_level_description': prompt, 'compositional_deconstruction': {'background': prompt, 'elements': []}}
- return json.dumps(caption, ensure_ascii=False, separators=(',', ':'))
-
-
-class Ideogram4Pipeline(diffusers.Ideogram4Pipeline):
- """SD.Next integration subclass for the diffusers-native Ideogram 4 pipeline.
-
- ``encode_prompt`` normalizes the prompt into the structured JSON the model expects, then
- drives the Qwen3-VL tap. The tap calls ``language_model`` submodules directly, bypassing the
- balanced-offload pre-forward hook, so the encoder is moved on-device for it and released after.
- """
-
- def encode_prompt(self, prompt, *args, **kwargs):
- prompt = prompt_to_json(prompt)
- self.text_encoder.to(self._execution_device)
- try:
- return super().encode_prompt(prompt, *args, **kwargs)
- finally:
- if shared.opts.diffusers_offload_mode != 'none':
- self.text_encoder.to(devices.cpu)
-
-
def pin_transformers(transformer, unconditional_transformer) -> bool:
"""Keep both transformers resident under balanced offload when they fit the budget.
-
Every denoise step runs both transformers, so balanced offload ping-pongs them across
PCIe each step. ``offload_never`` makes ``offload_allowed`` skip the per-step pre-sweep so
they stay resident, but only when both fit ``gpu_memory * max watermark`` (which leaves the
watermark headroom for activations); otherwise the normal offload path is kept.
"""
- if shared.opts.diffusers_offload_mode != 'balanced' or shared.gpu_memory <= 0:
+ if shared.opts.diffusers_offload_mode != 'balanced' or shared.gpu_memory <= 0 or not shared.opts.model_ideogram4_pin:
return False
if transformer is None or unconditional_transformer is None:
return False
@@ -60,7 +22,7 @@ def pin_transformers(transformer, unconditional_transformer) -> bool:
if fits:
transformer.offload_never = True
unconditional_transformer.offload_never = True
- log.debug(f'Load model: type=Ideogram4 offload=balanced transformers={size_gb:.1f} budget={budget_gb:.1f} action={"pin" if fits else "default"}')
+ log.debug(f'Load model: type=Ideogram4 offload=balanced transformers={size_gb:.2f} budget={budget_gb:.2f} action={"pin" if fits else "default"}')
return fits
@@ -70,36 +32,56 @@ def load_ideogram4(checkpoint_info, diffusers_load_config=None):
repo_id = sd_models.path_to_repo(checkpoint_info)
sd_models.hf_auth_check(checkpoint_info)
load_args, _ = model_quant.get_dit_args(diffusers_load_config, allow_quant=False)
- log.debug(f'Load model: type=Ideogram4 repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}')
+ log.debug(f'Load model: type=Ideogram4 repo="{repo_id}" offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args} conditional={shared.opts.model_ideogram4_enable_cg} enhance={shared.opts.model_ideogram4_enable_pe} pin={shared.opts.model_ideogram4_pin}')
+ from pipelines.ideogram.ideogram4 import Ideogram4Pipeline
generic.set_pipeline('Ideogram4', Ideogram4Pipeline)
if repo_id is None or repo_id.lower() == 'none':
return None
- # Each transformer loads independently from its subfolder, so each gets its own SDNQ config.
- cls = diffusers.Ideogram4Transformer2DModel
- transformer = generic.load_transformer(repo_id, cls_name=cls, subfolder="transformer", load_config=diffusers_load_config)
- unconditional_transformer = generic.load_transformer(repo_id, cls_name=cls, subfolder="unconditional_transformer", load_config=diffusers_load_config)
- pin_transformers(transformer, unconditional_transformer)
- # shared_te_map redirects to the shared Qwen3-VL repo (deduped with VQA + prompt-enhance);
- # the bundled text_encoder is the fallback when sharing is off. The vae, tokenizer, and
- # scheduler load from the repo via from_pretrained.
- text_encoder = generic.load_text_encoder(repo_id, cls_name=Qwen3VLModel, load_config=diffusers_load_config)
+ transformer_cls = diffusers.Ideogram4Transformer2DModel
+ transformer = generic.load_transformer(repo_id, cls_name=transformer_cls, subfolder="transformer", load_config=diffusers_load_config)
+ if shared.opts.model_ideogram4_enable_cg:
+ unconditional_transformer = generic.load_transformer(repo_id, cls_name=transformer_cls, subfolder="unconditional_transformer", load_config=diffusers_load_config)
+ else:
+ unconditional_transformer = None
+ text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3VLModel, load_config=diffusers_load_config)
+ components = {
+ 'transformer': transformer,
+ 'unconditional_transformer': unconditional_transformer,
+ 'text_encoder': text_encoder,
+ }
+
+ prompt_enhancer_head = None
+ if shared.opts.model_ideogram4_enable_pe:
+ enhancer_repo_id = "diffusers/qwen3-vl-8b-instruct-lm-head"
+ enhancer_cls = diffusers.Ideogram4PromptEnhancerHead
+ log.debug(f'Load model: enhancer="{enhancer_repo_id}" cls={enhancer_cls.__name__}')
+ prompt_enhancer_head = enhancer_cls.from_pretrained(
+ enhancer_repo_id,
+ torch_dtype=devices.dtype,
+ cache_dir=shared.opts.hfcache_dir
+ )
+ components['prompt_enhancer_head'] = prompt_enhancer_head
+
+ pin_transformers(transformer, unconditional_transformer)
pipe = Ideogram4Pipeline.from_pretrained(
repo_id,
cache_dir=shared.opts.diffusers_dir,
- transformer=transformer,
- unconditional_transformer=unconditional_transformer,
- text_encoder=text_encoder,
+ **components,
**load_args,
)
- # The pipeline decodes internally; the CFG scale slider drives guidance_scale, which is
- # mutually exclusive with the pipeline's default per-step guidance_schedule.
- pipe.task_args = {'output_type': 'pil', 'guidance_schedule': None} # pylint: disable=attribute-defined-outside-init
- # JSON captions must pass through verbatim; skip styles/wildcards that would strip the braces.
- pipe.keep_prompts = True # pylint: disable=attribute-defined-outside-init
- del transformer, unconditional_transformer, text_encoder
+ pipe.task_args = {
+ 'output_type': 'pil',
+ 'prompt_upsampling': shared.opts.model_ideogram4_enable_pe,
+ }
+
+ del transformer, unconditional_transformer, text_encoder, prompt_enhancer_head
devices.torch_gc(force=True, reason='load')
+
+ from pipelines.ideogram.patch_qwen import hijack_qwen3vl
+ hijack_qwen3vl()
+
return pipe