Instructions to use OzzyGT/ideogram4_caption_blocks with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use OzzyGT/ideogram4_caption_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_caption_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
| # 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 block: image OR text -> Ideogram 4 structured-JSON caption. | |
| Wraps a vision-language model (Gemma-4-E4B via `Gemma4ForConditionalGeneration`) with two modes, selected by input: | |
| - `image` given -> caption the image into Ideogram 4's native JSON schema (the `ideogram4-caption` Space logic). | |
| - `prompt` given (no image) -> enhance the short text idea into a JSON caption (Ideogram's "magic prompt", | |
| reusing the canonical `CAPTION_SYSTEM_MESSAGE` from `diffusers.pipelines.ideogram4.prompt_enhancer`). | |
| Either way the produced `caption` can be fed straight into the Ideogram 4 generation pipeline as the `prompt` | |
| (the model is trained on these JSON captions). This is the code-usable backend, minus the Gradio UI. | |
| """ | |
| import json | |
| import math | |
| import re | |
| import torch | |
| from diffusers.modular_pipelines.modular_pipeline import ModularPipelineBlocks, PipelineState, SequentialPipelineBlocks | |
| from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam | |
| from diffusers.pipelines.ideogram4.prompt_enhancer import CAPTION_SYSTEM_MESSAGE, CAPTION_USER_TEMPLATE | |
| from diffusers.utils import logging | |
| from transformers import AutoProcessor, Gemma4ForConditionalGeneration | |
| logger = logging.get_logger(__name__) # pylint: disable=invalid-name | |
| # Ideogram 4's native caption schema (from the official ideogram-oss/ideogram4 docs). Key ORDER matters for | |
| # quality, bbox is [ymin, xmin, ymax, xmax] in 0-1000 normalized coords, colors are UPPERCASE #RRGGBB. | |
| DEFAULT_INSTRUCTION = ( | |
| "You are a captioner for the Ideogram 4 image model. Look at the image and output ONE JSON object (no prose, " | |
| "no markdown fences) that reconstructs it in Ideogram 4's native caption schema. Keep keys in the exact order " | |
| "shown.\n\n" | |
| "SCHEMA (keys in order):\n" | |
| "- high_level_description: one or two sentences summarizing the whole image (subject + medium/style).\n" | |
| "- style_description: an object with EXACTLY ONE of `photo` or `art_style`:\n" | |
| " photo caption order: aesthetics, lighting, photo, medium, color_palette\n" | |
| " non-photo caption order: aesthetics, lighting, medium, art_style, color_palette\n" | |
| ' color_palette: optional, up to 16 UPPERCASE hex colors (e.g. "#1B1B2F").\n' | |
| "- compositional_deconstruction: {background, elements}.\n" | |
| " background: the global setting/backdrop.\n" | |
| " elements: list; keys in order per type:\n" | |
| " obj: type, bbox, desc, color_palette\n" | |
| " text: type, bbox, text, desc, color_palette\n\n" | |
| "RULES:\n" | |
| "- bbox is [ymin, xmin, ymax, xmax] in 0-1000 normalized coordinates (origin top-left), independent of " | |
| "resolution. bbox and per-element color_palette are optional (<=5 colors per element).\n" | |
| "- Main subject first. A coherent subject (one person/animal/object) is exactly ONE element; its parts go in " | |
| "its `desc`. `desc` is identity-first and concrete (color, material, pose, distinguishing features).\n" | |
| "- Commit to one concrete value each (no 'various'/'or similar'). `text` elements carry the literal in-image " | |
| "characters, verbatim. Colors UPPERCASE #RRGGBB. Describe only what is visible; do not invent." | |
| ) | |
| def _balance_json(frag: str) -> str: | |
| """Close unbalanced strings/brackets in a truncated JSON fragment so it can be parsed.""" | |
| stack, in_str, esc = [], False, False | |
| for ch in frag: | |
| if in_str: | |
| if esc: | |
| esc = False | |
| elif ch == "\\": | |
| esc = True | |
| elif ch == '"': | |
| in_str = False | |
| elif ch == '"': | |
| in_str = True | |
| elif ch in "{[": | |
| stack.append(ch) | |
| elif ch == "}" and stack and stack[-1] == "{": | |
| stack.pop() | |
| elif ch == "]" and stack and stack[-1] == "[": | |
| stack.pop() | |
| out = frag | |
| if in_str: | |
| out += '"' | |
| out = out.rstrip() | |
| if out.endswith(","): | |
| out = out[:-1] | |
| for ch in reversed(stack): | |
| out += "}" if ch == "{" else "]" | |
| return out | |
| def _parse_json(text: str): | |
| """Parse the model output into (dict|None, repaired: bool). Robust to markdown fences, trailing prose, and | |
| truncated output (balances brackets). `repaired=True` means the output was incomplete and was closed to parse.""" | |
| text = text.strip() | |
| text = re.sub(r"^```(?:json)?\s*", "", text) | |
| text = re.sub(r"\s*```$", "", text).strip() | |
| start = text.find("{") | |
| if start == -1: | |
| return None, False | |
| frag = text[start:] | |
| try: | |
| obj = json.loads(frag[: frag.rfind("}") + 1], strict=False) | |
| if isinstance(obj, dict): | |
| return obj, False | |
| except json.JSONDecodeError: | |
| pass | |
| try: | |
| obj, _ = json.JSONDecoder(strict=False).raw_decode(frag) | |
| if isinstance(obj, dict): | |
| return obj, False | |
| except json.JSONDecodeError: | |
| pass | |
| try: | |
| obj = json.loads(_balance_json(frag), strict=False) | |
| return (obj, True) if isinstance(obj, dict) else (None, False) | |
| except json.JSONDecodeError: | |
| return None, False | |
| class Ideogram4CaptionStep(ModularPipelineBlocks): | |
| model_name = "ideogram4_caption" | |
| def description(self) -> str: | |
| return ( | |
| "Turns an image OR a short text prompt into Ideogram 4's native structured-JSON caption using a " | |
| "vision-language model (Gemma-4). If `image` is given it is captioned; otherwise `prompt` is enhanced " | |
| "(magic-prompt) into a caption. Outputs `caption` (JSON string), `caption_json` (parsed dict, or None if " | |
| "unparseable), and `caption_raw` (raw decoded text)." | |
| ) | |
| def expected_components(self) -> list[ComponentSpec]: | |
| return [ | |
| ComponentSpec("caption_model", Gemma4ForConditionalGeneration, description="Vision-language captioner (Gemma-4)."), | |
| ComponentSpec("caption_processor", AutoProcessor, description="Processor paired with the captioner."), | |
| ] | |
| def inputs(self) -> list[InputParam]: | |
| return [ | |
| InputParam("image", description="PIL image to caption. If omitted, `prompt` is enhanced into a caption."), | |
| InputParam("prompt", type_hint=str, description="Short text idea to enhance into a JSON caption (used when no image is given)."), | |
| InputParam("instruction", default=DEFAULT_INSTRUCTION, description="Instruction for image-caption mode (ignored when enhancing a prompt)."), | |
| InputParam("height", type_hint=int, description="Target height; sets the aspect ratio hint for prompt enhancement."), | |
| InputParam("width", type_hint=int, description="Target width; sets the aspect ratio hint for prompt enhancement."), | |
| InputParam("max_new_tokens", type_hint=int, default=2048, description="Max tokens to generate (captions can be long)."), | |
| InputParam( | |
| "temperature", | |
| type_hint=float, | |
| default=0.0, | |
| description="Sampling temperature; 0 = greedy (deterministic).", | |
| ), | |
| ] | |
| def intermediate_outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam("caption", type_hint=str, description="Pretty-printed JSON caption (raw text if unparseable)."), | |
| OutputParam("caption_json", type_hint=dict, description="Parsed caption dict, or None if unparseable."), | |
| OutputParam("caption_raw", type_hint=str, description="Raw decoded model output."), | |
| ] | |
| def __call__(self, components, state: PipelineState): | |
| block_state = self.get_block_state(state) | |
| model = components.caption_model | |
| processor = components.caption_processor | |
| device = model.device # run on wherever the model physically is (offload-friendly) | |
| if block_state.image is not None: | |
| content = [ | |
| {"type": "image", "image": block_state.image}, | |
| {"type": "text", "text": block_state.instruction}, | |
| ] | |
| elif block_state.prompt: | |
| h = int(block_state.height or 1024) | |
| w = int(block_state.width or 1024) | |
| d = math.gcd(w, h) or 1 | |
| aspect_ratio = f"{w // d}:{h // d}" | |
| enhance_text = ( | |
| CAPTION_SYSTEM_MESSAGE | |
| + "\n\n" | |
| + CAPTION_USER_TEMPLATE.format(aspect_ratio=aspect_ratio, original_prompt=block_state.prompt) | |
| ) | |
| content = [{"type": "text", "text": enhance_text}] | |
| else: | |
| raise ValueError("Provide either `image` (to caption) or `prompt` (to enhance into a caption).") | |
| messages = [{"role": "user", "content": content}] | |
| inputs = processor.apply_chat_template( | |
| messages, | |
| add_generation_prompt=True, | |
| tokenize=True, | |
| return_dict=True, | |
| return_tensors="pt", | |
| enable_thinking=False, # Gemma-4 reasoning mode off for structured-JSON output | |
| ).to(device) | |
| do_sample = float(block_state.temperature) > 0 | |
| generated = model.generate( | |
| **inputs, | |
| max_new_tokens=int(block_state.max_new_tokens), | |
| do_sample=do_sample, | |
| temperature=float(block_state.temperature) if do_sample else None, | |
| ) | |
| input_len = inputs["input_ids"].shape[1] | |
| text = processor.decode(generated[0][input_len:], skip_special_tokens=True) | |
| data, _repaired = _parse_json(text) | |
| block_state.caption_json = data | |
| block_state.caption = json.dumps(data, indent=2, ensure_ascii=False) if data is not None else text | |
| block_state.caption_raw = text | |
| self.set_block_state(state, block_state) | |
| return components, state | |
| # auto_docstring | |
| class Ideogram4CaptionBlocks(SequentialPipelineBlocks): | |
| """ | |
| Standalone image/text -> Ideogram 4 structured-JSON caption pipeline (Gemma-4). Load with | |
| `ModularPipeline.from_pretrained(repo, trust_remote_code=True)` + | |
| `load_components(torch_dtype=torch.bfloat16)`, then either | |
| `pipe(image=img, output="caption")` (caption an image) or `pipe(prompt="a cat...", output="caption")` | |
| (enhance a text idea into a caption). | |
| """ | |
| model_name = "ideogram4_caption" | |
| block_classes = [Ideogram4CaptionStep] | |
| block_names = ["caption"] | |
| def description(self) -> str: | |
| return "Image or text -> Ideogram 4 structured-JSON caption (Gemma-4; captions an image, or enhances a prompt)." | |
| def outputs(self) -> list[OutputParam]: | |
| return [ | |
| OutputParam("caption", type_hint=str), | |
| OutputParam("caption_json", type_hint=dict), | |
| OutputParam("caption_raw", type_hint=str), | |
| ] | |