@@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum):
8383
8484async def create_image_parts (
8585 cls : type [IO .ComfyNode ],
86- images : Input .Image ,
86+ images : Input .Image | list [ Input . Image ] ,
8787 image_limit : int = 0 ,
8888) -> list [GeminiPart ]:
8989 image_parts : list [GeminiPart ] = []
9090 if image_limit < 0 :
9191 raise ValueError ("image_limit must be greater than or equal to 0 when creating Gemini image parts." )
92- total_images = get_number_of_images (images )
92+
93+ # Accept either a single (possibly-batched) tensor or a list of them; share URL budget across all.
94+ images_list : list [Input .Image ] = images if isinstance (images , list ) else [images ]
95+ total_images = sum (get_number_of_images (img ) for img in images_list )
9396 if total_images <= 0 :
9497 raise ValueError ("No images provided to create_image_parts; at least one image is required." )
9598
@@ -98,10 +101,18 @@ async def create_image_parts(
98101
99102 # Number of images we'll send as URLs (fileData)
100103 num_url_images = min (effective_max , 10 ) # Vertex API max number of image links
104+ upload_kwargs : dict = {"wait_label" : "Uploading reference images" }
105+ if effective_max > num_url_images :
106+ # Split path (e.g. 11+ images): suppress per-image counter to avoid a confusing dual-fraction label.
107+ upload_kwargs = {
108+ "wait_label" : f"Uploading reference images ({ num_url_images } +)" ,
109+ "show_batch_index" : False ,
110+ }
101111 reference_images_urls = await upload_images_to_comfyapi (
102112 cls ,
103- images ,
113+ images_list ,
104114 max_images = num_url_images ,
115+ ** upload_kwargs ,
105116 )
106117 for reference_image_url in reference_images_urls :
107118 image_parts .append (
@@ -112,15 +123,22 @@ async def create_image_parts(
112123 )
113124 )
114125 )
115- for idx in range (num_url_images , effective_max ):
116- image_parts .append (
117- GeminiPart (
118- inlineData = GeminiInlineData (
119- mimeType = GeminiMimeType .image_png ,
120- data = tensor_to_base64_string (images [idx ]),
126+ if effective_max > num_url_images :
127+ flat : list [torch .Tensor ] = []
128+ for tensor in images_list :
129+ if len (tensor .shape ) == 4 :
130+ flat .extend (tensor [i ] for i in range (tensor .shape [0 ]))
131+ else :
132+ flat .append (tensor )
133+ for idx in range (num_url_images , effective_max ):
134+ image_parts .append (
135+ GeminiPart (
136+ inlineData = GeminiInlineData (
137+ mimeType = GeminiMimeType .image_png ,
138+ data = tensor_to_base64_string (flat [idx ]),
139+ )
121140 )
122141 )
123- )
124142 return image_parts
125143
126144
@@ -891,23 +909,14 @@ def define_schema(cls):
891909 "9:16" ,
892910 "16:9" ,
893911 "21:9" ,
894- # "1:4",
895- # "4:1",
896- # "8:1",
897- # "1:8",
898912 ],
899913 default = "auto" ,
900914 tooltip = "If set to 'auto', matches your input image's aspect ratio; "
901915 "if no image is provided, a 16:9 square is usually generated." ,
902916 ),
903917 IO .Combo .Input (
904918 "resolution" ,
905- options = [
906- # "512px",
907- "1K" ,
908- "2K" ,
909- "4K" ,
910- ],
919+ options = ["1K" , "2K" , "4K" ],
911920 tooltip = "Target output resolution. For 2K/4K the native Gemini upscaler is used." ,
912921 ),
913922 IO .Combo .Input (
@@ -956,6 +965,7 @@ def define_schema(cls):
956965 ],
957966 is_api_node = True ,
958967 price_badge = GEMINI_IMAGE_2_PRICE_BADGE ,
968+ is_deprecated = True ,
959969 )
960970
961971 @classmethod
@@ -1016,6 +1026,197 @@ async def execute(
10161026 )
10171027
10181028
1029+ def _nano_banana_2_v2_model_inputs ():
1030+ return [
1031+ IO .Combo .Input (
1032+ "aspect_ratio" ,
1033+ options = [
1034+ "auto" ,
1035+ "1:1" ,
1036+ "2:3" ,
1037+ "3:2" ,
1038+ "3:4" ,
1039+ "4:3" ,
1040+ "4:5" ,
1041+ "5:4" ,
1042+ "9:16" ,
1043+ "16:9" ,
1044+ "21:9" ,
1045+ "1:4" ,
1046+ "4:1" ,
1047+ "8:1" ,
1048+ "1:8" ,
1049+ ],
1050+ default = "auto" ,
1051+ tooltip = "If set to 'auto', matches your input image's aspect ratio; "
1052+ "if no image is provided, a 16:9 square is usually generated." ,
1053+ ),
1054+ IO .Combo .Input (
1055+ "resolution" ,
1056+ options = ["1K" , "2K" , "4K" ],
1057+ tooltip = "Target output resolution. For 2K/4K the native Gemini upscaler is used." ,
1058+ ),
1059+ IO .Combo .Input (
1060+ "thinking_level" ,
1061+ options = ["MINIMAL" , "HIGH" ],
1062+ ),
1063+ IO .Autogrow .Input (
1064+ "images" ,
1065+ template = IO .Autogrow .TemplateNames (
1066+ IO .Image .Input ("image" ),
1067+ names = [f"image_{ i } " for i in range (1 , 15 )],
1068+ min = 0 ,
1069+ ),
1070+ tooltip = "Optional reference image(s). Up to 14 images total." ,
1071+ ),
1072+ IO .Custom ("GEMINI_INPUT_FILES" ).Input (
1073+ "files" ,
1074+ optional = True ,
1075+ tooltip = "Optional file(s) to use as context for the model. "
1076+ "Accepts inputs from the Gemini Generate Content Input Files node." ,
1077+ ),
1078+ ]
1079+
1080+
1081+ class GeminiNanoBanana2V2 (IO .ComfyNode ):
1082+
1083+ @classmethod
1084+ def define_schema (cls ):
1085+ return IO .Schema (
1086+ node_id = "GeminiNanoBanana2V2" ,
1087+ display_name = "Nano Banana 2" ,
1088+ category = "api node/image/Gemini" ,
1089+ description = "Generate or edit images synchronously via Google Vertex API." ,
1090+ inputs = [
1091+ IO .String .Input (
1092+ "prompt" ,
1093+ multiline = True ,
1094+ tooltip = "Text prompt describing the image to generate or the edits to apply. "
1095+ "Include any constraints, styles, or details the model should follow." ,
1096+ default = "" ,
1097+ ),
1098+ IO .DynamicCombo .Input (
1099+ "model" ,
1100+ options = [
1101+ IO .DynamicCombo .Option (
1102+ "Nano Banana 2 (Gemini 3.1 Flash Image)" ,
1103+ _nano_banana_2_v2_model_inputs (),
1104+ ),
1105+ ],
1106+ ),
1107+ IO .Int .Input (
1108+ "seed" ,
1109+ default = 42 ,
1110+ min = 0 ,
1111+ max = 0xFFFFFFFFFFFFFFFF ,
1112+ control_after_generate = True ,
1113+ tooltip = "When the seed is fixed to a specific value, the model makes a best effort to provide "
1114+ "the same response for repeated requests. Deterministic output isn't guaranteed. "
1115+ "Also, changing the model or parameter settings, such as the temperature, "
1116+ "can cause variations in the response even when you use the same seed value. "
1117+ "By default, a random seed value is used." ,
1118+ ),
1119+ IO .Combo .Input (
1120+ "response_modalities" ,
1121+ options = ["IMAGE" , "IMAGE+TEXT" ],
1122+ advanced = True ,
1123+ ),
1124+ IO .String .Input (
1125+ "system_prompt" ,
1126+ multiline = True ,
1127+ default = GEMINI_IMAGE_SYS_PROMPT ,
1128+ optional = True ,
1129+ tooltip = "Foundational instructions that dictate an AI's behavior." ,
1130+ advanced = True ,
1131+ ),
1132+ ],
1133+ outputs = [
1134+ IO .Image .Output (),
1135+ IO .String .Output (),
1136+ IO .Image .Output (
1137+ display_name = "thought_image" ,
1138+ tooltip = "First image from the model's thinking process. "
1139+ "Only available with thinking_level HIGH and IMAGE+TEXT modality." ,
1140+ ),
1141+ ],
1142+ hidden = [
1143+ IO .Hidden .auth_token_comfy_org ,
1144+ IO .Hidden .api_key_comfy_org ,
1145+ IO .Hidden .unique_id ,
1146+ ],
1147+ is_api_node = True ,
1148+ price_badge = IO .PriceBadge (
1149+ depends_on = IO .PriceBadgeDepends (widgets = ["model" , "model.resolution" ]),
1150+ expr = """
1151+ (
1152+ $r := $lookup(widgets, "model.resolution");
1153+ $prices := {"1k": 0.0696, "2k": 0.1014, "4k": 0.154};
1154+ {"type":"usd","usd": $lookup($prices, $r), "format":{"suffix":"/Image","approximate":true}}
1155+ )
1156+ """ ,
1157+ ),
1158+ )
1159+
1160+ @classmethod
1161+ async def execute (
1162+ cls ,
1163+ prompt : str ,
1164+ model : dict ,
1165+ seed : int ,
1166+ response_modalities : str ,
1167+ system_prompt : str = "" ,
1168+ ) -> IO .NodeOutput :
1169+ validate_string (prompt , strip_whitespace = True , min_length = 1 )
1170+ model_choice = model ["model" ]
1171+ if model_choice == "Nano Banana 2 (Gemini 3.1 Flash Image)" :
1172+ model_id = "gemini-3.1-flash-image-preview"
1173+ else :
1174+ model_id = model_choice
1175+
1176+ images = model .get ("images" ) or {}
1177+ parts : list [GeminiPart ] = [GeminiPart (text = prompt )]
1178+ if images :
1179+ image_tensors : list [Input .Image ] = [t for t in images .values () if t is not None ]
1180+ if image_tensors :
1181+ if sum (get_number_of_images (t ) for t in image_tensors ) > 14 :
1182+ raise ValueError ("The current maximum number of supported images is 14." )
1183+ parts .extend (await create_image_parts (cls , image_tensors ))
1184+ files = model .get ("files" )
1185+ if files is not None :
1186+ parts .extend (files )
1187+
1188+ image_config = GeminiImageConfig (imageSize = model ["resolution" ])
1189+ if model ["aspect_ratio" ] != "auto" :
1190+ image_config .aspectRatio = model ["aspect_ratio" ]
1191+
1192+ gemini_system_prompt = None
1193+ if system_prompt :
1194+ gemini_system_prompt = GeminiSystemInstructionContent (parts = [GeminiTextPart (text = system_prompt )], role = None )
1195+
1196+ response = await sync_op (
1197+ cls ,
1198+ ApiEndpoint (path = f"/proxy/vertexai/gemini/{ model_id } " , method = "POST" ),
1199+ data = GeminiImageGenerateContentRequest (
1200+ contents = [
1201+ GeminiContent (role = GeminiRole .user , parts = parts ),
1202+ ],
1203+ generationConfig = GeminiImageGenerationConfig (
1204+ responseModalities = (["IMAGE" ] if response_modalities == "IMAGE" else ["TEXT" , "IMAGE" ]),
1205+ imageConfig = image_config ,
1206+ thinkingConfig = GeminiThinkingConfig (thinkingLevel = model ["thinking_level" ]),
1207+ ),
1208+ systemInstruction = gemini_system_prompt ,
1209+ ),
1210+ response_model = GeminiGenerateContentResponse ,
1211+ price_extractor = calculate_tokens_price ,
1212+ )
1213+ return IO .NodeOutput (
1214+ await get_image_from_response (response ),
1215+ get_text_from_response (response ),
1216+ await get_image_from_response (response , thought = True ),
1217+ )
1218+
1219+
10191220class GeminiExtension (ComfyExtension ):
10201221 @override
10211222 async def get_node_list (self ) -> list [type [IO .ComfyNode ]]:
@@ -1024,6 +1225,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
10241225 GeminiImage ,
10251226 GeminiImage2 ,
10261227 GeminiNanoBanana2 ,
1228+ GeminiNanoBanana2V2 ,
10271229 GeminiInputFiles ,
10281230 ]
10291231
0 commit comments