feat(api-nodes): replace NanoBanana2 node with new node that uses Autogrow
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Build package / Build Test (3.10) (push) Has been cancelled
Build package / Build Test (3.11) (push) Has been cancelled
Build package / Build Test (3.12) (push) Has been cancelled
Build package / Build Test (3.13) (push) Has been cancelled
Build package / Build Test (3.14) (push) Has been cancelled

This commit is contained in:
bigcat88 2026-05-01 11:13:14 +03:00
parent 9c34f5f36a
commit 500184918d
No known key found for this signature in database
GPG Key ID: 1F0BF0EC3CF22721
3 changed files with 66 additions and 21 deletions

View File

@ -78,7 +78,7 @@ class NodeReplaceManager:
for input_map in replacement.input_mapping: for input_map in replacement.input_mapping:
if "set_value" in input_map: if "set_value" in input_map:
new_node_struct["inputs"][input_map["new_id"]] = input_map["set_value"] new_node_struct["inputs"][input_map["new_id"]] = input_map["set_value"]
elif "old_id" in input_map: elif "old_id" in input_map and input_map["old_id"] in node_struct["inputs"]:
new_node_struct["inputs"][input_map["new_id"]] = node_struct["inputs"][input_map["old_id"]] new_node_struct["inputs"][input_map["new_id"]] = node_struct["inputs"][input_map["old_id"]]
# finalize input replacement # finalize input replacement
prompt[node_number] = new_node_struct prompt[node_number] = new_node_struct

View File

@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum):
async def create_image_parts( async def create_image_parts(
cls: type[IO.ComfyNode], cls: type[IO.ComfyNode],
images: Input.Image, images: Input.Image | list[Input.Image],
image_limit: int = 0, image_limit: int = 0,
) -> list[GeminiPart]: ) -> list[GeminiPart]:
image_parts: list[GeminiPart] = [] image_parts: list[GeminiPart] = []
if image_limit < 0: if image_limit < 0:
raise ValueError("image_limit must be greater than or equal to 0 when creating Gemini image parts.") raise ValueError("image_limit must be greater than or equal to 0 when creating Gemini image parts.")
total_images = get_number_of_images(images)
# Accept either a single (possibly-batched) tensor or a list of them; share URL budget across all.
images_list: list[Input.Image] = images if isinstance(images, list) else [images]
total_images = sum(get_number_of_images(img) for img in images_list)
if total_images <= 0: if total_images <= 0:
raise ValueError("No images provided to create_image_parts; at least one image is required.") raise ValueError("No images provided to create_image_parts; at least one image is required.")
@ -100,7 +103,7 @@ async def create_image_parts(
num_url_images = min(effective_max, 10) # Vertex API max number of image links num_url_images = min(effective_max, 10) # Vertex API max number of image links
reference_images_urls = await upload_images_to_comfyapi( reference_images_urls = await upload_images_to_comfyapi(
cls, cls,
images, images_list,
max_images=num_url_images, max_images=num_url_images,
) )
for reference_image_url in reference_images_urls: for reference_image_url in reference_images_urls:
@ -112,15 +115,22 @@ async def create_image_parts(
) )
) )
) )
for idx in range(num_url_images, effective_max): if effective_max > num_url_images:
image_parts.append( flat: list[torch.Tensor] = []
GeminiPart( for tensor in images_list:
inlineData=GeminiInlineData( if len(tensor.shape) == 4:
mimeType=GeminiMimeType.image_png, flat.extend(tensor[i] for i in range(tensor.shape[0]))
data=tensor_to_base64_string(images[idx]), else:
flat.append(tensor)
for idx in range(num_url_images, effective_max):
image_parts.append(
GeminiPart(
inlineData=GeminiInlineData(
mimeType=GeminiMimeType.image_png,
data=tensor_to_base64_string(flat[idx]),
)
) )
) )
)
return image_parts return image_parts
@ -849,7 +859,7 @@ class GeminiNanoBanana2(IO.ComfyNode):
@classmethod @classmethod
def define_schema(cls): def define_schema(cls):
return IO.Schema( return IO.Schema(
node_id="GeminiNanoBanana2", node_id="GeminiNanoBanana2V2",
display_name="Nano Banana 2", display_name="Nano Banana 2",
category="api node/image/Gemini", category="api node/image/Gemini",
description="Generate or edit images synchronously via Google Vertex API.", description="Generate or edit images synchronously via Google Vertex API.",
@ -919,11 +929,14 @@ class GeminiNanoBanana2(IO.ComfyNode):
"thinking_level", "thinking_level",
options=["MINIMAL", "HIGH"], options=["MINIMAL", "HIGH"],
), ),
IO.Image.Input( IO.Autogrow.Input(
"images", "images",
optional=True, template=IO.Autogrow.TemplateNames(
tooltip="Optional reference image(s). " IO.Image.Input("image"),
"To include multiple images, use the Batch Images node (up to 14).", names=[f"image_{i}" for i in range(1, 15)],
min=0,
),
tooltip="Optional reference image(s). Up to 14 images total.",
), ),
IO.Custom("GEMINI_INPUT_FILES").Input( IO.Custom("GEMINI_INPUT_FILES").Input(
"files", "files",
@ -968,7 +981,7 @@ class GeminiNanoBanana2(IO.ComfyNode):
resolution: str, resolution: str,
response_modalities: str, response_modalities: str,
thinking_level: str, thinking_level: str,
images: Input.Image | None = None, images: IO.Autogrow.Type | None = None,
files: list[GeminiPart] | None = None, files: list[GeminiPart] | None = None,
system_prompt: str = "", system_prompt: str = "",
) -> IO.NodeOutput: ) -> IO.NodeOutput:
@ -977,10 +990,12 @@ class GeminiNanoBanana2(IO.ComfyNode):
model = "gemini-3.1-flash-image-preview" model = "gemini-3.1-flash-image-preview"
parts: list[GeminiPart] = [GeminiPart(text=prompt)] parts: list[GeminiPart] = [GeminiPart(text=prompt)]
if images is not None: if images:
if get_number_of_images(images) > 14: image_tensors: list[Input.Image] = [t for t in images.values() if t is not None]
raise ValueError("The current maximum number of supported images is 14.") if image_tensors:
parts.extend(await create_image_parts(cls, images)) if sum(get_number_of_images(t) for t in image_tensors) > 14:
raise ValueError("The current maximum number of supported images is 14.")
parts.extend(await create_image_parts(cls, image_tensors))
if files is not None: if files is not None:
parts.extend(files) parts.extend(files)

