feat: 新增 GPT-1.5 生图节点
This commit is contained in:
+3
-1
@@ -21,7 +21,7 @@ except Exception:
|
|||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
|
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
|
||||||
_MSG_TIMEOUT = "API 请求超时,请稍后重试或检查网络。"
|
_MSG_TIMEOUT = "API 请求超时,请稍后重试或检查网络。"
|
||||||
@@ -79,6 +79,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SeedanceMultiModal": SeedanceMultiModal,
|
"SeedanceMultiModal": SeedanceMultiModal,
|
||||||
"StreamPreview": StreamPreview,
|
"StreamPreview": StreamPreview,
|
||||||
"DoubaoImage": DoubaoImage,
|
"DoubaoImage": DoubaoImage,
|
||||||
|
"O1keyGPTImage": O1keyGPTImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
@@ -103,6 +104,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SeedanceMultiModal": "Seedance 多模态参考生视频",
|
"SeedanceMultiModal": "Seedance 多模态参考生视频",
|
||||||
"StreamPreview": "流式文本预览",
|
"StreamPreview": "流式文本预览",
|
||||||
"DoubaoImage": "豆包生图",
|
"DoubaoImage": "豆包生图",
|
||||||
|
"O1keyGPTImage": "o1key GPT Image",
|
||||||
}
|
}
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web"
|
WEB_DIRECTORY = "./web"
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
"""
|
||||||
|
GPT Image API 客户端
|
||||||
|
支持两个接口:
|
||||||
|
- POST /v1/images/generations/ 文生图 / 图生图(gpt-image-1 / gpt-image-1.5)
|
||||||
|
- POST /v1/images/edits/ 图像编辑(带蒙版 inpainting)
|
||||||
|
|
||||||
|
设计原则:
|
||||||
|
- 与 doubao_image_client.py 保持相同的异步 + 同步双入口模式
|
||||||
|
- 图像以 multipart/form-data 方式上传(edits 接口)
|
||||||
|
- generations 接口使用 JSON 请求体,图像以 data URI base64 内联传递
|
||||||
|
- 响应支持 url 和 b64_json 两种格式,优先处理 b64_json(避免二次下载)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from ..utils.config import get_api_key_or_raise, get_api_base_url
|
||||||
|
from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
|
||||||
|
|
||||||
|
# ── 接口端点 ──────────────────────────────────────────────────────────────────
|
||||||
|
_ENDPOINT_GENERATIONS = "/v1/images/generations/"
|
||||||
|
_ENDPOINT_EDITS = "/v1/images/edits/"
|
||||||
|
|
||||||
|
# ── 超时 ──────────────────────────────────────────────────────────────────────
|
||||||
|
_REQUEST_TIMEOUT = 300 # 秒
|
||||||
|
|
||||||
|
|
||||||
|
class GptImageClient:
|
||||||
|
"""
|
||||||
|
GPT Image API 客户端
|
||||||
|
|
||||||
|
接口说明:
|
||||||
|
generations:JSON body,支持 background / quality / size / n / model
|
||||||
|
edits:multipart/form-data,必须包含 image(PNG),可选 mask(PNG)
|
||||||
|
|
||||||
|
两个接口的响应格式相同:
|
||||||
|
{ "data": [ {"url": "..."} | {"b64_json": "..."} ] }
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.api_key = get_api_key_or_raise("O1KEY_API_KEY")
|
||||||
|
self.base_url = get_api_base_url()
|
||||||
|
|
||||||
|
# ── 认证头 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _auth_headers(self) -> dict:
|
||||||
|
return {"Authorization": f"Bearer {self.api_key}"}
|
||||||
|
|
||||||
|
def _json_headers(self) -> dict:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 图像转换工具 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _tensor_to_png_bytes(tensor: torch.Tensor) -> bytes:
|
||||||
|
"""
|
||||||
|
单张 ComfyUI IMAGE tensor [1, H, W, C] 或 [H, W, C] → PNG bytes
|
||||||
|
"""
|
||||||
|
if tensor.dim() == 4:
|
||||||
|
tensor = tensor.squeeze(0) # [H, W, C]
|
||||||
|
arr = (tensor.cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
|
||||||
|
img = Image.fromarray(arr)
|
||||||
|
buf = BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _mask_tensor_to_rgba_png_bytes(mask: torch.Tensor, image_size: tuple) -> bytes:
|
||||||
|
"""
|
||||||
|
ComfyUI MASK tensor [1, H, W] 或 [H, W] → RGBA PNG bytes
|
||||||
|
白色区域(mask=1)→ 透明(alpha=0),即 API 将在此处生成新内容。
|
||||||
|
"""
|
||||||
|
if mask.dim() == 3:
|
||||||
|
mask = mask.squeeze(0) # [H, W]
|
||||||
|
|
||||||
|
h, w = mask.shape
|
||||||
|
ih, iw = image_size
|
||||||
|
|
||||||
|
# 尺寸不一致时给出提示(API 侧也会报错)
|
||||||
|
if (h, w) != (ih, iw):
|
||||||
|
raise ValueError(
|
||||||
|
f"蒙版尺寸 ({h}×{w}) 与图像尺寸 ({ih}×{iw}) 不一致,请保持相同尺寸"
|
||||||
|
)
|
||||||
|
|
||||||
|
alpha = ((1.0 - mask.cpu().numpy()) * 255).clip(0, 255).astype(np.uint8)
|
||||||
|
rgba = np.zeros((h, w, 4), dtype=np.uint8)
|
||||||
|
rgba[:, :, 3] = alpha # 只设 alpha,RGB 全 0
|
||||||
|
|
||||||
|
buf = BytesIO()
|
||||||
|
Image.fromarray(rgba, mode="RGBA").save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _pil_list_to_tensor(images: List[Image.Image]) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
PIL Image 列表 → ComfyUI IMAGE tensor [B, H, W, C],值域 [0, 1]
|
||||||
|
RGBA 自动转换为 RGBA(保留透明通道)
|
||||||
|
"""
|
||||||
|
if not images:
|
||||||
|
placeholder = Image.new("RGBA", (512, 512), (128, 128, 128, 255))
|
||||||
|
images = [placeholder]
|
||||||
|
|
||||||
|
tensors = []
|
||||||
|
for img in images:
|
||||||
|
arr = np.array(img.convert("RGBA")).astype(np.float32) / 255.0
|
||||||
|
tensors.append(torch.from_numpy(arr))
|
||||||
|
|
||||||
|
return torch.stack(tensors, dim=0) # [B, H, W, 4]
|
||||||
|
|
||||||
|
# ── 响应解析(通用) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _parse_response(
|
||||||
|
self,
|
||||||
|
resp_json: dict,
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
"""
|
||||||
|
解析 data 列表,优先取 b64_json,回退到 url 下载
|
||||||
|
"""
|
||||||
|
if "error" in resp_json:
|
||||||
|
err = resp_json["error"]
|
||||||
|
msg = (
|
||||||
|
err.get("message") or err.get("msg") or json.dumps(err, ensure_ascii=False)
|
||||||
|
if isinstance(err, dict)
|
||||||
|
else str(err)
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"API 返回错误: {msg}")
|
||||||
|
|
||||||
|
data_list = resp_json.get("data")
|
||||||
|
if not data_list:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"API 响应中未找到 data 字段,完整响应:\n"
|
||||||
|
f"{json.dumps(resp_json, ensure_ascii=False, indent=2)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
images: List[Image.Image] = []
|
||||||
|
for idx, item in enumerate(data_list):
|
||||||
|
b64 = item.get("b64_json", "")
|
||||||
|
url = item.get("url", "")
|
||||||
|
|
||||||
|
if b64:
|
||||||
|
# 优先 base64(无需二次下载)
|
||||||
|
try:
|
||||||
|
img_bytes = base64.b64decode(b64)
|
||||||
|
img = Image.open(BytesIO(img_bytes))
|
||||||
|
images.append(img)
|
||||||
|
print(f"[o1key GPT Image] 第 {idx + 1} 张 base64 解码完成 "
|
||||||
|
f"({img.size[0]}×{img.size[1]})")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"第 {idx + 1} 张 base64 解码失败: {e}")
|
||||||
|
|
||||||
|
elif url and url.startswith("http"):
|
||||||
|
# 回退:下载 URL
|
||||||
|
async with session.get(url, allow_redirects=True) as r:
|
||||||
|
if r.status != 200:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"图像下载失败 HTTP {r.status},URL: {url}"
|
||||||
|
)
|
||||||
|
img_bytes = await r.read()
|
||||||
|
img = Image.open(BytesIO(img_bytes))
|
||||||
|
images.append(img)
|
||||||
|
print(f"[o1key GPT Image] 第 {idx + 1} 张下载完成 "
|
||||||
|
f"({img.size[0]}×{img.size[1]})")
|
||||||
|
else:
|
||||||
|
print(f"[o1key GPT Image] 警告:第 {idx + 1} 条数据既无 b64_json 也无 url,已跳过")
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
# ── 文生图 / 图生图(generations 接口)───────────────────────────────────
|
||||||
|
|
||||||
|
async def _generate_async(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
quality: str,
|
||||||
|
background: str,
|
||||||
|
size: str,
|
||||||
|
n: int,
|
||||||
|
seed: int,
|
||||||
|
image_tensor: Optional[torch.Tensor] = None,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
"""
|
||||||
|
调用 /v1/images/generations/ 接口。
|
||||||
|
当传入 image_tensor 时,以 data URI 格式内联图像(图生图)。
|
||||||
|
"""
|
||||||
|
body: dict = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"quality": quality,
|
||||||
|
"background": background,
|
||||||
|
"n": n,
|
||||||
|
"moderation": "low",
|
||||||
|
}
|
||||||
|
|
||||||
|
# size = "auto" 时不传该字段,让 API 自行决定
|
||||||
|
if size and size != "auto":
|
||||||
|
body["size"] = size
|
||||||
|
|
||||||
|
# seed > 0 时才传递(0 视为不指定)
|
||||||
|
if seed > 0:
|
||||||
|
body["seed"] = seed
|
||||||
|
|
||||||
|
# 图生图:将 tensor 转成 data URI 内联
|
||||||
|
if image_tensor is not None:
|
||||||
|
pil_images = tensor_to_pil(image_tensor)
|
||||||
|
data_urls = []
|
||||||
|
for img in pil_images:
|
||||||
|
b64 = encode_image_to_base64(img, format="PNG")
|
||||||
|
data_urls.append(f"data:image/png;base64,{b64}")
|
||||||
|
body["image"] = data_urls[0] if len(data_urls) == 1 else data_urls
|
||||||
|
mode = f"图生图(参考图 {len(data_urls)} 张)"
|
||||||
|
else:
|
||||||
|
mode = "文生图"
|
||||||
|
|
||||||
|
url = f"{self.base_url}{_ENDPOINT_GENERATIONS}"
|
||||||
|
print(f"[o1key GPT Image] {mode} | 模型={model} | quality={quality} | "
|
||||||
|
f"background={background} | size={size} | n={n}")
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
|
||||||
|
timeout = aiohttp.ClientTimeout(total=_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
||||||
|
t0 = time.time()
|
||||||
|
async with session.post(url, json=body, headers=self._json_headers()) as resp:
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
if resp.status != 200:
|
||||||
|
try:
|
||||||
|
err_json = json.loads(text)
|
||||||
|
err_obj = err_json.get("error", {})
|
||||||
|
msg = (
|
||||||
|
err_obj.get("message") or err_obj.get("msg") or text
|
||||||
|
if isinstance(err_obj, dict)
|
||||||
|
else str(err_obj) or text
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
msg = text
|
||||||
|
raise RuntimeError(f"请求失败 HTTP {resp.status}: {msg}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp_json = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError(f"响应 JSON 解析失败,原始内容:{text[:500]}")
|
||||||
|
|
||||||
|
print(f"[o1key GPT Image] API 响应耗时 {elapsed:.1f}s")
|
||||||
|
return await self._parse_response(resp_json, session)
|
||||||
|
|
||||||
|
# ── 图像编辑(edits 接口,必须带蒙版)───────────────────────────────────
|
||||||
|
|
||||||
|
async def _edit_async(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
quality: str,
|
||||||
|
background: str,
|
||||||
|
size: str,
|
||||||
|
n: int,
|
||||||
|
seed: int,
|
||||||
|
image_tensor: torch.Tensor,
|
||||||
|
mask_tensor: Optional[torch.Tensor] = None,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
"""
|
||||||
|
调用 /v1/images/edits/ 接口(multipart/form-data)。
|
||||||
|
image_tensor 取第一帧(edits 接口仅支持单张输入图)。
|
||||||
|
"""
|
||||||
|
# 取第一张图
|
||||||
|
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]
|
||||||
|
|
||||||
|
# 构建 multipart 表单
|
||||||
|
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")
|
||||||
|
|
||||||
|
if size and size != "auto":
|
||||||
|
form.add_field("size", size)
|
||||||
|
|
||||||
|
if seed > 0:
|
||||||
|
form.add_field("seed", str(seed))
|
||||||
|
|
||||||
|
# 主图(PNG)
|
||||||
|
form.add_field(
|
||||||
|
"image",
|
||||||
|
image_png,
|
||||||
|
filename="image.png",
|
||||||
|
content_type="image/png",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 蒙版(PNG,RGBA 格式,透明区域为待编辑区)
|
||||||
|
if mask_tensor is not None:
|
||||||
|
mask_png = self._mask_tensor_to_rgba_png_bytes(mask_tensor, (ih, iw))
|
||||||
|
form.add_field(
|
||||||
|
"mask",
|
||||||
|
mask_png,
|
||||||
|
filename="mask.png",
|
||||||
|
content_type="image/png",
|
||||||
|
)
|
||||||
|
mode = "图像编辑(带蒙版)"
|
||||||
|
else:
|
||||||
|
mode = "图像编辑(无蒙版)"
|
||||||
|
|
||||||
|
url = f"{self.base_url}{_ENDPOINT_EDITS}"
|
||||||
|
print(f"[o1key GPT Image] {mode} | 模型={model} | quality={quality} | "
|
||||||
|
f"background={background} | size={size} | n={n}")
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(ssl=False, force_close=True)
|
||||||
|
timeout = aiohttp.ClientTimeout(total=_REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
||||||
|
t0 = time.time()
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
data=form,
|
||||||
|
headers=self._auth_headers(), # Content-Type 由 FormData 自动设置
|
||||||
|
) as resp:
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
text = await resp.text()
|
||||||
|
|
||||||
|
if resp.status != 200:
|
||||||
|
try:
|
||||||
|
err_json = json.loads(text)
|
||||||
|
err_obj = err_json.get("error", {})
|
||||||
|
msg = (
|
||||||
|
err_obj.get("message") or err_obj.get("msg") or text
|
||||||
|
if isinstance(err_obj, dict)
|
||||||
|
else str(err_obj) or text
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
msg = text
|
||||||
|
raise RuntimeError(f"请求失败 HTTP {resp.status}: {msg}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp_json = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError(f"响应 JSON 解析失败,原始内容:{text[:500]}")
|
||||||
|
|
||||||
|
print(f"[o1key GPT Image] API 响应耗时 {elapsed:.1f}s")
|
||||||
|
return await self._parse_response(resp_json, session)
|
||||||
|
|
||||||
|
# ── 同步统一入口(供节点调用)────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_sync(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
model: str,
|
||||||
|
quality: str,
|
||||||
|
background: str,
|
||||||
|
size: str,
|
||||||
|
n: int,
|
||||||
|
seed: int,
|
||||||
|
image_tensor: Optional[torch.Tensor] = None,
|
||||||
|
mask_tensor: Optional[torch.Tensor] = None,
|
||||||
|
) -> List[Image.Image]:
|
||||||
|
"""
|
||||||
|
同步入口,在独立线程中运行事件循环,避免与 ComfyUI 主循环冲突。
|
||||||
|
|
||||||
|
路由逻辑:
|
||||||
|
- 无 image_tensor → generations 接口(文生图)
|
||||||
|
- 有 image_tensor,无 mask → generations 接口(图生图,data URI)
|
||||||
|
- 有 image_tensor,有 mask → edits 接口(图像编辑 + 蒙版)
|
||||||
|
"""
|
||||||
|
use_edits = (image_tensor is not None and mask_tensor is not None)
|
||||||
|
|
||||||
|
if use_edits:
|
||||||
|
coro = self._edit_async(
|
||||||
|
prompt=prompt, model=model, quality=quality,
|
||||||
|
background=background, size=size, n=n, seed=seed,
|
||||||
|
image_tensor=image_tensor, mask_tensor=mask_tensor,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
coro = self._generate_async(
|
||||||
|
prompt=prompt, model=model, quality=quality,
|
||||||
|
background=background, size=size, n=n, seed=seed,
|
||||||
|
image_tensor=image_tensor,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return loop.run_until_complete(coro)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
future = executor.submit(_run)
|
||||||
|
try:
|
||||||
|
return future.result(timeout=_REQUEST_TIMEOUT + 30)
|
||||||
|
except TimeoutError:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"o1key GPT Image 请求超时(>{_REQUEST_TIMEOUT}s),请检查网络或稍后重试"
|
||||||
|
)
|
||||||
+2
-1
@@ -19,5 +19,6 @@ 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 .doubao_image import DoubaoImage
|
from .doubao_image import DoubaoImage
|
||||||
|
from .o1key_gpt_image import O1keyGPTImage
|
||||||
|
|
||||||
__all__ = ['NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage']
|
__all__ = ['NanoBananaPro', 'BatchNanoBananaPro', 'GoogleGemini', 'LoadFile', 'ImageStitchPro', 'SaveCleanImage', 'BatchCleanMetadata', 'VideoPreview', 'KlingVideo', 'KlingFirstLastFrame', 'KlingMotionControlTest', 'AspectRatioPreset', 'GoogleVeo', 'FluxImageEdit', 'UniversalLLMChat', 'MultiResPreview', 'BatchImagesO1key', 'Seedance', 'SeedanceMultiModal', 'StreamPreview', 'DoubaoImage', 'O1keyGPTImage']
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""
|
||||||
|
o1key GPT Image 节点
|
||||||
|
支持 gpt-image-1 / gpt-image-1.5 模型的文生图、图生图、图像编辑(带蒙版)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from ..clients.gpt_image_client import GptImageClient
|
||||||
|
|
||||||
|
|
||||||
|
class O1keyGPTImage:
|
||||||
|
"""
|
||||||
|
o1key GPT Image 节点
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 文生图:仅提供 prompt
|
||||||
|
- 图生图:提供 prompt + image(无 mask)
|
||||||
|
- 图像编辑:提供 prompt + image + mask(白色区域将被替换)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- 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
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"prompt": ("STRING", {
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "Text prompt for GPT Image",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"seed": ("INT", {
|
||||||
|
"default": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 2**31 - 1,
|
||||||
|
"step": 1,
|
||||||
|
"display": "number",
|
||||||
|
"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", {
|
||||||
|
"tooltip": "Optional reference image for image editing.",
|
||||||
|
}),
|
||||||
|
"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",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
RETURN_NAMES = ("IMAGE",)
|
||||||
|
FUNCTION = "generate"
|
||||||
|
CATEGORY = "o1key/image"
|
||||||
|
OUTPUT_NODE = False
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
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",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
生成图像(文生图 / 图生图 / 图像编辑)
|
||||||
|
|
||||||
|
路由逻辑:
|
||||||
|
- 无 image → generations 接口(文生图)
|
||||||
|
- 有 image,无 mask → generations 接口(图生图)
|
||||||
|
- 有 image,有 mask → edits 接口(图像编辑 + 蒙版)
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# ── 1. 参数校验 ───────────────────────────────────────────────────────
|
||||||
|
if not prompt or not prompt.strip():
|
||||||
|
raise ValueError("提示词不能为空")
|
||||||
|
|
||||||
|
if mask is not None and image is None:
|
||||||
|
raise ValueError("提供了蒙版但未提供图像,请同时提供 image 和 mask")
|
||||||
|
|
||||||
|
# ── 2. 创建客户端 ─────────────────────────────────────────────────────
|
||||||
|
try:
|
||||||
|
client = GptImageClient()
|
||||||
|
except ValueError as e:
|
||||||
|
if str(e) == "未授权!":
|
||||||
|
print("[o1key GPT Image] 请联系作者授权后方可使用!")
|
||||||
|
raise ValueError("未授权!") from None
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ── 3. 调用 API ───────────────────────────────────────────────────────
|
||||||
|
try:
|
||||||
|
pil_images = client.run_sync(
|
||||||
|
prompt=prompt,
|
||||||
|
model=model,
|
||||||
|
quality=quality,
|
||||||
|
background=background,
|
||||||
|
size=size,
|
||||||
|
n=n,
|
||||||
|
seed=seed,
|
||||||
|
image_tensor=image,
|
||||||
|
mask_tensor=mask,
|
||||||
|
)
|
||||||
|
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. PIL → tensor ───────────────────────────────────────────────────
|
||||||
|
output_tensor = GptImageClient._pil_list_to_tensor(pil_images)
|
||||||
|
|
||||||
|
# ── 5. 完成日志 ───────────────────────────────────────────────────────
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(
|
||||||
|
f"[o1key GPT Image] 完成!耗时 {elapsed:.1f}s,"
|
||||||
|
f"输出 {output_tensor.shape[0]} 张 "
|
||||||
|
f"{output_tensor.shape[2]}×{output_tensor.shape[1]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (output_tensor,)
|
||||||
Reference in New Issue
Block a user