Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
This commit is contained in:
+220
-160
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
o1key GPT Image 节点
|
||||
支持 gpt-image-1 / gpt-image-1.5 模型的文生图、图生图、图像编辑(带蒙版)
|
||||
支持 GPT Image 2 / 2.5 系列的文生图、图生图和带蒙版图像编辑
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -8,10 +8,24 @@ import time
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from PIL import Image
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..clients.gpt_image_client import GptImageClient
|
||||
from ..clients.gpt_image_client import (
|
||||
GPT_IMAGE_MODEL_OPTIONS,
|
||||
GPT_IMAGE_ROUTE_OPTIONS,
|
||||
GptImageClient,
|
||||
resolve_gpt_image_model,
|
||||
)
|
||||
from ..utils.image_utils import parse_batch_prompts, pil_to_tensor, tensor_to_pil
|
||||
from ..utils.config import NETWORK_ROUTE_OPTIONS, get_base_url_by_route
|
||||
from ..utils.config import get_base_url_by_route
|
||||
from ..utils.o1key_image_catalog import (
|
||||
GPT_IMAGE_BACKGROUND_OPTIONS,
|
||||
GPT_IMAGE_EXACT_SIZE_OPTIONS,
|
||||
GPT_IMAGE_OUTPUT_FORMAT_OPTIONS,
|
||||
GPT_IMAGE_25_QUALITY_OPTIONS,
|
||||
resolve_gpt_image_quality,
|
||||
resolve_gpt_image_size,
|
||||
)
|
||||
from ..utils.file_utils import (
|
||||
ImageInfo,
|
||||
generate_timestamp_filename,
|
||||
@@ -68,31 +82,20 @@ def _make_node_progress_callback(progress_bar, task_index: int, total_tasks: int
|
||||
|
||||
|
||||
def _resolve_async_size(value: str) -> str:
|
||||
value = (value or "").strip()
|
||||
if not value or value == "智能" or value.lower() == "auto":
|
||||
return "auto"
|
||||
|
||||
first_part = value.split("(")[0].strip()
|
||||
normalized_size = first_part.lower().replace("*", "x").replace("×", "x")
|
||||
size_parts = [part.strip() for part in normalized_size.split("x")]
|
||||
if len(size_parts) == 2 and all(part.isdigit() for part in size_parts):
|
||||
return f"{int(size_parts[0])}x{int(size_parts[1])}"
|
||||
|
||||
allowed = {"auto", "1024x1024", "1K", "2K", "4K"}
|
||||
if first_part in allowed:
|
||||
return first_part
|
||||
|
||||
if "4K" in value:
|
||||
return "4K"
|
||||
if "2K" in value:
|
||||
return "2K"
|
||||
if "1K" in value:
|
||||
return "1K"
|
||||
|
||||
return "auto"
|
||||
return resolve_gpt_image_size(value)
|
||||
|
||||
|
||||
class O1keyGPTImage:
|
||||
MAX_GPT_IMAGE_REFERENCES = 9
|
||||
def _collect_autogrow_inputs(value) -> list:
|
||||
"""收集已连接的 Autogrow 输入,并兼容单个旧值。"""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, dict):
|
||||
return [item for item in value.values() if item is not None]
|
||||
return [value]
|
||||
|
||||
|
||||
class O1keyGPTImage(io.ComfyNode):
|
||||
"""
|
||||
o1key GPT Image 节点
|
||||
|
||||
@@ -104,7 +107,8 @@ class O1keyGPTImage:
|
||||
|
||||
参数:
|
||||
- prompt : 文本提示词(多行;用 --- 独占一行分隔批量提示词)
|
||||
- 模型 : 模型选择
|
||||
- 模型 : GPT Image 主模型
|
||||
- 模型线路 : 畅速、直连或专线
|
||||
- 分辨率 : 图像尺寸(auto 让 API 自动决定)
|
||||
- 生图数量 : 每条提示词生成数量 1-8
|
||||
- 质量 : 生成质量
|
||||
@@ -114,110 +118,144 @@ class O1keyGPTImage:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# 创建9个独立的参考图输入
|
||||
optional_inputs = {}
|
||||
for i in range(1, 10):
|
||||
optional_inputs[f"参考图{i}"] = ("IMAGE", {
|
||||
"tooltip": f"Optional reference image {i} for image editing.",
|
||||
})
|
||||
def define_schema(cls):
|
||||
reference_images = io.Autogrow.Input(
|
||||
"参考图组",
|
||||
template=io.Autogrow.TemplateNames(
|
||||
input=io.Image.Input("参考图"),
|
||||
names=[f"参考图{i}" for i in range(1, MAX_GPT_IMAGE_REFERENCES + 1)],
|
||||
min=0,
|
||||
),
|
||||
tooltip=f"连接后自动增加输入端口,合计最多 {MAX_GPT_IMAGE_REFERENCES} 张参考图。",
|
||||
)
|
||||
return io.Schema(
|
||||
node_id="O1keyGPTImage",
|
||||
display_name="gpt image",
|
||||
category="o1key/image",
|
||||
inputs=[
|
||||
io.String.Input(
|
||||
"prompt",
|
||||
default="",
|
||||
multiline=True,
|
||||
tooltip="Text prompt for GPT Image. Use --- on its own line to separate batch prompts.",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"模型",
|
||||
options=GPT_IMAGE_MODEL_OPTIONS,
|
||||
default="gpt-image-2.5-sunburst",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"模型线路",
|
||||
options=GPT_IMAGE_ROUTE_OPTIONS,
|
||||
default="畅速",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"分辨率",
|
||||
options=GPT_IMAGE_EXACT_SIZE_OPTIONS,
|
||||
default="智能",
|
||||
tooltip="Image size (智能 = API decides)",
|
||||
),
|
||||
io.Int.Input(
|
||||
"生图数量",
|
||||
default=1,
|
||||
min=1,
|
||||
max=8,
|
||||
step=1,
|
||||
display_mode=io.NumberDisplay.number,
|
||||
tooltip="How many images to generate per prompt",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"质量",
|
||||
options=GPT_IMAGE_25_QUALITY_OPTIONS,
|
||||
default="自动",
|
||||
tooltip="GPT Image 2 支持高/中/低/自动;GPT Image 2.5 另支持超高=xhigh、最高=max。",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"输出格式",
|
||||
options=["png", "jpeg", "webp"],
|
||||
default="png",
|
||||
tooltip="Generated image output format",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"背景",
|
||||
options=list(GPT_IMAGE_BACKGROUND_OPTIONS),
|
||||
default="auto",
|
||||
tooltip="透明背景仅支持 PNG 或 WebP 输出格式。",
|
||||
),
|
||||
io.Mask.Input(
|
||||
"遮罩",
|
||||
optional=True,
|
||||
tooltip="Optional mask for inpainting (white areas will be replaced)",
|
||||
),
|
||||
reference_images,
|
||||
io.Combo.Input(
|
||||
"缩放图片",
|
||||
options=["不缩放", "智能缩放"],
|
||||
default="智能缩放",
|
||||
tooltip="请求体超过 18 MiB 时,智能缩放会等比缩小占用最大的参考图。",
|
||||
),
|
||||
io.Int.Input(
|
||||
"seed",
|
||||
default=0,
|
||||
min=0,
|
||||
max=2**31 - 1,
|
||||
step=1,
|
||||
display_mode=io.NumberDisplay.number,
|
||||
control_after_generate=io.ControlAfterGenerate.randomize,
|
||||
tooltip="Random seed (0 = not specified)",
|
||||
),
|
||||
],
|
||||
outputs=[io.Image.Output(display_name="IMAGE")],
|
||||
# 兼容 Autogrow 改造前保存的参考图1~参考图9端口。
|
||||
accept_all_inputs=True,
|
||||
)
|
||||
|
||||
optional_inputs["模型"] = ([
|
||||
"gpt-image-2-按量",
|
||||
"gpt-image-2-次卡",
|
||||
], {
|
||||
"default": "gpt-image-2-次卡",
|
||||
})
|
||||
optional_inputs["网络"] = (NETWORK_ROUTE_OPTIONS, {
|
||||
"default": "全球加速",
|
||||
})
|
||||
optional_inputs["分辨率"] = ([
|
||||
"智能",
|
||||
# ── 1K ──
|
||||
"1024x1024(1K 正方形 1:1)",
|
||||
"1536x1024(1K 横版 3:2)",
|
||||
"1024x1536(1K 竖版 2:3)",
|
||||
"1360x1024(1K 横版 4:3)",
|
||||
"1024x1360(1K 竖版 3:4)",
|
||||
"1824x1024(1K 横版 16:9)",
|
||||
"1024x1824(1K 竖版 9:16)",
|
||||
# ── 2K ──
|
||||
"2048x2048(2K 正方形 1:1)",
|
||||
"3072x2048(2K 横版 3:2)",
|
||||
"2048x3072(2K 竖版 2:3)",
|
||||
"2736x2048(2K 横版 4:3)",
|
||||
"2048x2736(2K 竖版 3:4)",
|
||||
"3648x2048(2K 横版 16:9)",
|
||||
"2048x3648(2K 竖版 9:16)",
|
||||
# ── 4K ──
|
||||
"2880x2880(4K 正方形 1:1)",
|
||||
"3504x2336(4K 横版 3:2)",
|
||||
"2336x3504(4K 竖版 2:3)",
|
||||
"3264x2448(4K 横版 4:3)",
|
||||
"2448x3264(4K 竖版 3:4)",
|
||||
"3840x2160(4K 横版 16:9)",
|
||||
"2160x3840(4K 竖版 9:16)",
|
||||
], {
|
||||
"default": "智能",
|
||||
"tooltip": "Image size (智能 = API decides)",
|
||||
})
|
||||
optional_inputs["生图数量"] = ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"tooltip": "How many images to generate per prompt",
|
||||
})
|
||||
optional_inputs["质量"] = (["高", "中", "低", "自动"], {
|
||||
"default": "自动",
|
||||
"tooltip": "Image quality: 高=high, 中=medium, 低=low, 自动=auto",
|
||||
})
|
||||
optional_inputs["输出格式"] = (["png", "jpeg", "webp"], {
|
||||
"default": "jpeg",
|
||||
"tooltip": "Generated image output format",
|
||||
})
|
||||
optional_inputs["seed"] = ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 2**31 - 1,
|
||||
"step": 1,
|
||||
"display": "number",
|
||||
"control_after_generate": True,
|
||||
"tooltip": "Random seed (0 = not specified)",
|
||||
})
|
||||
optional_inputs["遮罩"] = ("MASK", {
|
||||
"tooltip": "Optional mask for inpainting (white areas will be replaced)",
|
||||
})
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Text prompt for GPT Image. Use --- on its own line to separate batch prompts.",
|
||||
}),
|
||||
},
|
||||
"optional": optional_inputs,
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("IMAGE",)
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "o1key/image"
|
||||
OUTPUT_NODE = False
|
||||
|
||||
def generate(
|
||||
self,
|
||||
@classmethod
|
||||
def execute(
|
||||
cls,
|
||||
prompt: str,
|
||||
模型: str = "gpt-image-2-次卡",
|
||||
网络: str = "全球加速",
|
||||
分辨率: str = "auto",
|
||||
模型: str = "gpt-image-2.5-sunburst",
|
||||
模型线路: str = "畅速",
|
||||
分辨率: str = "智能",
|
||||
质量: str = "自动",
|
||||
输出格式: str = "jpeg",
|
||||
输出格式: str = "png",
|
||||
生图数量: int = 1,
|
||||
seed: int = 0,
|
||||
遮罩=None,
|
||||
缩放图片: str = "智能缩放",
|
||||
背景: str = "auto",
|
||||
**kwargs,
|
||||
) -> io.NodeOutput:
|
||||
result = cls.generate(
|
||||
prompt=prompt,
|
||||
模型=模型,
|
||||
模型线路=模型线路,
|
||||
分辨率=分辨率,
|
||||
质量=质量,
|
||||
输出格式=输出格式,
|
||||
生图数量=生图数量,
|
||||
seed=seed,
|
||||
遮罩=遮罩,
|
||||
缩放图片=缩放图片,
|
||||
背景=背景,
|
||||
**kwargs,
|
||||
)
|
||||
return io.NodeOutput(*result)
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls,
|
||||
prompt: str,
|
||||
模型: str = "gpt-image-2.5-sunburst",
|
||||
模型线路: str = "畅速",
|
||||
分辨率: str = "智能",
|
||||
质量: str = "自动",
|
||||
输出格式: str = "png",
|
||||
生图数量: int = 1,
|
||||
seed: int = 0,
|
||||
遮罩=None,
|
||||
缩放图片: str = "智能缩放",
|
||||
背景: str = "auto",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -230,35 +268,45 @@ class O1keyGPTImage:
|
||||
- prompt 含 --- → 批量模式,逐条调用上述接口
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# ── 0. 收集多参考图输入 ────────────────────────────────────────────────
|
||||
reference_tensors = []
|
||||
for i in range(1, 10):
|
||||
key = f"参考图{i}"
|
||||
if key in kwargs and kwargs[key] is not None:
|
||||
reference_tensors.append(kwargs[key])
|
||||
reference_tensors = _collect_autogrow_inputs(kwargs.get("参考图组"))
|
||||
if not reference_tensors:
|
||||
# 兼容 Autogrow 改造前保存的固定参考图端口。
|
||||
reference_tensors = [
|
||||
kwargs[f"参考图{i}"]
|
||||
for i in range(1, MAX_GPT_IMAGE_REFERENCES + 1)
|
||||
if kwargs.get(f"参考图{i}") is not None
|
||||
]
|
||||
|
||||
图片 = reference_tensors if reference_tensors else None
|
||||
|
||||
# ── 1. 参数校验 ───────────────────────────────────────────────────────
|
||||
if 遮罩 is not None and 图片 is None:
|
||||
raise ValueError("提供了遮罩但未提供图片,请同时提供图片和遮罩")
|
||||
if 缩放图片 not in {"不缩放", "智能缩放"}:
|
||||
raise ValueError("缩放图片参数无效")
|
||||
if 输出格式 not in GPT_IMAGE_OUTPUT_FORMAT_OPTIONS:
|
||||
raise ValueError("GPT Image 输出格式无效")
|
||||
if 背景 not in GPT_IMAGE_BACKGROUND_OPTIONS:
|
||||
raise ValueError("GPT Image 背景参数无效")
|
||||
if 背景 == "transparent" and 输出格式 == "jpeg":
|
||||
raise ValueError("GPT Image 透明背景仅支持 PNG 或 WebP 输出格式")
|
||||
|
||||
# ── 2. 解析分辨率显示值 → API 参数值 ──────────────────────────────────
|
||||
size = _resolve_async_size(分辨率)
|
||||
|
||||
# ── 2b. 解析模型显示值 → API 参数值 ───────────────────────────────────
|
||||
_model_map = {"gpt-image-2-次卡": "gpt-image-2-c", "gpt-image-2-按量": "gpt-image-2"}
|
||||
model = _model_map.get(模型, 模型)
|
||||
# ── 2b. 主模型与线路共同解析为 API 模型名 ───────────────────────────
|
||||
model = resolve_gpt_image_model(模型, 模型线路)
|
||||
|
||||
# ── 2c. 解析质量显示值 → API 参数值 ───────────────────────────────────
|
||||
_quality_map = {"高": "high", "中": "medium", "低": "low", "自动": "auto"}
|
||||
quality = _quality_map.get(质量, "auto")
|
||||
quality = resolve_gpt_image_quality(模型, 质量)
|
||||
|
||||
# ── 3. 创建客户端 ─────────────────────────────────────────────────────
|
||||
try:
|
||||
client = GptImageClient()
|
||||
client.base_url = get_base_url_by_route(网络)
|
||||
client.base_url = get_base_url_by_route()
|
||||
client.response_log_enabled = False
|
||||
client.poll_log_enabled = False
|
||||
except ValueError as e:
|
||||
if str(e) == "未授权!":
|
||||
print("[o1key GPT Image] 请联系作者授权后方可使用!")
|
||||
@@ -271,6 +319,12 @@ class O1keyGPTImage:
|
||||
|
||||
# ── 5. 调用 API ───────────────────────────────────────────────────
|
||||
all_pil_images = []
|
||||
def _submitted(task_id, status, elapsed):
|
||||
print(f"[o1key GPT Image] 已提交 | task_id={task_id} | 状态={status} | 耗时={elapsed:.1f}s")
|
||||
|
||||
def _completed(task_id, image_count, elapsed, urls):
|
||||
del urls
|
||||
print(f"[o1key GPT Image] 完成 ✓ | task_id={task_id} | 生成={image_count} 张 | 耗时={elapsed:.1f}s")
|
||||
progress_total = len(batch_prompts) if batch_prompts else 1
|
||||
progress_bar = ProgressBar(progress_total * 100) if _PROGRESS_BAR_AVAILABLE else None
|
||||
|
||||
@@ -293,17 +347,22 @@ class O1keyGPTImage:
|
||||
image_tensor=图片,
|
||||
mask_tensor=遮罩,
|
||||
output_format=输出格式,
|
||||
background=背景,
|
||||
progress_callback=_make_node_progress_callback(progress_bar, idx, total),
|
||||
special_price_parallel=True,
|
||||
task_submitted_callback=_submitted,
|
||||
task_completed_callback=_completed,
|
||||
log_request_start=False,
|
||||
log_downloads=True,
|
||||
log_prefix=f"[o1key GPT Image] [{idx}/{total}]",
|
||||
resize_mode=缩放图片,
|
||||
)
|
||||
all_pil_images.extend(pil_images)
|
||||
snippet = p[:30] + ("..." if len(p) >= 30 else "")
|
||||
print(f"[o1key GPT Image] [{idx}/{total}] ✓ {snippet}")
|
||||
except InterruptProcessingException:
|
||||
raise
|
||||
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}")
|
||||
print(f"[o1key GPT Image] [{idx}/{total}] ❌ {error_msg}")
|
||||
if progress_bar is not None:
|
||||
progress_bar.update_absolute(idx * 100, total * 100)
|
||||
else:
|
||||
@@ -321,7 +380,14 @@ class O1keyGPTImage:
|
||||
image_tensor=图片,
|
||||
mask_tensor=遮罩,
|
||||
output_format=输出格式,
|
||||
background=背景,
|
||||
progress_callback=_make_node_progress_callback(progress_bar, 1, 1),
|
||||
special_price_parallel=True,
|
||||
task_submitted_callback=_submitted,
|
||||
task_completed_callback=_completed,
|
||||
log_request_start=False,
|
||||
log_downloads=True,
|
||||
resize_mode=缩放图片,
|
||||
)
|
||||
all_pil_images.extend(pil_images)
|
||||
except InterruptProcessingException:
|
||||
@@ -349,9 +415,10 @@ class O1keyGPTImage:
|
||||
return (output_tensor,)
|
||||
|
||||
finally:
|
||||
self._print_balance(client)
|
||||
cls._print_balance(client)
|
||||
|
||||
def _print_balance(self, client):
|
||||
@staticmethod
|
||||
def _print_balance(client):
|
||||
try:
|
||||
balance_data = client.query_balance_sync()
|
||||
balance_info = client.format_balance_info(balance_data)
|
||||
@@ -360,7 +427,7 @@ class O1keyGPTImage:
|
||||
pass
|
||||
|
||||
|
||||
class O1keyGPTImageBatch:
|
||||
class _LegacyO1keyGPTImageBatch:
|
||||
"""
|
||||
o1key GPT Image 批量节点
|
||||
|
||||
@@ -374,7 +441,7 @@ class O1keyGPTImageBatch:
|
||||
|
||||
PAIRING_MODES = ["按相同图片命名", "1*N", "不配对"]
|
||||
IMAGE_FORMATS = ["原始", "JPEG", "PNG", "WebP"]
|
||||
MODEL_OPTIONS = ["gpt-image-2-按量", "gpt-image-2-次卡"]
|
||||
MODEL_OPTIONS = GPT_IMAGE_ROUTE_OPTIONS
|
||||
QUALITY_OPTIONS = ["高", "中", "低", "自动"]
|
||||
RESOLUTION_OPTIONS = [
|
||||
"智能",
|
||||
@@ -424,11 +491,8 @@ class O1keyGPTImageBatch:
|
||||
"multiline": True,
|
||||
"tooltip": "提示词;可用独占一行的 --- 分隔多条批量提示词。",
|
||||
}),
|
||||
"模型": (cls.MODEL_OPTIONS, {
|
||||
"default": "gpt-image-2-次卡",
|
||||
}),
|
||||
"网络": (NETWORK_ROUTE_OPTIONS, {
|
||||
"default": "全球加速",
|
||||
"模型线路": (cls.MODEL_OPTIONS, {
|
||||
"default": "畅速",
|
||||
}),
|
||||
"分辨率": (cls.RESOLUTION_OPTIONS, {
|
||||
"default": "智能",
|
||||
@@ -561,12 +625,9 @@ class O1keyGPTImageBatch:
|
||||
return _resolve_async_size(分辨率)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_model(模型: str) -> str:
|
||||
model_map = {
|
||||
"gpt-image-2-次卡": "gpt-image-2-c",
|
||||
"gpt-image-2-按量": "gpt-image-2",
|
||||
}
|
||||
return model_map.get(模型, 模型)
|
||||
def _resolve_model(模型线路: str) -> str:
|
||||
# 直接返回,客户端会映射到 API 值
|
||||
return 模型线路
|
||||
|
||||
@staticmethod
|
||||
def _resolve_quality(质量: str) -> str:
|
||||
@@ -642,8 +703,7 @@ class O1keyGPTImageBatch:
|
||||
def process_batch(
|
||||
self,
|
||||
prompt: str,
|
||||
模型: str,
|
||||
网络: str,
|
||||
模型线路: str,
|
||||
分辨率: str,
|
||||
生图数量: int,
|
||||
质量: str,
|
||||
@@ -707,12 +767,12 @@ class O1keyGPTImageBatch:
|
||||
|
||||
output_folder = self._ensure_output_folder(保存路径)
|
||||
size = self._resolve_size(分辨率)
|
||||
model = self._resolve_model(模型)
|
||||
model = self._resolve_model(模型线路)
|
||||
quality = self._resolve_quality(质量)
|
||||
output_format = self._resolve_output_format(图片格式)
|
||||
|
||||
client = GptImageClient()
|
||||
client.base_url = get_base_url_by_route(网络)
|
||||
client.base_url = get_base_url_by_route()
|
||||
|
||||
progress_bar = ProgressBar(total_tasks * 100) if _PROGRESS_BAR_AVAILABLE else None
|
||||
results = []
|
||||
|
||||
Reference in New Issue
Block a user