Files
EricRollei-Local_LLM_Prompt…/ideogram_magic_node.py
T
Eric HissandCopilot 373ea8bda3 v2.2.0: Prompt Builder nodes, wildcard review tool, docs refresh
- Add Wildcard Prompt Composer and Wildcard Character Composer builder nodes (core/builder, web/prompt_builder.js, builders/, vp wildcard lists)

- Add browser-based wildcard review tool (tools/wildcard_review) with manifest-based undo

- Add wildcard catalog/curation, surreal compiler/lint/scene, profile token budget

- README: document all 18 nodes, bump to v2.2.0, fix stale node/platform lists; CHANGELOG 2.2.0

- Rewrite NODE_COMPARISON for 18 nodes; fix QUICKSTART (dead test_setup.py ref, menu path) and QUICK_REFERENCE node list

- gitignore review manifests and CLAUDE.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 20:36:44 -07:00

169 lines
8.9 KiB
Python

"""
Ideogram 4 Magic Prompt node.
Expands a plain-language idea (optionally guided by a reference image) into an
Ideogram 4 structured caption JSON, using a local LLM via LLMBackend and
Ideogram's official open-source magic-prompt system prompt.
Outputs:
prompt_json : structured caption (minified JSON) - wire to an Ideogram sampler,
or into the Ideogram 4 Composer's JSON import to refine on the canvas.
high_level : the high_level_description text (e.g. to feed the Composer's input).
width/height: pixel dims from the committed aspect ratio x megapixels.
report : status + any guideline warnings.
Author: Eric Hiss (GitHub: EricRollei). System prompt: Apache-2.0, ideogram-oss.
"""
import io
import numpy as np
from . import ideogram_magic_core as core
from .llm_backend import LLMBackend
from .wildcard_support import (
is_changed_token,
llm_block,
normalise_seed,
report_lines,
resolve_wildcards,
filter_notes_of,
lead_with_filter_notes,
screen_output,
wildcard_nsfw_input,
wildcard_optional_inputs,
)
REF_INSTRUCTIONS = {
"full": "Describe this reference image in detail: subjects, setting, composition, colors, lighting, and mood.",
"style_only": "Describe ONLY the visual style, medium, lighting, and mood of this image - not the specific subjects or scene content.",
"palette_only": "List the dominant colors of this image as a short palette (color names and approximate #RRGGBB hex), plus the overall color mood. Ignore subjects and composition.",
"subject_only": "Describe ONLY the main subject(s) in this image - appearance, clothing, pose, distinguishing features - not the background, style, or lighting.",
}
# Application labels (keep in sync with web/ideogram_magic.js templates). Picking one
# prefills the idea box (UI) and nudges the expander toward that format.
APPLICATIONS = [
"(custom)", "Portrait (photo)", "Product (photo)", "Lifestyle flat-lay (photo)",
"Fashion editorial (photo)", "Logo", "Event poster", "Vintage travel poster",
"Movie one-sheet", "T-shirt design", "Packaging / label", "Instagram post",
"YouTube thumbnail", "Book cover", "Album cover", "Editorial illustration",
"Comic / manga panel", "Infographic", "Business card", "Sticker",
"Sign / storefront", "Greeting card", "Menu",
]
class IdeogramMagicPrompt:
CATEGORY = "Eric Prompt Enhancers/Ideogram"
FUNCTION = "run"
RETURN_TYPES = ("STRING", "STRING", "INT", "INT", "STRING")
RETURN_NAMES = ("prompt_json", "high_level", "width", "height", "report")
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"idea": ("STRING", {"multiline": True, "default": "",
"tooltip": "Plain-language idea to expand into an Ideogram 4 caption."}),
"application": (APPLICATIONS, {"default": "(custom)",
"tooltip": "Pick a use case to prefill the idea box with a starter template and steer the expander toward that format."}),
"backend": (["lm_studio", "ollama", "qwen3_vl"], {"default": "lm_studio"}),
"endpoint": ("STRING", {"default": "http://localhost:1234/v1",
"tooltip": "LM Studio / Ollama base URL. For qwen3_vl, optionally a local model path."}),
"model": ("STRING", {"default": "", "tooltip": "Blank = auto-detect the loaded model."}),
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.05}),
"aspect_ratio": ("STRING", {"default": "auto",
"tooltip": "W:H (e.g. 16:9) or 'auto' to let the model pick."}),
"megapixels": ("FLOAT", {"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.25,
"tooltip": "Total output resolution; combined with the aspect ratio to set width/height."}),
"keep_bboxes": ("BOOLEAN", {"default": True,
"tooltip": "Keep element bounding boxes (for the Composer canvas). Off = let Ideogram place freely."}),
"use_tailoring": ("BOOLEAN", {"default": True,
"tooltip": "Apply Eric's documented tailoring addendum on top of the official prompt. See IDEOGRAM_NOTES.md."}),
"max_tokens": ("INT", {"default": 16384, "min": 1024, "max": 32768}),
},
"optional": {
"reference_image": ("IMAGE",),
"reference_influence": (["full", "style_only", "palette_only", "subject_only"],
{"default": "full",
"tooltip": "How a connected reference image guides the caption (needs a vision-capable backend)."}),
# New inputs are only ever APPENDED below this line, so saved workflows keep their widget order.
**wildcard_optional_inputs(),
**wildcard_nsfw_input(),
},
}
@classmethod
def IS_CHANGED(cls, wildcard_seed=-1, **kwargs):
"""wildcard_seed = -1 means a fresh roll each run, which must defeat ComfyUI's cache."""
return is_changed_token(wildcard_seed)
def run(self, idea, backend, endpoint, model, temperature, aspect_ratio,
megapixels, keep_bboxes, use_tailoring, max_tokens,
application="(custom)", reference_image=None, reference_influence="full",
wildcard_seed=-1, wildcard_llm_block=True, wildcard_instructions="", nsfw="off",
violence_filter="off"):
# Resolve wildcards in the idea (core/wildcards). The parser leaves JSON braces alone.
wildcard_resolution = resolve_wildcards(idea or "", seed=normalise_seed(wildcard_seed), legacy=True,
upstream_instructions=wildcard_instructions, nsfw=nsfw,
violence_filter=violence_filter)
idea = wildcard_resolution.text
if not (idea or "").strip() and reference_image is None:
return ("", "", 0, 0, "Empty idea - nothing to expand.")
ref_caption = ""
if reference_image is not None:
ref_caption = self._caption_reference(
reference_image, reference_influence, backend, endpoint, model, temperature)
r = core.expand(
idea or "", backend=backend, endpoint=endpoint, model=model,
temperature=temperature, aspect_ratio=aspect_ratio, megapixels=megapixels,
keep_bboxes=keep_bboxes, use_tailoring=use_tailoring, max_tokens=max_tokens,
reference_caption=ref_caption, application=application,
extra_system=llm_block(wildcard_resolution, wildcard_llm_block),
)
if not r["ok"]:
report = f"\u26a0 {r['error']}"
print(f"[Ideogram Magic] {report}")
return (r.get("prompt_json", ""), "", 0, 0, report)
warns = r.get("warnings", [])
report = ("\u2713 caption generated" if not warns else "\u26a0 " + "; ".join(warns))
report += f" [{r.get('aspect')}, {r['width']}\u00d7{r['height']}]"
wildcard_report = report_lines(wildcard_resolution)
if wildcard_report:
report = f"{report}\n{wildcard_report}"
print(f"[Ideogram Magic] {report}")
caption, caption_note = screen_output(r["prompt_json"], "Ideogram Magic caption")
summary, summary_note = screen_output(r["high_level"], "Ideogram Magic summary")
report = lead_with_filter_notes(report, filter_notes_of(wildcard_resolution, caption_note, summary_note))
if caption_note or summary_note:
return ("", "", r["width"], r["height"], report)
return (caption, summary, r["width"], r["height"], report)
def _caption_reference(self, image, influence, backend, endpoint, model, temperature):
try:
from PIL import Image
arr = image[0].detach().cpu().numpy()
arr = (np.clip(arr, 0, 1) * 255).astype(np.uint8)
if arr.ndim == 2:
arr = np.stack([arr] * 3, -1)
buf = io.BytesIO()
Image.fromarray(arr[..., :3]).save(buf, format="PNG")
be = LLMBackend(backend, endpoint, model or None, temperature)
if not be.supports_images():
print("[Ideogram Magic] Reference image ignored: backend/model is not vision-capable.")
return ""
res = be.caption_image(buf.getvalue(), label="reference",
prompt=REF_INSTRUCTIONS.get(influence, REF_INSTRUCTIONS["full"]))
if res.get("success"):
return res.get("caption", "") or ""
print(f"[Ideogram Magic] Reference caption failed: {res.get('error')}")
return ""
except Exception as e:
print(f"[Ideogram Magic] Reference caption error: {e}")
return ""