From c4bb8d9724300ea29bdd1505116db342a26722fe Mon Sep 17 00:00:00 2001 From: Jony <951565127@qq.com> Date: Thu, 16 Apr 2026 01:27:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=20gpt-image-1.5=20?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 gpt-image-1.5 / gpt-image-1.5-特价 模型选项 - 图生图统一走 edits 接口,支持多图批次输入(image[]) - 节点参数中文化:模型、分辨率、生图数量、图片、遮罩 - 移除 quality / background 前端参数(服务端暂不支持) - 请求超时上限调整为 900s --- clients/gpt_image_client.py | 77 ++++++++++++++++----------- nodes/o1key_gpt_image.py | 103 +++++++++++++++++------------------- 2 files changed, 94 insertions(+), 86 deletions(-) diff --git a/clients/gpt_image_client.py b/clients/gpt_image_client.py index 00a7b37..eaa303e 100644 --- a/clients/gpt_image_client.py +++ b/clients/gpt_image_client.py @@ -31,8 +31,14 @@ from ..utils.image_utils import tensor_to_pil, encode_image_to_base64 _ENDPOINT_GENERATIONS = "/v1/images/generations/" _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", +} + # ── 超时 ────────────────────────────────────────────────────────────────────── -_REQUEST_TIMEOUT = 300 # 秒 +_REQUEST_TIMEOUT = 900 # 秒 class GptImageClient: @@ -196,8 +202,11 @@ class GptImageClient: 调用 /v1/images/generations/ 接口。 当传入 image_tensor 时,以 data URI 格式内联图像(图生图)。 """ + # 模型名映射:UI 显示名 → API 参数名 + api_model = _MODEL_NAME_MAP.get(model, model) + body: dict = { - "model": model, + "model": api_model, "prompt": prompt, "quality": quality, "background": background, @@ -259,7 +268,9 @@ class GptImageClient: print(f"[o1key GPT Image] API 响应耗时 {elapsed:.1f}s") return await self._parse_response(resp_json, session) - # ── 图像编辑(edits 接口,必须带蒙版)─────────────────────────────────── + # ── 图像编辑(edits 接口,multipart/form-data)────────────────────────── + # 注意:o1key 中转服务的 edits 接口暂不支持 quality / background / moderation 参数, + # 这些字段暂时不传递,待服务方更新后可恢复。 async def _edit_async( self, @@ -275,37 +286,40 @@ class GptImageClient: ) -> List[Image.Image]: """ 调用 /v1/images/edits/ 接口(multipart/form-data)。 - image_tensor 取第一帧(edits 接口仅支持单张输入图)。 + 当前仅传递 model / prompt / n / size / image / mask, + quality / background / moderation 暂不支持(o1key 服务端限制)。 """ - # 取第一张图 - single = image_tensor[:1] if image_tensor.dim() == 4 else image_tensor.unsqueeze(0) - image_png = self._tensor_to_png_bytes(single) - ih, iw = single.shape[1], single.shape[2] + # 模型名映射:UI 显示名 → API 参数名 + api_model = _MODEL_NAME_MAP.get(model, model) - # 构建 multipart 表单 + # 将 batch tensor 拆成逐帧列表 + if image_tensor.dim() == 3: + 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, + # 待服务方更新后可重新加入。 form = aiohttp.FormData() - form.add_field("model", model) - form.add_field("prompt", prompt) - form.add_field("quality", quality) - form.add_field("background", background) - form.add_field("n", str(n)) - form.add_field("moderation", "low") + form.add_field("model", api_model) + form.add_field("prompt", prompt) + form.add_field("n", str(n)) if size and size != "auto": form.add_field("size", size) - if seed > 0: - form.add_field("seed", str(seed)) + # 多图:用 image[] 数组字段逐张附加,支持 gpt-image-1.5 最多 16 张 + for i in range(num_images): + frame = image_tensor[i:i+1] # [1,H,W,C] + img_bytes = self._tensor_to_png_bytes(frame) + form.add_field( + "image[]", + img_bytes, + filename=f"image_{i}.png", + content_type="image/png", + ) - # 主图(PNG) - form.add_field( - "image", - image_png, - filename="image.png", - content_type="image/png", - ) + ih, iw = image_tensor.shape[1], image_tensor.shape[2] - # 蒙版(PNG,RGBA 格式,透明区域为待编辑区) if mask_tensor is not None: mask_png = self._mask_tensor_to_rgba_png_bytes(mask_tensor, (ih, iw)) form.add_field( @@ -319,7 +333,7 @@ class GptImageClient: mode = "图像编辑(无蒙版)" url = f"{self.base_url}{_ENDPOINT_EDITS}" - print(f"[o1key GPT Image] {mode} | 模型={model} | quality={quality} | " + print(f"[o1key GPT Image] {mode} | 模型={model} | 参考图={num_images}张 | quality={quality} | " f"background={background} | size={size} | n={n}") connector = aiohttp.TCPConnector(ssl=False, force_close=True) @@ -330,7 +344,7 @@ class GptImageClient: async with session.post( url, data=form, - headers=self._auth_headers(), # Content-Type 由 FormData 自动设置 + headers=self._auth_headers(), ) as resp: elapsed = time.time() - t0 text = await resp.text() @@ -374,11 +388,12 @@ class GptImageClient: 同步入口,在独立线程中运行事件循环,避免与 ComfyUI 主循环冲突。 路由逻辑: - - 无 image_tensor → generations 接口(文生图) - - 有 image_tensor,无 mask → generations 接口(图生图,data URI) - - 有 image_tensor,有 mask → edits 接口(图像编辑 + 蒙版) + - 无 image_tensor → generations 接口(文生图,JSON body) + - 有 image_tensor → edits 接口(图生图/编辑,multipart/form-data) + 所有模型统一走 multipart,quality/background 通过表单字段传递, + new-api 开启"透传请求体"后原样转发给上游。 """ - use_edits = (image_tensor is not None and mask_tensor is not None) + use_edits = (image_tensor is not None) if use_edits: coro = self._edit_async( diff --git a/nodes/o1key_gpt_image.py b/nodes/o1key_gpt_image.py index b52cd42..0d7919e 100644 --- a/nodes/o1key_gpt_image.py +++ b/nodes/o1key_gpt_image.py @@ -15,19 +15,17 @@ class O1keyGPTImage: 功能: - 文生图:仅提供 prompt - - 图生图:提供 prompt + image(无 mask) - - 图像编辑:提供 prompt + image + mask(白色区域将被替换) + - 图生图:提供 prompt + 图片(无遮罩) + - 图像编辑:提供 prompt + 图片 + 遮罩(白色区域将被替换) 参数: - - prompt : 文本提示词(多行) - - seed : 随机种子(0 表示不指定) - - quality : 图像质量 low / medium / high - - background : 背景模式 auto / opaque / transparent - - size : 图像尺寸(auto 让 API 自动决定) - - n : 生成数量 1-8 - - image : 可选参考图(用于图生图或编辑) - - mask : 可选蒙版(白色区域将被替换) - - model : 模型选择 gpt-image-1 / gpt-image-1.5 + - prompt : 文本提示词(多行) + - 模型 : 模型选择 + - 分辨率 : 图像尺寸(auto 让 API 自动决定) + - 生图数量 : 生成数量 1-8 + - seed : 随机种子(0 表示不指定) + - 图片 : 可选参考图(用于图生图或编辑) + - 遮罩 : 可选蒙版(白色区域将被替换) """ @classmethod @@ -41,6 +39,26 @@ class O1keyGPTImage: }), }, "optional": { + "模型": ([ + "gpt-image-1", + "gpt-image-1.5", + "gpt-image-1-特价", + "gpt-image-1.5-特价", + ], { + "default": "gpt-image-1.5", + }), + "分辨率": (["auto", "1024x1024", "1024x1536", "1536x1024"], { + "default": "auto", + "tooltip": "Image size (auto = API decides)", + }), + "生图数量": ("INT", { + "default": 1, + "min": 1, + "max": 8, + "step": 1, + "display": "number", + "tooltip": "How many images to generate", + }), "seed": ("INT", { "default": 0, "min": 0, @@ -50,35 +68,12 @@ class O1keyGPTImage: "control_after_generate": True, "tooltip": "Random seed (0 = not specified)", }), - "quality": (["low", "medium", "high"], { - "default": "low", - "tooltip": "Image quality, affects cost and generation time.", - }), - "background": (["auto", "opaque", "transparent"], { - "default": "auto", - "tooltip": "Return image with or without background", - }), - "size": (["auto", "1024x1024", "1024x1536", "1536x1024"], { - "default": "auto", - "tooltip": "Image size (auto = API decides)", - }), - "n": ("INT", { - "default": 1, - "min": 1, - "max": 8, - "step": 1, - "display": "number", - "tooltip": "How many images to generate", - }), - "image": ("IMAGE", { + "图片": ("IMAGE", { "tooltip": "Optional reference image for image editing.", }), - "mask": ("MASK", { + "遮罩": ("MASK", { "tooltip": "Optional mask for inpainting (white areas will be replaced)", }), - "model": (["gpt-image-1", "gpt-image-1.5"], { - "default": "gpt-image-1.5", - }), }, } @@ -91,22 +86,20 @@ class O1keyGPTImage: def generate( self, prompt: str, + 模型: str = "gpt-image-1.5", + 分辨率: str = "auto", + 生图数量: int = 1, seed: int = 0, - quality: str = "low", - background: str = "auto", - size: str = "auto", - n: int = 1, - image=None, - mask=None, - model: str = "gpt-image-1.5", + 图片=None, + 遮罩=None, ): """ 生成图像(文生图 / 图生图 / 图像编辑) 路由逻辑: - - 无 image → generations 接口(文生图) - - 有 image,无 mask → generations 接口(图生图) - - 有 image,有 mask → edits 接口(图像编辑 + 蒙版) + - 无图片 → generations 接口(文生图) + - 有图片,无遮罩 → edits 接口(图生图) + - 有图片,有遮罩 → edits 接口(图像编辑 + 蒙版) """ start_time = time.time() @@ -114,8 +107,8 @@ class O1keyGPTImage: if not prompt or not prompt.strip(): raise ValueError("提示词不能为空") - if mask is not None and image is None: - raise ValueError("提供了蒙版但未提供图像,请同时提供 image 和 mask") + if 遮罩 is not None and 图片 is None: + raise ValueError("提供了遮罩但未提供图片,请同时提供图片和遮罩") # ── 2. 创建客户端 ───────────────────────────────────────────────────── try: @@ -130,14 +123,14 @@ class O1keyGPTImage: try: pil_images = client.run_sync( prompt=prompt, - model=model, - quality=quality, - background=background, - size=size, - n=n, + model=模型, + quality="low", + background="auto", + size=分辨率, + n=生图数量, seed=seed, - image_tensor=image, - mask_tensor=mask, + image_tensor=图片, + mask_tensor=遮罩, ) except Exception as e: error_msg = str(e).split('\n')[0]