feat: 发布 gpt-image-1.5 生图模型
- 新增 gpt-image-1.5 / gpt-image-1.5-特价 模型选项 - 图生图统一走 edits 接口,支持多图批次输入(image[]) - 节点参数中文化:模型、分辨率、生图数量、图片、遮罩 - 移除 quality / background 前端参数(服务端暂不支持) - 请求超时上限调整为 900s
This commit is contained in:
+46
-31
@@ -31,8 +31,14 @@ from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
|
|||||||
_ENDPOINT_GENERATIONS = "/v1/images/generations/"
|
_ENDPOINT_GENERATIONS = "/v1/images/generations/"
|
||||||
_ENDPOINT_EDITS = "/v1/images/edits/"
|
_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:
|
class GptImageClient:
|
||||||
@@ -196,8 +202,11 @@ class GptImageClient:
|
|||||||
调用 /v1/images/generations/ 接口。
|
调用 /v1/images/generations/ 接口。
|
||||||
当传入 image_tensor 时,以 data URI 格式内联图像(图生图)。
|
当传入 image_tensor 时,以 data URI 格式内联图像(图生图)。
|
||||||
"""
|
"""
|
||||||
|
# 模型名映射:UI 显示名 → API 参数名
|
||||||
|
api_model = _MODEL_NAME_MAP.get(model, model)
|
||||||
|
|
||||||
body: dict = {
|
body: dict = {
|
||||||
"model": model,
|
"model": api_model,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"quality": quality,
|
"quality": quality,
|
||||||
"background": background,
|
"background": background,
|
||||||
@@ -259,7 +268,9 @@ class GptImageClient:
|
|||||||
print(f"[o1key GPT Image] API 响应耗时 {elapsed:.1f}s")
|
print(f"[o1key GPT Image] API 响应耗时 {elapsed:.1f}s")
|
||||||
return await self._parse_response(resp_json, session)
|
return await self._parse_response(resp_json, session)
|
||||||
|
|
||||||
# ── 图像编辑(edits 接口,必须带蒙版)───────────────────────────────────
|
# ── 图像编辑(edits 接口,multipart/form-data)──────────────────────────
|
||||||
|
# 注意:o1key 中转服务的 edits 接口暂不支持 quality / background / moderation 参数,
|
||||||
|
# 这些字段暂时不传递,待服务方更新后可恢复。
|
||||||
|
|
||||||
async def _edit_async(
|
async def _edit_async(
|
||||||
self,
|
self,
|
||||||
@@ -275,37 +286,40 @@ class GptImageClient:
|
|||||||
) -> List[Image.Image]:
|
) -> List[Image.Image]:
|
||||||
"""
|
"""
|
||||||
调用 /v1/images/edits/ 接口(multipart/form-data)。
|
调用 /v1/images/edits/ 接口(multipart/form-data)。
|
||||||
image_tensor 取第一帧(edits 接口仅支持单张输入图)。
|
当前仅传递 model / prompt / n / size / image / mask,
|
||||||
|
quality / background / moderation 暂不支持(o1key 服务端限制)。
|
||||||
"""
|
"""
|
||||||
# 取第一张图
|
# 模型名映射:UI 显示名 → API 参数名
|
||||||
single = image_tensor[:1] if image_tensor.dim() == 4 else image_tensor.unsqueeze(0)
|
api_model = _MODEL_NAME_MAP.get(model, model)
|
||||||
image_png = self._tensor_to_png_bytes(single)
|
|
||||||
ih, iw = single.shape[1], single.shape[2]
|
|
||||||
|
|
||||||
# 构建 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 = aiohttp.FormData()
|
||||||
form.add_field("model", model)
|
form.add_field("model", api_model)
|
||||||
form.add_field("prompt", prompt)
|
form.add_field("prompt", prompt)
|
||||||
form.add_field("quality", quality)
|
form.add_field("n", str(n))
|
||||||
form.add_field("background", background)
|
|
||||||
form.add_field("n", str(n))
|
|
||||||
form.add_field("moderation", "low")
|
|
||||||
|
|
||||||
if size and size != "auto":
|
if size and size != "auto":
|
||||||
form.add_field("size", size)
|
form.add_field("size", size)
|
||||||
|
|
||||||
if seed > 0:
|
# 多图:用 image[] 数组字段逐张附加,支持 gpt-image-1.5 最多 16 张
|
||||||
form.add_field("seed", str(seed))
|
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)
|
ih, iw = image_tensor.shape[1], image_tensor.shape[2]
|
||||||
form.add_field(
|
|
||||||
"image",
|
|
||||||
image_png,
|
|
||||||
filename="image.png",
|
|
||||||
content_type="image/png",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 蒙版(PNG,RGBA 格式,透明区域为待编辑区)
|
|
||||||
if mask_tensor is not None:
|
if mask_tensor is not None:
|
||||||
mask_png = self._mask_tensor_to_rgba_png_bytes(mask_tensor, (ih, iw))
|
mask_png = self._mask_tensor_to_rgba_png_bytes(mask_tensor, (ih, iw))
|
||||||
form.add_field(
|
form.add_field(
|
||||||
@@ -319,7 +333,7 @@ class GptImageClient:
|
|||||||
mode = "图像编辑(无蒙版)"
|
mode = "图像编辑(无蒙版)"
|
||||||
|
|
||||||
url = f"{self.base_url}{_ENDPOINT_EDITS}"
|
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}")
|
f"background={background} | size={size} | n={n}")
|
||||||
|
|
||||||
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
|
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
|
||||||
@@ -330,7 +344,7 @@ class GptImageClient:
|
|||||||
async with session.post(
|
async with session.post(
|
||||||
url,
|
url,
|
||||||
data=form,
|
data=form,
|
||||||
headers=self._auth_headers(), # Content-Type 由 FormData 自动设置
|
headers=self._auth_headers(),
|
||||||
) as resp:
|
) as resp:
|
||||||
elapsed = time.time() - t0
|
elapsed = time.time() - t0
|
||||||
text = await resp.text()
|
text = await resp.text()
|
||||||
@@ -374,11 +388,12 @@ class GptImageClient:
|
|||||||
同步入口,在独立线程中运行事件循环,避免与 ComfyUI 主循环冲突。
|
同步入口,在独立线程中运行事件循环,避免与 ComfyUI 主循环冲突。
|
||||||
|
|
||||||
路由逻辑:
|
路由逻辑:
|
||||||
- 无 image_tensor → generations 接口(文生图)
|
- 无 image_tensor → generations 接口(文生图,JSON body)
|
||||||
- 有 image_tensor,无 mask → generations 接口(图生图,data URI)
|
- 有 image_tensor → edits 接口(图生图/编辑,multipart/form-data)
|
||||||
- 有 image_tensor,有 mask → edits 接口(图像编辑 + 蒙版)
|
所有模型统一走 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:
|
if use_edits:
|
||||||
coro = self._edit_async(
|
coro = self._edit_async(
|
||||||
|
|||||||
+48
-55
@@ -15,19 +15,17 @@ class O1keyGPTImage:
|
|||||||
|
|
||||||
功能:
|
功能:
|
||||||
- 文生图:仅提供 prompt
|
- 文生图:仅提供 prompt
|
||||||
- 图生图:提供 prompt + image(无 mask)
|
- 图生图:提供 prompt + 图片(无遮罩)
|
||||||
- 图像编辑:提供 prompt + image + mask(白色区域将被替换)
|
- 图像编辑:提供 prompt + 图片 + 遮罩(白色区域将被替换)
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- prompt : 文本提示词(多行)
|
- prompt : 文本提示词(多行)
|
||||||
- seed : 随机种子(0 表示不指定)
|
- 模型 : 模型选择
|
||||||
- quality : 图像质量 low / medium / high
|
- 分辨率 : 图像尺寸(auto 让 API 自动决定)
|
||||||
- background : 背景模式 auto / opaque / transparent
|
- 生图数量 : 生成数量 1-8
|
||||||
- size : 图像尺寸(auto 让 API 自动决定)
|
- seed : 随机种子(0 表示不指定)
|
||||||
- n : 生成数量 1-8
|
- 图片 : 可选参考图(用于图生图或编辑)
|
||||||
- image : 可选参考图(用于图生图或编辑)
|
- 遮罩 : 可选蒙版(白色区域将被替换)
|
||||||
- mask : 可选蒙版(白色区域将被替换)
|
|
||||||
- model : 模型选择 gpt-image-1 / gpt-image-1.5
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -41,6 +39,26 @@ class O1keyGPTImage:
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
"optional": {
|
"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", {
|
"seed": ("INT", {
|
||||||
"default": 0,
|
"default": 0,
|
||||||
"min": 0,
|
"min": 0,
|
||||||
@@ -50,35 +68,12 @@ class O1keyGPTImage:
|
|||||||
"control_after_generate": True,
|
"control_after_generate": True,
|
||||||
"tooltip": "Random seed (0 = not specified)",
|
"tooltip": "Random seed (0 = not specified)",
|
||||||
}),
|
}),
|
||||||
"quality": (["low", "medium", "high"], {
|
"图片": ("IMAGE", {
|
||||||
"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", {
|
|
||||||
"tooltip": "Optional reference image for image editing.",
|
"tooltip": "Optional reference image for image editing.",
|
||||||
}),
|
}),
|
||||||
"mask": ("MASK", {
|
"遮罩": ("MASK", {
|
||||||
"tooltip": "Optional mask for inpainting (white areas will be replaced)",
|
"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(
|
def generate(
|
||||||
self,
|
self,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
模型: str = "gpt-image-1.5",
|
||||||
|
分辨率: str = "auto",
|
||||||
|
生图数量: int = 1,
|
||||||
seed: int = 0,
|
seed: int = 0,
|
||||||
quality: str = "low",
|
图片=None,
|
||||||
background: str = "auto",
|
遮罩=None,
|
||||||
size: str = "auto",
|
|
||||||
n: int = 1,
|
|
||||||
image=None,
|
|
||||||
mask=None,
|
|
||||||
model: str = "gpt-image-1.5",
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
生成图像(文生图 / 图生图 / 图像编辑)
|
生成图像(文生图 / 图生图 / 图像编辑)
|
||||||
|
|
||||||
路由逻辑:
|
路由逻辑:
|
||||||
- 无 image → generations 接口(文生图)
|
- 无图片 → generations 接口(文生图)
|
||||||
- 有 image,无 mask → generations 接口(图生图)
|
- 有图片,无遮罩 → edits 接口(图生图)
|
||||||
- 有 image,有 mask → edits 接口(图像编辑 + 蒙版)
|
- 有图片,有遮罩 → edits 接口(图像编辑 + 蒙版)
|
||||||
"""
|
"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
@@ -114,8 +107,8 @@ class O1keyGPTImage:
|
|||||||
if not prompt or not prompt.strip():
|
if not prompt or not prompt.strip():
|
||||||
raise ValueError("提示词不能为空")
|
raise ValueError("提示词不能为空")
|
||||||
|
|
||||||
if mask is not None and image is None:
|
if 遮罩 is not None and 图片 is None:
|
||||||
raise ValueError("提供了蒙版但未提供图像,请同时提供 image 和 mask")
|
raise ValueError("提供了遮罩但未提供图片,请同时提供图片和遮罩")
|
||||||
|
|
||||||
# ── 2. 创建客户端 ─────────────────────────────────────────────────────
|
# ── 2. 创建客户端 ─────────────────────────────────────────────────────
|
||||||
try:
|
try:
|
||||||
@@ -130,14 +123,14 @@ class O1keyGPTImage:
|
|||||||
try:
|
try:
|
||||||
pil_images = client.run_sync(
|
pil_images = client.run_sync(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
model=model,
|
model=模型,
|
||||||
quality=quality,
|
quality="low",
|
||||||
background=background,
|
background="auto",
|
||||||
size=size,
|
size=分辨率,
|
||||||
n=n,
|
n=生图数量,
|
||||||
seed=seed,
|
seed=seed,
|
||||||
image_tensor=image,
|
image_tensor=图片,
|
||||||
mask_tensor=mask,
|
mask_tensor=遮罩,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e).split('\n')[0]
|
error_msg = str(e).split('\n')[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user