This commit is contained in:
pandy
2026-04-22 18:01:01 +08:00
parent a25fbf29c1
commit 37ce451010
3 changed files with 161 additions and 49 deletions
+42 -25
View File
@@ -8,16 +8,19 @@ import requests
import torch
from ._image_gpt_common import (
build_openai_edit_files,
calculate_exact_cost_gpt_image_2_from_usage,
GPT_IMAGE_2_PRESET_SIZES,
calculate_token_price_2,
decode_response_images,
empty_image,
estimate_price_range_2,
format_credits_value,
format_rmb_range,
format_rmb_value,
mask_to_png_bytes,
post_with_retries,
resolve_gpt_image_2_size,
tensor_to_bytes,
validate_gpt_image_2_size,
)
@@ -117,11 +120,7 @@ class PDOpenAIGPTImage2APIKey:
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")
files = build_openai_edit_files(image, mask)
data = {
"model": model,
@@ -135,9 +134,10 @@ class PDOpenAIGPTImage2APIKey:
headers = {
"X-API-KEY": api_key,
"Accept": "application/json",
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = requests.post(base_url, headers=headers, files=files, data=data, timeout=300)
response = post_with_retries(base_url, headers, data=data, files=files, timeout=300)
else:
payload = {
"model": model,
@@ -152,9 +152,10 @@ class PDOpenAIGPTImage2APIKey:
"X-API-KEY": api_key,
"Accept": "application/json",
"Content-Type": "application/json",
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = requests.post(base_url, headers=headers, json=payload, timeout=300)
response = post_with_retries(base_url, headers, json_payload=payload, timeout=300)
if response.status_code != 200:
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
@@ -173,23 +174,39 @@ class PDOpenAIGPTImage2APIKey:
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)
exact_cost = calculate_exact_cost_gpt_image_2_from_usage(result)
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)}"
)
lines = [
f"模型: {model}",
f"显示名称: OpenAI GPT Image 2",
f"模式: {'编辑' if is_edit_mode else '生成'}",
f"状态: 成功",
f"耗时: {duration:.2f} 秒",
f"图片数量: {len(image_tensors)}",
f"尺寸预设: {size}",
f"实际尺寸: {size_note}",
f"质量: {quality}",
f"背景: {background}",
]
if exact_cost is not None:
lines.extend([
f"实际价格(USD): ${exact_cost['usd']:.4f}",
f"实际价格(RMB): {format_rmb_value(exact_cost['usd'])}",
f"实际价格(Credits): {format_credits_value(exact_cost['usd'])}",
f"图像输入 Tokens: {exact_cost['image_tokens']}",
f"文本输入 Tokens: {exact_cost['text_tokens']}",
f"输出 Tokens: {exact_cost['output_tokens']}",
])
else:
lines.extend([
f"预估价格区间(USD): ~${min_cost:.3f}-${max_cost:.3f}",
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}",
f"Token 公式估算(USD): ${token_cost:.4f}",
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}",
])
info_str = "\n".join(lines)
return (final_image, info_str)
except requests.exceptions.Timeout:
@@ -207,5 +224,5 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PDOpenAIGPTImage2APIKey": "PD: GPT Image 2 (apikey)"
"PDOpenAIGPTImage2APIKey": "PD: GPT Image 2 (comfyui_apikey)"
}
+41 -24
View File
@@ -8,16 +8,19 @@ import requests
import torch
from ._image_gpt_common import (
build_openai_edit_files,
calculate_exact_cost_gpt_image_2_from_usage,
GPT_IMAGE_2_PRESET_SIZES,
calculate_token_price_2,
decode_response_images,
empty_image,
estimate_price_range_2,
format_credits_value,
format_rmb_range,
format_rmb_value,
mask_to_png_bytes,
post_with_retries,
resolve_gpt_image_2_size,
tensor_to_bytes,
validate_gpt_image_2_size,
)
@@ -116,11 +119,7 @@ class PDOpenAIGPTImage2AuthToken:
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")
files = build_openai_edit_files(image, mask)
data = {
"model": model,
@@ -133,9 +132,10 @@ class PDOpenAIGPTImage2AuthToken:
}
headers = {
"Authorization": f"Bearer {auth_token}",
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = requests.post(url, headers=headers, files=files, data=data, timeout=300)
response = post_with_retries(url, headers, data=data, files=files, timeout=300)
else:
url = f"{base_url}/proxy/openai/images/generations"
payload = {
@@ -150,9 +150,10 @@ class PDOpenAIGPTImage2AuthToken:
headers = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = requests.post(url, headers=headers, json=payload, timeout=300)
response = post_with_retries(url, headers, json_payload=payload, timeout=300)
if response.status_code != 200:
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
@@ -171,23 +172,39 @@ class PDOpenAIGPTImage2AuthToken:
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)
exact_cost = calculate_exact_cost_gpt_image_2_from_usage(result)
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)}"
)
lines = [
f"模型: {model}",
f"显示名称: OpenAI GPT Image 2",
f"模式: {'编辑' if is_edit_mode else '生成'}",
f"状态: 成功",
f"耗时: {duration:.2f} 秒",
f"图片数量: {len(image_tensors)}",
f"尺寸预设: {size}",
f"实际尺寸: {size_note}",
f"质量: {quality}",
f"背景: {background}",
]
if exact_cost is not None:
lines.extend([
f"实际价格(USD): ${exact_cost['usd']:.4f}",
f"实际价格(RMB): {format_rmb_value(exact_cost['usd'])}",
f"实际价格(Credits): {format_credits_value(exact_cost['usd'])}",
f"图像输入 Tokens: {exact_cost['image_tokens']}",
f"文本输入 Tokens: {exact_cost['text_tokens']}",
f"输出 Tokens: {exact_cost['output_tokens']}",
])
else:
lines.extend([
f"预估价格区间(USD): ~${min_cost:.3f}-${max_cost:.3f}",
f"预估价格区间(RMB): {format_rmb_range(min_cost, max_cost)}",
f"Token 公式估算(USD): ${token_cost:.4f}",
f"Token 公式估算(RMB): {format_rmb_value(token_cost)}",
])
info_str = "\n".join(lines)
return (final_image, info_str)
except requests.exceptions.Timeout:
+78
View File
@@ -1,5 +1,6 @@
import base64
import io
import time
import numpy as np
import requests
@@ -23,6 +24,7 @@ GPT_IMAGE_2_PRESET_SIZES = [
# 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
USD_TO_COMFY_CREDITS = 211.0
def empty_image():
@@ -86,6 +88,49 @@ def mask_to_png_bytes(mask):
return mask_buffer
def build_openai_edit_files(image, mask=None):
files = []
batch_size = image.shape[0] if image.ndim == 4 else 1
if image.ndim == 4:
for i in range(batch_size):
image_bytes = tensor_to_bytes(image[i : i + 1]).getvalue()
field_name = "image" if batch_size == 1 else "image[]"
files.append((field_name, (f"image_{i}.png", image_bytes, "image/png")))
else:
image_bytes = tensor_to_bytes(image).getvalue()
files.append(("image", ("image_0.png", image_bytes, "image/png")))
if mask is not None:
if image.ndim == 4 and image.shape[0] != 1:
raise ValueError("Mask editing only supports a single input image")
mask_bytes = mask_to_png_bytes(mask).getvalue()
files.append(("mask", ("mask.png", mask_bytes, "image/png")))
return files
def post_with_retries(url, headers, *, json_payload=None, data=None, files=None, timeout=300, retries=3, retry_delay=1.0):
last_error = None
for attempt in range(retries):
try:
response = requests.post(
url,
headers=headers,
json=json_payload,
data=data,
files=files,
timeout=timeout,
)
return response
except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as exc:
last_error = exc
if attempt == retries - 1:
raise
time.sleep(retry_delay * (attempt + 1))
raise last_error
def decode_response_images(result):
data_list = result.get("data", [])
image_tensors = []
@@ -160,6 +205,39 @@ def format_rmb_value(usd_amount):
return f"约¥{usd_to_cny(usd_amount):.2f}"
def usd_to_comfy_credits(usd_amount):
return usd_amount * USD_TO_COMFY_CREDITS
def format_credits_value(usd_amount):
return f"约 {usd_to_comfy_credits(usd_amount):.2f} credits"
def calculate_exact_cost_gpt_image_2_from_usage(result):
usage = result.get("usage") or {}
input_details = usage.get("input_tokens_details") or {}
image_tokens = input_details.get("image_tokens") or 0
text_tokens = input_details.get("text_tokens") or 0
output_tokens = usage.get("output_tokens") or 0
if image_tokens == 0 and text_tokens == 0 and output_tokens == 0:
return None
usd = (
(image_tokens * 8.0) +
(text_tokens * 5.0) +
(output_tokens * 30.0)
) / 1_000_000.0
return {
"usd": usd,
"image_tokens": image_tokens,
"text_tokens": text_tokens,
"output_tokens": output_tokens,
}
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":