Skip to content

Commit c011fb5

Browse files
authored
[Partner Nodes] new NanoBanana2 node with DynamicCombo/Autogrow (Comfy-Org#13753)
* feat(api-nodes): new NanoBanana2 node with DynamicCombo/Autogrow Signed-off-by: bigcat88 <bigcat88@icloud.com> * feat: improved status text on uploading Signed-off-by: bigcat88 <bigcat88@icloud.com> * feat: improved status text on uploading (2) Signed-off-by: bigcat88 <bigcat88@icloud.com> --------- Signed-off-by: bigcat88 <bigcat88@icloud.com>
1 parent c945a43 commit c011fb5

1 file changed

Lines changed: 222 additions & 20 deletions

File tree

comfy_api_nodes/nodes_gemini.py

Lines changed: 222 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum):
8383

8484
async 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+
10191220
class 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

Comments
 (0)