1- from io import BytesIO
1+ import base64
22import os
33from enum import Enum
4- from inspect import cleandoc
4+ from io import BytesIO
5+
56import numpy as np
67import torch
78from PIL import Image
8- import folder_paths
9- import base64
10- from comfy_api .latest import IO , ComfyExtension
119from typing_extensions import override
1210
13-
11+ import folder_paths
12+ from comfy_api .latest import IO , ComfyExtension , Input
1413from comfy_api_nodes .apis import (
15- OpenAIImageGenerationRequest ,
16- OpenAIImageEditRequest ,
17- OpenAIImageGenerationResponse ,
18- OpenAICreateResponse ,
19- OpenAIResponse ,
2014 CreateModelResponseProperties ,
21- Item ,
22- OutputContent ,
23- InputImageContent ,
2415 Detail ,
25- InputTextContent ,
26- InputMessage ,
27- InputMessageContentList ,
2816 InputContent ,
2917 InputFileContent ,
18+ InputImageContent ,
19+ InputMessage ,
20+ InputMessageContentList ,
21+ InputTextContent ,
22+ Item ,
23+ OpenAICreateResponse ,
24+ OpenAIResponse ,
25+ OutputContent ,
26+ )
27+ from comfy_api_nodes .apis .openai_api import (
28+ OpenAIImageEditRequest ,
29+ OpenAIImageGenerationRequest ,
30+ OpenAIImageGenerationResponse ,
3031)
31-
3232from comfy_api_nodes .util import (
33- downscale_image_tensor ,
34- download_url_to_bytesio ,
35- validate_string ,
36- tensor_to_base64_string ,
3733 ApiEndpoint ,
38- sync_op ,
34+ download_url_to_bytesio ,
35+ downscale_image_tensor ,
3936 poll_op ,
37+ sync_op ,
38+ tensor_to_base64_string ,
4039 text_filepath_to_data_uri ,
40+ validate_string ,
4141)
4242
43-
4443RESPONSES_ENDPOINT = "/proxy/openai/v1/responses"
4544STARTING_POINT_ID_PATTERN = r"<starting_point_id:(.*)>"
4645
@@ -98,17 +97,14 @@ async def validate_and_cast_response(response, timeout: int = None) -> torch.Ten
9897
9998
10099class OpenAIDalle2 (IO .ComfyNode ):
101- """
102- Generates images synchronously via OpenAI's DALL·E 2 endpoint.
103- """
104100
105101 @classmethod
106102 def define_schema (cls ):
107103 return IO .Schema (
108104 node_id = "OpenAIDalle2" ,
109105 display_name = "OpenAI DALL·E 2" ,
110106 category = "api node/image/OpenAI" ,
111- description = cleandoc ( cls . __doc__ or "" ) ,
107+ description = "Generates images synchronously via OpenAI's DALL·E 2 endpoint." ,
112108 inputs = [
113109 IO .String .Input (
114110 "prompt" ,
@@ -234,17 +230,14 @@ async def execute(
234230
235231
236232class OpenAIDalle3 (IO .ComfyNode ):
237- """
238- Generates images synchronously via OpenAI's DALL·E 3 endpoint.
239- """
240233
241234 @classmethod
242235 def define_schema (cls ):
243236 return IO .Schema (
244237 node_id = "OpenAIDalle3" ,
245238 display_name = "OpenAI DALL·E 3" ,
246239 category = "api node/image/OpenAI" ,
247- description = cleandoc ( cls . __doc__ or "" ) ,
240+ description = "Generates images synchronously via OpenAI's DALL·E 3 endpoint." ,
248241 inputs = [
249242 IO .String .Input (
250243 "prompt" ,
@@ -326,24 +319,30 @@ async def execute(
326319 return IO .NodeOutput (await validate_and_cast_response (response ))
327320
328321
322+ def calculate_tokens_price_image_1 (response : OpenAIImageGenerationResponse ) -> float | None :
323+ # https://platform.openai.com/docs/pricing
324+ return ((response .usage .input_tokens * 10.0 ) + (response .usage .output_tokens * 40.0 )) / 1_000_000.0
325+
326+
327+ def calculate_tokens_price_image_1_5 (response : OpenAIImageGenerationResponse ) -> float | None :
328+ return ((response .usage .input_tokens * 8.0 ) + (response .usage .output_tokens * 32.0 )) / 1_000_000.0
329+
330+
329331class OpenAIGPTImage1 (IO .ComfyNode ):
330- """
331- Generates images synchronously via OpenAI's GPT Image 1 endpoint.
332- """
333332
334333 @classmethod
335334 def define_schema (cls ):
336335 return IO .Schema (
337336 node_id = "OpenAIGPTImage1" ,
338337 display_name = "OpenAI GPT Image 1" ,
339338 category = "api node/image/OpenAI" ,
340- description = cleandoc ( cls . __doc__ or "" ) ,
339+ description = "Generates images synchronously via OpenAI's GPT Image 1 endpoint." ,
341340 inputs = [
342341 IO .String .Input (
343342 "prompt" ,
344343 default = "" ,
345344 multiline = True ,
346- tooltip = "Text prompt for GPT Image 1 " ,
345+ tooltip = "Text prompt for GPT Image" ,
347346 ),
348347 IO .Int .Input (
349348 "seed" ,
@@ -365,8 +364,8 @@ def define_schema(cls):
365364 ),
366365 IO .Combo .Input (
367366 "background" ,
368- default = "opaque " ,
369- options = ["opaque" , "transparent" ],
367+ default = "auto " ,
368+ options = ["auto" , " opaque" , "transparent" ],
370369 tooltip = "Return image with or without background" ,
371370 optional = True ,
372371 ),
@@ -397,6 +396,11 @@ def define_schema(cls):
397396 tooltip = "Optional mask for inpainting (white areas will be replaced)" ,
398397 optional = True ,
399398 ),
399+ IO .Combo .Input (
400+ "model" ,
401+ options = ["gpt-image-1" , "gpt-image-1.5" ],
402+ optional = True ,
403+ ),
400404 ],
401405 outputs = [
402406 IO .Image .Output (),
@@ -412,32 +416,34 @@ def define_schema(cls):
412416 @classmethod
413417 async def execute (
414418 cls ,
415- prompt ,
416- seed = 0 ,
417- quality = "low" ,
418- background = "opaque" ,
419- image = None ,
420- mask = None ,
421- n = 1 ,
422- size = "1024x1024" ,
419+ prompt : str ,
420+ seed : int = 0 ,
421+ quality : str = "low" ,
422+ background : str = "opaque" ,
423+ image : Input .Image | None = None ,
424+ mask : Input .Image | None = None ,
425+ n : int = 1 ,
426+ size : str = "1024x1024" ,
427+ model : str = "gpt-image-1" ,
423428 ) -> IO .NodeOutput :
424429 validate_string (prompt , strip_whitespace = False )
425- model = "gpt-image-1"
426- path = "/proxy/openai/images/generations"
427- content_type = "application/json"
428- request_class = OpenAIImageGenerationRequest
429- files = []
430430
431- if image is not None :
432- path = "/proxy/openai/images/edits"
433- request_class = OpenAIImageEditRequest
434- content_type = "multipart/form-data"
431+ if mask is not None and image is None :
432+ raise ValueError ("Cannot use a mask without an input image" )
435433
436- batch_size = image .shape [0 ]
434+ if model == "gpt-image-1" :
435+ price_extractor = calculate_tokens_price_image_1
436+ elif model == "gpt-image-1.5" :
437+ price_extractor = calculate_tokens_price_image_1_5
438+ else :
439+ raise ValueError (f"Unknown model: { model } " )
437440
441+ if image is not None :
442+ files = []
443+ batch_size = image .shape [0 ]
438444 for i in range (batch_size ):
439- single_image = image [i : i + 1 ]
440- scaled_image = downscale_image_tensor (single_image ).squeeze ()
445+ single_image = image [i : i + 1 ]
446+ scaled_image = downscale_image_tensor (single_image , total_pixels = 2048 * 2048 ).squeeze ()
441447
442448 image_np = (scaled_image .numpy () * 255 ).astype (np .uint8 )
443449 img = Image .fromarray (image_np )
@@ -450,44 +456,59 @@ async def execute(
450456 else :
451457 files .append (("image[]" , (f"image_{ i } .png" , img_byte_arr , "image/png" )))
452458
453- if mask is not None :
454- if image is None :
455- raise Exception ("Cannot use a mask without an input image" )
456- if image .shape [0 ] != 1 :
457- raise Exception ("Cannot use a mask with multiple image" )
458- if mask .shape [1 :] != image .shape [1 :- 1 ]:
459- raise Exception ("Mask and Image must be the same size" )
460- batch , height , width = mask .shape
461- rgba_mask = torch .zeros (height , width , 4 , device = "cpu" )
462- rgba_mask [:, :, 3 ] = 1 - mask .squeeze ().cpu ()
463-
464- scaled_mask = downscale_image_tensor (rgba_mask .unsqueeze (0 )).squeeze ()
465-
466- mask_np = (scaled_mask .numpy () * 255 ).astype (np .uint8 )
467- mask_img = Image .fromarray (mask_np )
468- mask_img_byte_arr = BytesIO ()
469- mask_img .save (mask_img_byte_arr , format = "PNG" )
470- mask_img_byte_arr .seek (0 )
471- files .append (("mask" , ("mask.png" , mask_img_byte_arr , "image/png" )))
472-
473- # Build the operation
474- response = await sync_op (
475- cls ,
476- ApiEndpoint (path = path , method = "POST" ),
477- response_model = OpenAIImageGenerationResponse ,
478- data = request_class (
479- model = model ,
480- prompt = prompt ,
481- quality = quality ,
482- background = background ,
483- n = n ,
484- seed = seed ,
485- size = size ,
486- ),
487- files = files if files else None ,
488- content_type = content_type ,
489- )
490-
459+ if mask is not None :
460+ if image .shape [0 ] != 1 :
461+ raise Exception ("Cannot use a mask with multiple image" )
462+ if mask .shape [1 :] != image .shape [1 :- 1 ]:
463+ raise Exception ("Mask and Image must be the same size" )
464+ _ , height , width = mask .shape
465+ rgba_mask = torch .zeros (height , width , 4 , device = "cpu" )
466+ rgba_mask [:, :, 3 ] = 1 - mask .squeeze ().cpu ()
467+
468+ scaled_mask = downscale_image_tensor (rgba_mask .unsqueeze (0 ), total_pixels = 2048 * 2048 ).squeeze ()
469+
470+ mask_np = (scaled_mask .numpy () * 255 ).astype (np .uint8 )
471+ mask_img = Image .fromarray (mask_np )
472+ mask_img_byte_arr = BytesIO ()
473+ mask_img .save (mask_img_byte_arr , format = "PNG" )
474+ mask_img_byte_arr .seek (0 )
475+ files .append (("mask" , ("mask.png" , mask_img_byte_arr , "image/png" )))
476+
477+ response = await sync_op (
478+ cls ,
479+ ApiEndpoint (path = "/proxy/openai/images/edits" , method = "POST" ),
480+ response_model = OpenAIImageGenerationResponse ,
481+ data = OpenAIImageEditRequest (
482+ model = model ,
483+ prompt = prompt ,
484+ quality = quality ,
485+ background = background ,
486+ n = n ,
487+ seed = seed ,
488+ size = size ,
489+ moderation = "low" ,
490+ ),
491+ content_type = "multipart/form-data" ,
492+ files = files ,
493+ price_extractor = price_extractor ,
494+ )
495+ else :
496+ response = await sync_op (
497+ cls ,
498+ ApiEndpoint (path = "/proxy/openai/images/generations" , method = "POST" ),
499+ response_model = OpenAIImageGenerationResponse ,
500+ data = OpenAIImageGenerationRequest (
501+ model = model ,
502+ prompt = prompt ,
503+ quality = quality ,
504+ background = background ,
505+ n = n ,
506+ seed = seed ,
507+ size = size ,
508+ moderation = "low" ,
509+ ),
510+ price_extractor = price_extractor ,
511+ )
491512 return IO .NodeOutput (await validate_and_cast_response (response ))
492513
493514
0 commit comments