View File

@ -13,6 +13,7 @@ async def register_replacements():
await register_replacements_preview3d() await register_replacements_preview3d()
await register_replacements_svdimg2vid() await register_replacements_svdimg2vid()
await register_replacements_conditioningavg() await register_replacements_conditioningavg()
await register_replacements_nanobanana2()
async def register_replacements_longeredge(): async def register_replacements_longeredge():
# No dynamic inputs here # No dynamic inputs here
@ -92,6 +93,35 @@ async def register_replacements_conditioningavg():
old_node_id="ConditioningAverage ", old_node_id="ConditioningAverage ",
)) ))
async def register_replacements_nanobanana2():
# GeminiNanoBanana2 replaced by GeminiNanoBanana2V2, which uses Autogrow for the images input.
await api.node_replacement.register(io.NodeReplace(
new_node_id="GeminiNanoBanana2V2",
old_node_id="GeminiNanoBanana2",
old_widget_ids=[
"prompt",
"model",
"seed",
"aspect_ratio",
"resolution",
"response_modalities",
"thinking_level",
"system_prompt",
],
input_mapping=[
{"new_id": "prompt", "old_id": "prompt"},
{"new_id": "model", "old_id": "model"},
{"new_id": "seed", "old_id": "seed"},
{"new_id": "aspect_ratio", "old_id": "aspect_ratio"},
{"new_id": "resolution", "old_id": "resolution"},
{"new_id": "response_modalities", "old_id": "response_modalities"},
{"new_id": "thinking_level", "old_id": "thinking_level"},
{"new_id": "images.image_1", "old_id": "images"},
{"new_id": "files", "old_id": "files"},
{"new_id": "system_prompt", "old_id": "system_prompt"},
],
))
class NodeReplacementsExtension(ComfyExtension): class NodeReplacementsExtension(ComfyExtension):
async def on_load(self) -> None: async def on_load(self) -> None:
await register_replacements() await register_replacements()