Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
+281
-134
@@ -8,9 +8,8 @@ import time
|
||||
import math
|
||||
import random
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Callable, List, Optional
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
@@ -20,12 +19,21 @@ from comfy_api.latest import io
|
||||
|
||||
from ..utils.image_utils import tensor_to_pil, pil_to_tensor, parse_batch_prompts
|
||||
from ..utils.config import (
|
||||
NETWORK_ROUTE_OPTIONS,
|
||||
get_base_url_by_route,
|
||||
get_api_key_or_raise,
|
||||
get_runtime_config_signature,
|
||||
)
|
||||
from ..utils.nano_banana_async import generate_nano_banana_async
|
||||
from ..utils.nano_banana_async import (
|
||||
generate_nano_banana_async,
|
||||
VERBOSE_LOG_ENABLED,
|
||||
)
|
||||
from ..utils.http2_client import create_http_client
|
||||
from ..clients.gemini_client import GeminiAPIClient
|
||||
from ..utils.nano_banana_models import (
|
||||
NANO_BANANA_MODEL_OPTIONS,
|
||||
NANO_BANANA_ROUTE_OPTIONS,
|
||||
resolve_nano_banana_model,
|
||||
)
|
||||
|
||||
try:
|
||||
from comfy.utils import ProgressBar
|
||||
@@ -41,19 +49,28 @@ except ImportError:
|
||||
InterruptProcessingException = RuntimeError
|
||||
processing_interrupted = lambda: False
|
||||
|
||||
REQUEST_LOG_ENABLED = False
|
||||
# 完整原始报文仅在显式开启详细日志时打印。
|
||||
REQUEST_LOG_ENABLED = VERBOSE_LOG_ENABLED
|
||||
|
||||
_NODE = "Nano Banana"
|
||||
_REQUEST_TIMEOUT = 900
|
||||
_INTERRUPT_CHECK_INTERVAL = 0.2
|
||||
MAX_REFERENCE_IMAGES = 14
|
||||
_MAX_GENERATION_CONCURRENCY = 12
|
||||
_MAX_DOWNLOAD_CONCURRENCY = 6
|
||||
_HTTP_MAX_CONNECTIONS = 32
|
||||
_HTTP_MAX_KEEPALIVE_CONNECTIONS = 16
|
||||
|
||||
_client_instance = None
|
||||
_client_config_signature = None
|
||||
|
||||
|
||||
def _get_client():
|
||||
global _client_instance
|
||||
if _client_instance is None:
|
||||
global _client_instance, _client_config_signature
|
||||
config_signature = get_runtime_config_signature()
|
||||
if _client_instance is None or config_signature != _client_config_signature:
|
||||
_client_instance = GeminiAPIClient()
|
||||
_client_config_signature = config_signature
|
||||
return _client_instance
|
||||
|
||||
|
||||
@@ -132,47 +149,29 @@ def _images_to_tensor_safe(images: List[Image.Image], node_label: str) -> torch.
|
||||
return pil_to_tensor(matched)
|
||||
|
||||
|
||||
MODEL_ID_MAP = {
|
||||
"Nano Banana Pro": "nano-banana-pro",
|
||||
"Nano Banana 2": "nano-banana-2",
|
||||
"Nano Banana": "nano-banana",
|
||||
def _collect_autogrow_inputs(value) -> list:
|
||||
"""Collect connected Autogrow slots while tolerating a single legacy value."""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, dict):
|
||||
return [item for item in value.values() if item is not None]
|
||||
return [value]
|
||||
|
||||
|
||||
THINKING_LEVEL_MAP = {
|
||||
"低": "minimal",
|
||||
"高": "high",
|
||||
}
|
||||
RESOLUTION_KEY_MAP = {
|
||||
"512px": "0.5k",
|
||||
"1K": "1k",
|
||||
"2K": "2k",
|
||||
"4K": "4k",
|
||||
}
|
||||
BILLING_SPECIAL_ONLY = {"nano-banana"}
|
||||
|
||||
|
||||
def _build_model_id(model_name: str, resolution: str, billing: str) -> str:
|
||||
base = MODEL_ID_MAP.get(model_name, "nano-banana-pro")
|
||||
|
||||
if base == "nano-banana":
|
||||
if billing == "官方":
|
||||
raise ValueError(f"模型 \"{model_name}\" 仅支持特价计费")
|
||||
return "nano-banana"
|
||||
|
||||
res_key = RESOLUTION_KEY_MAP.get(resolution, "2k")
|
||||
is_official = (billing == "官方")
|
||||
|
||||
if base == "nano-banana-pro" and res_key == "1k" and not is_official:
|
||||
return "nano-banana-pro"
|
||||
|
||||
if base == "nano-banana-2" and res_key == "0.5k":
|
||||
if is_official:
|
||||
raise ValueError("Nano Banana 2 的 512px 分辨率仅支持特价计费")
|
||||
return "nano-banana-2-0.5k"
|
||||
|
||||
model_id = f"{base}-{res_key}"
|
||||
if is_official:
|
||||
model_id += "-official"
|
||||
return model_id
|
||||
def _build_model_id(model_name: str, resolution: str, route: str) -> str:
|
||||
"""兼容旧调用签名;新模型名只由主模型和线路决定。"""
|
||||
del resolution
|
||||
return resolve_nano_banana_model(model_name, route)
|
||||
|
||||
|
||||
async def _generate_single(
|
||||
session: aiohttp.ClientSession,
|
||||
session: Any,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
prompt: str,
|
||||
@@ -180,10 +179,17 @@ async def _generate_single(
|
||||
resolution: str,
|
||||
aspect_ratio: str,
|
||||
images: Optional[List[Image.Image]] = None,
|
||||
enable_grounding: bool = False,
|
||||
image_urls: Optional[List[str]] = None,
|
||||
thinking_level: Optional[str] = None,
|
||||
progress_callback: Optional[Callable[[float], None]] = None,
|
||||
) -> List[Image.Image]:
|
||||
node_label: str = "Nano Banana",
|
||||
result_url_callback: Optional[Callable[[str], None]] = None,
|
||||
log_task_success: bool = True,
|
||||
upload_cache: Optional[dict] = None,
|
||||
download_semaphore: Optional[asyncio.Semaphore] = None,
|
||||
resize_mode: str = "不缩放",
|
||||
google_search: bool = False,
|
||||
) -> tuple[List[Image.Image], dict]:
|
||||
result_images, timing = await generate_nano_banana_async(
|
||||
session=session,
|
||||
base_url=base_url,
|
||||
@@ -193,18 +199,24 @@ async def _generate_single(
|
||||
resolution=resolution,
|
||||
aspect_ratio=aspect_ratio,
|
||||
images=images,
|
||||
enable_grounding=enable_grounding,
|
||||
image_urls=image_urls,
|
||||
upload_cache=upload_cache,
|
||||
download_semaphore=download_semaphore,
|
||||
thinking_level=thinking_level,
|
||||
node_label="Nano Banana",
|
||||
google_search=google_search,
|
||||
node_label=node_label,
|
||||
request_log_enabled=REQUEST_LOG_ENABLED,
|
||||
check_interrupt=_check_interrupt,
|
||||
progress_callback=progress_callback,
|
||||
result_url_callback=result_url_callback,
|
||||
log_task_success=log_task_success,
|
||||
resize_mode=resize_mode,
|
||||
)
|
||||
return result_images, timing["task_ms"], timing["parse_ms"]
|
||||
return result_images, timing
|
||||
|
||||
|
||||
async def _generate_single_task(
|
||||
session: aiohttp.ClientSession,
|
||||
session: Any,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
prompt: str,
|
||||
@@ -212,10 +224,14 @@ async def _generate_single_task(
|
||||
resolution: str,
|
||||
aspect_ratio: str,
|
||||
images: Optional[List[Image.Image]],
|
||||
image_urls: Optional[List[str]],
|
||||
global_task_index: int,
|
||||
enable_grounding: bool = False,
|
||||
thinking_level: Optional[str] = None,
|
||||
progress_callback: Optional[Callable[[float], None]] = None,
|
||||
upload_cache: Optional[dict] = None,
|
||||
download_semaphore: Optional[asyncio.Semaphore] = None,
|
||||
resize_mode: str = "不缩放",
|
||||
google_search: bool = False,
|
||||
) -> dict:
|
||||
result = {
|
||||
"global_task_index": global_task_index,
|
||||
@@ -225,8 +241,9 @@ async def _generate_single_task(
|
||||
"output_images": [],
|
||||
"error": None,
|
||||
}
|
||||
task_started = time.time()
|
||||
try:
|
||||
gen_images, task_ms, parse_ms = await _generate_single(
|
||||
gen_images, timing = await _generate_single(
|
||||
session=session,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
@@ -235,14 +252,22 @@ async def _generate_single_task(
|
||||
resolution=resolution,
|
||||
aspect_ratio=aspect_ratio,
|
||||
images=images if images else None,
|
||||
enable_grounding=enable_grounding,
|
||||
image_urls=image_urls if image_urls else None,
|
||||
thinking_level=thinking_level,
|
||||
google_search=google_search,
|
||||
progress_callback=progress_callback,
|
||||
node_label=f"Nano Banana#{global_task_index + 1}",
|
||||
upload_cache=upload_cache,
|
||||
download_semaphore=download_semaphore,
|
||||
resize_mode=resize_mode,
|
||||
)
|
||||
del task_ms, parse_ms
|
||||
result["output_images"] = gen_images
|
||||
result["success"] = True
|
||||
result["generated_count"] = len(gen_images)
|
||||
print(
|
||||
f"Nano Banana#{global_task_index + 1}: 完成 ✓ | "
|
||||
f"生成={len(gen_images)} 张 | 耗时={time.time() - task_started:.1f}s"
|
||||
)
|
||||
except InterruptProcessingException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -260,8 +285,10 @@ async def _process_batch_async(
|
||||
images_per_prompt: int,
|
||||
input_images: Optional[List[Image.Image]],
|
||||
pbar=None,
|
||||
enable_grounding: bool = False,
|
||||
thinking_level: Optional[str] = None,
|
||||
resize_mode: str = "不缩放",
|
||||
unlimited_downloads: bool = False,
|
||||
google_search: bool = False,
|
||||
) -> List[dict]:
|
||||
tasks_def = []
|
||||
for p_idx, prompt in enumerate(prompts):
|
||||
@@ -269,7 +296,7 @@ async def _process_batch_async(
|
||||
tasks_def.append((p_idx, sub_idx, prompt))
|
||||
|
||||
total_tasks = len(tasks_def)
|
||||
max_concurrent = 50
|
||||
max_concurrent = _MAX_GENERATION_CONCURRENCY
|
||||
num_batches = math.ceil(total_tasks / max_concurrent)
|
||||
|
||||
all_results = []
|
||||
@@ -277,9 +304,18 @@ async def _process_batch_async(
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
connector = aiohttp.TCPConnector(ssl=False, limit=0, limit_per_host=0)
|
||||
upload_cache = {}
|
||||
download_semaphore = (
|
||||
None
|
||||
if unlimited_downloads
|
||||
else asyncio.Semaphore(_MAX_DOWNLOAD_CONCURRENCY)
|
||||
)
|
||||
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with create_http_client(
|
||||
http2=True,
|
||||
max_connections=_HTTP_MAX_CONNECTIONS,
|
||||
max_keepalive_connections=_HTTP_MAX_KEEPALIVE_CONNECTIONS,
|
||||
) as session:
|
||||
for batch_idx in range(num_batches):
|
||||
_check_interrupt()
|
||||
start_idx = batch_idx * max_concurrent
|
||||
@@ -299,10 +335,14 @@ async def _process_batch_async(
|
||||
resolution=resolution,
|
||||
aspect_ratio=aspect_ratio,
|
||||
images=input_images,
|
||||
image_urls=None,
|
||||
global_task_index=i,
|
||||
enable_grounding=enable_grounding,
|
||||
thinking_level=thinking_level,
|
||||
google_search=google_search,
|
||||
progress_callback=_make_progress_callback(pbar),
|
||||
upload_cache=upload_cache,
|
||||
download_semaphore=download_semaphore,
|
||||
resize_mode=resize_mode,
|
||||
)
|
||||
)
|
||||
tasks.append(task)
|
||||
@@ -327,18 +367,23 @@ async def _process_batch_async(
|
||||
|
||||
batch_results.append(result_data)
|
||||
completed += 1
|
||||
prompt_snippet = (result_data.get("prompt", "") or "")[:30]
|
||||
task_num = result_data.get("global_task_index", "?")
|
||||
if isinstance(task_num, int):
|
||||
task_num += 1
|
||||
|
||||
if result_data and result_data.get("success", False):
|
||||
success_count += 1
|
||||
count = result_data.get("generated_count", 1)
|
||||
print(f"Nano Banana: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} → ✓成功({count}张)")
|
||||
else:
|
||||
fail_count += 1
|
||||
error_msg = result_data.get("error", "未知错误") if result_data else "未知错误"
|
||||
print(f"Nano Banana: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} → ✗失败: {error_msg}")
|
||||
print(f"Nano Banana#{task_num}: 失败 | error={error_msg}")
|
||||
|
||||
all_results.extend(batch_results)
|
||||
print(
|
||||
f"Nano Banana: 批次 {batch_idx + 1}/{num_batches} 完成 "
|
||||
f"| 成功={success_count} | 失败={fail_count} "
|
||||
f"| 总进度={completed}/{total_tasks}"
|
||||
)
|
||||
import gc; gc.collect()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@@ -347,8 +392,62 @@ async def _process_batch_async(
|
||||
|
||||
class NanoBanana(io.ComfyNode):
|
||||
|
||||
@staticmethod
|
||||
def _validate_model_config(model_name: str, aspect_ratio: str, resolution: str):
|
||||
"""验证模型配置是否合法"""
|
||||
# 验证模型名称
|
||||
valid_models = set(NANO_BANANA_MODEL_OPTIONS)
|
||||
if model_name not in valid_models:
|
||||
raise ValueError(
|
||||
f"模型 '{model_name}' 无效,支持的模型:{', '.join(sorted(valid_models))}"
|
||||
)
|
||||
|
||||
# 验证宽高比
|
||||
valid_aspect_ratios = {
|
||||
"智能", "1:1", "1:4", "1:8", "2:3", "3:2", "3:4",
|
||||
"4:1", "4:3", "4:5", "5:4", "8:1",
|
||||
"9:16", "16:9", "21:9",
|
||||
}
|
||||
if aspect_ratio not in valid_aspect_ratios:
|
||||
raise ValueError(
|
||||
f"宽高比 '{aspect_ratio}' 无效,支持的宽高比:{', '.join(sorted(valid_aspect_ratios))}"
|
||||
)
|
||||
|
||||
# 验证分辨率
|
||||
# “智能”仅由统一生图节点传入;独立节点仍保持原有下拉选项。
|
||||
valid_resolutions = {"智能", "1K", "2K", "4K"}
|
||||
if resolution not in valid_resolutions:
|
||||
raise ValueError(
|
||||
f"分辨率 '{resolution}' 无效,支持的分辨率:{', '.join(sorted(valid_resolutions))}"
|
||||
)
|
||||
|
||||
# Nano Banana 2 系列特有的宽高比
|
||||
nano_2_exclusive_ratios = {"1:4", "1:8", "4:1", "8:1"}
|
||||
# 其他模型使用了 Nano Banana 2 系列专属宽高比
|
||||
if model_name not in {"Nano Banana 2", "Nano Banana 2 Lite"} and aspect_ratio in nano_2_exclusive_ratios:
|
||||
raise ValueError(
|
||||
f"宽高比 {aspect_ratio} 仅支持 Nano Banana 2 系列模型,"
|
||||
f"当前模型 {model_name} 不支持此宽高比"
|
||||
)
|
||||
|
||||
# Nano Banana 只支持 1K
|
||||
if model_name == "Nano Banana" and resolution not in {"智能", "1K"}:
|
||||
raise ValueError(
|
||||
f"Nano Banana 模型仅支持 1K 分辨率,"
|
||||
f"当前选择的 {resolution} 不支持"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
reference_images = io.Autogrow.Input(
|
||||
"参考图组",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
input=io.Image.Input("参考图"),
|
||||
names=[f"参考图{i}" for i in range(1, MAX_REFERENCE_IMAGES + 1)],
|
||||
min=0,
|
||||
),
|
||||
tooltip=f"连接后自动增加输入端口,合计最多 {MAX_REFERENCE_IMAGES} 张参考图。",
|
||||
)
|
||||
return io.Schema(
|
||||
node_id="NanoBanana",
|
||||
display_name="Nano Banana",
|
||||
@@ -356,74 +455,97 @@ class NanoBanana(io.ComfyNode):
|
||||
inputs=[
|
||||
io.String.Input(
|
||||
"prompt",
|
||||
default="一个中国女子的OOTD",
|
||||
default="",
|
||||
multiline=True,
|
||||
),
|
||||
io.DynamicCombo.Input("模型", options=[
|
||||
io.DynamicCombo.Option("Nano Banana Pro", [
|
||||
io.Combo.Input("宽高比", options=[
|
||||
"智能", "1:1", "2:3", "3:2", "3:4", "4:3",
|
||||
"4:5", "5:4", "9:16", "16:9", "21:9",
|
||||
], default="智能"),
|
||||
io.Combo.Input("分辨率", options=["1K", "2K", "4K"], default="2K"),
|
||||
]),
|
||||
io.DynamicCombo.Option("Nano Banana 2", [
|
||||
io.Combo.Input("宽高比", options=[
|
||||
"智能", "1:1", "1:4", "1:8", "2:3", "3:2", "3:4",
|
||||
"4:1", "4:3", "4:5", "5:4", "8:1",
|
||||
"9:16", "16:9", "21:9",
|
||||
], default="智能"),
|
||||
io.Combo.Input("分辨率", options=["512px", "1K", "2K", "4K"], default="2K"),
|
||||
io.Combo.Input("思考深度", options=["高", "低"], default="高"),
|
||||
]),
|
||||
io.DynamicCombo.Option("Nano Banana", [
|
||||
io.Combo.Input("宽高比", options=[
|
||||
"智能", "1:1", "2:3", "3:2", "3:4", "4:3",
|
||||
"4:5", "5:4", "9:16", "16:9", "21:9",
|
||||
], default="智能"),
|
||||
io.Combo.Input("分辨率", options=["1K"], default="1K"),
|
||||
]),
|
||||
]),
|
||||
io.Int.Input("生图数量", default=1, min=1, max=1000, step=1),
|
||||
io.Combo.Input("网络", options=NETWORK_ROUTE_OPTIONS, default="全球加速"),
|
||||
io.Combo.Input("计费", options=["特价", "官方"], default="特价"),
|
||||
io.Combo.Input("谷歌搜索", options=["关闭", "打开"], default="关闭"),
|
||||
io.Int.Input("seed", default=0, min=0, max=0xFFFFFFFFFFFFFFFF),
|
||||
io.Image.Input("参考图1", optional=True),
|
||||
io.Image.Input("参考图2", optional=True),
|
||||
io.Image.Input("参考图3", optional=True),
|
||||
io.Image.Input("参考图4", optional=True),
|
||||
io.Image.Input("参考图5", optional=True),
|
||||
io.Image.Input("参考图6", optional=True),
|
||||
io.Image.Input("参考图7", optional=True),
|
||||
io.Image.Input("参考图8", optional=True),
|
||||
io.Image.Input("参考图9", optional=True),
|
||||
io.Combo.Input("模型", options=NANO_BANANA_MODEL_OPTIONS, default="Nano Banana 2"),
|
||||
io.Combo.Input(
|
||||
"模型线路",
|
||||
options=NANO_BANANA_ROUTE_OPTIONS,
|
||||
default="畅速",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"思考等级",
|
||||
options=["低", "高"],
|
||||
default="高",
|
||||
tooltip="仅 Nano Banana 2 生效:低=minimal,高=high。",
|
||||
),
|
||||
io.Combo.Input("分辨率", options=["1K", "2K", "4K"], default="2K"),
|
||||
io.Combo.Input("宽高比", options=[
|
||||
"智能", "1:1", "1:4", "1:8", "2:3", "3:2", "3:4",
|
||||
"4:1", "4:3", "4:5", "5:4", "8:1",
|
||||
"9:16", "16:9", "21:9",
|
||||
], default="智能"),
|
||||
io.Combo.Input(
|
||||
"生图数量",
|
||||
options=["1", "2", "4", "9"],
|
||||
default="1",
|
||||
tooltip="选择本次生成的图像数量。",
|
||||
),
|
||||
io.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=0xFFFFFFFFFFFFFFFF,
|
||||
),
|
||||
reference_images,
|
||||
io.Combo.Input(
|
||||
"缩放图片",
|
||||
options=["不缩放", "智能缩放"],
|
||||
default="不缩放",
|
||||
tooltip="请求体超过 18 MiB 时,智能缩放会等比缩小占用最大的参考图。",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output(display_name="输出图像"),
|
||||
],
|
||||
# Accept the former 参考图1~参考图9 keys when executing workflows
|
||||
# saved before the Autogrow migration.
|
||||
accept_all_inputs=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, prompt, 模型, 生图数量, 计费, 网络, 谷歌搜索, seed, **kwargs) -> io.NodeOutput:
|
||||
def execute(
|
||||
cls,
|
||||
prompt,
|
||||
模型,
|
||||
分辨率,
|
||||
宽高比,
|
||||
生图数量,
|
||||
模型线路="畅速",
|
||||
seed=0,
|
||||
思考等级="高",
|
||||
缩放图片="不缩放",
|
||||
**kwargs,
|
||||
) -> io.NodeOutput:
|
||||
start_time = time.time()
|
||||
was_interrupted = False
|
||||
生图数量 = int(生图数量)
|
||||
|
||||
model_name = 模型["模型"]
|
||||
宽高比 = 模型["宽高比"]
|
||||
分辨率 = 模型["分辨率"]
|
||||
思考深度 = 模型.get("思考深度")
|
||||
model_name = 模型
|
||||
# 兼容旧工作流/外部调用传入的“计费”字段。
|
||||
模型线路 = kwargs.pop("计费", 模型线路)
|
||||
unlimited_downloads = kwargs.pop("_o1key_unlimited_downloads", False) is True
|
||||
requested_google_search = kwargs.pop("_o1key_google_search", False) is True
|
||||
google_search = model_name == "Nano Banana 2" and requested_google_search
|
||||
resize_mode = str(缩放图片)
|
||||
if resize_mode not in {"不缩放", "智能缩放"}:
|
||||
raise ValueError("缩放图片参数无效")
|
||||
# 验证模型和宽高比、分辨率的组合是否合法
|
||||
cls._validate_model_config(model_name, 宽高比, 分辨率)
|
||||
|
||||
enable_grounding = (谷歌搜索 == "打开")
|
||||
if 思考等级 not in THINKING_LEVEL_MAP:
|
||||
raise ValueError("思考等级无效,仅支持:低、高")
|
||||
thinking_level = (
|
||||
THINKING_LEVEL_MAP[思考等级]
|
||||
if model_name == "Nano Banana 2"
|
||||
else None
|
||||
)
|
||||
|
||||
thinking_level = None
|
||||
if model_name == "Nano Banana 2" and 思考深度:
|
||||
thinking_level = "High" if 思考深度 == "高" else "Low"
|
||||
|
||||
actual_model = _build_model_id(model_name, 分辨率, 计费)
|
||||
actual_model = _build_model_id(model_name, 分辨率, 模型线路)
|
||||
|
||||
api_key = get_api_key_or_raise("O1KEY_API_KEY")
|
||||
base_url = get_base_url_by_route(网络)
|
||||
base_url = get_base_url_by_route()
|
||||
|
||||
pbar = ProgressBar(生图数量) if PROGRESS_BAR_AVAILABLE else None
|
||||
|
||||
@@ -431,30 +553,37 @@ class NanoBanana(io.ComfyNode):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed % (2**32))
|
||||
|
||||
input_images = []
|
||||
for i in range(1, 10):
|
||||
key = f"参考图{i}"
|
||||
if key in kwargs and kwargs[key] is not None:
|
||||
pil_imgs = tensor_to_pil(kwargs[key])
|
||||
input_images.extend(pil_imgs)
|
||||
reference_inputs = _collect_autogrow_inputs(kwargs.get("参考图组"))
|
||||
if not reference_inputs:
|
||||
# Keep execution compatibility with workflows created before
|
||||
# the Autogrow input replaced the nine fixed image sockets.
|
||||
reference_inputs = [
|
||||
kwargs[f"参考图{i}"]
|
||||
for i in range(1, 10)
|
||||
if kwargs.get(f"参考图{i}") is not None
|
||||
]
|
||||
|
||||
if len(input_images) > 14:
|
||||
raise ValueError(f"输入图像数量 {len(input_images)} 超过限制 14 张")
|
||||
input_images = []
|
||||
for image_input in reference_inputs:
|
||||
input_images.extend(tensor_to_pil(image_input))
|
||||
|
||||
if len(input_images) > MAX_REFERENCE_IMAGES:
|
||||
raise ValueError(
|
||||
f"输入图像数量 {len(input_images)} 超过限制 {MAX_REFERENCE_IMAGES} 张"
|
||||
)
|
||||
|
||||
batch_prompts = parse_batch_prompts(prompt)
|
||||
|
||||
grounding_str = " | 谷歌搜索接地" if enable_grounding else ""
|
||||
thinking_str = f" | 思考:{thinking_level}" if thinking_level else ""
|
||||
if batch_prompts:
|
||||
num_prompts = len(batch_prompts)
|
||||
total_images = num_prompts * 生图数量
|
||||
mode_str = f"批量提示词模式 ({num_prompts}个提示词)"
|
||||
if input_images:
|
||||
mode_str += f" (输入{len(input_images)}张)"
|
||||
print(f"Nano Banana: {mode_str} | {分辨率} {宽高比} | 共{total_images}张{grounding_str}{thinking_str}")
|
||||
print(f"Nano Banana: {mode_str} | {分辨率} {宽高比} | 共{total_images}张")
|
||||
else:
|
||||
mode_str = f"图生图模式 (输入{len(input_images)}张)" if input_images else "文生图模式"
|
||||
print(f"Nano Banana: {mode_str} | {分辨率} {宽高比} | {生图数量}张{grounding_str}{thinking_str}")
|
||||
print(f"Nano Banana: {mode_str} | {分辨率} {宽高比} | {生图数量}张")
|
||||
|
||||
if batch_prompts or 生图数量 > 1:
|
||||
prompts = batch_prompts if batch_prompts else [prompt]
|
||||
@@ -479,11 +608,14 @@ class NanoBanana(io.ComfyNode):
|
||||
images_per_prompt=images_per_prompt,
|
||||
input_images=input_images,
|
||||
pbar=pbar,
|
||||
enable_grounding=enable_grounding,
|
||||
thinking_level=thinking_level,
|
||||
google_search=google_search,
|
||||
resize_mode=resize_mode,
|
||||
unlimited_downloads=unlimited_downloads,
|
||||
))
|
||||
)
|
||||
finally:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
@@ -516,8 +648,11 @@ class NanoBanana(io.ComfyNode):
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
async def _do():
|
||||
connector = aiohttp.TCPConnector(ssl=False)
|
||||
async with aiohttp.ClientSession(connector=connector) as session:
|
||||
async with create_http_client(
|
||||
http2=True,
|
||||
max_connections=_HTTP_MAX_CONNECTIONS,
|
||||
max_keepalive_connections=_HTTP_MAX_KEEPALIVE_CONNECTIONS,
|
||||
) as session:
|
||||
return await _generate_single(
|
||||
session=session,
|
||||
base_url=base_url,
|
||||
@@ -527,25 +662,37 @@ class NanoBanana(io.ComfyNode):
|
||||
resolution=分辨率,
|
||||
aspect_ratio=宽高比,
|
||||
images=input_images if input_images else None,
|
||||
enable_grounding=enable_grounding,
|
||||
thinking_level=thinking_level,
|
||||
google_search=google_search,
|
||||
download_semaphore=(
|
||||
None
|
||||
if unlimited_downloads
|
||||
else asyncio.Semaphore(_MAX_DOWNLOAD_CONCURRENCY)
|
||||
),
|
||||
progress_callback=_make_progress_callback(pbar),
|
||||
resize_mode=resize_mode,
|
||||
)
|
||||
return loop.run_until_complete(_run_with_interrupt(_do()))
|
||||
finally:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(run_single)
|
||||
generated_images, task_ms, parse_ms = future.result(timeout=_REQUEST_TIMEOUT)
|
||||
generated_images, timing = future.result(timeout=_REQUEST_TIMEOUT)
|
||||
|
||||
output_tensor = _images_to_tensor_safe(generated_images, _NODE)
|
||||
elapsed = time.time() - start_time
|
||||
time_str = f"{elapsed:.3f}s" if elapsed < 1 else f"{elapsed:.2f}s"
|
||||
task_str = f"{task_ms/1000:.2f}s"
|
||||
parse_str = f"{parse_ms/1000:.2f}s"
|
||||
print(f"完成!总耗时 {time_str} | 异步任务 {task_str} | 解析 {parse_str} | 成功 {len(generated_images)}张")
|
||||
|
||||
task_str = f"{timing['task_ms']/1000:.2f}s"
|
||||
download_str = f"{timing['download_ms']/1000:.2f}s"
|
||||
parse_str = f"{max(0, timing['parse_ms'] - timing['download_ms'])/1000:.2f}s"
|
||||
inline_suffix = f" | 内联={timing['inline_images']}张" if timing['inline_images'] else ""
|
||||
print(
|
||||
f"Nano Banana: 完成 ✓ | task_id={timing['task_id']} | "
|
||||
f"生成={len(generated_images)} 张 | 耗时={time_str} "
|
||||
f"(生成 {task_str} | 下载 {download_str} | 解析 {parse_str}{inline_suffix})"
|
||||
)
|
||||
import gc; gc.collect()
|
||||
return io.NodeOutput(output_tensor)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user