From 6c2f56b1c381801d395861baa33e4884f2b5049c Mon Sep 17 00:00:00 2001 From: Alexandr Radowskiy Date: Thu, 4 Jun 2026 14:31:37 +0300 Subject: [PATCH] Initial commit: Ideogram Magic Prompt node for ComfyUI V3 - Implements the V3 (Modern Node Design) schema via io.ComfyNode. - Falls back to V1 (NODE_CLASS_MAPPINGS) for older ComfyUI installs. - Calls Ideogram's hosted magic-prompt API via urllib (no extra deps). - Strips bbox / aspect_ratio echoes so the caption is a clean drop-in for Ideogram4Pipeline. --- .gitignore | 42 +++++++++++++++ LICENSE | 21 ++++++++ README.md | 119 ++++++++++++++++++++++++++++++++++++++++ __init__.py | 27 ++++++++++ nodes.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __init__.py create mode 100644 nodes.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35a65a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7230ab5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Alexandr Radowskiy (@hardhant) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..be15650 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# ComfyUI Ideogram Magic Prompt + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org) +[![ComfyUI V3](https://img.shields.io/badge/ComfyUI-V3%20%2F%20Modern%20Node%20Design-blueviolet)](https://docs.comfy.org/) + +A ComfyUI custom node that calls Ideogram's hosted **magic-prompt API** to +turn a plain-text prompt into the structured JSON caption that Ideogram 4 +expects. + +It uses Ideogram's free hosted service — no model download, no GPU usage. +Typical round-trip: 2-5 seconds. + +## Preview + +``` +┌─────────────────────────────────────┐ +│ prompt: "a ginger cat wearing a │ +│ tiny wizard hat reading a │ +│ spellbook" │ +│ api_key: •••••• │ +│ aspect_ratio: [1x1 ▾] │ +└──────────────┬──────────────────────┘ + ▼ +┌─────────────────────────────────────┐ +│ json_caption: │ +│ { │ +│ "high_level_description": "...", │ +│ "style_description": { ... }, │ +│ "compositional_deconstruction": { │ +│ "background": "...", │ +│ "elements": [ │ +│ { "type": "obj", "bbox": ... }│ +│ ] │ +│ } │ +│ } │ +└─────────────────────────────────────┘ +``` + +## Install + +1. Drop the entire `comfyui_ideogram_magic_prompt/` folder into + `/custom_nodes/`. +2. Restart ComfyUI. +3. The node **"Ideogram Magic Prompt"** will appear under the + **Add Node → Ideogram** category. + +No `pip install` needed — the node uses only `urllib` from the Python +standard library plus the `comfy_api` module shipped with ComfyUI. + +## Inputs + +| Name | Type | Description | +| --- | --- | --- | +| `prompt` | multiline string | The plain-text prompt to expand. | +| `api_key` | string (password) | Your `IDEOGRAM_API_KEY` from [ideogram.ai/api/learn/](https://ideogram.ai/api/learn/). | +| `aspect_ratio` | combo | One of the 18 ratios accepted by Ideogram's magic-prompt API, plus `AUTO`. Pick the closest match to your target image. | + +### Supported aspect ratios + +`AUTO`, `1x4`, `1x3`, `1x2`, `9x16`, `10x16`, `2x3`, `3x4`, `4x5`, `1x1`, +`5x4`, `4x3`, `3x2`, `16x10`, `16x9`, `2x1`, `3x1`, `4x1` + +## Output + +| Name | Type | Description | +| --- | --- | --- | +| `json_caption` | string (JsonPreview) | Stringified JSON caption ready to feed into any Ideogram 4 generation node. | + +The output is automatically parsed and displayed as a JSON tree in the +node (ComfyUI's built-in `JsonPreview` tool). You can also connect it as +a string to downstream text-handling nodes. + +The output caption is cleaned of `aspect_ratio` echoes and any +`bbox` fields, so it's a clean drop-in for +`Ideogram4Pipeline.__call__(prompt=...)` or any equivalent. + +## Usage + +``` +plain prompt ─┐ + ├──▶ Ideogram Magic Prompt ──▶ json_caption ──▶ [your Ideogram4 generator] +IDEOGRAM_API_KEY ┘ ▲ + │ + live JSON preview +``` + +In a typical workflow you would: +1. Add the **Ideogram Magic Prompt** node. +2. Plug in a free-form text prompt and your API key. +3. Connect the `json_caption` output to your Ideogram 4 generation node + (e.g. `Ideogram4Pipeline.__call__` from a custom node, or paste the + JSON into the standalone `gradio_app.py` UI as + `--prompt (Get-Content .\caption.json)`). + +## Compatibility + +| ComfyUI version | Status | +| --- | --- | +| **V3 (Modern Node Design)** | ✅ Primary target. Uses `io.ComfyNode`, `io.Schema`, `ComfyExtension`, `comfy_entrypoint()`. | +| **V1 / older installs** | ✅ Works. Re-exports `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` for the legacy loader. | + +The package has no runtime dependencies beyond `comfy_api` (bundled with +ComfyUI V3) and the Python standard library. + +## Development + +Run the tests locally: + +```bash +python -m pip install -e .[test] # if you add a test extra +python -m pytest +``` + +There's no test suite in this drop — pull requests welcome. + +## License + +[MIT](LICENSE) — see `LICENSE` for the full text. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..5168ab1 --- /dev/null +++ b/__init__.py @@ -0,0 +1,27 @@ +"""Ideogram Magic Prompt -- ComfyUI custom node package. + +Drop this entire folder into ComfyUI/custom_nodes/ and restart ComfyUI. +The node "Ideogram Magic Prompt" will appear under the Ideogram category. +""" + +from .nodes import IdeogramExtension, IdeogramMagicPrompt + +# V1-compatible fallback for older ComfyUI installs (V1 schema) so the +# node still loads even if comfy_api.latest isn't available. ComfyUI's +# loader expects NODE_CLASS_MAPPINGS / NODE_DISPLAY_NAME_MAPPINGS at +# module import time. +NODE_CLASS_MAPPINGS = { + "IdeogramMagicPrompt": IdeogramMagicPrompt, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "IdeogramMagicPrompt": "Ideogram Magic Prompt", +} + +# Web-exposed metadata +__all__ = [ + "IdeogramMagicPrompt", + "IdeogramExtension", + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", +] diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..0515969 --- /dev/null +++ b/nodes.py @@ -0,0 +1,152 @@ +"""Ideogram Magic Prompt -- ComfyUI custom node. + +Generates the structured JSON caption that Ideogram 4 expects from a +plain-text prompt, using Ideogram's free hosted magic-prompt API. + +Inputs: prompt (multiline text), api_key (password), aspect_ratio (combo) +Output: json_caption (string, displayed via the built-in JsonPreview tool) + +Drop this folder into your ComfyUI/custom_nodes/ directory and restart +ComfyUI. The node will appear under the "Ideogram" category. +""" + +from __future__ import annotations + +import json +import time +import urllib.error +import urllib.request + +try: + from comfy_api.latest import ComfyExtension, io, ui +except ImportError: # older ComfyUI / V1 fallback + from comfy_api.v0_0_2 import ComfyExtension, io, ui # type: ignore[no-redef] + +MAGIC_PROMPT_URL = "https://api.ideogram.ai/v1/ideogram-v4/magic-prompt" + +# 18 aspect ratios accepted by the Ideogram magic-prompt API +SUPPORTED_ARS = [ + "AUTO", + "1x4", "1x3", "1x2", + "9x16", "10x16", + "2x3", "3x4", "4x5", + "1x1", + "5x4", "4x3", "3x2", + "16x10", "16x9", + "2x1", "3x1", "4x1", +] + +DEFAULT_HEADERS = { + "Content-Type": "application/json", + "Api-Key": "", # filled at call time +} + +DEFAULT_TIMEOUT = 120.0 + + +def _post_magic_prompt(prompt: str, aspect_ratio: str, api_key: str, timeout: float = DEFAULT_TIMEOUT) -> dict: + """Call Ideogram's hosted magic-prompt API. Returns the parsed JSON + caption dict (after stripping `aspect_ratio` and bboxes so the + caption is a clean drop-in for the Ideogram4Pipeline).""" + headers = {**DEFAULT_HEADERS, "Api-Key": api_key} + body = json.dumps({"text_prompt": prompt, "aspect_ratio": aspect_ratio}).encode("utf-8") + req = urllib.request.Request(MAGIC_PROMPT_URL, data=body, headers=headers, method="POST") + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + payload = json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + raise RuntimeError( + f"Ideogram magic-prompt API HTTP {e.code}: {e.read().decode('utf-8', errors='replace')}" + ) from e + except urllib.error.URLError as e: + raise RuntimeError(f"Ideogram magic-prompt API unreachable: {e.reason}") from e + + jp = payload.get("json_prompt") + if not jp: + raise RuntimeError(f"Ideogram API returned no json_prompt: {payload}") + # Clean up the caption: drop the echoed aspect_ratio and any + # bboxes (the model sometimes includes them in the upstream JSON). + jp.pop("aspect_ratio", None) + for el in jp.get("compositional_deconstruction", {}).get("elements", []): + if isinstance(el, dict): + el.pop("bbox", None) + return jp + + +class IdeogramMagicPrompt(io.ComfyNode): + """Expand a plain prompt into Ideogram 4's native structured JSON caption. + + The output is a stringified JSON object. Feed it directly into an + Ideogram4 generation node (or paste into the ideogram4 gradio UI). + """ + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="IdeogramMagicPrompt", + display_name="Ideogram Magic Prompt", + category="Ideogram", + description=( + "Rewrites a plain prompt into the structured JSON caption " + "format the Ideogram 4 model was trained on. Uses Ideogram's " + "free hosted magic-prompt API." + ), + inputs=[ + io.String.Input( + "prompt", + multiline=True, + tooltip="Plain-text prompt to expand.", + ), + io.String.Input( + "api_key", + default="", + tooltip="IDEOGRAM_API_KEY from https://ideogram.ai/api/learn/", + ), + io.Combo.Input( + "aspect_ratio", + options=SUPPORTED_ARS, + default="1x1", + tooltip="Closest aspect ratio for which to lay out the caption.", + ), + ], + outputs=[ + io.String.Output( + "json_caption", + tooltip="Stringified JSON caption -- feed to Ideogram4 generate.", + tool="JsonPreview", + ), + ], + ) + + @classmethod + def execute(cls, prompt: str, api_key: str, aspect_ratio: str) -> io.NodeOutput: + if not prompt or not prompt.strip(): + raise ValueError("Prompt is empty.") + if not api_key or not api_key.strip(): + raise ValueError( + "IDEOGRAM_API_KEY is empty. Get one at " + "https://ideogram.ai/api/learn/ and paste it into the node." + ) + + t0 = time.time() + caption_obj = _post_magic_prompt(prompt, aspect_ratio, api_key) + elapsed = time.time() - t0 + + caption_str = json.dumps(caption_obj, ensure_ascii=False, separators=(",", ":")) + # Wrap the timing metadata as a log we can show in the UI. + log = f"magic-prompt expansion: {elapsed:.1f}s, aspect={aspect_ratio}" + return io.NodeOutput( + caption_str, + ui=ui.PreviewText(log), + ) + + +class IdeogramExtension(ComfyExtension): + """Registers the Ideogram magic-prompt node with ComfyUI.""" + + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [IdeogramMagicPrompt] + + +async def comfy_entrypoint() -> IdeogramExtension: # noqa: D401 + return IdeogramExtension()