mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-01 23:37:33 +08:00
feat(api-nodes): add NanoBanana2 (#12660)
This commit is contained in:
parent
420e900f69
commit
fd41ec97cc
@ -127,9 +127,15 @@ class GeminiImageConfig(BaseModel):
|
|||||||
imageOutputOptions: GeminiImageOutputOptions = Field(default_factory=GeminiImageOutputOptions)
|
imageOutputOptions: GeminiImageOutputOptions = Field(default_factory=GeminiImageOutputOptions)
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiThinkingConfig(BaseModel):
|
||||||
|
includeThoughts: bool | None = Field(None)
|
||||||
|
thinkingLevel: str = Field(...)
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageGenerationConfig(GeminiGenerationConfig):
|
class GeminiImageGenerationConfig(GeminiGenerationConfig):
|
||||||
responseModalities: list[str] | None = Field(None)
|
responseModalities: list[str] | None = Field(None)
|
||||||
imageConfig: GeminiImageConfig | None = Field(None)
|
imageConfig: GeminiImageConfig | None = Field(None)
|
||||||
|
thinkingConfig: GeminiThinkingConfig | None = Field(None)
|
||||||
|
|
||||||
|
|
||||||
class GeminiImageGenerateContentRequest(BaseModel):
|
class GeminiImageGenerateContentRequest(BaseModel):
|
||||||
|
|||||||
@ -186,7 +186,7 @@ class ByteDanceSeedreamNode(IO.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return IO.Schema(
|
return IO.Schema(
|
||||||
node_id="ByteDanceSeedreamNode",
|
node_id="ByteDanceSeedreamNode",
|
||||||
display_name="ByteDance Seedream 5.0",
|
display_name="ByteDance Seedream 4.5 & 5.0",
|
||||||
category="api node/image/ByteDance",
|
category="api node/image/ByteDance",
|
||||||
description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.",
|
description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.",
|
||||||
inputs=[
|
inputs=[
|
||||||
|
|||||||
@ -29,6 +29,7 @@ from comfy_api_nodes.apis.gemini import (
|
|||||||
GeminiRole,
|
GeminiRole,
|
||||||
GeminiSystemInstructionContent,
|
GeminiSystemInstructionContent,
|
||||||
GeminiTextPart,
|
GeminiTextPart,
|
||||||
|
GeminiThinkingConfig,
|
||||||
Modality,
|
Modality,
|
||||||
)
|
)
|
||||||
from comfy_api_nodes.util import (
|
from comfy_api_nodes.util import (
|
||||||
@ -55,6 +56,21 @@ GEMINI_IMAGE_SYS_PROMPT = (
|
|||||||
"Prioritize generating the visual representation above any text, formatting, or conversational requests."
|
"Prioritize generating the visual representation above any text, formatting, or conversational requests."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GEMINI_IMAGE_2_PRICE_BADGE = IO.PriceBadge(
|
||||||
|
depends_on=IO.PriceBadgeDepends(widgets=["model", "resolution"]),
|
||||||
|
expr="""
|
||||||
|
(
|
||||||
|
$m := widgets.model;
|
||||||
|
$r := widgets.resolution;
|
||||||
|
$isFlash := $contains($m, "nano banana 2");
|
||||||
|
$flashPrices := {"1k": 0.0696, "2k": 0.0696, "4k": 0.123};
|
||||||
|
$proPrices := {"1k": 0.134, "2k": 0.134, "4k": 0.24};
|
||||||
|
$prices := $isFlash ? $flashPrices : $proPrices;
|
||||||
|
{"type":"usd","usd": $lookup($prices, $r), "format":{"suffix":"/Image","approximate":true}}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GeminiModel(str, Enum):
|
class GeminiModel(str, Enum):
|
||||||
"""
|
"""
|
||||||
@ -229,6 +245,10 @@ def calculate_tokens_price(response: GeminiGenerateContentResponse) -> float | N
|
|||||||
input_tokens_price = 2
|
input_tokens_price = 2
|
||||||
output_text_tokens_price = 12.0
|
output_text_tokens_price = 12.0
|
||||||
output_image_tokens_price = 120.0
|
output_image_tokens_price = 120.0
|
||||||
|
elif response.modelVersion == "gemini-3.1-flash-image-preview":
|
||||||
|
input_tokens_price = 0.5
|
||||||
|
output_text_tokens_price = 3.0
|
||||||
|
output_image_tokens_price = 60.0
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
final_price = response.usageMetadata.promptTokenCount * input_tokens_price
|
final_price = response.usageMetadata.promptTokenCount * input_tokens_price
|
||||||
@ -686,7 +706,7 @@ class GeminiImage2(IO.ComfyNode):
|
|||||||
),
|
),
|
||||||
IO.Combo.Input(
|
IO.Combo.Input(
|
||||||
"model",
|
"model",
|
||||||
options=["gemini-3-pro-image-preview"],
|
options=["gemini-3-pro-image-preview", "Nano Banana 2 (Gemini 3.1 Flash Image)"],
|
||||||
),
|
),
|
||||||
IO.Int.Input(
|
IO.Int.Input(
|
||||||
"seed",
|
"seed",
|
||||||
@ -750,19 +770,7 @@ class GeminiImage2(IO.ComfyNode):
|
|||||||
IO.Hidden.unique_id,
|
IO.Hidden.unique_id,
|
||||||
],
|
],
|
||||||
is_api_node=True,
|
is_api_node=True,
|
||||||
price_badge=IO.PriceBadge(
|
price_badge=GEMINI_IMAGE_2_PRICE_BADGE,
|
||||||
depends_on=IO.PriceBadgeDepends(widgets=["resolution"]),
|
|
||||||
expr="""
|
|
||||||
(
|
|
||||||
$r := widgets.resolution;
|
|
||||||
($contains($r,"1k") or $contains($r,"2k"))
|
|
||||||
? {"type":"usd","usd":0.134,"format":{"suffix":"/Image","approximate":true}}
|
|
||||||
: $contains($r,"4k")
|
|
||||||
? {"type":"usd","usd":0.24,"format":{"suffix":"/Image","approximate":true}}
|
|
||||||
: {"type":"text","text":"Token-based"}
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -779,6 +787,10 @@ class GeminiImage2(IO.ComfyNode):
|
|||||||
system_prompt: str = "",
|
system_prompt: str = "",
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_string(prompt, strip_whitespace=True, min_length=1)
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||||
|
if model == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
||||||
|
model = "gemini-3.1-flash-image-preview"
|
||||||
|
if response_modalities == "IMAGE+TEXT":
|
||||||
|
raise ValueError("IMAGE+TEXT is not currently available for the Nano Banana 2 model.")
|
||||||
|
|
||||||
parts: list[GeminiPart] = [GeminiPart(text=prompt)]
|
parts: list[GeminiPart] = [GeminiPart(text=prompt)]
|
||||||
if images is not None:
|
if images is not None:
|
||||||
@ -815,6 +827,168 @@ class GeminiImage2(IO.ComfyNode):
|
|||||||
return IO.NodeOutput(await get_image_from_response(response), get_text_from_response(response))
|
return IO.NodeOutput(await get_image_from_response(response), get_text_from_response(response))
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiNanoBanana2(IO.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="GeminiNanoBanana2",
|
||||||
|
display_name="Nano Banana 2",
|
||||||
|
category="api node/image/Gemini",
|
||||||
|
description="Generate or edit images synchronously via Google Vertex API.",
|
||||||
|
inputs=[
|
||||||
|
IO.String.Input(
|
||||||
|
"prompt",
|
||||||
|
multiline=True,
|
||||||
|
tooltip="Text prompt describing the image to generate or the edits to apply. "
|
||||||
|
"Include any constraints, styles, or details the model should follow.",
|
||||||
|
default="",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=["Nano Banana 2 (Gemini 3.1 Flash Image)"],
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=42,
|
||||||
|
min=0,
|
||||||
|
max=0xFFFFFFFFFFFFFFFF,
|
||||||
|
control_after_generate=True,
|
||||||
|
tooltip="When the seed is fixed to a specific value, the model makes a best effort to provide "
|
||||||
|
"the same response for repeated requests. Deterministic output isn't guaranteed. "
|
||||||
|
"Also, changing the model or parameter settings, such as the temperature, "
|
||||||
|
"can cause variations in the response even when you use the same seed value. "
|
||||||
|
"By default, a random seed value is used.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"aspect_ratio",
|
||||||
|
options=[
|
||||||
|
"auto",
|
||||||
|
"1:1",
|
||||||
|
"2:3",
|
||||||
|
"3:2",
|
||||||
|
"3:4",
|
||||||
|
"4:3",
|
||||||
|
"4:5",
|
||||||
|
"5:4",
|
||||||
|
"9:16",
|
||||||
|
"16:9",
|
||||||
|
"21:9",
|
||||||
|
# "1:4",
|
||||||
|
# "4:1",
|
||||||
|
# "8:1",
|
||||||
|
# "1:8",
|
||||||
|
],
|
||||||
|
default="auto",
|
||||||
|
tooltip="If set to 'auto', matches your input image's aspect ratio; "
|
||||||
|
"if no image is provided, a 16:9 square is usually generated.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"resolution",
|
||||||
|
options=[
|
||||||
|
# "512px",
|
||||||
|
"1K",
|
||||||
|
"2K",
|
||||||
|
"4K",
|
||||||
|
],
|
||||||
|
tooltip="Target output resolution. For 2K/4K the native Gemini upscaler is used.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"response_modalities",
|
||||||
|
options=["IMAGE"],
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"thinking_level",
|
||||||
|
options=["MINIMAL", "HIGH"],
|
||||||
|
),
|
||||||
|
IO.Image.Input(
|
||||||
|
"images",
|
||||||
|
optional=True,
|
||||||
|
tooltip="Optional reference image(s). "
|
||||||
|
"To include multiple images, use the Batch Images node (up to 14).",
|
||||||
|
),
|
||||||
|
IO.Custom("GEMINI_INPUT_FILES").Input(
|
||||||
|
"files",
|
||||||
|
optional=True,
|
||||||
|
tooltip="Optional file(s) to use as context for the model. "
|
||||||
|
"Accepts inputs from the Gemini Generate Content Input Files node.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"system_prompt",
|
||||||
|
multiline=True,
|
||||||
|
default=GEMINI_IMAGE_SYS_PROMPT,
|
||||||
|
optional=True,
|
||||||
|
tooltip="Foundational instructions that dictate an AI's behavior.",
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Image.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=GEMINI_IMAGE_2_PRICE_BADGE,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
seed: int,
|
||||||
|
aspect_ratio: str,
|
||||||
|
resolution: str,
|
||||||
|
response_modalities: str,
|
||||||
|
thinking_level: str,
|
||||||
|
images: Input.Image | None = None,
|
||||||
|
files: list[GeminiPart] | None = None,
|
||||||
|
system_prompt: str = "",
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(prompt, strip_whitespace=True, min_length=1)
|
||||||
|
if model == "Nano Banana 2 (Gemini 3.1 Flash Image)":
|
||||||
|
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 files is not None:
|
||||||
|
parts.extend(files)
|
||||||
|
|
||||||
|
image_config = GeminiImageConfig(imageSize=resolution)
|
||||||
|
if aspect_ratio != "auto":
|
||||||
|
image_config.aspectRatio = aspect_ratio
|
||||||
|
|
||||||
|
gemini_system_prompt = None
|
||||||
|
if system_prompt:
|
||||||
|
gemini_system_prompt = GeminiSystemInstructionContent(parts=[GeminiTextPart(text=system_prompt)], role=None)
|
||||||
|
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path=f"/proxy/vertexai/gemini/{model}", method="POST"),
|
||||||
|
data=GeminiImageGenerateContentRequest(
|
||||||
|
contents=[
|
||||||
|
GeminiContent(role=GeminiRole.user, parts=parts),
|
||||||
|
],
|
||||||
|
generationConfig=GeminiImageGenerationConfig(
|
||||||
|
responseModalities=(["IMAGE"] if response_modalities == "IMAGE" else ["TEXT", "IMAGE"]),
|
||||||
|
imageConfig=image_config,
|
||||||
|
thinkingConfig=GeminiThinkingConfig(thinkingLevel=thinking_level),
|
||||||
|
),
|
||||||
|
systemInstruction=gemini_system_prompt,
|
||||||
|
),
|
||||||
|
response_model=GeminiGenerateContentResponse,
|
||||||
|
price_extractor=calculate_tokens_price,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(await get_image_from_response(response), get_text_from_response(response))
|
||||||
|
|
||||||
|
|
||||||
class GeminiExtension(ComfyExtension):
|
class GeminiExtension(ComfyExtension):
|
||||||
@override
|
@override
|
||||||
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
@ -822,6 +996,7 @@ class GeminiExtension(ComfyExtension):
|
|||||||
GeminiNode,
|
GeminiNode,
|
||||||
GeminiImage,
|
GeminiImage,
|
||||||
GeminiImage2,
|
GeminiImage2,
|
||||||
|
GeminiNanoBanana2,
|
||||||
GeminiInputFiles,
|
GeminiInputFiles,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user