feat: 重命名异步生图节点为NanoBananaV2,移除gpt-5.4及旧官方模型配置
- AsyncImageGenerator → NanoBananaV2 - BatchAsyncImageGenerator → NanoBananaV2Batch - 移除 universal_llm 中的 gpt-5.4 模型 - 移除 models_config 中的 nano-banana-pro-官方 和 nano-banana-2-官方 Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
+7
-7
@@ -13,7 +13,7 @@ Comfyui_o1key - ComfyUI 自定义节点集合
|
|||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, SaveCleanImage, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, MultiResPreview, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, KVideo
|
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, SaveCleanImage, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, MultiResPreview, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, KVideo
|
||||||
from .nodes import K3Video, K3VideoFirstLast, K3MotionControl, K3MotionVideoCheck, AsyncImageGenerator, BatchAsyncImageGenerator
|
from .nodes import K3Video, K3VideoFirstLast, K3MotionControl, K3MotionVideoCheck, NanoBananaV2, NanoBananaV2Batch
|
||||||
|
|
||||||
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
|
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
|
||||||
_MSG_TIMEOUT = "API 请求超时,请稍后重试或检查网络。"
|
_MSG_TIMEOUT = "API 请求超时,请稍后重试或检查网络。"
|
||||||
@@ -47,8 +47,8 @@ def _wrap_generate_for_error_display(cls, attr="generate"):
|
|||||||
|
|
||||||
_wrap_generate_for_error_display(NanoBananaPro)
|
_wrap_generate_for_error_display(NanoBananaPro)
|
||||||
_wrap_generate_for_error_display(BatchNanoBananaPro)
|
_wrap_generate_for_error_display(BatchNanoBananaPro)
|
||||||
_wrap_generate_for_error_display(AsyncImageGenerator)
|
_wrap_generate_for_error_display(NanoBananaV2)
|
||||||
_wrap_generate_for_error_display(BatchAsyncImageGenerator)
|
_wrap_generate_for_error_display(NanoBananaV2Batch)
|
||||||
|
|
||||||
# ComfyUI 节点注册
|
# ComfyUI 节点注册
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
@@ -79,8 +79,8 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"K3VideoFirstLast": K3VideoFirstLast,
|
"K3VideoFirstLast": K3VideoFirstLast,
|
||||||
"K3MotionControl": K3MotionControl,
|
"K3MotionControl": K3MotionControl,
|
||||||
"K3MotionVideoCheck": K3MotionVideoCheck,
|
"K3MotionVideoCheck": K3MotionVideoCheck,
|
||||||
"AsyncImageGenerator": AsyncImageGenerator,
|
"NanoBananaV2": NanoBananaV2,
|
||||||
"BatchAsyncImageGenerator": BatchAsyncImageGenerator,
|
"NanoBananaV2Batch": NanoBananaV2Batch,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -111,8 +111,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"K3VideoFirstLast": "首尾帧 K3 自研",
|
"K3VideoFirstLast": "首尾帧 K3 自研",
|
||||||
"K3MotionControl": "动作控制 K3 自研",
|
"K3MotionControl": "动作控制 K3 自研",
|
||||||
"K3MotionVideoCheck": "视频时长检测 K3",
|
"K3MotionVideoCheck": "视频时长检测 K3",
|
||||||
"AsyncImageGenerator": "AI生图",
|
"NanoBananaV2": "Nano Banana V2",
|
||||||
"BatchAsyncImageGenerator": "AI生图(批量版)",
|
"NanoBananaV2Batch": "Nano Banana V2(批量)",
|
||||||
}
|
}
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web"
|
WEB_DIRECTORY = "./web"
|
||||||
|
|||||||
@@ -93,31 +93,6 @@ GEMINI_MODELS = [
|
|||||||
],
|
],
|
||||||
"supported_resolutions": ["512px", "1K", "2K", "4K"]
|
"supported_resolutions": ["512px", "1K", "2K", "4K"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "nano-banana-pro-官方",
|
|
||||||
"description": "Nano Banana Pro 官方,固定端点,适用于常规图像生成",
|
|
||||||
"enabled": True,
|
|
||||||
"provider": "gemini_async",
|
|
||||||
"endpoint_type": "standard",
|
|
||||||
"endpoint": "/v1beta/models/gemini-3-pro-image-preview:generateContent",
|
|
||||||
"supported_aspect_ratios": [
|
|
||||||
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
|
|
||||||
],
|
|
||||||
"supported_resolutions": ["1K", "2K", "4K"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "nano-banana-2-官方",
|
|
||||||
"description": "Nano Banana 2 官方,固定端点,快速图像生成模型",
|
|
||||||
"enabled": True,
|
|
||||||
"provider": "gemini_async",
|
|
||||||
"endpoint_type": "standard",
|
|
||||||
"endpoint": "/v1beta/models/gemini-3.1-flash-image-preview:generateContent",
|
|
||||||
"supported_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"
|
|
||||||
],
|
|
||||||
"supported_resolutions": ["512px", "1K", "2K", "4K"]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ from .universal_llm import UniversalLLMChat
|
|||||||
from .multi_res_preview import MultiResPreview
|
from .multi_res_preview import MultiResPreview
|
||||||
from .batch_images_o1key import BatchImagesO1key
|
from .batch_images_o1key import BatchImagesO1key
|
||||||
from .seedance_video import Seedance, SeedanceMultiModal
|
from .seedance_video import Seedance, SeedanceMultiModal
|
||||||
from .async_image_generation import AsyncImageGenerator, BatchAsyncImageGenerator
|
from .nano_banana_v2 import NanoBananaV2, NanoBananaV2Batch, AsyncImageGenerator, BatchAsyncImageGenerator
|
||||||
from .doubao_image import DoubaoImage
|
from .doubao_image import DoubaoImage
|
||||||
from .gpt_image import O1keyGPTImage
|
from .gpt_image import O1keyGPTImage
|
||||||
from .K_video import KVideo
|
from .K_video import KVideo
|
||||||
@@ -26,4 +26,4 @@ from .K3_video import K3Video
|
|||||||
from .K3_video_firstlast import K3VideoFirstLast
|
from .K3_video_firstlast import K3VideoFirstLast
|
||||||
from .K3_motion_control import K3MotionControl, K3MotionVideoCheck
|
from .K3_motion_control import K3MotionControl, K3MotionVideoCheck
|
||||||
|
|
||||||
__all__ = ['NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage', 'O1keyGPTImage', 'KVideo', 'K3Video', 'K3VideoFirstLast', 'K3MotionControl', 'K3MotionVideoCheck', 'AsyncImageGenerator', 'BatchAsyncImageGenerator']
|
__all__ = ['NanoBananaV2', 'NanoBananaV2Batch', 'NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage', 'O1keyGPTImage', 'KVideo', 'K3Video', 'K3VideoFirstLast', 'K3MotionControl', 'K3MotionVideoCheck', 'AsyncImageGenerator', 'BatchAsyncImageGenerator']
|
||||||
|
|||||||
@@ -790,7 +790,7 @@ class BatchNanoBananaPro:
|
|||||||
|
|
||||||
# 校验图片搜索(联网)与模型的兼容性
|
# 校验图片搜索(联网)与模型的兼容性
|
||||||
# 仅 nano-banana-2-限时特价 和 gemini-3.1-flash-image-preview 支持图片搜索
|
# 仅 nano-banana-2-限时特价 和 gemini-3.1-flash-image-preview 支持图片搜索
|
||||||
IMAGE_SEARCH_UNSUPPORTED_MODELS = ["nano-banana-pro-限时特价", "nano-banana-pro-官方计费", "gemini-3-pro-image-preview"]
|
IMAGE_SEARCH_UNSUPPORTED_MODELS = ["nano-banana-pro-次卡", "nano-banana-pro-官方计费", "gemini-3-pro-image-preview"]
|
||||||
if enable_image_search and 模型 in IMAGE_SEARCH_UNSUPPORTED_MODELS:
|
if enable_image_search and 模型 in IMAGE_SEARCH_UNSUPPORTED_MODELS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"模型 \"{模型}\" 不支持【图片搜索(联网)】功能!"
|
f"模型 \"{模型}\" 不支持【图片搜索(联网)】功能!"
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ class NanoBananaPro:
|
|||||||
|
|
||||||
# 校验图片搜索(联网)与模型的兼容性
|
# 校验图片搜索(联网)与模型的兼容性
|
||||||
# 仅 nano-banana-2-限时特价 和 gemini-3.1-flash-image-preview 支持图片搜索
|
# 仅 nano-banana-2-限时特价 和 gemini-3.1-flash-image-preview 支持图片搜索
|
||||||
IMAGE_SEARCH_UNSUPPORTED_MODELS = ["nano-banana-pro-限时特价", "nano-banana-pro-官方计费", "gemini-3-pro-image-preview"]
|
IMAGE_SEARCH_UNSUPPORTED_MODELS = ["nano-banana-pro-次卡", "nano-banana-pro-官方计费", "gemini-3-pro-image-preview"]
|
||||||
if enable_image_search and 模型 in IMAGE_SEARCH_UNSUPPORTED_MODELS:
|
if enable_image_search and 模型 in IMAGE_SEARCH_UNSUPPORTED_MODELS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"模型 \"{模型}\" 不支持【图片搜索(联网)】功能!"
|
f"模型 \"{模型}\" 不支持【图片搜索(联网)】功能!"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ ComfyUI 自定义节点,通过异步提交+轮询模式调用多种生图模
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import math
|
|
||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -30,6 +29,7 @@ import numpy as np
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ..utils.image_utils import tensor_to_pil, pil_to_tensor, parse_batch_prompts
|
from ..utils.image_utils import tensor_to_pil, pil_to_tensor, parse_batch_prompts
|
||||||
|
from ..utils.file_utils import load_images_from_folder, pair_images_by_name, pair_images_cartesian
|
||||||
from ..utils.config import get_api_key_or_raise
|
from ..utils.config import get_api_key_or_raise
|
||||||
from ..models_config import (
|
from ..models_config import (
|
||||||
get_enabled_async_models,
|
get_enabled_async_models,
|
||||||
@@ -60,11 +60,10 @@ except ImportError:
|
|||||||
MEMORY_MONITOR_AVAILABLE = False
|
MEMORY_MONITOR_AVAILABLE = False
|
||||||
|
|
||||||
DEBUG_LOG_ENABLED = False
|
DEBUG_LOG_ENABLED = False
|
||||||
REQUEST_LOG_ENABLED = True
|
REQUEST_LOG_ENABLED = False
|
||||||
|
|
||||||
_POLL_INTERVAL = 2 # 轮询间隔(秒)
|
_POLL_INTERVAL = 2 # 轮询间隔(秒)
|
||||||
_MAX_WAIT_TIME = 900 # 单任务最大等待时间(秒)
|
_MAX_WAIT_TIME = 900 # 单任务最大等待时间(秒)
|
||||||
_MAX_CONCURRENT = 50 # 最大并发提交数
|
|
||||||
|
|
||||||
|
|
||||||
def _images_to_tensor_safe(images: List[Image.Image], node_label: str) -> torch.Tensor:
|
def _images_to_tensor_safe(images: List[Image.Image], node_label: str) -> torch.Tensor:
|
||||||
@@ -87,7 +86,7 @@ def _images_to_tensor_safe(images: List[Image.Image], node_label: str) -> torch.
|
|||||||
return pil_to_tensor(matched)
|
return pil_to_tensor(matched)
|
||||||
|
|
||||||
|
|
||||||
class AsyncImageGenerator:
|
class NanoBananaV2:
|
||||||
"""
|
"""
|
||||||
异步生图节点(通用)
|
异步生图节点(通用)
|
||||||
|
|
||||||
@@ -97,7 +96,7 @@ class AsyncImageGenerator:
|
|||||||
- 支持批量提示词、多参考图、代理端口
|
- 支持批量提示词、多参考图、代理端口
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NODE_LABEL = "AI生图"
|
NODE_LABEL = "Nano Banana V2"
|
||||||
|
|
||||||
# Provider 注册表:provider 名称 → 类路径
|
# Provider 注册表:provider 名称 → 类路径
|
||||||
PROVIDER_CLASSES = {
|
PROVIDER_CLASSES = {
|
||||||
@@ -117,8 +116,7 @@ class AsyncImageGenerator:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
# 模型列表(排除官方计费渠道)
|
models = get_enabled_async_models()
|
||||||
models = [m for m in get_enabled_async_models() if "官方计费" not in m]
|
|
||||||
if not models:
|
if not models:
|
||||||
models = ["请在 models_config.py 中启用至少一个异步模型"]
|
models = ["请在 models_config.py 中启用至少一个异步模型"]
|
||||||
|
|
||||||
@@ -150,6 +148,12 @@ class AsyncImageGenerator:
|
|||||||
"max": 0xffffffffffffffff
|
"max": 0xffffffffffffffff
|
||||||
})
|
})
|
||||||
|
|
||||||
|
optional["分组令牌"] = ("STRING", {
|
||||||
|
"default": "",
|
||||||
|
"multiline": False,
|
||||||
|
"placeholder": "手动填写分组令牌将覆盖 .config 中的默认令牌"
|
||||||
|
})
|
||||||
|
|
||||||
optional["代理端口"] = ("STRING", {
|
optional["代理端口"] = ("STRING", {
|
||||||
"default": "",
|
"default": "",
|
||||||
"multiline": False,
|
"multiline": False,
|
||||||
@@ -168,7 +172,7 @@ class AsyncImageGenerator:
|
|||||||
"生图数量": ("INT", {
|
"生图数量": ("INT", {
|
||||||
"default": 1,
|
"default": 1,
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 1000,
|
"max": 9,
|
||||||
"step": 1
|
"step": 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -185,8 +189,11 @@ class AsyncImageGenerator:
|
|||||||
# Provider 工厂
|
# Provider 工厂
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
||||||
def _get_provider(self, model_id: str, proxy_url: Optional[str] = None) -> BaseAsyncImageProvider:
|
def _get_provider(self, model_id: str, proxy_url: Optional[str] = None, api_key_override: Optional[str] = None) -> BaseAsyncImageProvider:
|
||||||
"""根据模型 ID 获取或创建对应的 Provider 实例"""
|
"""根据模型 ID 获取或创建对应的 Provider 实例
|
||||||
|
|
||||||
|
api_key_override: 手动填写的分组令牌,非空时优先使用,空则回退到 .config
|
||||||
|
"""
|
||||||
provider_name = get_model_provider(model_id)
|
provider_name = get_model_provider(model_id)
|
||||||
if not provider_name:
|
if not provider_name:
|
||||||
raise ValueError(f"模型 \"{model_id}\" 不支持异步模式")
|
raise ValueError(f"模型 \"{model_id}\" 不支持异步模式")
|
||||||
@@ -210,7 +217,9 @@ class AsyncImageGenerator:
|
|||||||
module = importlib.import_module(module_path)
|
module = importlib.import_module(module_path)
|
||||||
provider_class = getattr(module, class_name)
|
provider_class = getattr(module, class_name)
|
||||||
|
|
||||||
api_key = get_api_key_or_raise("O1KEY_API_KEY")
|
api_key = api_key_override.strip() if api_key_override else ""
|
||||||
|
if not api_key:
|
||||||
|
api_key = get_api_key_or_raise("O1KEY_API_KEY")
|
||||||
self._provider = provider_class(api_key=api_key, proxy_url=proxy_url)
|
self._provider = provider_class(api_key=api_key, proxy_url=proxy_url)
|
||||||
self._provider_name = provider_name
|
self._provider_name = provider_name
|
||||||
return self._provider
|
return self._provider
|
||||||
@@ -413,7 +422,7 @@ class AsyncImageGenerator:
|
|||||||
pbar=None,
|
pbar=None,
|
||||||
**extra_kwargs,
|
**extra_kwargs,
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
"""异步批量处理"""
|
"""全并发处理(V2 节点最多 9 个任务,无需分批)"""
|
||||||
# 构建任务定义
|
# 构建任务定义
|
||||||
tasks_def = []
|
tasks_def = []
|
||||||
for p_idx, prompt in enumerate(prompts):
|
for p_idx, prompt in enumerate(prompts):
|
||||||
@@ -421,7 +430,6 @@ class AsyncImageGenerator:
|
|||||||
tasks_def.append((p_idx, sub_idx, prompt))
|
tasks_def.append((p_idx, sub_idx, prompt))
|
||||||
|
|
||||||
total_tasks = len(tasks_def)
|
total_tasks = len(tasks_def)
|
||||||
num_batches = math.ceil(total_tasks / _MAX_CONCURRENT)
|
|
||||||
all_results: List[dict] = []
|
all_results: List[dict] = []
|
||||||
completed = 0
|
completed = 0
|
||||||
|
|
||||||
@@ -431,68 +439,57 @@ class AsyncImageGenerator:
|
|||||||
connector = aiohttp.TCPConnector(ssl=False, limit=0, limit_per_host=0)
|
connector = aiohttp.TCPConnector(ssl=False, limit=0, limit_per_host=0)
|
||||||
|
|
||||||
async with aiohttp.ClientSession(connector=connector) as session:
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
for batch_idx in range(num_batches):
|
# 一次性全并发提交
|
||||||
self._check_interrupt()
|
batch_tasks = []
|
||||||
|
for i, (_, _, prompt) in enumerate(tasks_def):
|
||||||
start_idx = batch_idx * _MAX_CONCURRENT
|
task = asyncio.create_task(
|
||||||
end_idx = min(start_idx + _MAX_CONCURRENT, total_tasks)
|
self._execute_one(
|
||||||
|
session=session,
|
||||||
batch_tasks = []
|
provider=provider,
|
||||||
for i in range(start_idx, end_idx):
|
prompt=prompt,
|
||||||
_, _, prompt = tasks_def[i]
|
model=model,
|
||||||
|
resolution=resolution,
|
||||||
task = asyncio.create_task(
|
aspect_ratio=aspect_ratio,
|
||||||
self._execute_one(
|
input_images=input_images,
|
||||||
session=session,
|
global_task_index=i,
|
||||||
provider=provider,
|
on_progress=_on_progress,
|
||||||
prompt=prompt,
|
**extra_kwargs,
|
||||||
model=model,
|
|
||||||
resolution=resolution,
|
|
||||||
aspect_ratio=aspect_ratio,
|
|
||||||
input_images=input_images,
|
|
||||||
global_task_index=i,
|
|
||||||
on_progress=_on_progress,
|
|
||||||
**extra_kwargs,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
batch_tasks.append(task)
|
)
|
||||||
|
batch_tasks.append(task)
|
||||||
|
|
||||||
batch_results = []
|
for coro in asyncio.as_completed(batch_tasks):
|
||||||
for coro in asyncio.as_completed(batch_tasks):
|
result_data = None
|
||||||
result_data = None
|
try:
|
||||||
try:
|
result_data = await coro
|
||||||
result_data = await coro
|
except InterruptProcessingException:
|
||||||
except InterruptProcessingException:
|
# 用户取消:终止所有未完成任务
|
||||||
# 用户取消:终止所有未完成任务
|
for t in batch_tasks:
|
||||||
for t in batch_tasks:
|
t.cancel()
|
||||||
t.cancel()
|
raise
|
||||||
raise
|
except Exception as e2:
|
||||||
except Exception as e2:
|
# 意外错误(不应发生,_execute_one 内部已捕获常规异常)
|
||||||
# 意外错误(不应发生,_execute_one 内部已捕获常规异常)
|
result_data = {
|
||||||
result_data = {
|
"success": False,
|
||||||
"success": False,
|
"error": str(e2),
|
||||||
"error": str(e2),
|
"generated_count": 0,
|
||||||
"generated_count": 0,
|
"output_images": [],
|
||||||
"output_images": [],
|
"prompt": "",
|
||||||
"prompt": "",
|
}
|
||||||
}
|
|
||||||
|
|
||||||
batch_results.append(result_data)
|
all_results.append(result_data)
|
||||||
completed += 1
|
completed += 1
|
||||||
|
|
||||||
prompt_snippet = (result_data.get("prompt", "") or "")[:30]
|
prompt_snippet = (result_data.get("prompt", "") or "").replace("\n", " ")[:30]
|
||||||
if result_data and result_data.get("success"):
|
if result_data and result_data.get("success"):
|
||||||
count = result_data.get("generated_count", 1)
|
count = result_data.get("generated_count", 1)
|
||||||
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> OK({count}张)")
|
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> OK({count}张)")
|
||||||
else:
|
else:
|
||||||
error_msg = result_data.get("error", "未知错误") if result_data else "未知错误"
|
error_msg = result_data.get("error", "未知错误") if result_data else "未知错误"
|
||||||
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> FAIL: {error_msg}")
|
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> FAIL: {error_msg}")
|
||||||
|
|
||||||
all_results.extend(batch_results)
|
import gc
|
||||||
|
gc.collect()
|
||||||
import gc
|
|
||||||
gc.collect()
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
return all_results
|
return all_results
|
||||||
|
|
||||||
@@ -515,10 +512,11 @@ class AsyncImageGenerator:
|
|||||||
# 提取通用可选参数
|
# 提取通用可选参数
|
||||||
seed: int = kwargs.pop("seed", 0)
|
seed: int = kwargs.pop("seed", 0)
|
||||||
proxy_port: str = kwargs.pop("代理端口", "")
|
proxy_port: str = kwargs.pop("代理端口", "")
|
||||||
|
api_key_override: str = kwargs.pop("分组令牌", "")
|
||||||
|
|
||||||
# 初始化 Provider
|
# 初始化 Provider
|
||||||
proxy_url = BaseAsyncImageProvider.build_proxy_url(proxy_port)
|
proxy_url = BaseAsyncImageProvider.build_proxy_url(proxy_port)
|
||||||
provider = self._get_provider(模型, proxy_url=proxy_url)
|
provider = self._get_provider(模型, proxy_url=proxy_url, api_key_override=api_key_override)
|
||||||
|
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
print(f"{self.NODE_LABEL}: 已启用代理加速 -> {proxy_url}")
|
print(f"{self.NODE_LABEL}: 已启用代理加速 -> {proxy_url}")
|
||||||
@@ -578,13 +576,10 @@ class AsyncImageGenerator:
|
|||||||
images_per_prompt = 生图数量
|
images_per_prompt = 生图数量
|
||||||
total_tasks = 生图数量
|
total_tasks = 生图数量
|
||||||
|
|
||||||
if total_tasks > 100:
|
|
||||||
print(f" {self.NODE_LABEL}: 警告!批量生成 {total_tasks} 张图片,内存占用可能较高")
|
|
||||||
|
|
||||||
if pbar is not None:
|
if pbar is not None:
|
||||||
pbar = ProgressBar(total_tasks)
|
pbar = ProgressBar(total_tasks)
|
||||||
|
|
||||||
# 在独立线程中运行异步批量处理
|
# 在独立线程中运行异步全并发处理
|
||||||
def run_async():
|
def run_async():
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
@@ -607,30 +602,26 @@ class AsyncImageGenerator:
|
|||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
future = executor.submit(run_async)
|
future = executor.submit(run_async)
|
||||||
# 总超时 = 批次数 × 单任务超时,保证每批都有完整的时间窗口
|
|
||||||
num_batches = math.ceil(total_tasks / _MAX_CONCURRENT)
|
|
||||||
batch_timeout = num_batches * _MAX_WAIT_TIME
|
|
||||||
try:
|
try:
|
||||||
results = future.result(timeout=batch_timeout)
|
results = future.result(timeout=_MAX_WAIT_TIME)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
raise RuntimeError(f"任务执行超时({batch_timeout}秒),请减少数量或检查网络")
|
raise RuntimeError(f"任务执行超时({_MAX_WAIT_TIME}秒),请减少数量或检查网络")
|
||||||
|
|
||||||
# 统计结果
|
# 统计结果
|
||||||
success_count = sum(1 for r in results if r.get("success"))
|
|
||||||
fail_count = len(results) - success_count
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
time_str = f"{elapsed:.3f}s" if elapsed < 1 else f"{elapsed:.2f}s"
|
time_str = f"{elapsed:.3f}s" if elapsed < 1 else f"{elapsed:.2f}s"
|
||||||
print(f"{self.NODE_LABEL}: 完成!总耗时 {time_str} | 成功: {success_count}/{total_tasks} | 失败: {fail_count}")
|
|
||||||
|
|
||||||
# 打印失败详情
|
|
||||||
failed = [r for r in results if not r.get("success")]
|
failed = [r for r in results if not r.get("success")]
|
||||||
if failed:
|
if failed:
|
||||||
|
reason = failed[0].get("error", "未知错误")
|
||||||
|
print(f"{self.NODE_LABEL}: 失败!原因:{reason}")
|
||||||
for fr in failed:
|
for fr in failed:
|
||||||
idx = fr.get("global_task_index", -1) + 1
|
idx = fr.get("global_task_index", -1) + 1
|
||||||
prompt_snippet = (fr.get("prompt", "") or "")[:30]
|
prompt_snippet = (fr.get("prompt", "") or "").replace("\n", " ")[:30]
|
||||||
error_msg = fr.get("error", "未知错误")
|
error_msg = fr.get("error", "未知错误")
|
||||||
print(f" FAIL #{idx}: {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> {error_msg}")
|
print(f" FAIL #{idx}: {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> {error_msg}")
|
||||||
|
else:
|
||||||
|
print(f"{self.NODE_LABEL}: 完成!总耗时 {time_str}")
|
||||||
|
|
||||||
# 收集输出图像
|
# 收集输出图像
|
||||||
output_images = []
|
output_images = []
|
||||||
@@ -641,10 +632,10 @@ class AsyncImageGenerator:
|
|||||||
# 收集所有错误原因
|
# 收集所有错误原因
|
||||||
error_details = "\n".join(
|
error_details = "\n".join(
|
||||||
f" - {r.get('prompt', '未知提示词')[:40]}: {r.get('error', '未知错误')}"
|
f" - {r.get('prompt', '未知提示词')[:40]}: {r.get('error', '未知错误')}"
|
||||||
for r in results if not r.get("success")
|
for r in failed
|
||||||
)
|
)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"所有任务均失败 ({fail_count}/{total_tasks}):\n{error_details}"
|
f"所有任务均失败 ({len(failed)}/{len(results)}):\n{error_details}"
|
||||||
)
|
)
|
||||||
|
|
||||||
output_tensor = _images_to_tensor_safe(output_images, self.NODE_LABEL)
|
output_tensor = _images_to_tensor_safe(output_images, self.NODE_LABEL)
|
||||||
@@ -681,9 +672,9 @@ class AsyncImageGenerator:
|
|||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
class BatchAsyncImageGenerator(AsyncImageGenerator):
|
class NanoBananaV2Batch(NanoBananaV2):
|
||||||
"""
|
"""
|
||||||
AI生图(批量版)- 全并发提交 + 即时落盘
|
Nano Banana V2(批量)- 全并发提交 + 即时落盘
|
||||||
|
|
||||||
与原版区别:
|
与原版区别:
|
||||||
- 所有任务一次性全并发提交,不分批次
|
- 所有任务一次性全并发提交,不分批次
|
||||||
@@ -691,9 +682,35 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
- 最终从磁盘加载所有已保存的图像输出
|
- 最终从磁盘加载所有已保存的图像输出
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NODE_LABEL = "AI生图(批量版)"
|
NODE_LABEL = "Nano Banana V2(批量)"
|
||||||
RETURN_NAMES = ("输出图像",)
|
RETURN_NAMES = ("输出图像",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
types = super().INPUT_TYPES()
|
||||||
|
|
||||||
|
# 批量专用:文件夹图片路径
|
||||||
|
for i in range(1, 6):
|
||||||
|
types["optional"][f"图片路径(图{i})"] = ("STRING", {
|
||||||
|
"default": "",
|
||||||
|
"multiline": False,
|
||||||
|
"placeholder": f"填写后将加载文件夹中的图片作为第{i}组参考图"
|
||||||
|
})
|
||||||
|
|
||||||
|
types["optional"]["图片配对模式"] = (["不配对", "按相同图片命名", "1*N"], {
|
||||||
|
"default": "不配对"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 批量版恢复较大的生图数量上限
|
||||||
|
types["required"]["生图数量"] = ("INT", {
|
||||||
|
"default": 1,
|
||||||
|
"min": 1,
|
||||||
|
"max": 1000,
|
||||||
|
"step": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return types
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._output_file_paths: List[str] = []
|
self._output_file_paths: List[str] = []
|
||||||
@@ -720,9 +737,13 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
images_per_prompt: int,
|
images_per_prompt: int,
|
||||||
input_images: List[Image.Image],
|
input_images: List[Image.Image],
|
||||||
pbar=None,
|
pbar=None,
|
||||||
|
per_task_images: Optional[List[List[Image.Image]]] = None,
|
||||||
**extra_kwargs,
|
**extra_kwargs,
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
"""全并发处理:所有任务一次性提交,谁先完成谁先落盘"""
|
"""全并发处理:所有任务一次性提交,谁先完成谁先落盘
|
||||||
|
|
||||||
|
per_task_images: 可选,每个任务专属的图片列表,与 input_images 合并
|
||||||
|
"""
|
||||||
# 构建任务定义
|
# 构建任务定义
|
||||||
tasks_def = []
|
tasks_def = []
|
||||||
for p_idx, prompt in enumerate(prompts):
|
for p_idx, prompt in enumerate(prompts):
|
||||||
@@ -746,6 +767,9 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
# 一次性提交所有任务(全并发)
|
# 一次性提交所有任务(全并发)
|
||||||
batch_tasks = []
|
batch_tasks = []
|
||||||
for i, (_, _, prompt) in enumerate(tasks_def):
|
for i, (_, _, prompt) in enumerate(tasks_def):
|
||||||
|
task_imgs = list(input_images)
|
||||||
|
if per_task_images and i < len(per_task_images) and per_task_images[i]:
|
||||||
|
task_imgs = per_task_images[i] + task_imgs
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._execute_one(
|
self._execute_one(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -754,7 +778,7 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
model=model,
|
model=model,
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
aspect_ratio=aspect_ratio,
|
aspect_ratio=aspect_ratio,
|
||||||
input_images=input_images,
|
input_images=task_imgs,
|
||||||
global_task_index=i,
|
global_task_index=i,
|
||||||
on_progress=_on_progress,
|
on_progress=_on_progress,
|
||||||
**extra_kwargs,
|
**extra_kwargs,
|
||||||
@@ -796,7 +820,7 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
all_results.append(result_data)
|
all_results.append(result_data)
|
||||||
completed += 1
|
completed += 1
|
||||||
|
|
||||||
prompt_snippet = (result_data.get("prompt", "") or "")[:30]
|
prompt_snippet = (result_data.get("prompt", "") or "").replace("\n", " ")[:30]
|
||||||
if result_data and result_data.get("success"):
|
if result_data and result_data.get("success"):
|
||||||
count = result_data.get("saved_count", result_data.get("generated_count", 1))
|
count = result_data.get("saved_count", result_data.get("generated_count", 1))
|
||||||
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> OK({count}张) [已落盘]")
|
print(f"{self.NODE_LABEL}: [{completed}/{total_tasks}] {prompt_snippet}{'...' if len(prompt_snippet) >= 30 else ''} -> OK({count}张) [已落盘]")
|
||||||
@@ -823,9 +847,10 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
|
|
||||||
seed: int = kwargs.pop("seed", 0)
|
seed: int = kwargs.pop("seed", 0)
|
||||||
proxy_port: str = kwargs.pop("代理端口", "")
|
proxy_port: str = kwargs.pop("代理端口", "")
|
||||||
|
api_key_override: str = kwargs.pop("分组令牌", "")
|
||||||
|
|
||||||
proxy_url = BaseAsyncImageProvider.build_proxy_url(proxy_port)
|
proxy_url = BaseAsyncImageProvider.build_proxy_url(proxy_port)
|
||||||
provider = self._get_provider(模型, proxy_url=proxy_url)
|
provider = self._get_provider(模型, proxy_url=proxy_url, api_key_override=api_key_override)
|
||||||
|
|
||||||
if proxy_url:
|
if proxy_url:
|
||||||
print(f"{self.NODE_LABEL}: 已启用代理加速 -> {proxy_url}")
|
print(f"{self.NODE_LABEL}: 已启用代理加速 -> {proxy_url}")
|
||||||
@@ -859,6 +884,7 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
f"该模型支持的宽高比:{', '.join(supported_ratios)}"
|
f"该模型支持的宽高比:{', '.join(supported_ratios)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 收集参考图
|
||||||
input_images = []
|
input_images = []
|
||||||
for i in range(1, 10):
|
for i in range(1, 10):
|
||||||
key = f"参考图{i}"
|
key = f"参考图{i}"
|
||||||
@@ -869,24 +895,80 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
if input_images and len(input_images) > 14:
|
if input_images and len(input_images) > 14:
|
||||||
raise ValueError(f"输入图像数量 {len(input_images)} 超过限制 14 张")
|
raise ValueError(f"输入图像数量 {len(input_images)} 超过限制 14 张")
|
||||||
|
|
||||||
batch_prompts = parse_batch_prompts(prompt)
|
# 加载文件夹图片
|
||||||
|
folder_paths = []
|
||||||
|
for i in range(1, 6):
|
||||||
|
fp = kwargs.pop(f"图片路径(图{i})", "").strip()
|
||||||
|
if fp:
|
||||||
|
folder_paths.append(fp)
|
||||||
|
|
||||||
if batch_prompts:
|
pairing_mode = kwargs.pop("图片配对模式", "不配对")
|
||||||
num_prompts = len(batch_prompts)
|
per_task_images = None
|
||||||
total_images = num_prompts * 生图数量
|
if folder_paths:
|
||||||
mode_str = f"批量提示词模式 ({num_prompts}个提示词)"
|
# 按文件夹分组加载
|
||||||
if input_images:
|
folder_image_lists = [] # List[List[ImageInfo]]
|
||||||
mode_str += f" (输入{len(input_images)}张)"
|
for fp in folder_paths:
|
||||||
print(f"{self.NODE_LABEL}: {mode_str} | {分辨率} {宽高比} | 共{total_images}张")
|
try:
|
||||||
prompts_list = batch_prompts
|
infos = load_images_from_folder(fp)
|
||||||
images_per_prompt = 生图数量
|
if infos:
|
||||||
total_tasks = total_images
|
folder_image_lists.append(infos)
|
||||||
else:
|
except ValueError as e:
|
||||||
mode_str = f"图生图模式 (输入{len(input_images)}张)" if input_images else "文生图模式"
|
print(f"{self.NODE_LABEL}: {e}")
|
||||||
print(f"{self.NODE_LABEL}: {mode_str} | {分辨率} {宽高比} | {生图数量}张")
|
|
||||||
prompts_list = [prompt]
|
if folder_image_lists:
|
||||||
images_per_prompt = 生图数量
|
# 根据配对模式生成配对
|
||||||
total_tasks = 生图数量
|
if pairing_mode == "不配对":
|
||||||
|
if len(folder_image_lists) > 1:
|
||||||
|
raise ValueError("「不配对」模式只支持单个文件夹,请清空其他文件夹路径")
|
||||||
|
pairs = [(info,) for info in folder_image_lists[0]]
|
||||||
|
elif pairing_mode == "按相同图片命名":
|
||||||
|
pairs = list(pair_images_by_name(*folder_image_lists))
|
||||||
|
else: # 1*N
|
||||||
|
pairs = list(pair_images_cartesian(*folder_image_lists))
|
||||||
|
|
||||||
|
if pairs:
|
||||||
|
batch_prompts = parse_batch_prompts(prompt)
|
||||||
|
per_task_images = []
|
||||||
|
prompts_list = []
|
||||||
|
for pair in pairs:
|
||||||
|
task_imgs = [info.image for info in pair] + list(input_images)
|
||||||
|
if batch_prompts:
|
||||||
|
for bp in batch_prompts:
|
||||||
|
per_task_images.append(list(task_imgs))
|
||||||
|
prompts_list.append(bp)
|
||||||
|
else:
|
||||||
|
per_task_images.append(list(task_imgs))
|
||||||
|
prompts_list.append(prompt)
|
||||||
|
|
||||||
|
images_per_prompt = 1
|
||||||
|
total_tasks = len(prompts_list)
|
||||||
|
folder_count = len(folder_paths)
|
||||||
|
total_folder_imgs = sum(len(lst) for lst in folder_image_lists)
|
||||||
|
mode_str = f"文件夹批量模式 ({pairing_mode}, {folder_count}个文件夹, {total_folder_imgs}张图片→{len(pairs)}组)"
|
||||||
|
if batch_prompts:
|
||||||
|
mode_str += f" × {len(batch_prompts)}个提示词"
|
||||||
|
if input_images:
|
||||||
|
mode_str += f" (+{len(input_images)}张参考图)"
|
||||||
|
print(f"{self.NODE_LABEL}: {mode_str} | {分辨率} {宽高比} | 共{total_tasks}张")
|
||||||
|
|
||||||
|
if per_task_images is None:
|
||||||
|
batch_prompts = parse_batch_prompts(prompt)
|
||||||
|
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"{self.NODE_LABEL}: {mode_str} | {分辨率} {宽高比} | 共{total_images}张")
|
||||||
|
prompts_list = batch_prompts
|
||||||
|
images_per_prompt = 生图数量
|
||||||
|
total_tasks = total_images
|
||||||
|
else:
|
||||||
|
mode_str = f"图生图模式 (输入{len(input_images)}张)" if input_images else "文生图模式"
|
||||||
|
print(f"{self.NODE_LABEL}: {mode_str} | {分辨率} {宽高比} | {生图数量}张")
|
||||||
|
prompts_list = [prompt]
|
||||||
|
images_per_prompt = 生图数量
|
||||||
|
total_tasks = 生图数量
|
||||||
|
|
||||||
if total_tasks > 100:
|
if total_tasks > 100:
|
||||||
print(f" {self.NODE_LABEL}: 全并发模式,{total_tasks} 张图片将同时提交")
|
print(f" {self.NODE_LABEL}: 全并发模式,{total_tasks} 张图片将同时提交")
|
||||||
@@ -908,6 +990,7 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
images_per_prompt=images_per_prompt,
|
images_per_prompt=images_per_prompt,
|
||||||
input_images=input_images,
|
input_images=input_images,
|
||||||
pbar=pbar,
|
pbar=pbar,
|
||||||
|
per_task_images=per_task_images,
|
||||||
**extra_kwargs,
|
**extra_kwargs,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -927,7 +1010,9 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
|
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
time_str = f"{elapsed:.3f}s" if elapsed < 1 else f"{elapsed:.2f}s"
|
time_str = f"{elapsed:.3f}s" if elapsed < 1 else f"{elapsed:.2f}s"
|
||||||
print(f"{self.NODE_LABEL}: 完成!总耗时 {time_str} | 成功: {success_count}/{total_tasks} | 失败: {fail_count} | 已落盘: {len(self._output_file_paths)} 张")
|
avg_time = elapsed / success_count if success_count > 0 else 0
|
||||||
|
avg_str = f"{avg_time:.2f}s/张" if avg_time >= 1 else f"{avg_time:.3f}s/张"
|
||||||
|
print(f"{self.NODE_LABEL}: 完成!总耗时 {time_str} ({avg_str}) | 成功: {success_count}/{total_tasks} | 失败: {fail_count} | 已落盘: {len(self._output_file_paths)} 张")
|
||||||
|
|
||||||
failed = [r for r in results if not r.get("success")]
|
failed = [r for r in results if not r.get("success")]
|
||||||
if failed:
|
if failed:
|
||||||
@@ -990,3 +1075,8 @@ class BatchAsyncImageGenerator(AsyncImageGenerator):
|
|||||||
|
|
||||||
import gc
|
import gc
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
|
# 向后兼容别名(旧工作流中使用旧类名仍可正常加载)
|
||||||
|
AsyncImageGenerator = NanoBananaV2
|
||||||
|
BatchAsyncImageGenerator = NanoBananaV2Batch
|
||||||
@@ -26,7 +26,6 @@ from ..utils.file_types import FileList
|
|||||||
|
|
||||||
SUPPORTED_MODELS = [
|
SUPPORTED_MODELS = [
|
||||||
"gpt-5.5",
|
"gpt-5.5",
|
||||||
"gpt-5.4",
|
|
||||||
"gemini-3.1-flash-lite-preview",
|
"gemini-3.1-flash-lite-preview",
|
||||||
"gemini-3.1-pro-preview",
|
"gemini-3.1-pro-preview",
|
||||||
"deepseek-v4-pro",
|
"deepseek-v4-pro",
|
||||||
|
|||||||
Reference in New Issue
Block a user