update
This commit is contained in:
+42
-25
@@ -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
@@ -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:
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user