diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index aa1c4ce0b..18cf63141 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -308,6 +308,12 @@ class IdeogramGenerateRequest(BaseModel): ) +class IdeogramGenerateRequest(BaseModel): + image_request: ImageRequest = Field( + ..., description='The image generation request parameters.' + ) + + class Datum(BaseModel): prompt: Optional[str] = Field( None, description='The prompt used to generate this image.' @@ -845,16 +851,75 @@ class KlingVirtualTryOnResponse(BaseModel): data: Optional[Data6] = None -class ResourcePackType(str, Enum): - decreasing_total = 'decreasing_total' - constant_period = 'constant_period' +class KlingRequestError(KlingErrorResponse): + code: Optional[Code2] = Field( + None, + description='- 1200: Invalid request parameters\n- 1201: Invalid parameters\n- 1202: Invalid request method\n- 1203: Requested resource does not exist\n', + ) + + +class Code3(Enum): + int_5000 = 5000 + int_5001 = 5001 + int_5002 = 5002 + + +class KlingServerError(KlingErrorResponse): + code: Optional[Code3] = Field( + None, + description='- 5000: Internal server error\n- 5001: Service temporarily unavailable\n- 5002: Server internal timeout\n', + ) + + +class Code4(Enum): + int_1300 = 1300 + int_1301 = 1301 + int_1302 = 1302 + int_1303 = 1303 + int_1304 = 1304 + + +class KlingStrategyError(KlingErrorResponse): + code: Optional[Code4] = Field( + None, + description='- 1300: Trigger platform strategy\n- 1301: Trigger content security policy\n- 1302: API request too frequent\n- 1303: Concurrency/QPS exceeds limit\n- 1304: Trigger IP whitelist policy\n', + ) + + +class MinimaxBaseResponse(BaseModel): + status_code: int = Field( + ..., + description='Status code. 0 indicates success, other values indicate errors.', + ) + status_msg: str = Field( + ..., description='Specific error details or success message.' + ) + + +class File(BaseModel): + bytes: Optional[int] = Field(None, description='File size in bytes') + created_at: Optional[int] = Field( + None, description='Unix timestamp when the file was created, in seconds' + ) + download_url: Optional[str] = Field( + None, description='The URL to download the video' + ) + file_id: Optional[int] = Field(None, description='Unique identifier for the file') + filename: Optional[str] = Field(None, description='The name of the file') + purpose: Optional[str] = Field(None, description='The purpose of using the file') + + +class MinimaxFileRetrieveResponse(BaseModel): + base_resp: MinimaxBaseResponse + file: File class Status(str, Enum): - toBeOnline = 'toBeOnline' - online = 'online' - expired = 'expired' - runOut = 'runOut' + Queueing = 'Queueing' + Preparing = 'Preparing' + Processing = 'Processing' + Success = 'Success' + Fail = 'Fail' class ResourcePackSubscribeInfo(BaseModel): diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 6dcd9b4df..16708b8dc 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -97,7 +97,6 @@ import io import socket from typing import Dict, Type, Optional, Any, TypeVar, Generic, Callable, Tuple from enum import Enum -import time import json import requests from urllib.parse import urljoin, urlparse diff --git a/comfy_api_nodes/nodes_api.py b/comfy_api_nodes/nodes_api.py index dbdbcdf60..0d032d848 100644 --- a/comfy_api_nodes/nodes_api.py +++ b/comfy_api_nodes/nodes_api.py @@ -1,14 +1,23 @@ import io from inspect import cleandoc - +from comfy.comfy_types.node_typing import FileLocator +from typing import Literal from comfy.utils import common_upscale from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict from comfy_api_nodes.apis import ( OpenAIImageGenerationRequest, OpenAIImageEditRequest, - OpenAIImageGenerationResponse + OpenAIImageGenerationResponse, + MinimaxVideoGenerationRequest, + MinimaxVideoGenerationResponse, + MinimaxFileRetrieveResponse, + MinimaxTaskResultResponse, + IdeogramGenerateRequest, + IdeogramGenerateResponse, + ImageRequest, + Model ) -from comfy_api_nodes.apis.client import ApiEndpoint, HttpMethod, SynchronousOperation +from comfy_api_nodes.apis.client import ApiEndpoint, HttpMethod, SynchronousOperation, PollingOperation, EmptyRequest import numpy as np from PIL import Image @@ -16,6 +25,11 @@ import requests import torch import math import base64 +import logging +import json +import av +import os +import folder_paths def downscale_input(image): samples = image.movedim(-1,1) @@ -428,14 +442,168 @@ class OpenAIGPTImage1(ComfyNodeABC): return (img_tensor,) -class MinimaxVideoNode: +class IdeogramTextToImage(ComfyNodeABC): + """ + Generates images synchronously based on a given prompt and optional parameters. + + Images links are available for a limited period of time; if you would like to keep the image, you must download it. + """ + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + """ + Return a dictionary which contains config for all input fields. + Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". + Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. + The type can be a list for selection. + + Returns: `dict`: + - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` + - Value input_fields (`dict`): Contains input fields config: + * Key field_name (`string`): Name of a entry-point method's argument + * Value field_config (`tuple`): + + First value is a string indicate the type of field or a list for selection. + + Secound value is a config for type "INT", "STRING" or "FLOAT". + """ + return { + "required": { + "prompt": (IO.STRING, { + "multiline": True, + "default": "", + "tooltip": "Prompt for the image generation", + }), + "model": (IO.COMBO, { "options": ["V_2", "V_2_TURBO", "V_1", "V_1_TURBO"], "default": "V_2", "tooltip": "Model to use for image generation"}), + }, + "optional": { + "aspect_ratio": (IO.COMBO, { "options": ["ASPECT_1_1", "ASPECT_4_3", "ASPECT_3_4", "ASPECT_16_9", "ASPECT_9_16", "ASPECT_2_1", "ASPECT_1_2", "ASPECT_3_2", "ASPECT_2_3", "ASPECT_4_5", "ASPECT_5_4"], "default": "ASPECT_1_1", "tooltip": "The aspect ratio for image generation. Cannot be used with resolution" + }), + "resolution": (IO.COMBO, { "options": ["1024x1024", "1024x1792", "1792x1024"], + "default": "1024x1024", + "tooltip": "The resolution for image generation (V2 only). Cannot be used with aspect_ratio" + }), + "magic_prompt_option": (IO.COMBO, { "options": ["AUTO", "ON", "OFF"], + "default": "AUTO", + "tooltip": "Determine if MagicPrompt should be used in generation" + }), + "seed": (IO.INT, { + "default": 0, + "min": 0, + "max": 2147483647, + "step": 1, + "display": "number" + }), + "style_type": (IO.COMBO, { "options": ["NONE", "ANIME", "CINEMATIC", "CREATIVE", "DIGITAL_ART", "PHOTOGRAPHIC"], + "default": "NONE", + "tooltip": "Style type for generation (V2+ only)" + }), + "negative_prompt": (IO.STRING, { + "multiline": True, + "default": "", + "tooltip": "Description of what to exclude from the image (V1/V2 only)" + }), + "num_images": (IO.INT, { + "default": 1, + "min": 1, + "max": 8, + "step": 1, + "display": "number" + }), + "color_palette": (IO.STRING, { + "multiline": False, + "default": "", + "tooltip": "Color palette preset name or hex colors with weights (V2/V2_TURBO only)" + }), + }, + "hidden": { + "auth_token": "AUTH_TOKEN_COMFY_ORG" + } + } + + RETURN_TYPES = (IO.IMAGE,) + DESCRIPTION = cleandoc(__doc__ or "") # Handle potential None value + FUNCTION = "api_call" + API_NODE = True + CATEGORY = "Example" + + def api_call(self, prompt, model, aspect_ratio=None, resolution=None, + magic_prompt_option="AUTO", seed=0, style_type="NONE", + negative_prompt="", num_images=1, color_palette="", auth_token=None): + import torch + from PIL import Image + import io + import numpy as np + import requests + + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path="/proxy/ideogram/generate", + method=HttpMethod.POST, + request_model=IdeogramGenerateRequest, + response_model=IdeogramGenerateResponse + ), + request=IdeogramGenerateRequest( + image_request=ImageRequest( + prompt=prompt, + model=model, + num_images=num_images, + seed=seed, + aspect_ratio=aspect_ratio if aspect_ratio != "ASPECT_1_1" else None, + resolution=resolution if resolution != "1024x1024" else None, + magic_prompt_option=magic_prompt_option if magic_prompt_option != "AUTO" else None, + style_type=style_type if style_type != "NONE" else None, + negative_prompt=negative_prompt if negative_prompt else None, + color_palette=None + ) + ), + auth_token=auth_token + ) + + response = operation.execute() + + if not response.data or len(response.data) == 0: + raise Exception("No images were generated in the response") + image_url = response.data[0].url + + if not image_url: + raise Exception("No image URL was generated in the response") + img_response = requests.get(image_url) + if img_response.status_code != 200: + raise Exception("Failed to download the image") + + img = Image.open(io.BytesIO(img_response.content)) + img = img.convert("RGB") # Ensure RGB format + + # Convert to numpy array, normalize to float32 between 0 and 1 + img_array = np.array(img).astype(np.float32) / 255.0 + + # Convert to torch tensor and add batch dimension + img_tensor = torch.from_numpy(img_array)[None,] + + return (img_tensor,) + + """ + The node will always be re executed if any of the inputs change but + this method can be used to force the node to execute again even when the inputs don't change. + You can make this node return a number or a string. This value will be compared to the one returned the last time the node was + executed, if it is different the node will be executed again. + This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash + changes between executions the LoadImage node is executed again. + """ + #@classmethod + #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): + # return "" + + +class MinimaxTextToVideoNode: """ Generates videos synchronously based on a prompt, and optional parameters using Minimax's API. """ def __init__(self): self.output_dir = folder_paths.get_output_directory() - self.type = "output" + self.type: Literal["output"] = "output" @classmethod def INPUT_TYPES(s): @@ -597,13 +765,14 @@ class MinimaxVideoNode: return {"ui": {"images": results, "animated": (True,)}} - # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { "OpenAIDalle2": OpenAIDalle2, "OpenAIDalle3": OpenAIDalle3, "OpenAIGPTImage1": OpenAIGPTImage1, + "IdeogramTextToImage": IdeogramTextToImage, + "MinimaxTextToVideoNode": MinimaxTextToVideoNode, } # A dictionary that contains the friendly/humanly readable titles for the nodes @@ -611,4 +780,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "OpenAIDalle2": "OpenAI DALL·E 2", "OpenAIDalle3": "OpenAI DALL·E 3", "OpenAIGPTImage1": "OpenAI GPT Image 1", + "IdeogramTextToImage": "Ideogram Text to Image", + "MinimaxTextToVideoNode": "Minimax Text to Video", }