Files
MeteorAndyandClaude 6f2aa13575 Align with official API nodes: VIDEO/AUDIO output + real vendor formats
Rewrite each node to faithfully mirror the official comfy_api_nodes/
contract (apis/*.py + Comfy-Org/embedded-docs). The ComfyOrg proxy
forwards the body verbatim, so the official request/response models
ARE the real vendor API.

- Video nodes now return VIDEO (InputImpl.VideoFromFile), not file paths
- Audio nodes now return AUDIO ({waveform, sample_rate}), not file paths
- Request/response bodies match official pydantic models field-for-field
- Model ids, options, defaults, ranges match official + embedded docs
- Every node exposes api_key + base_url (overridable for relays/proxies)
- utils.py: add ComfyUI-native helpers — video_from_bytes,
  download_video_output, audio_from_bytes, download_audio_output,
  poll_until, validate_prompt, tensor_to_data_uri
- Rewrite nodes_minimax.py (submit -> poll -> retrieve -> download VIDEO)
- nodes_extra.py: base_url on all three, poll_until, validate_prompt
- Clean __init__.py (drop duplicate grok import/keys); 37 nodes total
- README: output-type alignment, node list, relay examples

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 20:07:04 +08:00

263 lines
8.4 KiB
Python

"""Ideogram image generation node (bring-your-own-key).
Faithfully mirrors the official ComfyUI ``IdeogramV3`` API node, but calls the
Ideogram API DIRECTLY (host ``api.ideogram.ai``, ``Api-Key`` header) instead of
going through the ComfyOrg proxy. The official node POSTs to
``/proxy/ideogram/ideogram-v3/generate`` which the proxy forwards verbatim to
``https://api.ideogram.ai/v1/ideogram-v3/generate`` as ``multipart/form-data``;
the request/response shapes below are taken from the official request model
(``comfy_api_nodes/apis/ideogram.py`` :: ``IdeogramV3Request`` /
``IdeogramGenerateResponse``) and the official node source.
Output: IMAGE (torch tensor [N, H, W, C], RGB, float32 0-1).
"""
import torch
from .utils import (
bytes_to_tensor,
download_bytes,
http_request,
run_async,
validate_prompt,
)
IDEOGRAM_API_BASE = "https://api.ideogram.ai"
# Maps the human-facing "W:H" labels (official combo options) to the WxH string
# the V3 API expects in the `aspect_ratio` form field.
V3_RATIO_MAP = {
"1:1": "1x1",
"1:3": "1x3",
"3:1": "3x1",
"1:2": "1x2",
"2:1": "2x1",
"9:16": "9x16",
"16:9": "16x9",
"10:16": "10x16",
"16:10": "16x10",
"2:3": "2x3",
"3:2": "3x2",
"3:4": "3x4",
"4:3": "4x3",
"4:5": "4x5",
"5:4": "5x4",
}
# Official IdeogramV3 combo options (order preserved from the node / docs).
V3_ASPECT_RATIOS = list(V3_RATIO_MAP.keys())
V3_RESOLUTIONS = [
"Auto",
"512x1536",
"576x1408",
"576x1472",
"576x1536",
"640x1344",
"640x1408",
"640x1472",
"640x1536",
"704x1152",
"704x1216",
"704x1280",
"704x1344",
"704x1408",
"704x1472",
"736x1312",
"768x1088",
"768x1216",
"768x1280",
"768x1344",
"800x1280",
"832x960",
"832x1024",
"832x1088",
"832x1152",
"832x1216",
"832x1248",
"864x1152",
"896x960",
"896x1024",
"896x1088",
"896x1120",
"896x1152",
"960x832",
"960x896",
"960x1024",
"960x1088",
"1024x832",
"1024x896",
"1024x960",
"1024x1024",
"1088x768",
"1088x832",
"1088x896",
"1088x960",
"1120x896",
"1152x704",
"1152x832",
"1152x864",
"1152x896",
"1216x704",
"1216x768",
"1216x832",
"1248x832",
"1280x704",
"1280x768",
"1280x800",
"1312x736",
"1344x640",
"1344x704",
"1344x768",
"1408x576",
"1408x640",
"1408x704",
"1472x576",
"1472x640",
"1472x704",
"1536x512",
"1536x576",
"1536x640",
]
# V3 magic prompt / style / speed enums (from IdeogramV3Request + official node).
V3_MAGIC_PROMPT_OPTIONS = ["AUTO", "ON", "OFF"]
V3_STYLE_TYPES = ["AUTO", "GENERAL", "REALISTIC", "DESIGN", "FICTION"]
V3_RENDERING_SPEEDS = ["DEFAULT", "TURBO", "QUALITY"]
class BYOKey_Ideogram:
"""Ideogram V3 image generation (bring-your-own-key). Output: IMAGE."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING", {"default": "", "tooltip": "Your Ideogram API key (sent as the 'Api-Key' header)."}),
"base_url": ("STRING", {"default": IDEOGRAM_API_BASE, "tooltip": "Ideogram API host. Override for a relay/proxy."}),
"prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Prompt for the image generation."}),
},
"optional": {
"aspect_ratio": (
V3_ASPECT_RATIOS,
{"default": "1:1", "tooltip": "The aspect ratio for image generation. Ignored if resolution is not set to Auto."},
),
"resolution": (
V3_RESOLUTIONS,
{"default": "Auto", "tooltip": "The resolution for image generation. If not set to Auto, this overrides the aspect_ratio setting."},
),
"magic_prompt_option": (
V3_MAGIC_PROMPT_OPTIONS,
{"default": "AUTO", "tooltip": "Determine if MagicPrompt should be used in generation."},
),
"seed": ("INT", {"default": 0, "min": 0, "max": 2147483647, "step": 1, "tooltip": "Random seed for generation (0 = unset)."}),
"num_images": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1, "tooltip": "Number of images to generate (1-8)."}),
"rendering_speed": (
V3_RENDERING_SPEEDS,
{"default": "DEFAULT", "tooltip": "Controls the trade-off between generation speed and quality."},
),
"style_type": (
V3_STYLE_TYPES,
{"default": "AUTO", "tooltip": "The type of style to apply (V3)."},
),
"negative_prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Text specifying what to avoid in the generation."}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output",)
FUNCTION = "execute"
CATEGORY = "api/byokey/image"
DESCRIPTION = "Generates images using the Ideogram V3 model (direct API, bring your own key)."
@staticmethod
def _build_url(base_url: str, path: str) -> str:
base = (base_url or IDEOGRAM_API_BASE).rstrip("/")
# The real Ideogram endpoint is /v1/ideogram-v3/generate. The official
# node hits /proxy/ideogram/ideogram-v3/generate (proxy strips the /v1).
# Support both bare hosts and relays that already include a suffix.
if base.endswith("/ideogram-v3/generate"):
return base
if base.endswith("/v1"):
return f"{base}/{path}"
if "api.ideogram.ai" in base and "/v1" not in base:
return f"{base}/v1/{path}"
return f"{base}/{path}"
def execute(
self,
api_key,
base_url=IDEOGRAM_API_BASE,
prompt="",
aspect_ratio="1:1",
resolution="Auto",
magic_prompt_option="AUTO",
seed=0,
num_images=1,
rendering_speed="DEFAULT",
style_type="AUTO",
negative_prompt="",
):
prompt = validate_prompt(prompt, min_length=1)
if rendering_speed == "BALANCED": # backward-compat with older official option
rendering_speed = "DEFAULT"
# Build the multipart form. Ideogram's v3/generate endpoint expects
# multipart/form-data; scalar fields are sent as plain string parts.
# Mirrors IdeogramV3Request field-for-field (names + value formats).
form: dict[str, str] = {
"prompt": prompt,
"rendering_speed": rendering_speed,
}
# Resolution overrides aspect_ratio (matches official node logic).
if resolution != "Auto":
form["resolution"] = resolution
elif aspect_ratio != "1:1":
v3_aspect = V3_RATIO_MAP.get(aspect_ratio)
if v3_aspect:
form["aspect_ratio"] = v3_aspect
if magic_prompt_option != "AUTO":
form["magic_prompt"] = magic_prompt_option
if style_type and style_type != "AUTO":
form["style_type"] = style_type
if negative_prompt:
form["negative_prompt"] = negative_prompt
if seed != 0:
form["seed"] = str(seed)
if num_images > 1:
form["num_images"] = str(num_images)
headers = {"Api-Key": api_key}
url = self._build_url(base_url, "ideogram-v3/generate")
resp = run_async(self._generate(url, headers, form))
data = resp.get("data") or []
if not data:
raise RuntimeError(f"No images were generated in the Ideogram response: {resp}")
image_urls = [item.get("url") for item in data if item.get("url")]
if not image_urls:
raise RuntimeError("No image URLs were generated in the Ideogram response")
return (run_async(self._download_images(image_urls)),)
@staticmethod
async def _generate(url: str, headers: dict, form: dict) -> dict:
import aiohttp
data = aiohttp.FormData()
for key, value in form.items():
data.add_field(key, value)
return await http_request("POST", url, headers, data=data)
@staticmethod
async def _download_images(image_urls: list) -> torch.Tensor:
tensors = []
for image_url in image_urls:
raw = await download_bytes(image_url)
tensors.append(bytes_to_tensor(raw, mode="RGB"))
return torch.cat(tensors, dim=0)