Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/node_replace_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def apply_replacements(self, prompt: dict[str, NodeStruct]):
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
Expand Down
55 changes: 35 additions & 20 deletions comfy_api_nodes/nodes_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand All @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -919,11 +929,14 @@ def define_schema(cls):
"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",
Expand Down Expand Up @@ -968,7 +981,7 @@ async def execute(
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:
Expand All @@ -977,10 +990,12 @@ async def execute(
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)

Expand Down
30 changes: 30 additions & 0 deletions comfy_extras/nodes_replacements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading