From 324cfcc08829d53785f2327b1a90e9a3eb60a160 Mon Sep 17 00:00:00 2001 From: thot experiment <94414189+thot-experiment@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:48:17 -0700 Subject: [PATCH] add ideogram v3 (#83) --- comfy_api_nodes/apis/__init__.py | 882 ++++++++++++++++++++++++++---- comfy_api_nodes/apis/client.py | 5 + comfy_api_nodes/nodes_ideogram.py | 286 ++++++++-- 3 files changed, 1037 insertions(+), 136 deletions(-) diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index 77e6d34e4..274489c23 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: -# filename: filtered-openapi.yaml -# timestamp: 2025-04-30T19:05:50+00:00 +# filename: https://stagingapi.comfy.org/openapi +# timestamp: 2025-05-01T00:26:01+00:00 from __future__ import annotations @@ -9,29 +9,29 @@ from enum import Enum from typing import Any, Dict, List, Literal, Optional, Union from uuid import UUID -from pydantic import AnyUrl, BaseModel, Field, RootModel - +from pydantic import AnyUrl, BaseModel, Field, RootModel, confloat, conint, constr +bytes_aliased=bytes class BFLFluxProGenerateRequest(BaseModel): - guidance_scale: Optional[float] = Field( - None, description='The guidance scale for generation.', ge=1.0, le=20.0 + guidance_scale: Optional[confloat(ge=1.0, le=20.0)] = Field( + None, description='The guidance scale for generation.' ) - height: int = Field( - ..., description='The height of the image to generate.', ge=64, le=2048 + height: conint(ge=64, le=2048) = Field( + ..., description='The height of the image to generate.' ) negative_prompt: Optional[str] = Field( None, description='The negative prompt for image generation.' ) - num_images: Optional[int] = Field( - None, description='The number of images to generate.', ge=1, le=4 + num_images: Optional[conint(ge=1, le=4)] = Field( + None, description='The number of images to generate.' ) - num_inference_steps: Optional[int] = Field( - None, description='The number of inference steps.', ge=1, le=100 + num_inference_steps: Optional[conint(ge=1, le=100)] = Field( + None, description='The number of inference steps.' ) prompt: str = Field(..., description='The text prompt for image generation.') seed: Optional[int] = Field(None, description='The seed value for reproducibility.') - width: int = Field( - ..., description='The width of the image to generate.', ge=64, le=2048 + width: conint(ge=64, le=2048) = Field( + ..., description='The width of the image to generate.' ) @@ -40,6 +40,47 @@ class BFLFluxProGenerateResponse(BaseModel): polling_url: str = Field(..., description='URL to poll for the generation result.') +class ComfyNode(BaseModel): + category: Optional[str] = Field( + None, + description='UI category where the node is listed, used for grouping nodes.', + ) + comfy_node_name: Optional[str] = Field( + None, description='Unique identifier for the node' + ) + deprecated: Optional[bool] = Field( + None, + description='Indicates if the node is deprecated. Deprecated nodes are hidden in the UI.', + ) + description: Optional[str] = Field( + None, description="Brief description of the node's functionality or purpose." + ) + experimental: Optional[bool] = Field( + None, + description='Indicates if the node is experimental, subject to changes or removal.', + ) + function: Optional[str] = Field( + None, description='Name of the entry-point function to execute the node.' + ) + input_types: Optional[str] = Field(None, description='Defines input parameters') + output_is_list: Optional[List[bool]] = Field( + None, description='Boolean values indicating if each output is a list.' + ) + return_names: Optional[str] = Field( + None, description='Names of the outputs for clarity in workflows.' + ) + return_types: Optional[str] = Field( + None, description='Specifies the types of outputs produced by the node.' + ) + + +class ComfyNodeCloudBuildInfo(BaseModel): + build_id: Optional[str] = None + location: Optional[str] = None + project_id: Optional[str] = None + project_number: Optional[str] = None + + class Customer(BaseModel): createdAt: Optional[datetime] = Field( None, description='The date and time the user was created' @@ -69,11 +110,64 @@ class CustomerStorageResourceResponse(BaseModel): ) +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.' + ) + + class ErrorResponse(BaseModel): error: str message: str +class GitCommitSummary(BaseModel): + author: Optional[str] = Field(None, description='The author of the commit') + branch_name: Optional[str] = Field( + None, description='The branch where the commit was made' + ) + commit_hash: Optional[str] = Field(None, description='The hash of the commit') + commit_name: Optional[str] = Field(None, description='The name of the commit') + status_summary: Optional[Dict[str, str]] = Field( + None, description='A map of operating system to status pairs' + ) + timestamp: Optional[datetime] = Field( + None, description='The timestamp when the commit was made' + ) + + +class IdeogramColorPalette1(BaseModel): + name: str = Field(..., description='Name of the preset color palette') + + +class Member(BaseModel): + color: Optional[constr(pattern=r'^#[0-9A-Fa-f]{6}$')] = Field( + None, description='Hexadecimal color code' + ) + weight: Optional[confloat(ge=0.0, le=1.0)] = Field( + None, description='Optional weight for the color (0-1)' + ) + + +class IdeogramColorPalette2(BaseModel): + members: List[Member] = Field( + ..., description='Array of color definitions with optional weights' + ) + + +class IdeogramColorPalette( + RootModel[Union[IdeogramColorPalette1, IdeogramColorPalette2]] +): + root: Union[IdeogramColorPalette1, IdeogramColorPalette2] = Field( + ..., + description='A color palette specification that can either use a preset name or explicit color definitions with weights', + ) + + class ImageRequest(BaseModel): aspect_ratio: Optional[str] = Field( None, @@ -90,11 +184,8 @@ class ImageRequest(BaseModel): None, description='Optional. Description of what to exclude. Only for V_1, V_1_TURBO, V_2, V_2_TURBO.', ) - num_images: Optional[int] = Field( - 1, - description='Optional. Number of images to generate (1-8). Defaults to 1.', - ge=1, - le=8, + num_images: Optional[conint(ge=1, le=8)] = Field( + 1, description='Optional. Number of images to generate (1-8). Defaults to 1.' ) prompt: str = Field( ..., description='Required. The prompt to use to generate the image.' @@ -103,11 +194,8 @@ class ImageRequest(BaseModel): None, description="Optional. Resolution (e.g., 'RESOLUTION_1024_1024'). Only for model V_2. Cannot be used with aspect_ratio.", ) - seed: Optional[int] = Field( - None, - description='Optional. A number between 0 and 2147483647.', - ge=0, - le=2147483647, + seed: Optional[conint(ge=0, le=2147483647)] = Field( + None, description='Optional. A number between 0 and 2147483647.' ) style_type: Optional[str] = Field( None, @@ -150,6 +238,19 @@ class IdeogramGenerateResponse(BaseModel): ) +class ColorPalette(BaseModel): + name: str = Field(..., description='Name of the color palette', examples=['PASTEL']) + + +class MagicPrompt(str, Enum): + ON = 'ON' + OFF = 'OFF' + + +class StyleType(str, Enum): + GENERAL = 'GENERAL' + + class KlingErrorResponse(BaseModel): code: int = Field( ..., @@ -168,41 +269,29 @@ class AspectRatio(str, Enum): class Config(BaseModel): - horizontal: Optional[float] = Field( + horizontal: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls camera's movement along horizontal axis (x-axis). Negative indicates left, positive indicates right.", - ge=-10.0, - le=10.0, ) - pan: Optional[float] = Field( + pan: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls camera's rotation in vertical plane (x-axis). Negative indicates downward rotation, positive indicates upward rotation.", - ge=-10.0, - le=10.0, ) - roll: Optional[float] = Field( + roll: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.", - ge=-10.0, - le=10.0, ) - tilt: Optional[float] = Field( + tilt: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls camera's rotation in horizontal plane (y-axis). Negative indicates left rotation, positive indicates right rotation.", - ge=-10.0, - le=10.0, ) - vertical: Optional[float] = Field( + vertical: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward.", - ge=-10.0, - le=10.0, ) - zoom: Optional[float] = Field( + zoom: Optional[confloat(ge=-10.0, le=10.0)] = Field( None, description="Controls change in camera's focal length. Negative indicates narrower field of view, positive indicates wider field of view.", - ge=-10.0, - le=10.0, ) @@ -265,11 +354,9 @@ class KlingImage2VideoRequest(BaseModel): description='The callback notification address. Server will notify when the task status changes.', ) camera_control: Optional[CameraControl] = None - cfg_scale: Optional[float] = Field( + cfg_scale: Optional[confloat(ge=0.0, le=1.0)] = Field( 0.5, description="Flexibility in video generation. The higher the value, the lower the model's degree of flexibility, and the stronger the relevance to the user's prompt.", - ge=0.0, - le=1.0, ) duration: Optional[Duration] = Field('5', description='Video length in seconds') dynamic_masks: Optional[List[DynamicMask]] = Field( @@ -293,11 +380,11 @@ class KlingImage2VideoRequest(BaseModel): description='Video generation mode. std: Standard Mode, which is cost-effective. pro: Professional Mode, generates videos with longer duration but higher quality output.', ) model_name: Optional[ModelName] = Field('kling-v1', description='Model Name') - negative_prompt: Optional[str] = Field( - None, description='Negative text prompt', max_length=2500 + negative_prompt: Optional[constr(max_length=2500)] = Field( + None, description='Negative text prompt' ) - prompt: Optional[str] = Field( - None, description='Positive text prompt', max_length=2500 + prompt: Optional[constr(max_length=2500)] = Field( + None, description='Positive text prompt' ) static_mask: Optional[AnyUrl] = Field( None, @@ -343,12 +430,12 @@ class KlingImage2VideoResponse(BaseModel): class Config1(BaseModel): - horizontal: Optional[float] = Field(None, ge=-10.0, le=10.0) - pan: Optional[float] = Field(None, ge=-10.0, le=10.0) - roll: Optional[float] = Field(None, ge=-10.0, le=10.0) - tilt: Optional[float] = Field(None, ge=-10.0, le=10.0) - vertical: Optional[float] = Field(None, ge=-10.0, le=10.0) - zoom: Optional[float] = Field(None, ge=-10.0, le=10.0) + 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 CameraControl1(BaseModel): @@ -368,18 +455,18 @@ class KlingText2VideoRequest(BaseModel): None, description='The callback notification address' ) camera_control: Optional[CameraControl1] = None - cfg_scale: Optional[float] = Field( - 0.5, description='Flexibility in video generation', ge=0.0, le=1.0 + cfg_scale: Optional[confloat(ge=0.0, le=1.0)] = Field( + 0.5, description='Flexibility in video generation' ) duration: Optional[Duration] = '5' external_task_id: Optional[str] = Field(None, description='Customized Task ID') mode: Optional[Mode] = Field('std', description='Video generation mode') model_name: Optional[ModelName1] = Field('kling-v1', description='Model Name') - negative_prompt: Optional[str] = Field( - None, description='Negative text prompt', max_length=2500 + negative_prompt: Optional[constr(max_length=2500)] = Field( + None, description='Negative text prompt' ) - prompt: Optional[str] = Field( - None, description='Positive text prompt', max_length=2500 + prompt: Optional[constr(max_length=2500)] = Field( + None, description='Positive text prompt' ) @@ -550,6 +637,36 @@ class LumaVideoModelOutputResolution( root: Union[LumaVideoModelOutputResolution1, str] +class MachineStats(BaseModel): + cpu_capacity: Optional[str] = Field(None, description='Total CPU on the machine.') + disk_capacity: Optional[str] = Field( + None, description='Total disk capacity on the machine.' + ) + gpu_type: Optional[str] = Field( + None, description='The GPU type. eg. NVIDIA Tesla K80' + ) + initial_cpu: Optional[str] = Field( + None, description='Initial CPU available before the job starts.' + ) + initial_disk: Optional[str] = Field( + None, description='Initial disk available before the job starts.' + ) + initial_ram: Optional[str] = Field( + None, description='Initial RAM available before the job starts.' + ) + machine_name: Optional[str] = Field(None, description='Name of the machine.') + memory_capacity: Optional[str] = Field( + None, description='Total memory on the machine.' + ) + os_version: Optional[str] = Field( + None, description='The operating system version. eg. Ubuntu Linux 20.04' + ) + pip_freeze: Optional[str] = Field(None, description='The pip freeze output') + vram_time_series: Optional[Dict[str, Any]] = Field( + None, description='Time series of VRAM usage.' + ) + + class MinimaxBaseResponse(BaseModel): status_code: int = Field( ..., @@ -631,10 +748,9 @@ class MinimaxVideoGenerationRequest(BaseModel): ..., description='Required. ID of model. Options: T2V-01-Director, I2V-01-Director, S2V-01, I2V-01, I2V-01-live, T2V-01', ) - prompt: Optional[str] = Field( + 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].', - max_length=2000, ) prompt_optimizer: Optional[bool] = Field( True, @@ -653,6 +769,29 @@ class MinimaxVideoGenerationResponse(BaseModel): ) +class NodeStatus(str, Enum): + NodeStatusActive = 'NodeStatusActive' + NodeStatusDeleted = 'NodeStatusDeleted' + NodeStatusBanned = 'NodeStatusBanned' + + +class NodeVersionStatus(str, Enum): + NodeVersionStatusActive = 'NodeVersionStatusActive' + NodeVersionStatusDeleted = 'NodeVersionStatusDeleted' + NodeVersionStatusBanned = 'NodeVersionStatusBanned' + NodeVersionStatusPending = 'NodeVersionStatusPending' + NodeVersionStatusFlagged = 'NodeVersionStatusFlagged' + + +class NodeVersionUpdateRequest(BaseModel): + changelog: Optional[str] = Field( + None, description='The changelog describing the version changes.' + ) + deprecated: Optional[bool] = Field( + None, description='Whether the version is deprecated.' + ) + + class Moderation(str, Enum): low = 'low' auto = 'auto' @@ -794,13 +933,22 @@ class OpenAIImageGenerationResponse(BaseModel): usage: Optional[Usage] = None -class AspectRatio2(RootModel[float]): - root: float = Field( - ..., - description='Aspect ratio (width / height)', - ge=0.4, - le=2.5, - title='Aspectratio', +class PersonalAccessToken(BaseModel): + createdAt: Optional[datetime] = Field( + None, description='[Output Only]The date and time the token was created.' + ) + description: Optional[str] = Field( + None, + description="Optional. A more detailed description of the token's intended use.", + ) + id: Optional[UUID] = Field(None, description='Unique identifier for the GitCommit') + name: Optional[str] = Field( + None, + description='Required. The name of the token. Can be a simple description.', + ) + token: Optional[str] = Field( + None, + description='[Output Only]. The personal access token. Only returned during creation.', ) @@ -808,10 +956,9 @@ class IngredientsMode(str, Enum): creative = 'creative' precise = 'precise' -bytes_aliased = bytes class PikaBodyGenerate22C2vGenerate22PikascenesPost(BaseModel): - aspectRatio: Optional[AspectRatio2] = Field( + aspectRatio: Optional[confloat(ge=0.4, le=2.5)] = Field( None, description='Aspect ratio (width / height)', title='Aspectratio' ) duration: Optional[int] = Field(5, title='Duration') @@ -846,7 +993,7 @@ class PikaBodyGenerate22KeyframeGenerate22PikaframesPost(BaseModel): class PikaBodyGenerate22T2vGenerate22T2vPost(BaseModel): - aspectRatio: Optional[AspectRatio2] = Field( + aspectRatio: Optional[confloat(ge=0.4, le=2.5)] = Field( None, description='Aspect ratio (width / height)', title='Aspectratio' ) duration: Optional[int] = Field(5, title='Duration') @@ -925,7 +1072,7 @@ class PixverseImageVideoRequest(BaseModel): water_mark: Optional[bool] = None -class AspectRatio4(str, Enum): +class AspectRatio2(str, Enum): field_16_9 = '16:9' field_4_3 = '4:3' field_1_1 = '1:1' @@ -934,7 +1081,7 @@ class AspectRatio4(str, Enum): class PixverseTextVideoRequest(BaseModel): - aspect_ratio: AspectRatio4 + aspect_ratio: AspectRatio2 duration: Duration2 model: Model1 motion_mode: Optional[MotionMode] = None @@ -1004,26 +1151,29 @@ class PixverseVideoResultResponse(BaseModel): Resp: Optional[Resp2] = None -class RGBColorItem(RootModel[int]): - root: int = Field(..., ge=0, le=255) +class PublisherStatus(str, Enum): + PublisherStatusActive = 'PublisherStatusActive' + PublisherStatusBanned = 'PublisherStatusBanned' -class RGBColor(RootModel[List[RGBColorItem]]): - root: List[RGBColorItem] = Field( - ..., - description='RGB color values', - examples=[[255, 0, 0]], - max_length=3, - min_length=3, - ) +class PublisherUser(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.') + name: Optional[str] = Field(None, description='The name for this user.') + + +class RgbItem(RootModel[conint(ge=0, le=255)]): + root: conint(ge=0, le=255) + + +class RGBColor(BaseModel): + rgb: List[RgbItem] = Field(..., max_length=3, min_length=3) class Controls(BaseModel): - artistic_level: Optional[int] = Field( + artistic_level: Optional[conint(ge=0, le=5)] = Field( None, description='Defines artistic tone of your image. At a simple level, the person looks straight at the camera in a static and clean style. Dynamic and eccentric levels introduce movement and creativity.', - ge=0, - le=5, ) background_color: Optional[RGBColor] = None colors: Optional[List[RGBColor]] = Field( @@ -1039,7 +1189,7 @@ class RecraftImageGenerationRequest(BaseModel): model: str = Field( ..., description='The model to use for generation (e.g., "recraftv3")' ) - n: int = Field(..., description='The number of images to generate', ge=1, le=4) + n: conint(ge=1, le=4) = Field(..., description='The number of images to generate') prompt: str = Field( ..., description='The text prompt describing the image to generate' ) @@ -1050,6 +1200,10 @@ class RecraftImageGenerationRequest(BaseModel): None, description='The style to apply to the generated image (e.g., "digital_illustration")', ) + style_id: Optional[str] = Field( + None, + description='The style ID to apply to the generated image (e.g., "123e4567-e89b-12d3-a456-426614174000"). If style_id is provided, style should not be provided.', + ) class Datum2(BaseModel): @@ -1067,6 +1221,12 @@ class RecraftImageGenerationResponse(BaseModel): data: List[Datum2] = Field(..., description='Array of generated image information') +class RenderingSpeed(str, Enum): + BALANCED = 'BALANCED' + TURBO = 'TURBO' + QUALITY = 'QUALITY' + + class RunwayAspectRatioEnum(str, Enum): field_1280_720 = '1280:720' field_720_1280 = '720:1280' @@ -1102,7 +1262,7 @@ class RunwayPromptImageDetailedObject(BaseModel): ..., description="The position of the image in the output video. 'last' is currently supported for gen3a_turbo only.", ) - uri: str = Field( + uri: AnyUrl = Field( ..., description='A HTTPS URL or data URI containing an encoded image.' ) @@ -1132,6 +1292,210 @@ class RunwayTaskStatusResponse(BaseModel): status: Optional[RunwayTaskStatusEnum] = None +class Name(str, Enum): + content_moderation = 'content_moderation' + + +class StabilityContentModerationResponse(BaseModel): + errors: List[str] = Field( + ..., + description='One or more error messages indicating what went wrong.', + examples=[['some-field: is required']], + min_length=1, + ) + id: constr(min_length=1) = Field( + ..., + description='A unique identifier associated with this error. Please include this in any [support tickets](https://kb.stability.ai/knowledge-base/kb-tickets/new) you file, as it will greatly assist us in diagnosing the root cause of the problem.', + examples=['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], + ) + name: Name = Field( + ..., + description='Our content moderation system has flagged some part of your request and subsequently denied it. You were not charged for this request. While this may at times be frustrating, it is necessary to maintain the integrity of our platform and ensure a safe experience for all users. If you would like to provide feedback, please use the [Support Form](https://kb.stability.ai/knowledge-base/kb-tickets/new).', + ) + + +class StabilityStabilityClientID(RootModel[constr(max_length=256)]): + root: constr(max_length=256) = Field( + ..., + description='The name of your application, used to help us communicate app-specific debugging or moderation issues to you.', + examples=['my-awesome-app'], + ) + + +class StabilityStabilityClientUserID(RootModel[constr(max_length=256)]): + root: constr(max_length=256) = Field( + ..., + description='A unique identifier for your end user. Used to help us communicate user-specific debugging or moderation issues to you. Feel free to obfuscate this value to protect user privacy.', + examples=['DiscordUser#9999'], + ) + + +class StabilityStabilityClientVersion(RootModel[constr(max_length=256)]): + root: constr(max_length=256) = Field( + ..., + description='The version of your application, used to help us communicate version-specific debugging or moderation issues to you.', + examples=['1.2.1'], + ) + + +class StorageFile(BaseModel): + file_path: Optional[str] = Field(None, description='Path to the file in storage') + id: Optional[UUID] = Field( + None, description='Unique identifier for the storage file' + ) + public_url: Optional[str] = Field(None, description='Public URL') + + +class StripeAddress(BaseModel): + city: Optional[str] = None + country: Optional[str] = None + line1: Optional[str] = None + line2: Optional[str] = None + postal_code: Optional[str] = None + state: Optional[str] = None + + +class StripeAmountDetails(BaseModel): + tip: Optional[Dict[str, Any]] = None + + +class StripeBillingDetails(BaseModel): + address: Optional[StripeAddress] = None + email: Optional[str] = None + name: Optional[str] = None + phone: Optional[str] = None + tax_id: Optional[Any] = None + + +class Checks(BaseModel): + address_line1_check: Optional[Any] = None + address_postal_code_check: Optional[Any] = None + cvc_check: Optional[str] = None + + +class ExtendedAuthorization(BaseModel): + status: Optional[str] = None + + +class IncrementalAuthorization(BaseModel): + status: Optional[str] = None + + +class Multicapture(BaseModel): + status: Optional[str] = None + + +class NetworkToken(BaseModel): + used: Optional[bool] = None + + +class Overcapture(BaseModel): + maximum_amount_capturable: Optional[int] = None + status: Optional[str] = None + + +class StripeCardDetails(BaseModel): + amount_authorized: Optional[int] = None + authorization_code: Optional[Any] = None + brand: Optional[str] = None + checks: Optional[Checks] = None + country: Optional[str] = None + exp_month: Optional[int] = None + exp_year: Optional[int] = None + extended_authorization: Optional[ExtendedAuthorization] = None + fingerprint: Optional[str] = None + funding: Optional[str] = None + incremental_authorization: Optional[IncrementalAuthorization] = None + installments: Optional[Any] = None + last4: Optional[str] = None + mandate: Optional[Any] = None + multicapture: Optional[Multicapture] = None + network: Optional[str] = None + network_token: Optional[NetworkToken] = None + network_transaction_id: Optional[str] = None + overcapture: Optional[Overcapture] = None + regulated_status: Optional[str] = None + three_d_secure: Optional[Any] = None + wallet: Optional[Any] = None + + +class Object(str, Enum): + charge = 'charge' + + +class Object1(str, Enum): + event = 'event' + + +class Type4(str, Enum): + payment_intent_succeeded = 'payment_intent.succeeded' + + +class StripeOutcome(BaseModel): + advice_code: Optional[Any] = None + network_advice_code: Optional[Any] = None + network_decline_code: Optional[Any] = None + network_status: Optional[str] = None + reason: Optional[Any] = None + risk_level: Optional[str] = None + risk_score: Optional[int] = None + seller_message: Optional[str] = None + type: Optional[str] = None + + +class Object2(str, Enum): + payment_intent = 'payment_intent' + + +class StripePaymentMethodDetails(BaseModel): + card: Optional[StripeCardDetails] = None + type: Optional[str] = None + + +class Card(BaseModel): + installments: Optional[Any] = None + mandate_options: Optional[Any] = None + network: Optional[Any] = None + request_three_d_secure: Optional[str] = None + + +class StripePaymentMethodOptions(BaseModel): + card: Optional[Card] = None + + +class StripeRefundList(BaseModel): + data: Optional[List[Dict[str, Any]]] = None + has_more: Optional[bool] = None + object: Optional[str] = None + total_count: Optional[int] = None + url: Optional[str] = None + + +class StripeRequestInfo(BaseModel): + id: Optional[str] = None + idempotency_key: Optional[str] = None + + +class StripeShipping(BaseModel): + address: Optional[StripeAddress] = None + carrier: Optional[str] = None + name: Optional[str] = None + phone: Optional[str] = None + tracking_number: Optional[str] = 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.' + ) + isApproved: Optional[bool] = Field( + None, description='Indicates if the user is approved.' + ) + name: Optional[str] = Field(None, description='The name for this user.') + + class Veo2GenVidPollRequest(BaseModel): operationName: str = Field( ..., @@ -1142,7 +1506,7 @@ class Veo2GenVidPollRequest(BaseModel): ) -class Error(BaseModel): +class Error1(BaseModel): code: Optional[int] = Field(None, description='Error code') message: Optional[str] = Field(None, description='Error message') @@ -1174,7 +1538,7 @@ class Response(BaseModel): class Veo2GenVidPollResponse(BaseModel): done: Optional[bool] = None - error: Optional[Error] = Field( + error: Optional[Error1] = Field( None, description='Error details if operation failed' ) name: Optional[str] = None @@ -1235,6 +1599,125 @@ class Veo2GenVidResponse(BaseModel): ) +class WorkflowRunStatus(str, Enum): + WorkflowRunStatusStarted = 'WorkflowRunStatusStarted' + WorkflowRunStatusFailed = 'WorkflowRunStatusFailed' + WorkflowRunStatusCompleted = 'WorkflowRunStatusCompleted' + + +class ActionJobResult(BaseModel): + action_job_id: Optional[str] = Field( + None, description='Identifier of the job this result belongs to' + ) + action_run_id: Optional[str] = Field( + None, description='Identifier of the run this result belongs to' + ) + author: Optional[str] = Field(None, description='The author of the commit') + avg_vram: Optional[int] = Field( + None, description='The average VRAM used by the job' + ) + branch_name: Optional[str] = Field( + None, description='Name of the relevant git branch' + ) + comfy_run_flags: Optional[str] = Field( + None, description='The comfy run flags. E.g. `--low-vram`' + ) + commit_hash: Optional[str] = Field(None, description='The hash of the commit') + commit_id: Optional[str] = Field(None, description='The ID of the commit') + commit_message: Optional[str] = Field(None, description='The message of the commit') + commit_time: Optional[int] = Field( + None, description='The Unix timestamp when the commit was made' + ) + cuda_version: Optional[str] = Field(None, description='CUDA version used') + end_time: Optional[int] = Field( + None, description='The end time of the job as a Unix timestamp.' + ) + git_repo: Optional[str] = Field(None, description='The repository name') + id: Optional[UUID] = Field(None, description='Unique identifier for the job result') + job_trigger_user: Optional[str] = Field( + None, description='The user who triggered the job.' + ) + machine_stats: Optional[MachineStats] = None + operating_system: Optional[str] = Field(None, description='Operating system used') + peak_vram: Optional[int] = Field(None, description='The peak VRAM used by the job') + pr_number: Optional[str] = Field(None, description='The pull request number') + python_version: Optional[str] = Field(None, description='PyTorch version used') + pytorch_version: Optional[str] = Field(None, description='PyTorch version used') + start_time: Optional[int] = Field( + None, description='The start time of the job as a Unix timestamp.' + ) + status: Optional[WorkflowRunStatus] = None + storage_file: Optional[StorageFile] = None + workflow_name: Optional[str] = Field(None, description='Name of the workflow') + + +class IdeogramV3EditRequest(BaseModel): + color_palette: Optional[IdeogramColorPalette] = None + image: Optional[bytes_aliased] = Field( + None, + description='The image being edited (max size 10MB); only JPEG, WebP and PNG formats are supported at this time.', + ) + magic_prompt: Optional[str] = Field( + None, + description='Determine if MagicPrompt should be used in generating the request or not.', + ) + mask: Optional[bytes_aliased] = Field( + None, + description='A black and white image of the same size as the image being edited (max size 10MB). Black regions in the mask should match up with the regions of the image that you would like to edit; only JPEG, WebP and PNG formats are supported at this time.', + ) + num_images: Optional[int] = Field( + None, description='The number of images to generate.' + ) + prompt: str = Field( + ..., description='The prompt used to describe the edited result.' + ) + rendering_speed: RenderingSpeed + seed: Optional[int] = Field( + None, description='Random seed. Set for reproducible generation.' + ) + style_codes: Optional[List[constr(pattern=r'^[0-9A-Fa-f]{8}$')]] = Field( + None, + description='A list of 8 character hexadecimal codes representing the style of the image. Cannot be used in conjunction with style_reference_images or style_type.', + ) + style_reference_images: Optional[List[bytes_aliased]] = Field( + None, + description='A set of images to use as style references (maximum total size 10MB across all style references). The images should be in JPEG, PNG or WebP format.', + ) + + +class IdeogramV3Request(BaseModel): + aspect_ratio: Optional[str] = Field( + None, description='Aspect ratio in format WxH', examples=['1x3'] + ) + color_palette: Optional[ColorPalette] = None + magic_prompt: Optional[MagicPrompt] = Field( + None, description='Whether to enable magic prompt enhancement' + ) + negative_prompt: Optional[str] = Field( + None, description='Text prompt specifying what to avoid in the generation' + ) + num_images: Optional[conint(ge=1)] = Field( + None, description='Number of images to generate' + ) + prompt: str = Field(..., description='The text prompt for image generation') + rendering_speed: RenderingSpeed + resolution: Optional[str] = Field( + None, description='Image resolution in format WxH', examples=['1280x800'] + ) + seed: Optional[int] = Field( + None, description='Seed value for reproducible generation' + ) + style_codes: Optional[List[constr(pattern=r'^[0-9A-Fa-f]{8}$')]] = Field( + None, description='Array of style codes in hexadecimal format' + ) + style_reference_images: Optional[List[str]] = Field( + None, description='Array of reference image URLs or identifiers' + ) + style_type: Optional[StyleType] = Field( + None, description='The type of style to apply' + ) + + class LumaGenerationRequest(BaseModel): aspect_ratio: LumaAspectRatio callback_url: Optional[AnyUrl] = Field( @@ -1276,23 +1759,169 @@ class LumaUpscaleVideoGenerationRequest(BaseModel): resolution: Optional[LumaVideoModelOutputResolution] = None +class NodeVersion(BaseModel): + changelog: Optional[str] = Field( + None, description='Summary of changes made in this version' + ) + comfy_node_extract_status: Optional[str] = Field( + None, description='The status of comfy node extraction process.' + ) + createdAt: Optional[datetime] = Field( + None, description='The date and time the version was created.' + ) + dependencies: Optional[List[str]] = Field( + None, description='A list of pip dependencies required by the node.' + ) + deprecated: Optional[bool] = Field( + None, description='Indicates if this version is deprecated.' + ) + downloadUrl: Optional[str] = Field( + None, description='[Output Only] URL to download this version of the node' + ) + id: Optional[str] = None + node_id: Optional[str] = Field( + None, description='The unique identifier of the node.' + ) + status: Optional[NodeVersionStatus] = None + status_reason: Optional[str] = Field( + None, description='The reason for the status change.' + ) + version: Optional[str] = Field( + None, + description='The version identifier, following semantic versioning. Must be unique for the node.', + ) + + class PikaHTTPValidationError(BaseModel): detail: Optional[List[PikaValidationError]] = Field(None, title='Detail') +class PublisherMember(BaseModel): + id: Optional[str] = Field( + None, description='The unique identifier for the publisher member.' + ) + role: Optional[str] = Field( + None, description='The role of the user in the publisher.' + ) + user: Optional[PublisherUser] = None + + class RunwayImageToVideoRequest(BaseModel): duration: RunwayDurationEnum model: RunwayModelEnum promptImage: RunwayPromptImageObject - promptText: Optional[str] = Field( - None, description='Text prompt for the generation', max_length=1000 + promptText: Optional[constr(max_length=1000)] = Field( + None, description='Text prompt for the generation' ) ratio: RunwayAspectRatioEnum - seed: int = Field( - ..., description='Random seed for generation', ge=0, le=4294967295 + seed: conint(ge=0, le=4294967295) = Field( + ..., description='Random seed for generation' ) +class StripeCharge(BaseModel): + amount: Optional[int] = None + amount_captured: Optional[int] = None + amount_refunded: Optional[int] = None + application: Optional[str] = None + application_fee: Optional[str] = None + application_fee_amount: Optional[int] = None + balance_transaction: Optional[str] = None + billing_details: Optional[StripeBillingDetails] = None + calculated_statement_descriptor: Optional[str] = None + captured: Optional[bool] = None + created: Optional[int] = None + currency: Optional[str] = None + customer: Optional[str] = None + description: Optional[str] = None + destination: Optional[Any] = None + dispute: Optional[Any] = None + disputed: Optional[bool] = None + failure_balance_transaction: Optional[Any] = None + failure_code: Optional[Any] = None + failure_message: Optional[Any] = None + fraud_details: Optional[Dict[str, Any]] = None + id: Optional[str] = None + invoice: Optional[Any] = None + livemode: Optional[bool] = None + metadata: Optional[Dict[str, Any]] = None + object: Optional[Object] = None + on_behalf_of: Optional[Any] = None + order: Optional[Any] = None + outcome: Optional[StripeOutcome] = None + paid: Optional[bool] = None + payment_intent: Optional[str] = None + payment_method: Optional[str] = None + payment_method_details: Optional[StripePaymentMethodDetails] = None + radar_options: Optional[Dict[str, Any]] = None + receipt_email: Optional[str] = None + receipt_number: Optional[str] = None + receipt_url: Optional[str] = None + refunded: Optional[bool] = None + refunds: Optional[StripeRefundList] = None + review: Optional[Any] = None + shipping: Optional[StripeShipping] = None + source: Optional[Any] = None + source_transfer: Optional[Any] = None + statement_descriptor: Optional[Any] = None + statement_descriptor_suffix: Optional[Any] = None + status: Optional[str] = None + transfer_data: Optional[Any] = None + transfer_group: Optional[Any] = None + + +class StripeChargeList(BaseModel): + data: Optional[List[StripeCharge]] = None + has_more: Optional[bool] = None + object: Optional[str] = None + total_count: Optional[int] = None + url: Optional[str] = None + + +class StripePaymentIntent(BaseModel): + amount: Optional[int] = None + amount_capturable: Optional[int] = None + amount_details: Optional[StripeAmountDetails] = None + amount_received: Optional[int] = None + application: Optional[str] = None + application_fee_amount: Optional[int] = None + automatic_payment_methods: Optional[Any] = None + canceled_at: Optional[int] = None + cancellation_reason: Optional[str] = None + capture_method: Optional[str] = None + charges: Optional[StripeChargeList] = None + client_secret: Optional[str] = None + confirmation_method: Optional[str] = None + created: Optional[int] = None + currency: Optional[str] = None + customer: Optional[str] = None + description: Optional[str] = None + id: Optional[str] = None + invoice: Optional[str] = None + last_payment_error: Optional[Any] = None + latest_charge: Optional[str] = None + livemode: Optional[bool] = None + metadata: Optional[Dict[str, Any]] = None + next_action: Optional[Any] = None + object: Optional[Object2] = None + on_behalf_of: Optional[Any] = None + payment_method: Optional[str] = None + payment_method_configuration_details: Optional[Any] = None + payment_method_options: Optional[StripePaymentMethodOptions] = None + payment_method_types: Optional[List[str]] = None + processing: Optional[Any] = None + receipt_email: Optional[str] = None + review: Optional[Any] = None + setup_future_usage: Optional[Any] = None + shipping: Optional[StripeShipping] = None + source: Optional[Any] = None + statement_descriptor: Optional[Any] = None + statement_descriptor_suffix: Optional[Any] = None + status: Optional[str] = None + transfer_data: Optional[Any] = None + transfer_group: Optional[Any] = None + + class LumaGeneration(BaseModel): assets: Optional[LumaAssets] = None created_at: Optional[datetime] = Field( @@ -1313,3 +1942,64 @@ class LumaGeneration(BaseModel): ] ] = Field(None, description='The request of the generation') state: Optional[LumaState] = None + + +class Publisher(BaseModel): + createdAt: Optional[datetime] = Field( + None, description='The date and time the publisher was created.' + ) + description: Optional[str] = None + id: Optional[str] = Field( + None, + description="The unique identifier for the publisher. It's akin to a username. Should be lowercase.", + ) + logo: Optional[str] = Field(None, description="URL to the publisher's logo.") + members: Optional[List[PublisherMember]] = Field( + None, description='A list of members in the publisher.' + ) + name: Optional[str] = None + source_code_repo: Optional[str] = None + status: Optional[PublisherStatus] = None + support: Optional[str] = None + website: Optional[str] = None + + +class Data2(BaseModel): + object: Optional[StripePaymentIntent] = None + + +class StripeEvent(BaseModel): + api_version: Optional[str] = None + created: Optional[int] = None + data: Data2 + id: str + livemode: Optional[bool] = None + object: Object1 + pending_webhooks: Optional[int] = None + request: Optional[StripeRequestInfo] = None + type: Type4 + + +class Node(BaseModel): + author: Optional[str] = None + category: Optional[str] = Field(None, description='The category of the node.') + description: Optional[str] = None + downloads: Optional[int] = Field( + None, description='The number of downloads of the node.' + ) + icon: Optional[str] = Field(None, description="URL to the node's icon.") + id: Optional[str] = Field(None, description='The unique identifier of the node.') + latest_version: Optional[NodeVersion] = None + license: Optional[str] = Field( + None, description="The path to the LICENSE file in the node's repository." + ) + name: Optional[str] = Field(None, description='The display name of the node.') + publisher: Optional[Publisher] = None + rating: Optional[float] = Field(None, description='The average rating of the node.') + repository: Optional[str] = Field(None, description="URL to the node's repository.") + status: Optional[NodeStatus] = None + status_detail: Optional[str] = Field( + None, description='The status detail of the node.' + ) + tags: Optional[List[str]] = None + translations: Optional[Dict[str, Dict[str, Any]]] = None diff --git a/comfy_api_nodes/apis/client.py b/comfy_api_nodes/apis/client.py index 22e011f47..b376aafe6 100644 --- a/comfy_api_nodes/apis/client.py +++ b/comfy_api_nodes/apis/client.py @@ -431,6 +431,11 @@ class SynchronousOperation(Generic[T, R]): else self.request.model_dump(exclude_none=True) ) + if request_dict: + for key, value in request_dict.items(): + if isinstance(value, Enum): + request_dict[key] = value.value + # Debug log for request logging.debug( f"[DEBUG] API Request: {self.endpoint.method.value} {self.endpoint.path}" diff --git a/comfy_api_nodes/nodes_ideogram.py b/comfy_api_nodes/nodes_ideogram.py index 653e797a0..51e81286f 100644 --- a/comfy_api_nodes/nodes_ideogram.py +++ b/comfy_api_nodes/nodes_ideogram.py @@ -1,9 +1,14 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict from inspect import cleandoc +from PIL import Image +import numpy as np +import io from comfy_api_nodes.apis import ( IdeogramGenerateRequest, IdeogramGenerateResponse, ImageRequest, + IdeogramV3Request, + IdeogramV3EditRequest, ) from comfy_api_nodes.apis.client import ( @@ -17,7 +22,7 @@ from comfy_api_nodes.apinode_utils import ( bytesio_to_image_tensor, ) -RESOLUTION_MAPPING = { +V1_V1_RES_MAP = { "Auto":"AUTO", "512 x 1536":"RESOLUTION_512_1536", "576 x 1408":"RESOLUTION_576_1408", @@ -99,7 +104,7 @@ RESOLUTION_MAPPING = { "1536 x 640":"RESOLUTION_1536_640", } -ASPECT_RATIO_MAPPING = { +V1_V2_RATIO_MAP = { "1:1":"ASPECT_1_1", "4:3":"ASPECT_4_3", "3:4":"ASPECT_3_4", @@ -113,6 +118,97 @@ ASPECT_RATIO_MAPPING = { "5:4":"ASPECT_5_4", } +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" +] + def download_and_process_image(image_url): """Helper function to download and process image from URL""" @@ -156,7 +252,7 @@ class IdeogramV1(ComfyNodeABC): "aspect_ratio": ( IO.COMBO, { - "options": list(ASPECT_RATIO_MAPPING.keys()), + "options": list(V1_V2_RATIO_MAP.keys()), "default": "1:1", "tooltip": "The aspect ratio for image generation.", }, @@ -214,7 +310,7 @@ class IdeogramV1(ComfyNodeABC): auth_token=None, ): # Determine the model based on turbo setting - aspect_ratio = ASPECT_RATIO_MAPPING.get(aspect_ratio, None) + aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) model = "V_1_TURBO" if turbo else "V_1" operation = SynchronousOperation( @@ -286,7 +382,7 @@ class IdeogramV2(ComfyNodeABC): "aspect_ratio": ( IO.COMBO, { - "options": list(ASPECT_RATIO_MAPPING.keys()), + "options": list(V1_V2_RATIO_MAP.keys()), "default": "1:1", "tooltip": "The aspect ratio for image generation. Ignored if resolution is not set to AUTO.", }, @@ -294,7 +390,7 @@ class IdeogramV2(ComfyNodeABC): "resolution": ( IO.COMBO, { - "options": list(RESOLUTION_MAPPING.keys()), + "options": list(V1_V1_RES_MAP.keys()), "default": "Auto", "tooltip": "The resolution for image generation. If not set to AUTO, this overrides the aspect_ratio setting.", }, @@ -370,8 +466,8 @@ class IdeogramV2(ComfyNodeABC): color_palette="", auth_token=None, ): - aspect_ratio = ASPECT_RATIO_MAPPING.get(aspect_ratio, None) - resolution = RESOLUTION_MAPPING.get(resolution, None) + aspect_ratio = V1_V2_RATIO_MAP.get(aspect_ratio, None) + resolution = V1_V1_RES_MAP.get(resolution, None) # Determine the model based on turbo setting model = "V_2_TURBO" if turbo else "V_2" @@ -422,11 +518,11 @@ class IdeogramV2(ComfyNodeABC): return (download_and_process_image(image_url),) - class IdeogramV3(ComfyNodeABC): """ Generates images synchronously using the Ideogram V3 model. + Supports both regular image generation from text prompts and image editing with mask. Images links are available for a limited period of time; if you would like to keep the image, you must download it. """ @@ -442,17 +538,39 @@ class IdeogramV3(ComfyNodeABC): { "multiline": True, "default": "", - "tooltip": "Prompt for the image generation", + "tooltip": "Prompt for the image generation or editing", }, ), }, "optional": { + "image": ( + IO.IMAGE, + { + "default": None, + "tooltip": "Optional reference image for image editing.", + }, + ), + "mask": ( + IO.MASK, + { + "default": None, + "tooltip": "Optional mask for inpainting (white areas will be replaced)", + }, + ), "aspect_ratio": ( IO.COMBO, { - "options": list(ASPECT_RATIO_MAPPING.keys()), + "options": list(V3_RATIO_MAP.keys()), "default": "1:1", - "tooltip": "The aspect ratio for image generation.", + "tooltip": "The aspect ratio for image generation. Ignored if resolution is not set to Auto.", + }, + ), + "resolution": ( + IO.COMBO, + { + "options": V3_RESOLUTIONS, + "default": "Auto", + "tooltip": "The resolution for image generation. If not set to Auto, this overrides the aspect_ratio setting.", }, ), "magic_prompt_option": ( @@ -478,6 +596,14 @@ class IdeogramV3(ComfyNodeABC): IO.INT, {"default": 1, "min": 1, "max": 8, "step": 1, "display": "number"}, ), + "rendering_speed": ( + IO.COMBO, + { + "options": ["BALANCED", "TURBO", "QUALITY"], + "default": "BALANCED", + "tooltip": "Controls the trade-off between generation speed and quality", + }, + ), }, "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, } @@ -491,38 +617,119 @@ class IdeogramV3(ComfyNodeABC): def api_call( self, prompt, - aspect_ratio="ASPECT_1_1", + image=None, + mask=None, + resolution="Auto", + aspect_ratio="1:1", magic_prompt_option="AUTO", seed=0, num_images=1, + rendering_speed="BALANCED", auth_token=None, ): - aspect_ratio = ASPECT_RATIO_MAPPING.get(aspect_ratio, None) - # V3 model - no turbo option - model = "V_3" + # Check if both image and mask are provided for editing mode + if image is not None and mask is not None: + # Edit mode + path = "/proxy/ideogram/ideogram-v3/edit" - 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, - magic_prompt_option=( - magic_prompt_option if magic_prompt_option != "AUTO" else None - ), - ) - ), - auth_token=auth_token, - ) + # Process image and mask + input_tensor = image.squeeze().cpu() + # 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 = io.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 = io.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 + + # Execute the operation for edit mode + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=path, + method=HttpMethod.POST, + request_model=IdeogramV3EditRequest, + response_model=IdeogramGenerateResponse, + ), + request=edit_request, + files={ + "image": img_binary, + "mask": mask_binary, + }, + content_type="multipart/form-data", + auth_token=auth_token, + ) + + 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: + # Generation mode + path = "/proxy/ideogram/ideogram-v3/generate" + + # 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 + + # Execute the operation for generation mode + operation = SynchronousOperation( + endpoint=ApiEndpoint( + path=path, + method=HttpMethod.POST, + request_model=IdeogramV3Request, + response_model=IdeogramGenerateResponse, + ), + request=gen_request, + auth_token=auth_token, + ) + + # Execute the operation and process response response = operation.execute() if not response.data or len(response.data) == 0: @@ -534,15 +741,14 @@ class IdeogramV3(ComfyNodeABC): return (download_and_process_image(image_url),) - NODE_CLASS_MAPPINGS = { "IdeogramV1": IdeogramV1, "IdeogramV2": IdeogramV2, - #"IdeogramV3": IdeogramV3, + "IdeogramV3": IdeogramV3, } NODE_DISPLAY_NAME_MAPPINGS = { "IdeogramV1": "Ideogram V1", "IdeogramV2": "Ideogram V2", - #"IdeogramV3": "Ideogram V3", + "IdeogramV3": "Ideogram V3", }