diff --git a/.github/workflows/update-api-stubs.yml b/.github/workflows/update-api-stubs.yml index 2ae99b673..fe9e4cd6a 100644 --- a/.github/workflows/update-api-stubs.yml +++ b/.github/workflows/update-api-stubs.yml @@ -22,10 +22,19 @@ jobs: run: | python -m pip install --upgrade pip pip install 'datamodel-code-generator[http]' + npm install @redocly/cli + + - name: Download OpenAPI spec + run: | + curl -o openapi.yaml https://api.comfy.org/openapi + + - name: Filter OpenAPI spec with Redocly + run: | + npx @redocly/cli bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly.yaml --remove-unused-components - name: Generate API models run: | - datamodel-codegen --use-subclass-enum --url https://api.comfy.org/openapi --output comfy_api_nodes/apis --output-model-type pydantic_v2.BaseModel + datamodel-codegen --use-subclass-enum --input filtered-openapi.yaml --output comfy_api_nodes/apis --output-model-type pydantic_v2.BaseModel - name: Check for changes id: git-check diff --git a/.gitignore b/.gitignore index 61881b8a4..4e8cea71e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ venv/ *.log web_custom_versions/ .DS_Store +openapi.yaml +filtered-openapi.yaml +uv.lock diff --git a/comfy_api_nodes/README.md b/comfy_api_nodes/README.md new file mode 100644 index 000000000..75b1d696a --- /dev/null +++ b/comfy_api_nodes/README.md @@ -0,0 +1,30 @@ +# ComfyUI API Nodes + +## Introduction + +Below are a collection of nodes that work by calling external APIs. More information available in our [docs](https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes). + +## Development + +API stubs are generated through automatic codegen tools from OpenAPI definitions. Since the Comfy Org OpenAPI definition contains many things from the Comfy Registry as well, we use redocly/cli to filter out only the paths relevant for API nodes. + +### Redocly Instructions + +**Tip** +When developing locally, use the `redocly-dev.yaml` file to generate pydantic models. This lets you use stubs for APIs that are not marked `Released` yet. + +Before your API node PR merges, make sure to add the `Released` tag to the `openapi.yaml` file and test in staging. + +```bash +# Download the OpenAPI file from prod server. +curl -o openapi.yaml https://api.comfy.org/openapi + +# Filter out unneeded API definitions. +npm install -g @redocly/cli +redocly bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly-dev.yaml --remove-unused-components + +# Generate the pydantic datamodels for validation. +datamodel-codegen --use-subclass-enum --input filtered-openapi.yaml --output comfy_api_nodes/apis/__init__.py --output-model-type pydantic_v2.BaseModel + +``` + diff --git a/comfy_api_nodes/apis/PixverseController.py b/comfy_api_nodes/apis/PixverseController.py index 29a3ab33b..949fce171 100644 --- a/comfy_api_nodes/apis/PixverseController.py +++ b/comfy_api_nodes/apis/PixverseController.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: https://api.comfy.org/openapi -# timestamp: 2025-04-23T15:56:33+00:00 +# filename: filtered-openapi.yaml +# timestamp: 2025-04-24T22:16:48+00:00 from __future__ import annotations diff --git a/comfy_api_nodes/apis/PixverseDto.py b/comfy_api_nodes/apis/PixverseDto.py index 399512214..3a2c95fbc 100644 --- a/comfy_api_nodes/apis/PixverseDto.py +++ b/comfy_api_nodes/apis/PixverseDto.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: https://api.comfy.org/openapi -# timestamp: 2025-04-23T15:56:33+00:00 +# filename: filtered-openapi.yaml +# timestamp: 2025-04-24T22:16:48+00:00 from __future__ import annotations diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index e7ea9b332..0e28cc481 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: https://api.comfy.org/openapi -# timestamp: 2025-04-23T15:56:33+00:00 +# filename: filtered-openapi.yaml +# timestamp: 2025-04-24T22:29:35+00:00 from __future__ import annotations @@ -8,34 +8,14 @@ from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional -from pydantic import AnyUrl, BaseModel, Field, confloat, conint - -class Customer(BaseModel): - createdAt: Optional[datetime] = Field( - None, description='The date and time the user was created' - ) - email: Optional[str] = Field(None, description='The email address for this user') - id: str = Field(..., description='The firebase UID of the user') - name: Optional[str] = Field(None, description='The name for this user') - updatedAt: Optional[datetime] = Field( - None, description='The date and time the user was last updated' - ) - - -class Error(BaseModel): - details: Optional[List[str]] = Field( - None, - description='Optional detailed information about the error or hints for resolving it.', - ) - message: Optional[str] = Field( - None, description='A clear and concise description of the error.' - ) +from pydantic import BaseModel, Field, conint, constr class ErrorResponse(BaseModel): error: str message: str + class ImageRequest(BaseModel): aspect_ratio: Optional[str] = Field( None, @@ -71,6 +51,12 @@ class ImageRequest(BaseModel): ) +class IdeogramGenerateRequest(BaseModel): + image_request: ImageRequest = Field( + ..., description='The image generation request parameters.' + ) + + class Datum(BaseModel): is_image_safe: Optional[bool] = Field( None, description='Indicates whether the image is considered safe.' @@ -91,6 +77,15 @@ class Datum(BaseModel): url: Optional[str] = Field(None, description='URL to the generated image.') +class IdeogramGenerateResponse(BaseModel): + created: Optional[datetime] = Field( + None, description='Timestamp when the generation was created.' + ) + data: Optional[List[Datum]] = Field( + None, description='Array of generated image information.' + ) + + class Code(Enum): int_1100 = 1100 int_1101 = 1101 @@ -106,135 +101,14 @@ class Code1(Enum): int_1004 = 1004 -class AspectRatio(str, Enum): - field_16_9 = '16:9' - field_9_16 = '9:16' - field_1_1 = '1:1' - - -class Config(BaseModel): - horizontal: Optional[confloat(ge=-10.0, le=10.0)] = None - pan: Optional[confloat(ge=-10.0, le=10.0)] = None - roll: Optional[confloat(ge=-10.0, le=10.0)] = None - tilt: Optional[confloat(ge=-10.0, le=10.0)] = None - vertical: Optional[confloat(ge=-10.0, le=10.0)] = None - zoom: Optional[confloat(ge=-10.0, le=10.0)] = None - - -class Type(str, Enum): - simple = 'simple' - down_back = 'down_back' - forward_up = 'forward_up' - right_turn_forward = 'right_turn_forward' - left_turn_forward = 'left_turn_forward' - - -class CameraControl(BaseModel): - config: Optional[Config] = None - type: Optional[Type] = Field(None, description='Predefined camera movements type') - - -class Duration(str, Enum): - field_5 = 5 - field_10 = 10 - - -class Mode(str, Enum): - std = 'std' - pro = 'pro' - - -class TaskInfo(BaseModel): - external_task_id: Optional[str] = None - - -class Video(BaseModel): - duration: Optional[str] = Field(None, description='Total video duration') - id: Optional[str] = Field(None, description='Generated video ID') - url: Optional[AnyUrl] = Field(None, description='URL for generated video') - - -class TaskResult(BaseModel): - videos: Optional[List[Video]] = None - - -class TaskStatus(str, Enum): - submitted = 'submitted' - processing = 'processing' - succeed = 'succeed' - failed = 'failed' - - -class Data(BaseModel): - created_at: Optional[int] = Field(None, description='Task creation time') - task_id: Optional[str] = Field(None, description='Task ID') - task_info: Optional[TaskInfo] = None - task_result: Optional[TaskResult] = None - task_status: Optional[TaskStatus] = None - updated_at: Optional[int] = Field(None, description='Task update time') - - -class AspectRatio1(str, Enum): - field_16_9 = '16:9' - field_9_16 = '9:16' - field_1_1 = '1:1' - field_4_3 = '4:3' - field_3_4 = '3:4' - field_3_2 = '3:2' - field_2_3 = '2:3' - field_21_9 = '21:9' - - -class ImageReference(str, Enum): - subject = 'subject' - face = 'face' - - -class Image(BaseModel): - index: Optional[int] = Field(None, description='Image Number (0-9)') - url: Optional[AnyUrl] = Field(None, description='URL for generated image') - - -class TaskResult1(BaseModel): - images: Optional[List[Image]] = None - - -class Data1(BaseModel): - created_at: Optional[int] = Field(None, description='Task creation time') - task_id: Optional[str] = Field(None, description='Task ID') - task_result: Optional[TaskResult1] = None - task_status: Optional[TaskStatus] = None - task_status_msg: Optional[str] = Field(None, description='Task status information') - updated_at: Optional[int] = Field(None, description='Task update time') - - -class AspectRatio2(str, Enum): - field_16_9 = '16:9' - field_9_16 = '9:16' - field_1_1 = '1:1' - - -class CameraControl1(BaseModel): - config: Optional[Config] = None - type: Optional[Type] = Field(None, description='Predefined camera movements type') - - -class ModelName2(str, Enum): - kling_v1 = 'kling-v1' - kling_v1_6 = 'kling-v1-6' - - -class TaskResult2(BaseModel): - videos: Optional[List[Video]] = None - - -class Data2(BaseModel): - created_at: Optional[int] = Field(None, description='Task creation time') - task_id: Optional[str] = Field(None, description='Task ID') - task_info: Optional[TaskInfo] = None - task_result: Optional[TaskResult2] = None - task_status: Optional[TaskStatus] = None - updated_at: Optional[int] = Field(None, description='Task update time') +class KlingErrorResponse(BaseModel): + code: int = Field( + ..., description='Error code value as defined in the API documentation' + ) + message: str = Field(..., description='Human-readable error message') + request_id: str = Field( + ..., description='Request ID for tracking and troubleshooting' + ) class Code2(Enum): @@ -244,43 +118,141 @@ class Code2(Enum): int_1203 = 1203 -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): - effective_time: Optional[int] = Field( - None, description='Effective time, Unix timestamp in ms' - ) - invalid_time: Optional[int] = Field( - None, description='Expiration time, Unix timestamp in ms' - ) - purchase_time: Optional[int] = Field( - None, description='Purchase time, Unix timestamp in ms' - ) - remaining_quantity: Optional[float] = Field( - None, description='Remaining quantity (updated with a 12-hour delay)' - ) - resource_pack_id: Optional[str] = Field(None, description='Resource package ID') - resource_pack_name: Optional[str] = Field(None, description='Resource package name') - resource_pack_type: Optional[ResourcePackType] = Field( +class MinimaxTaskResultResponse(BaseModel): + base_resp: MinimaxBaseResponse + file_id: Optional[str] = Field( None, - description='Resource package type (decreasing_total=decreasing total, constant_period=constant periodicity)', + description='After the task status changes to Success, this field returns the file ID corresponding to the generated video.', ) - status: Optional[Status] = Field(None, description='Resource Package Status') - total_quantity: Optional[float] = Field(None, description='Total quantity') + status: Status = Field( + ..., + description="Task status: 'Queueing' (in queue), 'Preparing' (task is preparing), 'Processing' (generating), 'Success' (task completed successfully), or 'Fail' (task failed).", + ) + task_id: str = Field(..., description='The task ID being queried.') -class Background(str, Enum): - transparent = 'transparent' - opaque = 'opaque' + +class Model(str, Enum): + T2V_01_Director = 'T2V-01-Director' + I2V_01_Director = 'I2V-01-Director' + S2V_01 = 'S2V-01' + I2V_01 = 'I2V-01' + I2V_01_live = 'I2V-01-live' + T2V_01 = 'T2V-01' + + +class SubjectReferenceItem(BaseModel): + image: Optional[str] = Field( + None, description='URL or base64 encoding of the subject reference image.' + ) + mask: Optional[str] = Field( + None, + description='URL or base64 encoding of the mask for the subject reference image.', + ) + + +class MinimaxVideoGenerationRequest(BaseModel): + callback_url: Optional[str] = Field( + None, + description='Optional. URL to receive real-time status updates about the video generation task.', + ) + first_frame_image: Optional[str] = Field( + None, + description='URL or base64 encoding of the first frame image. Required when model is I2V-01, I2V-01-Director, or I2V-01-live.', + ) + model: Model = Field( + ..., + description='Required. ID of model. Options: T2V-01-Director, I2V-01-Director, S2V-01, I2V-01, I2V-01-live, T2V-01', + ) + prompt: Optional[constr(max_length=2000)] = Field( + None, + description='Description of the video. Should be less than 2000 characters. Supports camera movement instructions in [brackets].', + ) + prompt_optimizer: Optional[bool] = Field( + True, + description='If true (default), the model will automatically optimize the prompt. Set to false for more precise control.', + ) + subject_reference: Optional[List[SubjectReferenceItem]] = Field( + None, + description='Only available when model is S2V-01. The model will generate a video based on the subject uploaded through this parameter.', + ) + + +class MinimaxVideoGenerationResponse(BaseModel): + base_resp: MinimaxBaseResponse + task_id: str = Field( + ..., description='The task ID for the asynchronous video generation task.' + ) class Moderation(str, Enum): @@ -294,12 +266,6 @@ class OutputFormat(str, Enum): jpeg = 'jpeg' -class Quality(str, Enum): - low = 'low' - medium = 'medium' - high = 'high' - - class OpenAIImageEditRequest(BaseModel): background: Optional[str] = Field( None, description='Background transparency', examples=['opaque'] @@ -337,7 +303,12 @@ class OpenAIImageEditRequest(BaseModel): ) -class Quality1(str, Enum): +class Background(str, Enum): + transparent = 'transparent' + opaque = 'opaque' + + +class Quality(str, Enum): low = 'low' medium = 'medium' high = 'high' @@ -381,7 +352,7 @@ class OpenAIImageGenerationRequest(BaseModel): description='A text description of the desired image', examples=['Draw a rocket in front of a blackhole in deep space'], ) - quality: Optional[Quality1] = Field( + quality: Optional[Quality] = Field( None, description='The quality of the generated image', examples=['high'] ) response_format: Optional[ResponseFormat] = Field( @@ -410,13 +381,17 @@ class Datum1(BaseModel): class OpenAIImageGenerationResponse(BaseModel): data: Optional[List[Datum1]] = None -class User(BaseModel): - email: Optional[str] = Field(None, description='The email address for this user.') - id: Optional[str] = Field(None, description='The unique id for this user.') - isAdmin: Optional[bool] = Field( - None, description='Indicates if the user has admin privileges.' + + +class KlingAccountError(KlingErrorResponse): + code: Optional[Code] = Field( + None, + description='- 1100: Account exception\n- 1101: Account in arrears (postpaid scenario)\n- 1102: Resource pack depleted or expired (prepaid scenario)\n- 1103: Unauthorized access to requested resource\n', ) - isApproved: Optional[bool] = Field( - None, description='Indicates if the user is approved.' + + +class KlingAuthenticationError(KlingErrorResponse): + code: Optional[Code1] = Field( + None, + description='- 1000: Authentication failed\n- 1001: Authorization is empty\n- 1002: Authorization is invalid\n- 1003: Authorization is not yet valid\n- 1004: Authorization has expired\n', ) - name: Optional[str] = Field(None, description='The name for this user.') diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 74f1e7b2b..49fde8600 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -99,11 +99,10 @@ from typing import ( Any, TypeVar, Generic, - Callable, + ) from pydantic import BaseModel from enum import Enum -import time import json import requests from urllib.parse import urljoin diff --git a/comfy_api_nodes/nodes_api.py b/comfy_api_nodes/nodes_api.py index a9ca7765d..f9973b544 100644 --- a/comfy_api_nodes/nodes_api.py +++ b/comfy_api_nodes/nodes_api.py @@ -2,21 +2,38 @@ import base64 import io import math from inspect import cleandoc - -import numpy as np -import requests -import torch -from PIL import Image - +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.utils import common_upscale from comfy_api_nodes.apis import ( OpenAIImageEditRequest, OpenAIImageGenerationRequest, + OpenAIImageEditRequest, 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 +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) @@ -435,14 +452,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): @@ -604,13 +775,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 @@ -618,4 +790,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", } diff --git a/comfy_api_nodes/redocly-dev.yaml b/comfy_api_nodes/redocly-dev.yaml new file mode 100644 index 000000000..d9e3cab70 --- /dev/null +++ b/comfy_api_nodes/redocly-dev.yaml @@ -0,0 +1,10 @@ +# This file is used to filter the Comfy Org OpenAPI spec for schemas related to API Nodes. +# This is used for development purposes to generate stubs for unreleased API endpoints. +apis: + filter: + root: openapi.yaml + decorators: + filter-in: + property: tags + value: ['API Nodes'] + matchStrategy: all diff --git a/comfy_api_nodes/redocly.yaml b/comfy_api_nodes/redocly.yaml new file mode 100644 index 000000000..d102345b1 --- /dev/null +++ b/comfy_api_nodes/redocly.yaml @@ -0,0 +1,10 @@ +# This file is used to filter the Comfy Org OpenAPI spec for schemas related to API Nodes. + +apis: + filter: + root: openapi.yaml + decorators: + filter-in: + property: tags + value: ['API Nodes', 'Released'] + matchStrategy: all