feat: 新增「返回格式」参数支持 URL 模式图片返回

- nano_banana_pro / batch_nano_banana_pro 新增「返回格式」下拉参数(url/base64,默认 url)
- url 模式下请求端点自动追加 ?image_format=url 查询参数
- parse_response_async 新增 fileData 分支,解析服务端 URL 返回并异步下载图片
- batch 节点「代理加速」参数重命名为「代理端口(如7897)」,与 pro 节点保持一致
- image_format 完整透传:UI → generate → _process_batch_async → _generate_single_task → generate_single_async → get_endpoint
This commit is contained in:
Jony
2026-04-26 18:00:08 +08:00
parent 35333b296b
commit 8299594646
3 changed files with 97 additions and 39 deletions
+75 -37
View File
@@ -55,81 +55,87 @@ class GeminiAPIClient(BaseAPIClient):
return None return None
return f"http://127.0.0.1:{port}" return f"http://127.0.0.1:{port}"
def get_endpoint(self, model: str = "", resolution: str = "2K", **kwargs) -> str: def get_endpoint(self, model: str = "", resolution: str = "2K", image_format: str = "base64", **kwargs) -> str:
""" """
根据模型和分辨率获取 API 端点 根据模型和分辨率获取 API 端点
Args: Args:
model: 模型名称 model: 模型名称
resolution: 分辨率(1K, 2K, 4K resolution: 分辨率(1K, 2K, 4K
image_format: 返回格式,"url" 时追加 ?image_format=url 查询参数
Returns: Returns:
API 端点路径 API 端点路径
""" """
from ..models_config import get_model_endpoint from ..models_config import get_model_endpoint
# 特殊处理:动态端点模型(根据分辨率选择) # 特殊处理:动态端点模型(根据分辨率选择)
if model == "nano-banana-pro-限时特价": if model == "nano-banana-pro-限时特价":
if resolution == "1K": if resolution == "1K":
return "/v1beta/models/nano-banana-pro:generateContent" endpoint = "/v1beta/models/nano-banana-pro:generateContent"
elif resolution == "2K": elif resolution == "2K":
return "/v1beta/models/nano-banana-pro-2k:generateContent" endpoint = "/v1beta/models/nano-banana-pro-2k:generateContent"
elif resolution == "4K": elif resolution == "4K":
return "/v1beta/models/nano-banana-pro-4k:generateContent" endpoint = "/v1beta/models/nano-banana-pro-4k:generateContent"
else: else:
return "/v1beta/models/nano-banana-pro-2k:generateContent" endpoint = "/v1beta/models/nano-banana-pro-2k:generateContent"
elif model == "nano-banana-2-限时特价": elif model == "nano-banana-2-限时特价":
if resolution == "512px": if resolution == "512px":
return "/v1beta/models/nano-banana-2-0.5k:generateContent" endpoint = "/v1beta/models/nano-banana-2-0.5k:generateContent"
elif resolution == "1K": elif resolution == "1K":
return "/v1beta/models/nano-banana-2-1k:generateContent" endpoint = "/v1beta/models/nano-banana-2-1k:generateContent"
elif resolution == "2K": elif resolution == "2K":
return "/v1beta/models/nano-banana-2-2k:generateContent" endpoint = "/v1beta/models/nano-banana-2-2k:generateContent"
elif resolution == "4K": elif resolution == "4K":
return "/v1beta/models/nano-banana-2-4k:generateContent" endpoint = "/v1beta/models/nano-banana-2-4k:generateContent"
else: else:
return "/v1beta/models/nano-banana-2-2k:generateContent" endpoint = "/v1beta/models/nano-banana-2-2k:generateContent"
elif model == "nano-banana-2-官方计费": elif model == "nano-banana-2-官方计费":
if resolution == "512px": if resolution == "512px":
return "/v1beta/models/nano-banana-2-0.5k-official:generateContent" endpoint = "/v1beta/models/nano-banana-2-0.5k-official:generateContent"
elif resolution == "1K": elif resolution == "1K":
return "/v1beta/models/nano-banana-2-1k-official:generateContent" endpoint = "/v1beta/models/nano-banana-2-1k-official:generateContent"
elif resolution == "2K": elif resolution == "2K":
return "/v1beta/models/nano-banana-2-2k-official:generateContent" endpoint = "/v1beta/models/nano-banana-2-2k-official:generateContent"
elif resolution == "4K": elif resolution == "4K":
return "/v1beta/models/nano-banana-2-4k-official:generateContent" endpoint = "/v1beta/models/nano-banana-2-4k-official:generateContent"
else: else:
return "/v1beta/models/nano-banana-2-2k-official:generateContent" endpoint = "/v1beta/models/nano-banana-2-2k-official:generateContent"
elif model == "nano-banana-pro-官方计费": elif model == "nano-banana-pro-官方计费":
if resolution == "1K": if resolution == "1K":
return "/v1beta/models/nano-banana-pro-1k-official:generateContent" endpoint = "/v1beta/models/nano-banana-pro-1k-official:generateContent"
elif resolution == "2K": elif resolution == "2K":
return "/v1beta/models/nano-banana-pro-2k-official:generateContent" endpoint = "/v1beta/models/nano-banana-pro-2k-official:generateContent"
elif resolution == "4K": elif resolution == "4K":
return "/v1beta/models/nano-banana-pro-4k-official:generateContent" endpoint = "/v1beta/models/nano-banana-pro-4k-official:generateContent"
else: else:
return "/v1beta/models/nano-banana-pro-2k-official:generateContent" endpoint = "/v1beta/models/nano-banana-pro-2k-official:generateContent"
elif model == "gemini-3-pro-image-preview-url": elif model == "gemini-3-pro-image-preview-url":
if resolution == "1K": if resolution == "1K":
return "/v1beta/models/gemini-3-pro-image-preview-url:generateContent" endpoint = "/v1beta/models/gemini-3-pro-image-preview-url:generateContent"
elif resolution == "2K": elif resolution == "2K":
return "/v1beta/models/gemini-3-pro-image-preview-2k-url:generateContent" endpoint = "/v1beta/models/gemini-3-pro-image-preview-2k-url:generateContent"
elif resolution == "4K": elif resolution == "4K":
return "/v1beta/models/gemini-3-pro-image-preview-4k-url:generateContent" endpoint = "/v1beta/models/gemini-3-pro-image-preview-4k-url:generateContent"
else: else:
return "/v1beta/models/gemini-3-pro-image-preview-2k-url:generateContent" endpoint = "/v1beta/models/gemini-3-pro-image-preview-2k-url:generateContent"
# 其他模型:从配置文件读取端点 else:
endpoint = get_model_endpoint(model) # 其他模型:从配置文件读取端点
if endpoint: endpoint = get_model_endpoint(model)
return endpoint if not endpoint:
# 兜底:使用标准模式端点
# 兜底:使用标准模式端点 endpoint = "/v1beta/models/gemini-3-pro-image-preview:generateContent"
return "/v1beta/models/gemini-3-pro-image-preview:generateContent"
# url 模式:追加查询参数
if image_format == "url":
endpoint = endpoint + "?image_format=url"
return endpoint
def get_http_error_message(self, status_code: int, error_message: str) -> Optional[str]: def get_http_error_message(self, status_code: int, error_message: str) -> Optional[str]:
"""Gemini 请求 429/503/504 时返回中文错误文案。""" """Gemini 请求 429/503/504 时返回中文错误文案。"""
@@ -415,12 +421,39 @@ class GeminiAPIClient(BaseAPIClient):
if img_data: if img_data:
img = decode_base64_to_pil(img_data) img = decode_base64_to_pil(img_data)
images.append(img) images.append(img)
# 记录格式信息 # 记录格式信息
if format_info["type"] is None: if format_info["type"] is None:
format_info["type"] = "base64" format_info["type"] = "base64"
format_info["size"] = len(img_data) * 3 / 4 # Base64 解码后的字节数 format_info["size"] = len(img_data) * 3 / 4 # Base64 解码后的字节数
format_info["resolution"] = f"{img.size[0]}x{img.size[1]}" format_info["resolution"] = f"{img.size[0]}x{img.size[1]}"
# 方式1b: fileData?image_format=url 模式下服务端返回 URL 替代 inlineData
elif "fileData" in part:
if part.get("thought") is True:
continue
file_data = part["fileData"]
url = file_data.get("fileUri") or file_data.get("file_uri", "")
if url:
try:
download_start = time.time()
async with session.get(url) as img_response:
if img_response.status == 200:
img_bytes = await img_response.read()
download_time = time.time() - download_start
img_size = len(img_bytes)
speed = img_size / download_time if download_time > 0 else 0
img = Image.open(BytesIO(img_bytes))
images.append(img)
if format_info["type"] is None:
format_info["type"] = "url"
format_info["size"] = img_size
format_info["resolution"] = f"{img.size[0]}x{img.size[1]}"
format_info["download_speed"] = speed
except Exception:
pass # 静默失败
# 方式2: text 中的 URL - 改为异步下载 # 方式2: text 中的 URL - 改为异步下载
elif "text" in part: elif "text" in part:
@@ -525,6 +558,7 @@ class GeminiAPIClient(BaseAPIClient):
debug_request: bool = False, debug_request: bool = False,
enable_grounding: bool = False, enable_grounding: bool = False,
enable_image_search: bool = False, enable_image_search: bool = False,
image_format: str = "base64",
) -> tuple[List[Image.Image], Dict[str, Any]]: ) -> tuple[List[Image.Image], Dict[str, Any]]:
""" """
单次异步生成请求(极简单行日志) 单次异步生成请求(极简单行日志)
@@ -555,7 +589,7 @@ class GeminiAPIClient(BaseAPIClient):
# ========== 1. 构建请求 ========== # ========== 1. 构建请求 ==========
build_start = time.time() build_start = time.time()
endpoint = self.get_endpoint(model=model, resolution=resolution) endpoint = self.get_endpoint(model=model, resolution=resolution, image_format=image_format)
request_body = self.build_request_body( request_body = self.build_request_body(
prompt=prompt, prompt=prompt,
images=images, images=images,
@@ -697,6 +731,7 @@ class GeminiAPIClient(BaseAPIClient):
debug_request: bool = False, debug_request: bool = False,
enable_grounding: bool = False, enable_grounding: bool = False,
enable_image_search: bool = False, enable_image_search: bool = False,
image_format: str = "base64",
) -> List[Image.Image]: ) -> List[Image.Image]:
""" """
批量全并发生成 - 改进版:支持分批处理和内存管理 批量全并发生成 - 改进版:支持分批处理和内存管理
@@ -760,6 +795,7 @@ class GeminiAPIClient(BaseAPIClient):
debug_request=debug_request, debug_request=debug_request,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
), ),
name=f"task_{task_index}" name=f"task_{task_index}"
) )
@@ -831,6 +867,7 @@ class GeminiAPIClient(BaseAPIClient):
debug_request: bool = False, debug_request: bool = False,
enable_grounding: bool = False, enable_grounding: bool = False,
enable_image_search: bool = False, enable_image_search: bool = False,
image_format: str = "base64",
) -> List[Image.Image]: ) -> List[Image.Image]:
""" """
同步生成接口(用于 ComfyUI) 同步生成接口(用于 ComfyUI)
@@ -863,6 +900,7 @@ class GeminiAPIClient(BaseAPIClient):
debug_request=debug_request, debug_request=debug_request,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
) )
return self.run_async_in_thread(coro) return self.run_async_in_thread(coro)
+11 -2
View File
@@ -215,7 +215,7 @@ class BatchNanoBananaPro:
"default": "不配对" "default": "不配对"
}) })
optional_inputs["代理加速"] = ("STRING", { optional_inputs["代理端口(如7897"] = ("STRING", {
"default": "", "default": "",
"multiline": False, "multiline": False,
"placeholder": "本地代理端口,如 7897Clash Verge)或 10808v2rayN),留空不使用" "placeholder": "本地代理端口,如 7897Clash Verge)或 10808v2rayN),留空不使用"
@@ -242,6 +242,9 @@ class BatchNanoBananaPro:
"图片搜索(联网)": (["关闭", "打开"], { "图片搜索(联网)": (["关闭", "打开"], {
"default": "关闭" "default": "关闭"
}), }),
"返回格式": (["url", "base64"], {
"default": "url"
}),
"seed": ("INT", { "seed": ("INT", {
"default": 0, "default": 0,
"min": 0, "min": 0,
@@ -412,6 +415,7 @@ class BatchNanoBananaPro:
enable_grounding: bool = True, enable_grounding: bool = True,
enable_image_search: bool = False, enable_image_search: bool = False,
base_filename: str = None, base_filename: str = None,
image_format: str = "url",
) -> dict: ) -> dict:
""" """
执行单个生成任务 执行单个生成任务
@@ -458,6 +462,7 @@ class BatchNanoBananaPro:
debug_request=REQUEST_LOG_ENABLED, debug_request=REQUEST_LOG_ENABLED,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
) )
if gen_result: if gen_result:
# 正确解包元组:第一个元素是图像列表,第二个是计时信息 # 正确解包元组:第一个元素是图像列表,第二个是计时信息
@@ -532,6 +537,7 @@ class BatchNanoBananaPro:
prompts_per_task: Optional[List[str]] = None, prompts_per_task: Optional[List[str]] = None,
enable_grounding: bool = True, enable_grounding: bool = True,
enable_image_search: bool = False, enable_image_search: bool = False,
image_format: str = "url",
) -> List[dict]: ) -> List[dict]:
""" """
异步批量处理所有任务 - 改进版:支持分批保存 异步批量处理所有任务 - 改进版:支持分批保存
@@ -623,6 +629,7 @@ class BatchNanoBananaPro:
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
base_filename=base_filename, base_filename=base_filename,
image_format=image_format,
) )
) )
tasks.append(task) tasks.append(task)
@@ -748,7 +755,8 @@ class BatchNanoBananaPro:
# 从 kwargs 提取搜索参数(界面显示为「关闭/打开」,转为 bool 供调用) # 从 kwargs 提取搜索参数(界面显示为「关闭/打开」,转为 bool 供调用)
enable_grounding: bool = (kwargs.pop("谷歌搜索(联网)", "关闭") == "打开") enable_grounding: bool = (kwargs.pop("谷歌搜索(联网)", "关闭") == "打开")
enable_image_search: bool = (kwargs.pop("图片搜索(联网)", "关闭") == "打开") enable_image_search: bool = (kwargs.pop("图片搜索(联网)", "关闭") == "打开")
proxy_port: str = kwargs.pop("代理加速", "") proxy_port: str = kwargs.pop("代理端口(如7897", "")
image_format: str = kwargs.pop("返回格式", "url")
try: try:
@@ -916,6 +924,7 @@ class BatchNanoBananaPro:
prompts_per_task=prompts_per_task, prompts_per_task=prompts_per_task,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
) )
) )
except Exception as e: except Exception as e:
+11
View File
@@ -183,6 +183,9 @@ class NanoBananaPro:
"图片搜索(联网)": (["关闭", "打开"], { "图片搜索(联网)": (["关闭", "打开"], {
"default": "关闭" "default": "关闭"
}), }),
"返回格式": (["url", "base64"], {
"default": "url"
}),
"seed": ("INT", { "seed": ("INT", {
"default": 0, "default": 0,
"min": 0, "min": 0,
@@ -293,6 +296,7 @@ class NanoBananaPro:
enable_grounding: bool = False, enable_grounding: bool = False,
enable_image_search: bool = False, enable_image_search: bool = False,
save_to_disk: bool = True, save_to_disk: bool = True,
image_format: str = "url",
) -> dict: ) -> dict:
"""执行单个生成任务""" """执行单个生成任务"""
result = { result = {
@@ -317,6 +321,7 @@ class NanoBananaPro:
debug_request=REQUEST_LOG_ENABLED, debug_request=REQUEST_LOG_ENABLED,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
) )
if gen_result: if gen_result:
images_list, _ = gen_result images_list, _ = gen_result
@@ -352,6 +357,7 @@ class NanoBananaPro:
enable_grounding: bool = False, enable_grounding: bool = False,
enable_image_search: bool = False, enable_image_search: bool = False,
save_to_disk: bool = True, save_to_disk: bool = True,
image_format: str = "url",
) -> List[dict]: ) -> List[dict]:
"""异步批量处理:每个提示词独立调用 API""" """异步批量处理:每个提示词独立调用 API"""
# 构建任务列表:(prompt, sub_index) 用于 images_per_prompt > 1 的情况 # 构建任务列表:(prompt, sub_index) 用于 images_per_prompt > 1 的情况
@@ -394,6 +400,7 @@ class NanoBananaPro:
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
save_to_disk=save_to_disk, save_to_disk=save_to_disk,
image_format=image_format,
) )
) )
tasks.append(task) tasks.append(task)
@@ -471,6 +478,7 @@ class NanoBananaPro:
enable_grounding: bool = (kwargs.pop("谷歌搜索(联网)", "关闭") == "打开") enable_grounding: bool = (kwargs.pop("谷歌搜索(联网)", "关闭") == "打开")
enable_image_search: bool = (kwargs.pop("图片搜索(联网)", "关闭") == "打开") enable_image_search: bool = (kwargs.pop("图片搜索(联网)", "关闭") == "打开")
proxy_port: str = kwargs.pop("代理端口(如7897", "") proxy_port: str = kwargs.pop("代理端口(如7897", "")
image_format: str = kwargs.pop("返回格式", "url")
# 创建 ComfyUI 原生进度条 # 创建 ComfyUI 原生进度条
pbar = None pbar = None
@@ -629,6 +637,7 @@ class NanoBananaPro:
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
save_to_disk=False, save_to_disk=False,
image_format=image_format,
) )
) )
finally: finally:
@@ -694,6 +703,7 @@ class NanoBananaPro:
debug_request=REQUEST_LOG_ENABLED, debug_request=REQUEST_LOG_ENABLED,
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
image_format=image_format,
) )
break break
except RuntimeError as e: except RuntimeError as e:
@@ -731,6 +741,7 @@ class NanoBananaPro:
enable_grounding=enable_grounding, enable_grounding=enable_grounding,
enable_image_search=enable_image_search, enable_image_search=enable_image_search,
save_to_disk=False, save_to_disk=False,
image_format=image_format,
) )
) )
finally: finally: