Buckets:
| import math | |
| import re | |
| from io import BytesIO | |
| from typing_extensions import override | |
| from comfy.utils import common_upscale | |
| from comfy_api.latest import IO, ComfyExtension | |
| from PIL import Image | |
| import numpy as np | |
| import torch | |
| from comfy_api_nodes.apis.ideogram import ( | |
| Ideogram45Request, | |
| IdeogramGenerateResponse, | |
| IdeogramPImageRequest, | |
| IdeogramV3Request, | |
| IdeogramV3EditRequest, | |
| IdeogramV4Request, | |
| ) | |
| from comfy_api_nodes.util import ( | |
| ApiEndpoint, | |
| bytesio_to_image_tensor, | |
| download_url_as_bytesio, | |
| download_url_to_image_tensor, | |
| resize_mask_to_image, | |
| sync_op, | |
| tensor_to_bytesio, | |
| validate_string, | |
| ) | |
| V3_RATIO_MAP = { | |
| "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", | |
| "1:1":"1x1", | |
| } | |
| 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" | |
| ] | |
| IDEOGRAM_45_GENERATE_PATH = "/proxy/ideogram/v2/image/generate/ideogram-4-5" | |
| IDEOGRAM_45_PRECISE_EDIT_PATH = "/proxy/ideogram/v2/image/precise-edit/ideogram-4-5" | |
| IDEOGRAM_45_MODELS = ["ideogram-4.5"] | |
| IDEOGRAM_45_MAX_IMAGES = 5 | |
| IDEOGRAM_45_MAX_PIXELS = 4194304 | |
| IDEOGRAM_45_MAX_SIDE = 4608 | |
| IDEOGRAM_45_SIZES = [ | |
| "(2K) 2048x2048 (1:1)", | |
| "(2K) 1440x2880 (1:2)", | |
| "(2K) 2880x1440 (2:1)", | |
| "(2K) 1664x2496 (2:3)", | |
| "(2K) 2496x1664 (3:2)", | |
| "(2K) 1792x2240 (4:5)", | |
| "(2K) 2240x1792 (5:4)", | |
| "(2K) 1440x2560 (9:16)", | |
| "(2K) 2560x1440 (16:9)", | |
| "(2K) 1600x2560 (5:8)", | |
| "(2K) 2560x1600 (8:5)", | |
| "(2K) 1728x2304 (3:4)", | |
| "(2K) 2304x1728 (4:3)", | |
| "(2K) 1296x3168 (9:22)", | |
| "(2K) 3168x1296 (22:9)", | |
| "(2K) 1152x2944 (9:23)", | |
| "(2K) 2944x1152 (23:9)", | |
| "(2K) 1248x3328 (3:8)", | |
| "(2K) 3328x1248 (8:3)", | |
| "(2K) 1280x3072 (5:12)", | |
| "(2K) 3072x1280 (12:5)", | |
| "(2K) 1024x3072 (1:3)", | |
| "(2K) 3072x1024 (3:1)", | |
| "(1K) 1024x1024 (1:1)", | |
| "(1K) 896x1120 (4:5)", | |
| "(1K) 1120x896 (5:4)", | |
| "(1K) 864x1152 (3:4)", | |
| "(1K) 1152x864 (4:3)", | |
| "(1K) 832x1248 (2:3)", | |
| "(1K) 1248x832 (3:2)", | |
| "(1K) 800x1280 (5:8)", | |
| "(1K) 1280x800 (8:5)", | |
| "(1K) 720x1280 (9:16)", | |
| "(1K) 1280x720 (16:9)", | |
| "(1K) 720x1440 (1:2)", | |
| "(1K) 1440x720 (2:1)", | |
| ] | |
| IDEOGRAM_45_EDIT_SIZES = [ | |
| s for s in IDEOGRAM_45_SIZES if all(int(v) % 32 == 0 for v in s.split(" ")[1].split("x")) | |
| ] | |
| _IMAGE_REF_RE = re.compile(r"@image(?P<idx>\d*)(?!\w)", re.IGNORECASE | re.ASCII) | |
| async def download_and_process_images(image_urls): | |
| """Helper function to download and process multiple images from URLs""" | |
| # Initialize list to store image tensors | |
| image_tensors = [] | |
| for image_url in image_urls: | |
| # Using functions from apinode_utils.py to handle downloading and processing | |
| image_bytesio = await download_url_as_bytesio(image_url) # Download image content to BytesIO | |
| img_tensor = bytesio_to_image_tensor(image_bytesio, mode="RGB") # Convert to torch.Tensor with RGB mode | |
| image_tensors.append(img_tensor) | |
| # Stack tensors to match (N, width, height, channels) | |
| if image_tensors: | |
| stacked_tensors = torch.cat(image_tensors, dim=0) | |
| else: | |
| raise Exception("No valid images were processed") | |
| return stacked_tensors | |
| class IdeogramV3(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramV3", | |
| display_name="Ideogram V3", | |
| category="partner/image/Ideogram", | |
| description="Generates images using the Ideogram V3 model. " | |
| "Supports both regular image generation from text prompts and image editing with mask.", | |
| inputs=[ | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| tooltip="Prompt for the image generation or editing", | |
| ), | |
| IO.Image.Input( | |
| "image", | |
| tooltip="Optional reference image for image editing.", | |
| optional=True, | |
| ), | |
| IO.Mask.Input( | |
| "mask", | |
| tooltip="Optional mask for inpainting (white areas will be replaced)", | |
| optional=True, | |
| ), | |
| IO.Combo.Input( | |
| "aspect_ratio", | |
| options=list(V3_RATIO_MAP.keys()), | |
| default="1:1", | |
| tooltip="The aspect ratio for image generation. Ignored if resolution is not set to Auto.", | |
| optional=True, | |
| ), | |
| IO.Combo.Input( | |
| "resolution", | |
| options=V3_RESOLUTIONS, | |
| default="Auto", | |
| tooltip="The resolution for image generation. " | |
| "If not set to Auto, this overrides the aspect_ratio setting.", | |
| optional=True, | |
| ), | |
| IO.Combo.Input( | |
| "magic_prompt_option", | |
| options=["AUTO", "ON", "OFF"], | |
| default="AUTO", | |
| tooltip="Determine if MagicPrompt should be used in generation", | |
| optional=True, | |
| advanced=True, | |
| ), | |
| IO.Int.Input( | |
| "seed", | |
| default=0, | |
| min=0, | |
| max=2147483647, | |
| step=1, | |
| control_after_generate=True, | |
| display_mode=IO.NumberDisplay.number, | |
| optional=True, | |
| ), | |
| IO.Int.Input( | |
| "num_images", | |
| default=1, | |
| min=1, | |
| max=8, | |
| step=1, | |
| display_mode=IO.NumberDisplay.number, | |
| optional=True, | |
| ), | |
| IO.Combo.Input( | |
| "rendering_speed", | |
| options=["DEFAULT", "TURBO", "QUALITY"], | |
| default="DEFAULT", | |
| tooltip="Controls the trade-off between generation speed and quality", | |
| optional=True, | |
| advanced=True, | |
| ), | |
| IO.Image.Input( | |
| "character_image", | |
| tooltip="Image to use as character reference.", | |
| optional=True, | |
| ), | |
| IO.Mask.Input( | |
| "character_mask", | |
| tooltip="Optional mask for character reference image.", | |
| optional=True, | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=IO.PriceBadge( | |
| depends_on=IO.PriceBadgeDepends(widgets=["rendering_speed", "num_images"], inputs=["character_image"]), | |
| expr=""" | |
| ( | |
| $n := widgets.num_images; | |
| $speed := widgets.rendering_speed; | |
| $hasChar := inputs.character_image.connected; | |
| $base := | |
| $contains($speed,"quality") ? ($hasChar ? 0.286 : 0.1287) : | |
| $contains($speed,"default") ? ($hasChar ? 0.2145 : 0.0858) : | |
| $contains($speed,"turbo") ? ($hasChar ? 0.143 : 0.0429) : | |
| 0.0858; | |
| {"type":"usd","usd": $round($base * $n, 2)} | |
| ) | |
| """, | |
| ), | |
| ) | |
| async def execute( | |
| cls, | |
| prompt, | |
| image=None, | |
| mask=None, | |
| resolution="Auto", | |
| aspect_ratio="1:1", | |
| magic_prompt_option="AUTO", | |
| seed=0, | |
| num_images=1, | |
| rendering_speed="DEFAULT", | |
| character_image=None, | |
| character_mask=None, | |
| ): | |
| if rendering_speed == "BALANCED": # for backward compatibility | |
| rendering_speed = "DEFAULT" | |
| character_img_binary = None | |
| character_mask_binary = None | |
| if character_image is not None: | |
| input_tensor = character_image.squeeze().cpu() | |
| if character_mask is not None: | |
| character_mask = resize_mask_to_image(character_mask, character_image, allow_gradient=False) | |
| character_mask = 1.0 - character_mask | |
| if character_mask.shape[1:] != character_image.shape[1:-1]: | |
| raise Exception("Character mask and image must be the same size") | |
| mask_np = (character_mask.squeeze().cpu().numpy() * 255).astype(np.uint8) | |
| mask_img = Image.fromarray(mask_np) | |
| mask_byte_arr = BytesIO() | |
| mask_img.save(mask_byte_arr, format="PNG") | |
| mask_byte_arr.seek(0) | |
| character_mask_binary = mask_byte_arr | |
| character_mask_binary.name = "mask.png" | |
| img_np = (input_tensor.numpy() * 255).astype(np.uint8) | |
| img = Image.fromarray(img_np) | |
| img_byte_arr = BytesIO() | |
| img.save(img_byte_arr, format="PNG") | |
| img_byte_arr.seek(0) | |
| character_img_binary = img_byte_arr | |
| character_img_binary.name = "image.png" | |
| elif character_mask is not None: | |
| raise Exception("Character mask requires character image to be present") | |
| # Check if both image and mask are provided for editing mode | |
| if image is not None and mask is not None: | |
| # Process image and mask | |
| input_tensor = image.squeeze().cpu() | |
| # Resize mask to match image dimension | |
| mask = resize_mask_to_image(mask, image, allow_gradient=False) | |
| # Invert mask, as Ideogram API will edit black areas instead of white areas (opposite of convention). | |
| mask = 1.0 - mask | |
| # Validate mask dimensions match image | |
| if mask.shape[1:] != image.shape[1:-1]: | |
| raise Exception("Mask and Image must be the same size") | |
| # Process image | |
| img_np = (input_tensor.numpy() * 255).astype(np.uint8) | |
| img = Image.fromarray(img_np) | |
| img_byte_arr = BytesIO() | |
| img.save(img_byte_arr, format="PNG") | |
| img_byte_arr.seek(0) | |
| img_binary = img_byte_arr | |
| img_binary.name = "image.png" | |
| # Process mask - white areas will be replaced | |
| mask_np = (mask.squeeze().cpu().numpy() * 255).astype(np.uint8) | |
| mask_img = Image.fromarray(mask_np) | |
| mask_byte_arr = BytesIO() | |
| mask_img.save(mask_byte_arr, format="PNG") | |
| mask_byte_arr.seek(0) | |
| mask_binary = mask_byte_arr | |
| mask_binary.name = "mask.png" | |
| # Create edit request | |
| edit_request = IdeogramV3EditRequest( | |
| prompt=prompt, | |
| rendering_speed=rendering_speed, | |
| ) | |
| # Add optional parameters | |
| if magic_prompt_option != "AUTO": | |
| edit_request.magic_prompt = magic_prompt_option | |
| if seed != 0: | |
| edit_request.seed = seed | |
| if num_images > 1: | |
| edit_request.num_images = num_images | |
| files = { | |
| "image": img_binary, | |
| "mask": mask_binary, | |
| } | |
| if character_img_binary: | |
| files["character_reference_images"] = character_img_binary | |
| if character_mask_binary: | |
| files["character_mask_binary"] = character_mask_binary | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path="/proxy/ideogram/ideogram-v3/edit", method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=edit_request, | |
| files=files, | |
| content_type="multipart/form-data", | |
| ) | |
| elif image is not None or mask is not None: | |
| # If only one of image or mask is provided, raise an error | |
| raise Exception("Ideogram V3 image editing requires both an image AND a mask") | |
| else: | |
| # Create generation request | |
| gen_request = IdeogramV3Request( | |
| prompt=prompt, | |
| rendering_speed=rendering_speed, | |
| ) | |
| # Handle resolution vs aspect ratio | |
| if resolution != "Auto": | |
| gen_request.resolution = resolution | |
| elif aspect_ratio != "1:1": | |
| v3_aspect = V3_RATIO_MAP.get(aspect_ratio) | |
| if v3_aspect: | |
| gen_request.aspect_ratio = v3_aspect | |
| # Add optional parameters | |
| if magic_prompt_option != "AUTO": | |
| gen_request.magic_prompt = magic_prompt_option | |
| if seed != 0: | |
| gen_request.seed = seed | |
| if num_images > 1: | |
| gen_request.num_images = num_images | |
| files = {} | |
| if character_img_binary: | |
| files["character_reference_images"] = character_img_binary | |
| if character_mask_binary: | |
| files["character_mask_binary"] = character_mask_binary | |
| if files: | |
| gen_request.style_type = "AUTO" | |
| response = await sync_op( | |
| cls, | |
| endpoint=ApiEndpoint(path="/proxy/ideogram/ideogram-v3/generate", method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=gen_request, | |
| files=files if files else None, | |
| content_type="multipart/form-data", | |
| ) | |
| if not response.data or len(response.data) == 0: | |
| raise Exception("No images were generated in the response") | |
| image_urls = [image_data.url for image_data in response.data if image_data.url] | |
| if not image_urls: | |
| raise Exception("No image URLs were generated in the response") | |
| return IO.NodeOutput(await download_and_process_images(image_urls)) | |
| class IdeogramV4(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramV4", | |
| display_name="Ideogram V4", | |
| category="partner/image/Ideogram", | |
| description="Generates images using the Ideogram 4.0 model from a text prompt.", | |
| inputs=[ | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| tooltip="Text prompt for the image generation.", | |
| ), | |
| IO.Combo.Input( | |
| "resolution", | |
| options=[ | |
| "Auto", | |
| "2048x2048 (1:1)", | |
| "1440x2880 (1:2)", | |
| "2880x1440 (2:1)", | |
| "1664x2496 (2:3)", | |
| "2496x1664 (3:2)", | |
| "1792x2240 (4:5)", | |
| "2240x1792 (5:4)", | |
| "1440x2560 (9:16)", | |
| "2560x1440 (16:9)", | |
| "1600x2560 (5:8)", | |
| "2560x1600 (8:5)", | |
| "1728x2304 (3:4)", | |
| "2304x1728 (4:3)", | |
| "1296x3168 (9:22)", | |
| "3168x1296 (22:9)", | |
| "1152x2944 (9:23)", | |
| "2944x1152 (23:9)", | |
| "1248x3328 (3:8)", | |
| "3328x1248 (8:3)", | |
| "1280x3072 (5:12)", | |
| "3072x1280 (12:5)", | |
| ], | |
| default="Auto", | |
| ), | |
| IO.Combo.Input( | |
| "rendering_speed", | |
| options=["DEFAULT", "TURBO", "QUALITY"], | |
| default="DEFAULT", | |
| tooltip="Controls the trade-off between generation speed and quality.", | |
| ), | |
| IO.Int.Input( | |
| "seed", | |
| default=0, | |
| min=0, | |
| max=2147483647, | |
| step=1, | |
| control_after_generate=True, | |
| display_mode=IO.NumberDisplay.number, | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=IO.PriceBadge( | |
| depends_on=IO.PriceBadgeDepends(widgets=["rendering_speed"]), | |
| expr=""" | |
| ( | |
| $speed := widgets.rendering_speed; | |
| $price := | |
| $contains($speed,"turbo") ? 0.0429 : | |
| $contains($speed,"quality") ? 0.143 : | |
| 0.0858; | |
| {"type":"usd","usd": $price} | |
| ) | |
| """, | |
| ), | |
| ) | |
| async def execute( | |
| cls, | |
| prompt: str, | |
| resolution: str, | |
| rendering_speed: str, | |
| seed: int, | |
| ): | |
| validate_string(prompt, strip_whitespace=True, min_length=1) | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path="/proxy/ideogram/ideogram-v4/generate", method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=IdeogramV4Request( | |
| text_prompt=prompt, | |
| resolution=resolution.split(" ")[0] if resolution != "Auto" else None, | |
| rendering_speed=rendering_speed, | |
| ), | |
| ) | |
| if not response.data or len(response.data) == 0: | |
| raise Exception("No images were generated in the response") | |
| image_urls = [image_data.url for image_data in response.data if image_data.url] | |
| if not image_urls: | |
| raise Exception("No image URLs were generated in the response") | |
| return IO.NodeOutput(await download_and_process_images(image_urls)) | |
| class IdeogramPImage(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramPImage", | |
| display_name="Ideogram & Pruna P-Image", | |
| category="partner/image/Ideogram", | |
| description="Generates images using P-Image, Ideogram's fast text-to-image model. " | |
| "Strong typography and photorealism; " | |
| "supports Ideogram 4.0 structured JSON captions for exact text, " | |
| "colors and layout.", | |
| inputs=[ | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| tooltip="Text prompt. Also accepts an Ideogram 4.0 structured JSON caption " | |
| "(exact colors as #RRGGBB hexes, exact text strings, bounding-box " | |
| "layout) — set prompt_upsampling to OFF to use it verbatim.", | |
| ), | |
| IO.Combo.Input( | |
| "quality", | |
| options=["VERY_LOW", "LOW", "MEDIUM", "HIGH"], | |
| default="MEDIUM", | |
| tooltip="Speed/price/quality tier. MEDIUM is the everyday default; HIGH for " | |
| "complex prompts, fine detail and difficult text; VERY_LOW/LOW for " | |
| "drafts at scale. Difficult text renders poorly below MEDIUM.", | |
| ), | |
| IO.Combo.Input( | |
| "resolution", | |
| options=["1K", "2K"], | |
| default="1K", | |
| tooltip="Output size class (exact pixels follow the aspect ratio, e.g. " | |
| "16:9 gives 1280x720 at 1K and 2560x1440 at 2K). " | |
| "Prefer HIGH + 2K for crisp typography.", | |
| ), | |
| IO.Combo.Input( | |
| "aspect_ratio", | |
| options=list(V3_RATIO_MAP.keys()), | |
| default="1:1", | |
| tooltip="The aspect ratio for image generation.", | |
| ), | |
| IO.Combo.Input( | |
| "prompt_upsampling", | |
| options=["AUTO", "ON", "OFF"], | |
| default="AUTO", | |
| tooltip="Expands short prompts into a detailed structured caption before " | |
| "generation (the rewritten prompt is returned as final_prompt). " | |
| "Set OFF when supplying your own JSON caption or exact wording.", | |
| ), | |
| IO.Int.Input( | |
| "seed", | |
| default=42, | |
| min=0, | |
| max=2147483647, | |
| step=1, | |
| control_after_generate=True, | |
| display_mode=IO.NumberDisplay.number, | |
| optional=True, | |
| tooltip="Seed for reproducible generation. With prompt_upsampling OFF, " | |
| "the same seed and settings return the same image; with ON/AUTO " | |
| "the prompt rewrite varies per run — reproduce a result by reusing " | |
| "its final_prompt output with prompt_upsampling OFF and the same " | |
| "seed.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| IO.String.Output( | |
| "final_prompt", | |
| tooltip="The prompt the image was actually generated from (the rewritten " | |
| "structured caption when prompt_upsampling ran, else your prompt). " | |
| "Feed it back with prompt_upsampling OFF and the same seed to " | |
| "reproduce this image.", | |
| ), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=IO.PriceBadge( | |
| depends_on=IO.PriceBadgeDepends(widgets=["quality", "resolution"]), | |
| expr=""" | |
| ( | |
| $q := widgets.quality; | |
| $is2k := $contains(widgets.resolution, "2k"); | |
| $usd := | |
| $contains($q, "very_low") ? ($is2k ? 0.00858 : 0.00429) : | |
| $contains($q, "high") ? ($is2k ? 0.0429 : 0.02145) : | |
| $contains($q, "medium") ? ($is2k ? 0.0286 : 0.0143) : | |
| ($is2k ? 0.02145 : 0.010725); | |
| {"type": "usd", "usd": $usd} | |
| ) | |
| """, | |
| ), | |
| ) | |
| async def execute( | |
| cls, | |
| prompt: str, | |
| quality: str = "MEDIUM", | |
| resolution: str = "1K", | |
| aspect_ratio: str = "1:1", | |
| prompt_upsampling: str = "AUTO", | |
| seed: int = 42, | |
| ): | |
| validate_string(prompt, strip_whitespace=True, min_length=1) | |
| request = IdeogramPImageRequest( | |
| prompt=prompt, | |
| quality=quality, | |
| resolution=resolution, | |
| aspect_ratio=V3_RATIO_MAP[aspect_ratio], | |
| prompt_upsampling=prompt_upsampling, | |
| seed=seed, | |
| ) | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path="/proxy/ideogram/text-to-image/p-image-ideogram", method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=request, | |
| ) | |
| if not response.data: | |
| raise Exception("No images were generated in the response") | |
| image_urls = [image_data.url for image_data in response.data if image_data.url] | |
| if not image_urls: | |
| if any(image_data.is_image_safe is False for image_data in response.data): | |
| raise Exception( | |
| "The generation was blocked by Ideogram's content safety filter. " | |
| "Adjust the prompt and try again." | |
| ) | |
| raise Exception("No image URLs were generated in the response") | |
| return IO.NodeOutput( | |
| await download_and_process_images(image_urls), | |
| response.data[0].prompt or prompt, | |
| ) | |
| def _resolve_image_refs(prompt: str, total_images: int) -> str: | |
| parts = [] | |
| pos = 0 | |
| prev_end = -1 | |
| for match in _IMAGE_REF_RE.finditer(prompt): | |
| start = match.start() | |
| if start > 0 and start != prev_end and (prompt[start - 1].isalnum() or prompt[start - 1] == "_"): | |
| continue | |
| idx = int(match.group("idx") or 1) | |
| if not 1 <= idx <= total_images: | |
| raise ValueError( | |
| f"The prompt references @Image{idx}, but only {total_images} images " | |
| f"are connected (a batched input counts once per image)." | |
| ) | |
| parts.append(prompt[pos:start]) | |
| parts.append(f"image {idx}") | |
| pos = match.end() | |
| prev_end = match.end() | |
| parts.append(prompt[pos:]) | |
| return "".join(parts) | |
| def _ideogram_45_images(model: dict) -> list[torch.Tensor]: | |
| images = [image for key in model["images"] for image in model["images"][key]] | |
| if len(images) > IDEOGRAM_45_MAX_IMAGES: | |
| raise ValueError( | |
| f"A maximum of {IDEOGRAM_45_MAX_IMAGES} images is supported; got {len(images)} " | |
| f"(a batched input counts once per image)." | |
| ) | |
| for i, image in enumerate(images, start=1): | |
| height, width = image.shape[0], image.shape[1] | |
| if max(width, height) > 6 * min(width, height): | |
| raise ValueError(f"Image {i} is {width}x{height}; its aspect ratio must be between 1:6 and 6:1.") | |
| return images | |
| def _ideogram_45_image_file(image: torch.Tensor) -> BytesIO: | |
| image = image.unsqueeze(0) | |
| height, width = image.shape[1], image.shape[2] | |
| scale = min(1.0, IDEOGRAM_45_MAX_SIDE / max(width, height), math.sqrt(IDEOGRAM_45_MAX_PIXELS / (width * height))) | |
| while True: | |
| new_width, new_height = max(1, round(width * scale)), max(1, round(height * scale)) | |
| if math.ceil(new_width / 32) * math.ceil(new_height / 32) * 1024 <= IDEOGRAM_45_MAX_PIXELS: | |
| break | |
| scale *= 0.995 | |
| if (new_width, new_height) != (width, height): | |
| image = common_upscale(image.movedim(-1, 1), new_width, new_height, "lanczos", "disabled").movedim(1, -1) | |
| return tensor_to_bytesio(image, total_pixels=None, mime_type="image/png") | |
| async def _ideogram_45_output(cls: type[IO.ComfyNode], response: IdeogramGenerateResponse) -> torch.Tensor: | |
| data = response.data or [] | |
| urls = [item.url for item in data if item.url] | |
| if not urls: | |
| if any(item.is_image_safe is False for item in data): | |
| raise Exception( | |
| "The result was blocked by Ideogram's content safety filter. " | |
| "Adjust the prompt or images and try again." | |
| ) | |
| raise Exception("No images were generated in the response") | |
| return torch.cat([await download_url_to_image_tensor(url, cls=cls) for url in urls]) | |
| def _ideogram_45_quality_input(options: list[str]) -> IO.Combo.Input: | |
| return IO.Combo.Input( | |
| "quality", | |
| options=options, | |
| default="medium", | |
| tooltip="Quality tier. Higher tiers cost more and take longer.", | |
| ) | |
| def _ideogram_45_seed_input(tooltip: str) -> IO.Int.Input: | |
| return IO.Int.Input( | |
| "seed", | |
| default=42, | |
| min=0, | |
| max=2147483647, | |
| step=1, | |
| control_after_generate=True, | |
| display_mode=IO.NumberDisplay.number, | |
| tooltip=tooltip, | |
| ) | |
| def _ideogram_45_edit_inputs(with_size: bool) -> list: | |
| inputs = [ | |
| IO.Autogrow.Input( | |
| "images", | |
| template=IO.Autogrow.TemplateNames( | |
| IO.Image.Input("image"), | |
| names=[f"image_{i}" for i in range(1, IDEOGRAM_45_MAX_IMAGES + 1)], | |
| min=1, | |
| ), | |
| tooltip="Image 1 is the image to edit; images 2-5 are optional references. " | |
| "Refer to them in the prompt as @Image1, @Image2, ...; a batched input counts once per image.", | |
| ), | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| tooltip="Editing instructions. Supports @Image1-style references to the input images.", | |
| ), | |
| ] | |
| if with_size: | |
| inputs.extend( | |
| [ | |
| IO.Combo.Input( | |
| "size", | |
| options=["auto", "source", *IDEOGRAM_45_EDIT_SIZES, "custom"], | |
| default="auto", | |
| tooltip="Output size. 'auto' picks a ~2K canvas from the images and prompt, 'source' keeps " | |
| "the size of image 1 (images above ~4 MP are scaled down first), and a preset with a different " | |
| "aspect ratio recomposes the scene. Select 'custom' to use the width and height below.", | |
| ), | |
| IO.Int.Input( | |
| "width", | |
| default=2048, | |
| min=256, | |
| max=IDEOGRAM_45_MAX_SIDE, | |
| step=32, | |
| tooltip="Custom output width. Used only when size is set to 'custom'.", | |
| ), | |
| IO.Int.Input( | |
| "height", | |
| default=2048, | |
| min=256, | |
| max=IDEOGRAM_45_MAX_SIDE, | |
| step=32, | |
| tooltip="Custom output height. Used only when size is set to 'custom'.", | |
| ), | |
| ] | |
| ) | |
| inputs.extend( | |
| [ | |
| _ideogram_45_quality_input(["very_low", "low", "medium", "high"]), | |
| _ideogram_45_seed_input("Seed for generation. The same images, prompt, settings and seed give the same result."), | |
| ] | |
| ) | |
| return inputs | |
| def _ideogram_45_price_badge() -> IO.PriceBadge: | |
| return IO.PriceBadge( | |
| depends_on=IO.PriceBadgeDepends(widgets=["model", "model.quality"]), | |
| expr=""" | |
| ( | |
| $q := $lookup(widgets, "model.quality"); | |
| {"type": "usd", "usd": $q = "very_low" ? 0.01144 : $q = "low" ? 0.0429 : $q = "high" ? 0.286 : 0.0858} | |
| ) | |
| """, | |
| ) | |
| class IdeogramTextToImageApi(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramTextToImageApi", | |
| display_name="Ideogram 4.5 Text to Image", | |
| category="partner/image/Ideogram", | |
| description="Generates images from a text prompt using Ideogram 4.5.", | |
| inputs=[ | |
| IO.DynamicCombo.Input( | |
| "model", | |
| options=[ | |
| IO.DynamicCombo.Option( | |
| model_id, | |
| [ | |
| IO.String.Input( | |
| "prompt", | |
| multiline=True, | |
| default="", | |
| tooltip="Text prompt. Also accepts an Ideogram structured JSON caption, " | |
| "for example a previous final_prompt.", | |
| ), | |
| IO.Combo.Input( | |
| "size", | |
| options=["auto", *IDEOGRAM_45_SIZES], | |
| default="auto", | |
| tooltip="Output size. 'auto' lets the model pick a canvas that suits the prompt.", | |
| ), | |
| _ideogram_45_quality_input(["low", "medium", "high"]), | |
| IO.Combo.Input( | |
| "magic_prompt", | |
| options=["auto", "on", "off"], | |
| default="auto", | |
| tooltip="Rewrites the prompt into a detailed structured caption before " | |
| "generating; 'off' keeps your wording as literal as possible. " | |
| "The caption is returned as final_prompt.", | |
| advanced=True, | |
| ), | |
| _ideogram_45_seed_input( | |
| "Seed for generation. Text-to-image is not reproducible from the seed alone " | |
| "because the prompt is rewritten on every run; to reproduce an image, reuse " | |
| "its final_prompt with magic_prompt set to 'off' and the same seed." | |
| ), | |
| ], | |
| ) | |
| for model_id in IDEOGRAM_45_MODELS | |
| ], | |
| tooltip="Model to use.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| IO.String.Output( | |
| "final_prompt", | |
| tooltip="The structured caption the image was generated from. Feed it back with " | |
| "magic_prompt set to 'off' and the same seed to reproduce the image.", | |
| ), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=_ideogram_45_price_badge(), | |
| ) | |
| async def execute(cls, model: dict): | |
| validate_string(model["prompt"], strip_whitespace=True, min_length=1, max_length=10000) | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path=IDEOGRAM_45_GENERATE_PATH, method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=Ideogram45Request( | |
| prompt=model["prompt"], | |
| quality=model["quality"], | |
| seed=model["seed"], | |
| size=None if model["size"] == "auto" else model["size"].split(" ")[1], | |
| magic_prompt=model["magic_prompt"], | |
| ), | |
| ) | |
| image = await _ideogram_45_output(cls, response) | |
| return IO.NodeOutput(image, response.data[0].prompt or model["prompt"]) | |
| class IdeogramEditApi(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramEditApi", | |
| display_name="Ideogram 4.5 Edit", | |
| category="partner/image/Ideogram", | |
| description="Edits or combines up to 5 images guided by a text prompt using Ideogram 4.5. " | |
| "Re-renders the whole image and can change its size or aspect ratio; " | |
| "use Ideogram 4.5 Precise Edit to keep untouched pixels unchanged.", | |
| inputs=[ | |
| IO.DynamicCombo.Input( | |
| "model", | |
| options=[ | |
| IO.DynamicCombo.Option(model_id, _ideogram_45_edit_inputs(with_size=True)) | |
| for model_id in IDEOGRAM_45_MODELS | |
| ], | |
| tooltip="Model to use.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=_ideogram_45_price_badge(), | |
| ) | |
| async def execute(cls, model: dict): | |
| validate_string(model["prompt"], strip_whitespace=True, min_length=1, max_length=10000) | |
| images = _ideogram_45_images(model) | |
| size = model["size"] | |
| if size == "custom": | |
| width, height = model["width"], model["height"] | |
| if width * height > IDEOGRAM_45_MAX_PIXELS: | |
| raise ValueError( | |
| f"Custom size {width}x{height} exceeds the maximum of {IDEOGRAM_45_MAX_PIXELS} pixels (2048x2048)." | |
| ) | |
| if max(width, height) > 6 * min(width, height): | |
| raise ValueError(f"Custom size {width}x{height} exceeds the maximum aspect ratio of 6:1.") | |
| size = f"{width}x{height}" | |
| elif size == "auto": | |
| size = None | |
| elif size != "source": | |
| size = size.split(" ")[1] | |
| prompt = _resolve_image_refs(model["prompt"], len(images)) | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path=IDEOGRAM_45_GENERATE_PATH, method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=Ideogram45Request(prompt=prompt, quality=model["quality"], seed=model["seed"], size=size), | |
| files=[ | |
| ("images", (f"image_{i}.png", _ideogram_45_image_file(image), "image/png")) | |
| for i, image in enumerate(images, start=1) | |
| ], | |
| content_type="multipart/form-data", | |
| ) | |
| return IO.NodeOutput(await _ideogram_45_output(cls, response)) | |
| class IdeogramPreciseEditApi(IO.ComfyNode): | |
| def define_schema(cls): | |
| return IO.Schema( | |
| node_id="IdeogramPreciseEditApi", | |
| display_name="Ideogram 4.5 Precise Edit", | |
| category="partner/image/Ideogram", | |
| description="Edits an image guided by a text prompt using Ideogram 4.5 precise editing: only what the " | |
| "prompt asks for changes, untouched pixels stay identical and the output keeps the size of image 1 " | |
| "(images above ~4 MP are scaled down first). Accepts up to 4 reference images.", | |
| inputs=[ | |
| IO.DynamicCombo.Input( | |
| "model", | |
| options=[ | |
| IO.DynamicCombo.Option(model_id, _ideogram_45_edit_inputs(with_size=False)) | |
| for model_id in IDEOGRAM_45_MODELS | |
| ], | |
| tooltip="Model to use.", | |
| ), | |
| ], | |
| outputs=[ | |
| IO.Image.Output(), | |
| ], | |
| hidden=[ | |
| IO.Hidden.auth_token_comfy_org, | |
| IO.Hidden.api_key_comfy_org, | |
| IO.Hidden.unique_id, | |
| ], | |
| is_api_node=True, | |
| price_badge=_ideogram_45_price_badge(), | |
| ) | |
| async def execute(cls, model: dict): | |
| validate_string(model["prompt"], strip_whitespace=True, min_length=1, max_length=10000) | |
| images = _ideogram_45_images(model) | |
| prompt = _resolve_image_refs(model["prompt"], len(images)) | |
| files = [("image", ("image_1.png", _ideogram_45_image_file(images[0]), "image/png"))] | |
| files.extend( | |
| ("reference_images", (f"image_{i}.png", _ideogram_45_image_file(image), "image/png")) | |
| for i, image in enumerate(images[1:], start=2) | |
| ) | |
| response = await sync_op( | |
| cls, | |
| ApiEndpoint(path=IDEOGRAM_45_PRECISE_EDIT_PATH, method="POST"), | |
| response_model=IdeogramGenerateResponse, | |
| data=Ideogram45Request(prompt=prompt, quality=model["quality"], seed=model["seed"]), | |
| files=files, | |
| content_type="multipart/form-data", | |
| ) | |
| return IO.NodeOutput(await _ideogram_45_output(cls, response)) | |
| class IdeogramExtension(ComfyExtension): | |
| async def get_node_list(self) -> list[type[IO.ComfyNode]]: | |
| return [ | |
| IdeogramV3, | |
| IdeogramV4, | |
| IdeogramPImage, | |
| IdeogramTextToImageApi, | |
| IdeogramEditApi, | |
| IdeogramPreciseEditApi, | |
| ] | |
| async def comfy_entrypoint() -> IdeogramExtension: | |
| return IdeogramExtension() | |
Xet Storage Details
- Size:
- 41.3 kB
- Xet hash:
- 802861acdf23049a75a03641d96fe22b00b0e64665e05dbbfebc38459415c1f9
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.