217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
"""
|
||
Runware Image Inference Settings Structured Prompt
|
||
Builds settings.structuredPrompt for Ideogram 4.0 and compatible models.
|
||
Connect the output to Runware Image Inference Settings → structuredPrompt.
|
||
Mutually exclusive with positivePrompt at the API level.
|
||
"""
|
||
|
||
import json
|
||
from typing import Any, Dict, List, Tuple
|
||
|
||
|
||
class RunwareImageInferenceSettingsStructuredPrompt:
|
||
"""Ideogram-style structured JSON prompt for image inference settings."""
|
||
|
||
_ELEMENT_SLOTS = 8
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
optional: Dict[str, tuple] = {
|
||
"background": ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "compositional_deconstruction.background — setting/surface/atmosphere, not discrete subjects.",
|
||
}),
|
||
"useStyleDescription": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "Enabled",
|
||
"label_off": "Disabled",
|
||
"tooltip": "Include style_description from JSON below.",
|
||
}),
|
||
"style_description": ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "style_description object as JSON (aesthetics, lighting, photo OR art_style, medium, color_palette). Only when 'Use Style Description' is enabled.",
|
||
}),
|
||
"useElementsJson": ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "Enabled",
|
||
"label_off": "Disabled",
|
||
"tooltip": "Set elements from a JSON array instead of element slots 1–8.",
|
||
}),
|
||
"elementsJson": ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "JSON array of element objects ({type, desc, text?, bbox?, color_palette?}). Only when 'Use Elements JSON' is enabled.",
|
||
}),
|
||
}
|
||
|
||
for i in range(1, cls._ELEMENT_SLOTS + 1):
|
||
optional[f"use_element_{i}"] = ("BOOLEAN", {
|
||
"default": False,
|
||
"label_on": "Enabled",
|
||
"label_off": "Disabled",
|
||
"tooltip": f"Include element slot {i} in compositional_deconstruction.elements.",
|
||
})
|
||
optional[f"element_type_{i}"] = (["obj", "text"], {
|
||
"default": "obj",
|
||
"tooltip": f"Element {i} type: obj (desc interpreted) or text (text rendered literally).",
|
||
})
|
||
optional[f"element_desc_{i}"] = ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": f"Element {i} desc — natural language treatment/position notes.",
|
||
})
|
||
optional[f"element_text_{i}"] = ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": f"Element {i} text — literal copy for type text only.",
|
||
})
|
||
optional[f"element_bbox_{i}"] = ("STRING", {
|
||
"default": "",
|
||
"tooltip": f"Element {i} optional bbox JSON: [y_min, x_min, y_max, x_max] in 0–1000 coords.",
|
||
})
|
||
optional[f"element_color_palette_{i}"] = ("STRING", {
|
||
"default": "",
|
||
"tooltip": f"Element {i} optional hex colours, comma-separated (e.g. #1F3B26,#7A5A2F).",
|
||
})
|
||
|
||
required = {
|
||
"high_level_description": ("STRING", {
|
||
"multiline": True,
|
||
"default": "",
|
||
"tooltip": "Global scene summary (required).",
|
||
}),
|
||
}
|
||
return {"required": required, "optional": optional}
|
||
|
||
RETURN_TYPES = ("RUNWAREIMAGEINFERENCESTRUCTUREDPROMPT",)
|
||
RETURN_NAMES = ("structuredPrompt",)
|
||
FUNCTION = "build_structured_prompt"
|
||
CATEGORY = "Runware"
|
||
DESCRIPTION = (
|
||
"Build settings.structuredPrompt for Ideogram 4.0 (snake_case schema) from "
|
||
"high_level_description, background, style_description, and elements. "
|
||
"Connect to Runware Image Inference Settings. Do not send positivePrompt in the same request."
|
||
)
|
||
|
||
@staticmethod
|
||
def _parse_json_object(raw: str, field_name: str) -> Dict[str, Any]:
|
||
text = (raw or "").strip()
|
||
if not text:
|
||
raise ValueError(f"{field_name} is empty.")
|
||
try:
|
||
parsed = json.loads(text)
|
||
except json.JSONDecodeError as e:
|
||
raise ValueError(f"{field_name} must be valid JSON: {e}") from e
|
||
if not isinstance(parsed, dict):
|
||
raise ValueError(f"{field_name} must be a JSON object.")
|
||
return parsed
|
||
|
||
@staticmethod
|
||
def _parse_json_array(raw: str, field_name: str) -> List[Any]:
|
||
text = (raw or "").strip()
|
||
if not text:
|
||
raise ValueError(f"{field_name} is empty.")
|
||
try:
|
||
parsed = json.loads(text)
|
||
except json.JSONDecodeError as e:
|
||
raise ValueError(f"{field_name} must be valid JSON: {e}") from e
|
||
if not isinstance(parsed, list):
|
||
raise ValueError(f"{field_name} must be a JSON array.")
|
||
return parsed
|
||
|
||
@classmethod
|
||
def _build_element_from_slot(cls, kwargs: Dict[str, Any], index: int) -> Dict[str, Any]:
|
||
el_type = str(kwargs.get(f"element_type_{index}", "obj")).strip() or "obj"
|
||
if el_type not in ("obj", "text"):
|
||
raise ValueError(f"element_type_{index} must be obj or text.")
|
||
|
||
element: Dict[str, Any] = {"type": el_type}
|
||
|
||
desc = str(kwargs.get(f"element_desc_{index}") or "").strip()
|
||
if desc:
|
||
element["desc"] = desc
|
||
|
||
if el_type == "text":
|
||
text = str(kwargs.get(f"element_text_{index}") or "").strip()
|
||
if text:
|
||
element["text"] = text
|
||
|
||
bbox_raw = str(kwargs.get(f"element_bbox_{index}") or "").strip()
|
||
if bbox_raw:
|
||
try:
|
||
bbox = json.loads(bbox_raw)
|
||
except json.JSONDecodeError as e:
|
||
raise ValueError(f"element_bbox_{index} must be valid JSON: {e}") from e
|
||
if not isinstance(bbox, list):
|
||
raise ValueError(f"element_bbox_{index} must be a JSON array.")
|
||
element["bbox"] = bbox
|
||
|
||
palette_raw = str(kwargs.get(f"element_color_palette_{index}") or "").strip()
|
||
if palette_raw:
|
||
palette = [h.strip() for h in palette_raw.split(",") if h.strip()]
|
||
if palette:
|
||
element["color_palette"] = palette
|
||
|
||
if el_type == "text" and "text" not in element and not desc:
|
||
raise ValueError(
|
||
f"Element slot {index}: type text requires element_text_{index} or element_desc_{index}."
|
||
)
|
||
if el_type == "obj" and not desc:
|
||
raise ValueError(f"Element slot {index}: type obj requires element_desc_{index}.")
|
||
|
||
return element
|
||
|
||
@classmethod
|
||
def _collect_elements(cls, kwargs: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
if kwargs.get("useElementsJson", False):
|
||
return cls._parse_json_array(
|
||
kwargs.get("elementsJson", ""),
|
||
"elementsJson",
|
||
)
|
||
|
||
elements: List[Dict[str, Any]] = []
|
||
for i in range(1, cls._ELEMENT_SLOTS + 1):
|
||
if not kwargs.get(f"use_element_{i}", False):
|
||
continue
|
||
elements.append(cls._build_element_from_slot(kwargs, i))
|
||
return elements
|
||
|
||
def build_structured_prompt(self, **kwargs) -> Tuple[Dict[str, Any]]:
|
||
hld = str(kwargs.get("high_level_description") or "").strip()
|
||
if not hld:
|
||
raise ValueError("high_level_description is required.")
|
||
|
||
structured: Dict[str, Any] = {"high_level_description": hld}
|
||
|
||
if kwargs.get("useStyleDescription", False):
|
||
style_raw = kwargs.get("style_description", "")
|
||
structured["style_description"] = self._parse_json_object(
|
||
style_raw,
|
||
"style_description",
|
||
)
|
||
|
||
comp: Dict[str, Any] = {}
|
||
background = str(kwargs.get("background") or "").strip()
|
||
if background:
|
||
comp["background"] = background
|
||
|
||
elements = self._collect_elements(kwargs)
|
||
if elements:
|
||
comp["elements"] = elements
|
||
|
||
if comp:
|
||
structured["compositional_deconstruction"] = comp
|
||
|
||
return (structured,)
|
||
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"RunwareImageInferenceSettingsStructuredPrompt": RunwareImageInferenceSettingsStructuredPrompt,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"RunwareImageInferenceSettingsStructuredPrompt": "Runware Image Inference Settings Structured Prompt",
|
||
}
|