diff --git a/app/node_replace_manager.py b/app/node_replace_manager.py index d9aab5b22..cf339b6eb 100644 --- a/app/node_replace_manager.py +++ b/app/node_replace_manager.py @@ -78,7 +78,7 @@ class NodeReplaceManager: for input_map in replacement.input_mapping: if "set_value" in input_map: 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"]] # finalize input replacement prompt[node_number] = new_node_struct diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py index 2b77a022e..1187cc4d7 100644 --- a/comfy_api_nodes/nodes_gemini.py +++ b/comfy_api_nodes/nodes_gemini.py @@ -83,13 +83,16 @@ class GeminiImageModel(str, Enum): async def create_image_parts( cls: type[IO.ComfyNode], - images: Input.Image, + images: Input.Image | list[Input.Image], image_limit: int = 0, ) -> list[GeminiPart]: image_parts: list[GeminiPart] = [] if image_limit < 0: 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: 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 reference_images_urls = await upload_images_to_comfyapi( cls, - images, + images_list, max_images=num_url_images, ) 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): - image_parts.append( - GeminiPart( - inlineData=GeminiInlineData( - mimeType=GeminiMimeType.image_png, - data=tensor_to_base64_string(images[idx]), + if effective_max > num_url_images: + flat: list[torch.Tensor] = [] + for tensor in images_list: + if len(tensor.shape) == 4: + flat.extend(tensor[i] for i in range(tensor.shape[0])) + 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 @@ -849,7 +859,7 @@ class GeminiNanoBanana2(IO.ComfyNode): @classmethod def define_schema(cls): return IO.Schema( - node_id="GeminiNanoBanana2", + node_id="GeminiNanoBanana2V2", display_name="Nano Banana 2", category="api node/image/Gemini", description="Generate or edit images synchronously via Google Vertex API.", @@ -919,11 +929,14 @@ class GeminiNanoBanana2(IO.ComfyNode): "thinking_level", options=["MINIMAL", "HIGH"], ), - IO.Image.Input( + IO.Autogrow.Input( "images", - optional=True, - tooltip="Optional reference image(s). " - "To include multiple images, use the Batch Images node (up to 14).", + template=IO.Autogrow.TemplateNames( + IO.Image.Input("image"), + 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( "files", @@ -968,7 +981,7 @@ class GeminiNanoBanana2(IO.ComfyNode): resolution: str, response_modalities: str, thinking_level: str, - images: Input.Image | None = None, + images: IO.Autogrow.Type | None = None, files: list[GeminiPart] | None = None, system_prompt: str = "", ) -> IO.NodeOutput: @@ -977,10 +990,12 @@ class GeminiNanoBanana2(IO.ComfyNode): model = "gemini-3.1-flash-image-preview" parts: list[GeminiPart] = [GeminiPart(text=prompt)] - if images is not None: - if get_number_of_images(images) > 14: - raise ValueError("The current maximum number of supported images is 14.") - parts.extend(await create_image_parts(cls, images)) + if images: + image_tensors: list[Input.Image] = [t for t in images.values() if t is not None] + if image_tensors: + 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: parts.extend(files) diff --git a/comfy_extras/nodes_replacements.py b/comfy_extras/nodes_replacements.py index 7684e854c..e5c388ae9 100644 --- a/comfy_extras/nodes_replacements.py +++ b/comfy_extras/nodes_replacements.py @@ -13,6 +13,7 @@ async def register_replacements(): await register_replacements_preview3d() await register_replacements_svdimg2vid() await register_replacements_conditioningavg() + await register_replacements_nanobanana2() async def register_replacements_longeredge(): # No dynamic inputs here @@ -92,6 +93,35 @@ async def register_replacements_conditioningavg(): 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): async def on_load(self) -> None: await register_replacements()