diff --git a/py/IMAGE_GPT2_apikey.py b/py/IMAGE_GPT2_apikey.py index 7f0a948..f68f49d 100644 --- a/py/IMAGE_GPT2_apikey.py +++ b/py/IMAGE_GPT2_apikey.py @@ -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)" } diff --git a/py/IMAGE_GPT2_authtoken.py b/py/IMAGE_GPT2_authtoken.py index ae17510..f19a359 100644 --- a/py/IMAGE_GPT2_authtoken.py +++ b/py/IMAGE_GPT2_authtoken.py @@ -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: diff --git a/py/_image_gpt_common.py b/py/_image_gpt_common.py index a298cac..b40ddd6 100644 --- a/py/_image_gpt_common.py +++ b/py/_image_gpt_common.py @@ -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":