314 lines
12 KiB
Python
314 lines
12 KiB
Python
"""
|
|
PD: OpenAI GPT Image Node (ComfyUI Auth Token)
|
|
基于 gemini_image_api 的 Auth Token 认证方式
|
|
支持文生图和图片编辑
|
|
"""
|
|
|
|
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_AUTHTOKEN] Error loading image: {e}")
|
|
return torch.zeros((1, 512, 512, 3))
|
|
|
|
|
|
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,
|
|
"placeholder": "Paste your Auth Token here"
|
|
}),
|
|
"prompt": ("STRING", {
|
|
"multiline": True,
|
|
"default": "",
|
|
"tooltip": "Describe what you want to generate or edit"
|
|
}),
|
|
"model": (["gpt-image-1", "gpt-image-1.5"], {
|
|
"default": "gpt-image-1",
|
|
"tooltip": "GPT Image model version"
|
|
}),
|
|
"quality": (["low", "medium", "high"], {
|
|
"default": "low",
|
|
"tooltip": "Image quality, affects cost and generation time"
|
|
}),
|
|
"background": (["auto", "opaque", "transparent"], {
|
|
"default": "auto",
|
|
"tooltip": "Background transparency (GPT Image only)"
|
|
}),
|
|
"size": (["auto", "1024x1024", "1024x1536", "1536x1024"], {
|
|
"default": "auto",
|
|
"tooltip": "Image size"
|
|
}),
|
|
"n": ("INT", {
|
|
"default": 1,
|
|
"min": 1,
|
|
"max": 8,
|
|
"step": 1,
|
|
"tooltip": "Number of 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, 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
|
|
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
|
|
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')
|
|
|
|
# 表单数据 - multipart/form-data 模式下所有值都应该是字符串
|
|
data = {
|
|
"model": model,
|
|
"prompt": prompt,
|
|
"n": str(n),
|
|
"size": size,
|
|
"quality": quality,
|
|
"background": background,
|
|
"moderation": "low"
|
|
}
|
|
|
|
# 请求头
|
|
headers = {
|
|
"Authorization": f"Bearer {auth_token}",
|
|
"User-Agent": "ComfyUI-PD-Node/2.0"
|
|
}
|
|
|
|
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,
|
|
"n": n,
|
|
"size": size,
|
|
"quality": quality,
|
|
"background": background,
|
|
"moderation": "low",
|
|
"response_format": "url"
|
|
}
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {auth_token}",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "ComfyUI-PD-Node/2.0"
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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}"
|
|
)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
|
|
# 节点注册
|
|
NODE_CLASS_MAPPINGS = {
|
|
"PDOpenAIGPTImageAuthToken": PDOpenAIGPTImageAuthToken
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"PDOpenAIGPTImageAuthToken": "PD: GPT Image (comfyui_AuthToken)"
|
|
}
|