From 949f7bb18069bd398fd6a34fa60ccb4b5b6c4c16 Mon Sep 17 00:00:00 2001 From: o1key <951565127@qq.com> Date: Wed, 22 Apr 2026 14:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20GPT=20Image=20=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B4=A8=E9=87=8F=E5=8F=82=E6=95=B0=E3=80=81?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=8F=90=E7=A4=BA=E8=AF=8D=E5=8F=8A=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增「质量」下拉参数(高/中/低/自动),默认 auto,两个接口均传递 quality 字段 - 支持批量提示词模式:prompt 中用单独一行 --- 分隔多条提示词,逐条调用 API 并合并输出 - 模型列表更新:移除 gpt-image-1 / gpt-image-1-特价,新增 gpt-image-2,默认改为 gpt-image-2 - size 字段始终传递(auto 也上传),移除 seed 参数传递 Co-Authored-By: Claude Sonnet 4.5 --- clients/gpt_image_client.py | 21 +++---- nodes/gpt_image.py | 113 +++++++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 47 deletions(-) diff --git a/clients/gpt_image_client.py b/clients/gpt_image_client.py index a9b7b94..3b13d78 100644 --- a/clients/gpt_image_client.py +++ b/clients/gpt_image_client.py @@ -33,7 +33,6 @@ _ENDPOINT_EDITS = "/v1/images/edits/" # ── 模型名映射(UI 显示名 → API 实际参数名)───────────────────────────────── _MODEL_NAME_MAP = { - "gpt-image-1-特价": "gpt-image-1-special", "gpt-image-1.5-特价": "gpt-image-1.5-special", "gpt-image-2-特价": "gpt-image-2-special", } @@ -215,13 +214,7 @@ class GptImageClient: "moderation": "low", } - # size = "auto" 时不传该字段,让 API 自行决定 - if size and size != "auto": - body["size"] = size - - # seed > 0 时才传递(0 视为不指定) - if seed > 0: - body["seed"] = seed + body["size"] = size if size else "auto" # 图生图:将 tensor 转成 data URI 内联 if image_tensor is not None: @@ -298,15 +291,15 @@ class GptImageClient: image_tensor = image_tensor.unsqueeze(0) # [H,W,C] → [1,H,W,C] num_images = image_tensor.shape[0] - # o1key 中转服务的 edits 接口暂不支持 quality / background / moderation / seed, + # o1key 中转服务的 edits 接口暂不支持 background / moderation / seed, # 待服务方更新后可重新加入。 form = aiohttp.FormData() - form.add_field("model", api_model) - form.add_field("prompt", prompt) - form.add_field("n", str(n)) + form.add_field("model", api_model) + form.add_field("prompt", prompt) + form.add_field("n", str(n)) + form.add_field("quality", quality) - if size and size != "auto": - form.add_field("size", size) + form.add_field("size", size if size else "auto") # 多图:用 image[] 数组字段逐张附加,支持 gpt-image-1.5 最多 16 张 for i in range(num_images): diff --git a/nodes/gpt_image.py b/nodes/gpt_image.py index f325fc4..647d852 100644 --- a/nodes/gpt_image.py +++ b/nodes/gpt_image.py @@ -7,6 +7,7 @@ import time import torch from ..clients.gpt_image_client import GptImageClient +from ..utils.image_utils import parse_batch_prompts class O1keyGPTImage: @@ -17,13 +18,15 @@ class O1keyGPTImage: - 文生图:仅提供 prompt - 图生图:提供 prompt + 图片(无遮罩) - 图像编辑:提供 prompt + 图片 + 遮罩(白色区域将被替换) + - 批量模式:prompt 中用单独一行 --- 分隔多条提示词 参数: - - prompt : 文本提示词(多行) + - prompt : 文本提示词(多行;用 --- 独占一行分隔批量提示词) - 模型 : 模型选择 - 分辨率 : 图像尺寸(auto 让 API 自动决定) - - 生图数量 : 生成数量 1-8 + - 生图数量 : 每条提示词生成数量 1-8 - seed : 随机种子(0 表示不指定) + - 质量 : 生成质量 - 图片 : 可选参考图(用于图生图或编辑) - 遮罩 : 可选蒙版(白色区域将被替换) """ @@ -35,18 +38,17 @@ class O1keyGPTImage: "prompt": ("STRING", { "default": "", "multiline": True, - "tooltip": "Text prompt for GPT Image", + "tooltip": "Text prompt for GPT Image. Use --- on its own line to separate batch prompts.", }), }, "optional": { "模型": ([ - "gpt-image-1", + "gpt-image-2", "gpt-image-1.5", "gpt-image-2-特价", - "gpt-image-1-特价", "gpt-image-1.5-特价", ], { - "default": "gpt-image-1.5", + "default": "gpt-image-2", }), "分辨率": ([ "auto(默认)", @@ -67,7 +69,7 @@ class O1keyGPTImage: "max": 8, "step": 1, "display": "number", - "tooltip": "How many images to generate", + "tooltip": "How many images to generate per prompt", }), "seed": ("INT", { "default": 0, @@ -78,6 +80,10 @@ class O1keyGPTImage: "control_after_generate": True, "tooltip": "Random seed (0 = not specified)", }), + "质量": (["高", "中", "低", "自动"], { + "default": "自动", + "tooltip": "Image quality: 高=high, 中=medium, 低=low, 自动=auto", + }), "图片": ("IMAGE", { "tooltip": "Optional reference image for image editing.", }), @@ -96,34 +102,37 @@ class O1keyGPTImage: def generate( self, prompt: str, - 模型: str = "gpt-image-1.5", + 模型: str = "gpt-image-2", 分辨率: str = "auto", + 质量: str = "自动", 生图数量: int = 1, seed: int = 0, 图片=None, 遮罩=None, ): """ - 生成图像(文生图 / 图生图 / 图像编辑) + 生成图像(文生图 / 图生图 / 图像编辑 / 批量提示词) 路由逻辑: - 无图片 → generations 接口(文生图) - 有图片,无遮罩 → edits 接口(图生图) - 有图片,有遮罩 → edits 接口(图像编辑 + 蒙版) + - prompt 含 --- → 批量模式,逐条调用上述接口 """ start_time = time.time() # ── 1. 参数校验 ─────────────────────────────────────────────────────── - if not prompt or not prompt.strip(): - raise ValueError("提示词不能为空") - if 遮罩 is not None and 图片 is None: raise ValueError("提供了遮罩但未提供图片,请同时提供图片和遮罩") # ── 2. 解析分辨率显示值 → API 参数值 ────────────────────────────────── size = 分辨率.split("(")[0].strip() - # ── 2. 创建客户端 ───────────────────────────────────────────────────── + # ── 2b. 解析质量显示值 → API 参数值 ─────────────────────────────────── + _quality_map = {"高": "high", "中": "medium", "低": "low", "自动": "auto"} + quality = _quality_map.get(质量, "auto") + + # ── 3. 创建客户端 ───────────────────────────────────────────────────── try: client = GptImageClient() except ValueError as e: @@ -132,28 +141,66 @@ class O1keyGPTImage: raise ValueError("未授权!") from None raise - # ── 3. 调用 API ─────────────────────────────────────────────────────── - try: - pil_images = client.run_sync( - prompt=prompt, - model=模型, - quality="low", - background="auto", - size=size, - n=生图数量, - seed=seed, - image_tensor=图片, - mask_tensor=遮罩, - ) - except Exception as e: - error_msg = str(e).split('\n')[0] - print(f"[o1key GPT Image] ❌ {error_msg}") - raise RuntimeError(error_msg) from None + # ── 4. 解析批量提示词 ───────────────────────────────────────────────── + batch_prompts = parse_batch_prompts(prompt) - # ── 4. PIL → tensor ─────────────────────────────────────────────────── - output_tensor = GptImageClient._pil_list_to_tensor(pil_images) + # ── 5. 调用 API ─────────────────────────────────────────────────────── + all_pil_images = [] - # ── 5. 完成日志 ─────────────────────────────────────────────────────── + if batch_prompts: + # 批量模式:逐条提示词调用 + total = len(batch_prompts) + print(f"[o1key GPT Image] 批量模式 | {total} 条提示词 | 每条生成 {生图数量} 张") + for idx, p in enumerate(batch_prompts, 1): + try: + pil_images = client.run_sync( + prompt=p, + model=模型, + quality=quality, + background="auto", + size=size, + n=生图数量, + seed=seed, + image_tensor=图片, + mask_tensor=遮罩, + ) + all_pil_images.extend(pil_images) + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}") + except Exception as e: + error_msg = str(e).split('\n')[0] + snippet = p[:30] + ("..." if len(p) >= 30 else "") + print(f"[o1key GPT Image] [{idx}/{total}] ❌ {snippet} → {error_msg}") + else: + # 单提示词模式 + if not prompt or not prompt.strip(): + raise ValueError("提示词不能为空") + try: + pil_images = client.run_sync( + prompt=prompt, + model=模型, + quality=quality, + background="auto", + size=size, + n=生图数量, + seed=seed, + image_tensor=图片, + mask_tensor=遮罩, + ) + all_pil_images.extend(pil_images) + except Exception as e: + error_msg = str(e).split('\n')[0] + print(f"[o1key GPT Image] ❌ {error_msg}") + raise RuntimeError(error_msg) from None + + # ── 6. 检查是否有可用图像 ───────────────────────────────────────────── + if not all_pil_images: + raise RuntimeError("所有提示词均生成失败,无可用图像输出") + + # ── 7. PIL → tensor ─────────────────────────────────────────────────── + output_tensor = GptImageClient._pil_list_to_tensor(all_pil_images) + + # ── 8. 完成日志 ─────────────────────────────────────────────────────── elapsed = time.time() - start_time print( f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s,"