GPT2
213
This commit is contained in:
+30
-43
@@ -1,74 +1,61 @@
|
||||
"""
|
||||
Comfyui_PDuse - PD ComfyUI 自定义节点套件
|
||||
包含多种实用的图像处理和工具节点
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 初始化节点映射
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
def get_ext_dir(subpath=None, mkdir=False):
|
||||
"""获取扩展目录路径"""
|
||||
dir = os.path.dirname(__file__)
|
||||
|
||||
def get_ext_dir(subpath=None):
|
||||
base_dir = os.path.dirname(__file__)
|
||||
if subpath is not None:
|
||||
dir = os.path.join(dir, subpath)
|
||||
return os.path.abspath(dir)
|
||||
base_dir = os.path.join(base_dir, subpath)
|
||||
return os.path.abspath(base_dir)
|
||||
|
||||
|
||||
def safe_import_module(module_path, file_name):
|
||||
"""安全导入模块,避免导入错误导致崩溃"""
|
||||
try:
|
||||
imported_module = importlib.import_module(module_path)
|
||||
|
||||
# 检查模块是否包含所需的映射
|
||||
if hasattr(imported_module, 'NODE_CLASS_MAPPINGS'):
|
||||
|
||||
if hasattr(imported_module, "NODE_CLASS_MAPPINGS"):
|
||||
NODE_CLASS_MAPPINGS.update(imported_module.NODE_CLASS_MAPPINGS)
|
||||
print(f"✅ 成功加载模块: {file_name}")
|
||||
|
||||
if hasattr(imported_module, 'NODE_DISPLAY_NAME_MAPPINGS'):
|
||||
print(f"[PD_NODE] Loaded module: {file_name}")
|
||||
|
||||
if hasattr(imported_module, "NODE_DISPLAY_NAME_MAPPINGS"):
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(imported_module.NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载模块 {file_name} 失败: {e}")
|
||||
print(f"[PD_NODE] Failed to load module {file_name}: {e}")
|
||||
return False
|
||||
|
||||
# 动态扫描并加载 py/ 目录下的所有模块
|
||||
|
||||
py_dir = get_ext_dir("py")
|
||||
|
||||
if os.path.exists(py_dir):
|
||||
files = os.listdir(py_dir)
|
||||
|
||||
for file in files:
|
||||
if not file.endswith(".py") or file.startswith("_"):
|
||||
for file_name in os.listdir(py_dir):
|
||||
if not file_name.endswith(".py") or file_name.startswith("_"):
|
||||
continue
|
||||
|
||||
module_name = os.path.splitext(file)[0]
|
||||
module_path = f"{__name__}.py.{module_name}"
|
||||
|
||||
safe_import_module(module_path, file)
|
||||
else:
|
||||
print(f"警告: py目录不存在: {py_dir}")
|
||||
|
||||
# Web界面目录
|
||||
module_name = os.path.splitext(file_name)[0]
|
||||
module_path = f"{__name__}.py.{module_name}"
|
||||
safe_import_module(module_path, file_name)
|
||||
else:
|
||||
print(f"[PD_NODE] Missing py directory: {py_dir}")
|
||||
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
# 显示加载信息
|
||||
if NODE_CLASS_MAPPINGS:
|
||||
print("=" * 50)
|
||||
print("🎨 Comfyui_PDuse 节点套件加载完成")
|
||||
print("[PD_NODE] Comfyui-PD_comfy-api-node loaded")
|
||||
print("=" * 50)
|
||||
print(f"📦 总计加载节点数量: {len(NODE_CLASS_MAPPINGS)}")
|
||||
print("📋 已加载的节点:")
|
||||
for name, display_name in NODE_DISPLAY_NAME_MAPPINGS.items():
|
||||
print(f" • {display_name}")
|
||||
print(f"[PD_NODE] Total nodes: {len(NODE_CLASS_MAPPINGS)}")
|
||||
for node_name, display_name in NODE_DISPLAY_NAME_MAPPINGS.items():
|
||||
print(f"[PD_NODE] {node_name} -> {display_name}")
|
||||
print("=" * 50)
|
||||
else:
|
||||
print("⚠️ 警告: 没有找到任何可用的节点")
|
||||
print("[PD_NODE] Warning: no nodes were loaded")
|
||||
|
||||
|
||||
# 导出变量
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
PD: OpenAI GPT Image 2 Node (ComfyUI API Key)
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
from ._image_gpt_common import (
|
||||
GPT_IMAGE_2_PRESET_SIZES,
|
||||
calculate_token_price_2,
|
||||
decode_response_images,
|
||||
empty_image,
|
||||
estimate_price_range_2,
|
||||
format_rmb_range,
|
||||
format_rmb_value,
|
||||
mask_to_png_bytes,
|
||||
resolve_gpt_image_2_size,
|
||||
tensor_to_bytes,
|
||||
validate_gpt_image_2_size,
|
||||
)
|
||||
|
||||
|
||||
class PDOpenAIGPTImage2APIKey:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"api_key": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Your ComfyUI API Key"
|
||||
}),
|
||||
"prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Text prompt for GPT Image 2"
|
||||
}),
|
||||
"model": (["gpt-image-2"], {
|
||||
"default": "gpt-image-2"
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 2**31 - 1,
|
||||
"step": 1,
|
||||
"tooltip": "not implemented yet in backend"
|
||||
}),
|
||||
"quality": (["auto", "low", "medium", "high"], {
|
||||
"default": "auto",
|
||||
"tooltip": "GPT Image 2 quality"
|
||||
}),
|
||||
"background": (["auto", "opaque"], {
|
||||
"default": "auto",
|
||||
"tooltip": "GPT Image 2 does not support transparent background"
|
||||
}),
|
||||
"size": (GPT_IMAGE_2_PRESET_SIZES, {
|
||||
"default": "1024x1024",
|
||||
"tooltip": "Preset output size"
|
||||
}),
|
||||
"custom_width": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 3840,
|
||||
"step": 16,
|
||||
"tooltip": "Override width when both width and height are non-zero"
|
||||
}),
|
||||
"custom_height": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 3840,
|
||||
"step": 16,
|
||||
"tooltip": "Override height when both width and height are non-zero"
|
||||
}),
|
||||
"num_images": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"tooltip": "How many images to generate"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE", {
|
||||
"tooltip": "Optional reference image for image editing."
|
||||
}),
|
||||
"mask": ("MASK", {
|
||||
"tooltip": "Optional mask for inpainting (white areas will be replaced)"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("image", "info")
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "PD_Tools/Image_Generation"
|
||||
|
||||
def generate_image(self, api_key, prompt, model, seed, quality, background, size, custom_width, custom_height, num_images, unique_id, image=None, mask=None):
|
||||
if not api_key or not api_key.strip():
|
||||
error_msg = "Error: ComfyUI API Key is required"
|
||||
print(f"[IMAGE_GPT2_APIKEY] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
try:
|
||||
resolved_size = validate_gpt_image_2_size(size, custom_width, custom_height)
|
||||
except Exception as e:
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
api_key = api_key.strip()
|
||||
is_edit_mode = image is not None
|
||||
base_url = "https://api.comfy.org/proxy/openai/images/edits" if is_edit_mode else "https://api.comfy.org/proxy/openai/images/generations"
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if is_edit_mode:
|
||||
files = {
|
||||
"image": ("image.png", tensor_to_bytes(image), "image/png")
|
||||
}
|
||||
if mask is not None:
|
||||
files["mask"] = ("mask.png", mask_to_png_bytes(mask), "image/png")
|
||||
|
||||
data = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"n": str(num_images),
|
||||
"size": resolved_size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
response = requests.post(base_url, headers=headers, files=files, data=data, timeout=300)
|
||||
else:
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"n": num_images,
|
||||
"size": resolved_size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
response = requests.post(base_url, headers=headers, json=payload, timeout=300)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
|
||||
print(f"[IMAGE_GPT2_APIKEY] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
result = response.json()
|
||||
image_tensors = decode_response_images(result)
|
||||
if not image_tensors:
|
||||
error_msg = "Error: Failed to process output images"
|
||||
print(f"[IMAGE_GPT2_APIKEY] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
final_image = torch.cat(image_tensors, dim=0)
|
||||
duration = time.time() - start_time
|
||||
min_cost, max_cost = estimate_price_range_2(quality, num_images)
|
||||
token_cost = calculate_token_price_2(num_images)
|
||||
size_note = resolve_gpt_image_2_size(size, custom_width, custom_height)
|
||||
|
||||
info_str = (
|
||||
f"模型: {model}\n"
|
||||
f"显示名称: OpenAI GPT Image 2\n"
|
||||
f"模式: {'编辑' if is_edit_mode else '生成'}\n"
|
||||
f"状态: 成功\n"
|
||||
f"耗时: {duration:.2f} 秒\n"
|
||||
f"图片数量: {len(image_tensors)}\n"
|
||||
f"尺寸预设: {size}\n"
|
||||
f"实际尺寸: {size_note}\n"
|
||||
f"质量: {quality}\n"
|
||||
f"背景: {background}\n"
|
||||
f"预估价格区间(USD): ~${min_cost:.3f}-${max_cost:.3f}\n"
|
||||
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}\n"
|
||||
f"Token 公式估算(USD): ${token_cost:.4f}\n"
|
||||
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}"
|
||||
)
|
||||
return (final_image, info_str)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return (empty_image(), "Error: Request timeout (300s)")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
return (empty_image(), f"Error: Connection failed - {str(e)}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PDOpenAIGPTImage2APIKey": PDOpenAIGPTImage2APIKey
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PDOpenAIGPTImage2APIKey": "PD: GPT Image 2 (apikey)"
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
PD: OpenAI GPT Image 2 Node (ComfyUI Auth Token)
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
from ._image_gpt_common import (
|
||||
GPT_IMAGE_2_PRESET_SIZES,
|
||||
calculate_token_price_2,
|
||||
decode_response_images,
|
||||
empty_image,
|
||||
estimate_price_range_2,
|
||||
format_rmb_range,
|
||||
format_rmb_value,
|
||||
mask_to_png_bytes,
|
||||
resolve_gpt_image_2_size,
|
||||
tensor_to_bytes,
|
||||
validate_gpt_image_2_size,
|
||||
)
|
||||
|
||||
|
||||
class PDOpenAIGPTImage2AuthToken:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"auth_token": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Paste your Auth Token here"
|
||||
}),
|
||||
"prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Describe what you want to generate or edit"
|
||||
}),
|
||||
"model": (["gpt-image-2"], {
|
||||
"default": "gpt-image-2",
|
||||
"tooltip": "Locked GPT Image 2 model"
|
||||
}),
|
||||
"quality": (["auto", "low", "medium", "high"], {
|
||||
"default": "auto",
|
||||
"tooltip": "GPT Image 2 quality"
|
||||
}),
|
||||
"background": (["auto", "opaque"], {
|
||||
"default": "auto",
|
||||
"tooltip": "GPT Image 2 does not support transparent background"
|
||||
}),
|
||||
"size": (GPT_IMAGE_2_PRESET_SIZES, {
|
||||
"default": "1024x1024",
|
||||
"tooltip": "Preset output size"
|
||||
}),
|
||||
"custom_width": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 3840,
|
||||
"step": 16,
|
||||
"tooltip": "Override width when both width and height are non-zero"
|
||||
}),
|
||||
"custom_height": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 3840,
|
||||
"step": 16,
|
||||
"tooltip": "Override height when both width and height are non-zero"
|
||||
}),
|
||||
"num_images": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"tooltip": "How many images to generate"
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE", {"default": None, "tooltip": "Input image for editing"}),
|
||||
"mask": ("MASK", {"default": None, "tooltip": "Mask for inpainting"}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("image", "info")
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "PD_Tools/Image_Generation"
|
||||
|
||||
def generate_image(self, auth_token, prompt, model, quality, background, size, custom_width, custom_height, num_images, seed, unique_id, image=None, mask=None):
|
||||
if not auth_token or not auth_token.strip():
|
||||
error_msg = "Error: Auth Token is required"
|
||||
print(f"[IMAGE_GPT2_AUTHTOKEN] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
try:
|
||||
resolved_size = validate_gpt_image_2_size(size, custom_width, custom_height)
|
||||
except Exception as e:
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
auth_token = auth_token.strip()
|
||||
if auth_token.lower().startswith("bearer "):
|
||||
auth_token = auth_token[7:].strip()
|
||||
|
||||
base_url = "https://api.comfy.org"
|
||||
is_edit_mode = image is not None
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if is_edit_mode:
|
||||
url = f"{base_url}/proxy/openai/images/edits"
|
||||
files = {
|
||||
"image": ("image.png", tensor_to_bytes(image), "image/png")
|
||||
}
|
||||
if mask is not None:
|
||||
files["mask"] = ("mask.png", mask_to_png_bytes(mask), "image/png")
|
||||
|
||||
data = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"n": str(num_images),
|
||||
"size": resolved_size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
response = requests.post(url, headers=headers, files=files, data=data, timeout=300)
|
||||
else:
|
||||
url = f"{base_url}/proxy/openai/images/generations"
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"n": num_images,
|
||||
"size": resolved_size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
|
||||
print(f"[IMAGE_GPT2_AUTHTOKEN] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
result = response.json()
|
||||
image_tensors = decode_response_images(result)
|
||||
if not image_tensors:
|
||||
error_msg = "Error: Failed to process output images"
|
||||
print(f"[IMAGE_GPT2_AUTHTOKEN] {error_msg}")
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
final_image = torch.cat(image_tensors, dim=0)
|
||||
duration = time.time() - start_time
|
||||
min_cost, max_cost = estimate_price_range_2(quality, num_images)
|
||||
token_cost = calculate_token_price_2(num_images)
|
||||
size_note = resolve_gpt_image_2_size(size, custom_width, custom_height)
|
||||
|
||||
info_str = (
|
||||
f"模型: {model}\n"
|
||||
f"显示名称: OpenAI GPT Image 2\n"
|
||||
f"模式: {'编辑' if is_edit_mode else '生成'}\n"
|
||||
f"状态: 成功\n"
|
||||
f"耗时: {duration:.2f} 秒\n"
|
||||
f"图片数量: {len(image_tensors)}\n"
|
||||
f"尺寸预设: {size}\n"
|
||||
f"实际尺寸: {size_note}\n"
|
||||
f"质量: {quality}\n"
|
||||
f"背景: {background}\n"
|
||||
f"预估价格区间(USD): ~${min_cost:.3f}-${max_cost:.3f}\n"
|
||||
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}\n"
|
||||
f"Token 公式估算(USD): ${token_cost:.4f}\n"
|
||||
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}"
|
||||
)
|
||||
return (final_image, info_str)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return (empty_image(), "Error: Request timeout (300s)")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
return (empty_image(), f"Error: Connection failed - {str(e)}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PDOpenAIGPTImage2AuthToken": PDOpenAIGPTImage2AuthToken
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PDOpenAIGPTImage2AuthToken": "PD: GPT Image 2 (comfyui_AuthToken)"
|
||||
}
|
||||
+63
-186
@@ -1,86 +1,47 @@
|
||||
"""
|
||||
PD: OpenAI GPT Image Node (ComfyUI API Key)
|
||||
使用 ComfyUI API Key 调用 ComfyUI 代理 API
|
||||
参数和官方节点完全一样
|
||||
PD: OpenAI GPT Image 1 & 1.5 Node (ComfyUI API Key)
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import torch
|
||||
import requests
|
||||
import time
|
||||
import io
|
||||
import base64
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def tensor_to_bytes(tensor, max_pixels=2048*2048):
|
||||
"""将 Tensor 转换为 PNG BytesIO,并进行下采样"""
|
||||
if tensor.ndim == 4:
|
||||
tensor = tensor[0]
|
||||
|
||||
# 下采样大图片
|
||||
height, width = tensor.shape[:2]
|
||||
current_pixels = height * width
|
||||
if current_pixels > max_pixels:
|
||||
scale = (max_pixels / current_pixels) ** 0.5
|
||||
new_height = int(height * scale)
|
||||
new_width = int(width * scale)
|
||||
|
||||
# 使用 PIL 进行高质量缩放
|
||||
array = np.clip(255. * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
array = np.clip(255. * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
buffered.seek(0)
|
||||
return buffered
|
||||
|
||||
|
||||
def load_image_to_tensor(image_data):
|
||||
"""将图片数据(URL 或 bytes)转换为 Tensor"""
|
||||
try:
|
||||
if isinstance(image_data, str) and image_data.startswith("http"):
|
||||
response = requests.get(image_data, timeout=30)
|
||||
response.raise_for_status()
|
||||
img = Image.open(io.BytesIO(response.content))
|
||||
else:
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
|
||||
img = img.convert("RGB")
|
||||
image_np = np.array(img).astype(np.float32) / 255.0
|
||||
return torch.from_numpy(image_np).unsqueeze(0)
|
||||
except Exception as e:
|
||||
print(f"[IMAGE_GPT_APIKEY] Error loading image: {e}")
|
||||
return torch.zeros((1, 512, 512, 3))
|
||||
from ._image_gpt_common import (
|
||||
calculate_token_price_1_or_1_5,
|
||||
decode_response_images,
|
||||
empty_image,
|
||||
estimate_price_range_1_or_1_5,
|
||||
format_rmb_range,
|
||||
format_rmb_value,
|
||||
load_image_to_tensor,
|
||||
mask_to_png_bytes,
|
||||
tensor_to_bytes,
|
||||
)
|
||||
|
||||
|
||||
class PDOpenAIGPTImageAPIKey:
|
||||
"""
|
||||
PD: OpenAI GPT Image (API Key)
|
||||
使用 ComfyUI API Key 调用 ComfyUI 代理 API
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"api_key": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Your ComfyUI API Key"
|
||||
}),
|
||||
"prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Text prompt for GPT Image"
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Text prompt for GPT Image 1 or 1.5"
|
||||
}),
|
||||
"model": (["gpt-image-1", "gpt-image-1.5"], {
|
||||
"default": "gpt-image-1.5",
|
||||
"tooltip": "GPT Image model version"
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 2**31 - 1,
|
||||
"step": 1,
|
||||
"tooltip": "not implemented yet in backend"
|
||||
@@ -98,9 +59,9 @@ class PDOpenAIGPTImageAPIKey:
|
||||
"tooltip": "Image size"
|
||||
}),
|
||||
"n": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"tooltip": "How many images to generate"
|
||||
}),
|
||||
@@ -112,9 +73,6 @@ class PDOpenAIGPTImageAPIKey:
|
||||
"mask": ("MASK", {
|
||||
"tooltip": "Optional mask for inpainting (white areas will be replaced)"
|
||||
}),
|
||||
"model": (["gpt-image-1", "gpt-image-1.5"], {
|
||||
"default": "gpt-image-1"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
@@ -126,57 +84,25 @@ class PDOpenAIGPTImageAPIKey:
|
||||
FUNCTION = "generate_image"
|
||||
CATEGORY = "PD_Tools/Image_Generation"
|
||||
|
||||
def generate_image(self, api_key, prompt, seed, quality, background, size, n, unique_id, image=None, mask=None, model="gpt-image-1"):
|
||||
"""生成或编辑图片"""
|
||||
|
||||
# 验证 API Key
|
||||
def generate_image(self, api_key, prompt, model, seed, quality, background, size, n, unique_id, image=None, mask=None):
|
||||
if not api_key or not api_key.strip():
|
||||
error_msg = "Error: ComfyUI API Key is required"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
# 清理 API Key
|
||||
api_key = api_key.strip()
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
# 判断模式:编辑 vs 生成
|
||||
api_key = api_key.strip()
|
||||
is_edit_mode = image is not None
|
||||
|
||||
# ComfyUI 代理 API URL
|
||||
if is_edit_mode:
|
||||
base_url = "https://api.comfy.org/proxy/openai/images/edits"
|
||||
else:
|
||||
base_url = "https://api.comfy.org/proxy/openai/images/generations"
|
||||
|
||||
base_url = "https://api.comfy.org/proxy/openai/images/edits" if is_edit_mode else "https://api.comfy.org/proxy/openai/images/generations"
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
try:
|
||||
if is_edit_mode:
|
||||
# 图片编辑模式 - 使用 multipart/form-data
|
||||
# 准备文件
|
||||
files = {}
|
||||
img_bytes = tensor_to_bytes(image)
|
||||
files['image'] = ('image.png', img_bytes, 'image/png')
|
||||
|
||||
# 处理 mask
|
||||
files = {
|
||||
"image": ("image.png", tensor_to_bytes(image), "image/png")
|
||||
}
|
||||
if mask is not None:
|
||||
m_tensor = mask
|
||||
if m_tensor.ndim == 3:
|
||||
m_tensor = m_tensor[0]
|
||||
|
||||
mask_h, mask_w = m_tensor.shape
|
||||
rgba = np.zeros((mask_h, mask_w, 4), dtype=np.uint8)
|
||||
|
||||
mask_np = m_tensor.cpu().numpy()
|
||||
alpha = ((1.0 - mask_np) * 255).astype(np.uint8)
|
||||
rgba[..., 3] = alpha
|
||||
|
||||
mask_pil = Image.fromarray(rgba)
|
||||
mask_buffer = io.BytesIO()
|
||||
mask_pil.save(mask_buffer, format="PNG")
|
||||
mask_buffer.seek(0)
|
||||
files['mask'] = ('mask.png', mask_buffer, 'image/png')
|
||||
files["mask"] = ("mask.png", mask_to_png_bytes(mask), "image/png")
|
||||
|
||||
# 表单数据 - multipart/form-data 模式下所有值都应该是字符串
|
||||
data = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
@@ -186,22 +112,13 @@ class PDOpenAIGPTImageAPIKey:
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.0"
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
|
||||
print(f"[IMAGE_GPT_APIKEY] Sending edit request")
|
||||
print(f"[IMAGE_GPT_APIKEY] Model: {model}, Size: {size}, Quality: {quality}")
|
||||
print(f"[IMAGE_GPT_APIKEY] Image size: {image.shape}, Mask: {'Yes' if mask is not None else 'No'}")
|
||||
|
||||
response = requests.post(base_url, headers=headers, files=files, data=data, timeout=300)
|
||||
|
||||
else:
|
||||
# 文生图模式 - 使用 JSON
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
@@ -209,104 +126,64 @@ class PDOpenAIGPTImageAPIKey:
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"response_format": "url"
|
||||
"moderation": "low"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"X-API-KEY": api_key,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.0"
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
|
||||
print(f"[IMAGE_GPT_APIKEY] Sending generation request")
|
||||
print(f"[IMAGE_GPT_APIKEY] Model: {model}, Size: {size}, Quality: {quality}")
|
||||
print(f"[IMAGE_GPT_APIKEY] Prompt length: {len(prompt)} chars")
|
||||
|
||||
response = requests.post(base_url, headers=headers, json=payload, timeout=300)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
result = response.json()
|
||||
data_list = result.get("data", [])
|
||||
|
||||
if not data_list:
|
||||
error_msg = "Error: No image data in response"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
# 提取图片
|
||||
image_tensors = []
|
||||
for item in data_list:
|
||||
if "url" in item:
|
||||
img_tensor = load_image_to_tensor(item["url"])
|
||||
image_tensors.append(img_tensor)
|
||||
elif "b64_json" in item:
|
||||
img_data = base64.b64decode(item["b64_json"])
|
||||
img_tensor = load_image_to_tensor(img_data)
|
||||
image_tensors.append(img_tensor)
|
||||
|
||||
image_tensors = decode_response_images(result)
|
||||
if not image_tensors:
|
||||
error_msg = "Error: Failed to process output images"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
final_image = torch.cat(image_tensors, dim=0)
|
||||
|
||||
# 计算耗时和成本
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# 估算成本
|
||||
cost_map = {
|
||||
"gpt-image-1": {"low": 0.02, "medium": 0.04, "high": 0.08},
|
||||
"gpt-image-1.5": {"low": 0.03, "medium": 0.06, "high": 0.12}
|
||||
}
|
||||
cost = cost_map.get(model, {}).get(quality, 0.04)
|
||||
total_cost = cost * n
|
||||
duration = time.time() - start_time
|
||||
min_cost, max_cost = estimate_price_range_1_or_1_5(model, quality, n)
|
||||
token_cost = calculate_token_price_1_or_1_5(model, n)
|
||||
|
||||
info_str = (
|
||||
f"Model: {model}\n"
|
||||
f"Mode: {'Edit' if is_edit_mode else 'Generate'}\n"
|
||||
f"Status: Success\n"
|
||||
f"Time: {duration:.2f}s\n"
|
||||
f"Images: {len(image_tensors)}\n"
|
||||
f"Size: {size}\n"
|
||||
f"Quality: {quality}\n"
|
||||
f"Background: {background}\n"
|
||||
f"Est. Cost: ${total_cost:.3f}"
|
||||
f"模型: {model}\n"
|
||||
f"显示名称: OpenAI GPT Image 1 & 1.5\n"
|
||||
f"模式: {'编辑' if is_edit_mode else '生成'}\n"
|
||||
f"状态: 成功\n"
|
||||
f"耗时: {duration:.2f} 秒\n"
|
||||
f"图片数量: {len(image_tensors)}\n"
|
||||
f"尺寸: {size}\n"
|
||||
f"质量: {quality}\n"
|
||||
f"背景: {background}\n"
|
||||
f"预估价格区间(USD): ${min_cost:.3f}-${max_cost:.3f}\n"
|
||||
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}\n"
|
||||
f"Token 公式估算(USD): ${token_cost:.4f}\n"
|
||||
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}"
|
||||
)
|
||||
|
||||
print(f"[IMAGE_GPT_APIKEY] Success! Generated {len(image_tensors)} images in {duration:.2f}s")
|
||||
return (final_image, info_str)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "Error: Request timeout (300s)"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
return (empty_image(), "Error: Request timeout (300s)")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
error_msg = f"Error: Connection failed - {str(e)}"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
return (empty_image(), f"Error: Connection failed - {str(e)}")
|
||||
except Exception as e:
|
||||
error_msg = f"Error: {str(e)}"
|
||||
print(f"[IMAGE_GPT_APIKEY] {error_msg}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
|
||||
# 节点注册
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PDOpenAIGPTImageAPIKey": PDOpenAIGPTImageAPIKey
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PDOpenAIGPTImageAPIKey": "PD: GPT Image (apikey)"
|
||||
"PDOpenAIGPTImageAPIKey": "PD: GPT Image 1 & 1.5 (apikey)"
|
||||
}
|
||||
|
||||
+57
-185
@@ -1,85 +1,41 @@
|
||||
"""
|
||||
PD: OpenAI GPT Image Node (ComfyUI Auth Token)
|
||||
基于 gemini_image_api 的 Auth Token 认证方式
|
||||
支持文生图和图片编辑
|
||||
PD: OpenAI GPT Image 1 & 1.5 Node (ComfyUI Auth Token)
|
||||
"""
|
||||
|
||||
import torch
|
||||
import requests
|
||||
import time
|
||||
import io
|
||||
import base64
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
import requests
|
||||
import torch
|
||||
|
||||
def tensor_to_bytes(tensor, max_pixels=2048*2048):
|
||||
"""将 Tensor 转换为 PNG BytesIO,并进行下采样"""
|
||||
if tensor.ndim == 4:
|
||||
tensor = tensor[0]
|
||||
|
||||
# 下采样大图片
|
||||
height, width = tensor.shape[:2]
|
||||
current_pixels = height * width
|
||||
if current_pixels > max_pixels:
|
||||
scale = (max_pixels / current_pixels) ** 0.5
|
||||
new_height = int(height * scale)
|
||||
new_width = int(width * scale)
|
||||
|
||||
# 使用 PIL 进行高质量缩放
|
||||
array = np.clip(255. * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
array = np.clip(255. * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
buffered.seek(0)
|
||||
return buffered
|
||||
|
||||
|
||||
def load_image_to_tensor(image_data):
|
||||
"""将图片数据(URL 或 bytes)转换为 Tensor"""
|
||||
try:
|
||||
if isinstance(image_data, str) and image_data.startswith("http"):
|
||||
response = requests.get(image_data, timeout=30)
|
||||
response.raise_for_status()
|
||||
img = Image.open(io.BytesIO(response.content))
|
||||
else:
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
|
||||
img = img.convert("RGB")
|
||||
image_np = np.array(img).astype(np.float32) / 255.0
|
||||
return torch.from_numpy(image_np).unsqueeze(0)
|
||||
except Exception as e:
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Error loading image: {e}")
|
||||
return torch.zeros((1, 512, 512, 3))
|
||||
from ._image_gpt_common import (
|
||||
calculate_token_price_1_or_1_5,
|
||||
decode_response_images,
|
||||
empty_image,
|
||||
estimate_price_range_1_or_1_5,
|
||||
format_rmb_range,
|
||||
format_rmb_value,
|
||||
mask_to_png_bytes,
|
||||
tensor_to_bytes,
|
||||
)
|
||||
|
||||
|
||||
class PDOpenAIGPTImageAuthToken:
|
||||
"""
|
||||
PD: OpenAI GPT Image (Auth Token)
|
||||
使用 ComfyUI Auth Token 调用 GPT Image API
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"auth_token": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"placeholder": "Paste your Auth Token here"
|
||||
}),
|
||||
"prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"default": "",
|
||||
"tooltip": "Describe what you want to generate or edit"
|
||||
}),
|
||||
"model": (["gpt-image-1", "gpt-image-1.5"], {
|
||||
"default": "gpt-image-1",
|
||||
"default": "gpt-image-1.5",
|
||||
"tooltip": "GPT Image model version"
|
||||
}),
|
||||
"quality": (["low", "medium", "high"], {
|
||||
@@ -88,22 +44,22 @@ class PDOpenAIGPTImageAuthToken:
|
||||
}),
|
||||
"background": (["auto", "opaque", "transparent"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Background transparency (GPT Image only)"
|
||||
"tooltip": "Background transparency"
|
||||
}),
|
||||
"size": (["auto", "1024x1024", "1024x1536", "1536x1024"], {
|
||||
"default": "auto",
|
||||
"tooltip": "Image size"
|
||||
}),
|
||||
"n": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 1,
|
||||
"tooltip": "Number of images to generate"
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xFFFFFFFFFFFFFFFF
|
||||
}),
|
||||
},
|
||||
@@ -122,57 +78,28 @@ class PDOpenAIGPTImageAuthToken:
|
||||
CATEGORY = "PD_Tools/Image_Generation"
|
||||
|
||||
def generate_image(self, auth_token, prompt, model, quality, background, size, n, seed, unique_id, image=None, mask=None):
|
||||
"""生成或编辑图片"""
|
||||
|
||||
# 验证 Token
|
||||
if not auth_token or not auth_token.strip():
|
||||
error_msg = "Error: Auth Token is required"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
# 清理 Token
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
auth_token = auth_token.strip()
|
||||
if auth_token.lower().startswith("bearer "):
|
||||
auth_token = auth_token[7:].strip()
|
||||
|
||||
# API 基础地址
|
||||
base_url = "https://api.comfy.org"
|
||||
|
||||
# 判断模式:编辑 vs 生成
|
||||
is_edit_mode = image is not None
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
try:
|
||||
if is_edit_mode:
|
||||
# 图片编辑模式 - 使用 multipart/form-data
|
||||
url = f"{base_url}/proxy/openai/images/edits"
|
||||
|
||||
# 准备文件
|
||||
files = {}
|
||||
img_bytes = tensor_to_bytes(image)
|
||||
files['image'] = ('image.png', img_bytes, 'image/png')
|
||||
|
||||
# 处理 mask
|
||||
files = {
|
||||
"image": ("image.png", tensor_to_bytes(image), "image/png")
|
||||
}
|
||||
if mask is not None:
|
||||
m_tensor = mask
|
||||
if m_tensor.ndim == 3:
|
||||
m_tensor = m_tensor[0]
|
||||
|
||||
mask_h, mask_w = m_tensor.shape
|
||||
rgba = np.zeros((mask_h, mask_w, 4), dtype=np.uint8)
|
||||
|
||||
mask_np = m_tensor.cpu().numpy()
|
||||
alpha = ((1.0 - mask_np) * 255).astype(np.uint8)
|
||||
rgba[..., 3] = alpha
|
||||
|
||||
mask_pil = Image.fromarray(rgba)
|
||||
mask_buffer = io.BytesIO()
|
||||
mask_pil.save(mask_buffer, format="PNG")
|
||||
mask_buffer.seek(0)
|
||||
files['mask'] = ('mask.png', mask_buffer, 'image/png')
|
||||
files["mask"] = ("mask.png", mask_to_png_bytes(mask), "image/png")
|
||||
|
||||
# 表单数据 - multipart/form-data 模式下所有值都应该是字符串
|
||||
data = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
@@ -182,23 +109,13 @@ class PDOpenAIGPTImageAuthToken:
|
||||
"background": background,
|
||||
"moderation": "low"
|
||||
}
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.0"
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Sending edit request to {url}")
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Model: {model}, Size: {size}, Quality: {quality}, Background: {background}")
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Image size: {image.shape}, Mask: {'Yes' if mask is not None else 'No'}")
|
||||
|
||||
response = requests.post(url, headers=headers, files=files, data=data, timeout=300)
|
||||
|
||||
else:
|
||||
# 文生图模式 - 使用 JSON
|
||||
url = f"{base_url}/proxy/openai/images/generations"
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
@@ -206,108 +123,63 @@ class PDOpenAIGPTImageAuthToken:
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"background": background,
|
||||
"moderation": "low",
|
||||
"response_format": "url"
|
||||
"moderation": "low"
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "ComfyUI-PD-Node/2.0"
|
||||
"User-Agent": "ComfyUI-PD-Node/2.1"
|
||||
}
|
||||
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Sending generation request to {url}")
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Model: {model}, Size: {size}, Quality: {quality}, Background: {background}")
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Prompt length: {len(prompt)} chars")
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
||||
|
||||
# 检查响应
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
result = response.json()
|
||||
data_list = result.get("data", [])
|
||||
|
||||
if not data_list:
|
||||
error_msg = "Error: No image data in response"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
# 提取图片
|
||||
image_tensors = []
|
||||
for item in data_list:
|
||||
if "url" in item:
|
||||
img_tensor = load_image_to_tensor(item["url"])
|
||||
image_tensors.append(img_tensor)
|
||||
elif "b64_json" in item:
|
||||
img_data = base64.b64decode(item["b64_json"])
|
||||
img_tensor = load_image_to_tensor(img_data)
|
||||
image_tensors.append(img_tensor)
|
||||
|
||||
image_tensors = decode_response_images(result)
|
||||
if not image_tensors:
|
||||
error_msg = "Error: Failed to process output images"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), error_msg)
|
||||
|
||||
final_image = torch.cat(image_tensors, dim=0)
|
||||
|
||||
# 计算耗时和成本
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# 估算成本
|
||||
if model == "gpt-image-1":
|
||||
# GPT Image 1: $10/1M input tokens, $40/1M output tokens
|
||||
# 估算:约 1000 tokens input, 2000 tokens output per image
|
||||
cost = (1000 * 10 + 2000 * 40) / 1_000_000
|
||||
elif model == "gpt-image-1.5":
|
||||
# GPT Image 1.5: $8/1M input tokens, $32/1M output tokens
|
||||
cost = (1000 * 8 + 2000 * 32) / 1_000_000
|
||||
else:
|
||||
cost = 0.10 # 默认估算
|
||||
total_cost = cost * n
|
||||
duration = time.time() - start_time
|
||||
min_cost, max_cost = estimate_price_range_1_or_1_5(model, quality, n)
|
||||
token_cost = calculate_token_price_1_or_1_5(model, n)
|
||||
|
||||
info_str = (
|
||||
f"Model: {model}\n"
|
||||
f"Mode: {'Edit' if is_edit_mode else 'Generate'}\n"
|
||||
f"Status: Success\n"
|
||||
f"Time: {duration:.2f}s\n"
|
||||
f"Images: {len(image_tensors)}\n"
|
||||
f"Size: {size}\n"
|
||||
f"Quality: {quality}\n"
|
||||
f"Background: {background}\n"
|
||||
f"Est. Cost: ${total_cost:.4f}"
|
||||
f"模型: {model}\n"
|
||||
f"显示名称: OpenAI GPT Image 1 & 1.5\n"
|
||||
f"模式: {'编辑' if is_edit_mode else '生成'}\n"
|
||||
f"状态: 成功\n"
|
||||
f"耗时: {duration:.2f} 秒\n"
|
||||
f"图片数量: {len(image_tensors)}\n"
|
||||
f"尺寸: {size}\n"
|
||||
f"质量: {quality}\n"
|
||||
f"背景: {background}\n"
|
||||
f"预估价格区间(USD): ${min_cost:.3f}-${max_cost:.3f}\n"
|
||||
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}\n"
|
||||
f"Token 公式估算(USD): ${token_cost:.4f}\n"
|
||||
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}"
|
||||
)
|
||||
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] Success! Generated {len(image_tensors)} images in {duration:.2f}s")
|
||||
return (final_image, info_str)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "Error: Request timeout (300s)"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
return (empty_image(), "Error: Request timeout (300s)")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
error_msg = f"Error: Connection failed - {str(e)}"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
|
||||
return (empty_image(), f"Error: Connection failed - {str(e)}")
|
||||
except Exception as e:
|
||||
error_msg = f"Error: {str(e)}"
|
||||
print(f"[IMAGE_GPT_AUTHTOKEN] {error_msg}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return (torch.zeros((1, 512, 512, 3)), error_msg)
|
||||
return (empty_image(), f"Error: {str(e)}")
|
||||
|
||||
|
||||
# 节点注册
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PDOpenAIGPTImageAuthToken": PDOpenAIGPTImageAuthToken
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PDOpenAIGPTImageAuthToken": "PD: GPT Image (comfyui_AuthToken)"
|
||||
"PDOpenAIGPTImageAuthToken": "PD: GPT Image 1 & 1.5 (comfyui_AuthToken)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import base64
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
|
||||
GPT_IMAGE_EDIT_MAX_PIXELS = 2048 * 2048
|
||||
GPT_IMAGE_2_PRESET_SIZES = [
|
||||
"1024x1024",
|
||||
"1536x1024",
|
||||
"1024x1536",
|
||||
"2048x2048",
|
||||
"2048x1152",
|
||||
"3840x2160",
|
||||
"2160x3840",
|
||||
"auto",
|
||||
]
|
||||
|
||||
# USD/CNY reference rate used for RMB estimation in node info output.
|
||||
# Based on the most recent publicly indexed April 2026 daily spot pages we could verify:
|
||||
# 2026-04-03: 1 USD ≈ 6.88154 CNY
|
||||
USD_TO_CNY_RATE = 6.8815
|
||||
|
||||
|
||||
def empty_image():
|
||||
return torch.zeros((1, 512, 512, 3))
|
||||
|
||||
|
||||
def tensor_to_bytes(tensor, max_pixels=GPT_IMAGE_EDIT_MAX_PIXELS):
|
||||
if tensor.ndim == 4:
|
||||
tensor = tensor[0]
|
||||
|
||||
height, width = tensor.shape[:2]
|
||||
current_pixels = height * width
|
||||
if current_pixels > max_pixels:
|
||||
scale = (max_pixels / current_pixels) ** 0.5
|
||||
new_height = max(1, int(height * scale))
|
||||
new_width = max(1, int(width * scale))
|
||||
array = np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
array = np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
image = Image.fromarray(array)
|
||||
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format="PNG")
|
||||
buffered.seek(0)
|
||||
return buffered
|
||||
|
||||
|
||||
def load_image_to_tensor(image_data):
|
||||
try:
|
||||
if isinstance(image_data, str) and image_data.startswith("http"):
|
||||
response = requests.get(image_data, timeout=30)
|
||||
response.raise_for_status()
|
||||
img = Image.open(io.BytesIO(response.content))
|
||||
else:
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
|
||||
img = img.convert("RGB")
|
||||
image_np = np.array(img).astype(np.float32) / 255.0
|
||||
return torch.from_numpy(image_np).unsqueeze(0)
|
||||
except Exception:
|
||||
return empty_image()
|
||||
|
||||
|
||||
def mask_to_png_bytes(mask):
|
||||
mask_tensor = mask
|
||||
if mask_tensor.ndim == 3:
|
||||
mask_tensor = mask_tensor[0]
|
||||
|
||||
mask_h, mask_w = mask_tensor.shape
|
||||
rgba = np.zeros((mask_h, mask_w, 4), dtype=np.uint8)
|
||||
mask_np = mask_tensor.cpu().numpy()
|
||||
alpha = ((1.0 - mask_np) * 255).astype(np.uint8)
|
||||
rgba[..., 3] = alpha
|
||||
|
||||
mask_pil = Image.fromarray(rgba)
|
||||
mask_buffer = io.BytesIO()
|
||||
mask_pil.save(mask_buffer, format="PNG")
|
||||
mask_buffer.seek(0)
|
||||
return mask_buffer
|
||||
|
||||
|
||||
def decode_response_images(result):
|
||||
data_list = result.get("data", [])
|
||||
image_tensors = []
|
||||
for item in data_list:
|
||||
if "url" in item:
|
||||
img_tensor = load_image_to_tensor(item["url"])
|
||||
image_tensors.append(img_tensor)
|
||||
elif "b64_json" in item:
|
||||
img_data = base64.b64decode(item["b64_json"])
|
||||
img_tensor = load_image_to_tensor(img_data)
|
||||
image_tensors.append(img_tensor)
|
||||
return image_tensors
|
||||
|
||||
|
||||
def estimate_price_range_1_or_1_5(model, quality, n):
|
||||
ranges = {
|
||||
"gpt-image-1": {
|
||||
"low": (0.011, 0.016),
|
||||
"medium": (0.042, 0.063),
|
||||
"high": (0.167, 0.250),
|
||||
},
|
||||
"gpt-image-1.5": {
|
||||
"low": (0.009, 0.013),
|
||||
"medium": (0.034, 0.050),
|
||||
"high": (0.133, 0.200),
|
||||
},
|
||||
}
|
||||
model_ranges = ranges.get(model, ranges["gpt-image-1"])
|
||||
min_cost, max_cost = model_ranges.get(quality, model_ranges["medium"])
|
||||
return (min_cost * n, max_cost * n)
|
||||
|
||||
|
||||
def calculate_token_price_1_or_1_5(model, n):
|
||||
if model == "gpt-image-1":
|
||||
cost = (1000 * 10 + 2000 * 40) / 1_000_000
|
||||
else:
|
||||
cost = (1000 * 8 + 2000 * 32) / 1_000_000
|
||||
return cost * n
|
||||
|
||||
|
||||
def estimate_price_range_2(quality, n):
|
||||
ranges = {
|
||||
"auto": (0.005, 0.250),
|
||||
"low": (0.005, 0.030),
|
||||
"medium": (0.041, 0.120),
|
||||
"high": (0.165, 0.250),
|
||||
}
|
||||
min_cost, max_cost = ranges.get(quality, ranges["auto"])
|
||||
return (min_cost * n, max_cost * n)
|
||||
|
||||
|
||||
def calculate_token_price_2(n):
|
||||
cost = (1000 * 8 + 2000 * 30) / 1_000_000
|
||||
return cost * n
|
||||
|
||||
|
||||
def resolve_gpt_image_2_size(size, custom_width, custom_height):
|
||||
if custom_width > 0 and custom_height > 0:
|
||||
return f"{custom_width}x{custom_height}"
|
||||
return size
|
||||
|
||||
|
||||
def usd_to_cny(usd_amount):
|
||||
return usd_amount * USD_TO_CNY_RATE
|
||||
|
||||
|
||||
def format_rmb_range(min_usd, max_usd):
|
||||
return f"约¥{usd_to_cny(min_usd):.2f}-¥{usd_to_cny(max_usd):.2f}"
|
||||
|
||||
|
||||
def format_rmb_value(usd_amount):
|
||||
return f"约¥{usd_to_cny(usd_amount):.2f}"
|
||||
|
||||
|
||||
def validate_gpt_image_2_size(size, custom_width=0, custom_height=0):
|
||||
resolved_size = resolve_gpt_image_2_size(size, custom_width, custom_height)
|
||||
if resolved_size == "auto":
|
||||
return resolved_size
|
||||
|
||||
try:
|
||||
width_str, height_str = resolved_size.lower().split("x")
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
except Exception as exc:
|
||||
raise ValueError(f"Invalid size: {resolved_size}") from exc
|
||||
|
||||
if width > 3840 or height > 3840:
|
||||
raise ValueError("Width and height must not exceed 3840")
|
||||
if width % 16 != 0 or height % 16 != 0:
|
||||
raise ValueError("Width and height must be multiples of 16")
|
||||
|
||||
shorter_edge = min(width, height)
|
||||
longer_edge = max(width, height)
|
||||
if shorter_edge == 0 or (longer_edge / shorter_edge) > 3:
|
||||
raise ValueError("Aspect ratio must not exceed 3:1")
|
||||
|
||||
total_pixels = width * height
|
||||
if total_pixels < 655360 or total_pixels > 8294400:
|
||||
raise ValueError("Total pixels must be between 655360 and 8294400")
|
||||
|
||||
return resolved_size
|
||||
Reference in New Issue
Block a user