update GPT

update GPT
This commit is contained in:
pandy
2026-04-22 19:45:52 +08:00
parent 37ce451010
commit 4d88ff8e62
13 changed files with 519 additions and 159 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"newFileLocation": "root",
"newFileFolderPath": "",
"attachmentFolderPath": "./image"
}
+1
View File
@@ -0,0 +1 @@
{}
+33
View File
@@ -0,0 +1,33 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"footnotes": false,
"properties": true,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"bases": true,
"webviewer": false
}
+196
View File
@@ -0,0 +1,196 @@
{
"main": {
"id": "d17591b790a4e845",
"type": "split",
"children": [
{
"id": "3177b9c61827cc7a",
"type": "tabs",
"children": [
{
"id": "1c73e0277fd6237f",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "README.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "README"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "85a0d9b441e10230",
"type": "split",
"children": [
{
"id": "0ac3115ffa34abca",
"type": "tabs",
"children": [
{
"id": "2657aa2522003f89",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "文件列表"
}
},
{
"id": "4db09918e8fceb8b",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "搜索"
}
},
{
"id": "cc6ce1517e5ab669",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "书签"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "31dbf3f34798f6f3",
"type": "split",
"children": [
{
"id": "3db7b52c4d00a03a",
"type": "tabs",
"children": [
{
"id": "f18aa31ed938818a",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "README.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "README 的反向链接列表"
}
},
{
"id": "d7007bd9e7fb6503",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "README.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "README 的出链列表"
}
},
{
"id": "2d52fd729103eb97",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "标签"
}
},
{
"id": "7eeba929d8dc0368",
"type": "leaf",
"state": {
"type": "all-properties",
"state": {
"sortOrder": "frequency",
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-archive",
"title": "添加笔记属性"
}
},
{
"id": "1811ca85d176dcc3",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "README.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "README 的大纲"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:打开快速切换": false,
"graph:查看关系图谱": false,
"canvas:新建白板": false,
"daily-notes:打开/创建今天的日记": false,
"templates:插入模板": false,
"command-palette:打开命令面板": false,
"bases:新建数据库": false
}
},
"active": "1c73e0277fd6237f",
"lastOpenFiles": [
"py/IMAGE_GPT2_apikey.py",
"py/IMAGE_GeminiNanoBanana2.py",
"image/Pasted image 20260422192905.png",
"image/Pasted image 20260422192828.png",
"image/Pasted image 20260422192822.png",
"Pasted image 20260422192201.png",
"README.md"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

+22 -13
View File
@@ -31,7 +31,28 @@ pip install -r comfyui-PD_comfy-api-node/requirements.txt
2. 登录并创建 API Key
3. 复制 Key 并粘贴到节点中
## 📝 更新日志
### v2.0.0 -2026.0422
- 更新chatgpt image2 节点
- PD: Nano Banana 2 (ComfyUI Key) 节点更新,增加重试3次机制,放置出黑图,稳定性增加。
### v2.0.0 -2026.0112
- 新增 Auth Token 认证方式
- 新增 GPT Image 节点
- 优化成本计算
- 改进错误处理
### v1.0.0
- 初始版本
- 支持 Gemini 图像生成
- 支持 API Key 认证
### 节点介绍
#### PD: GPT Image 2 (comfyui_apikey)
- 支持输入多图参考
- 支持单图输入,GPT image2 节点comfyui 格式。
![[Pasted image 20260422192905.png]]
![[Pasted image 20260422192828.png]]
#### PD: Gemini Image (ComfyUI Key)
![alt text](image.png)
@@ -54,23 +75,11 @@ pip install -r comfyui-PD_comfy-api-node/requirements.txt
![Auth Token 节点示例](image/comfyui-node-auth.png)
**优点**:
- ✅ 无需申请 API Key
- ✅ 使用现有账号权限
## 📝 更新日志
### v2.0.0 -2026.0112
- 新增 Auth Token 认证方式
- 新增 GPT Image 节点
- 优化成本计算
- 改进错误处理
### v1.0.0
- 初始版本
- 支持 Gemini 图像生成
- 支持 API Key 认证
## ⚠️ 注意事项
1. **API Key 安全**:请妥善保管 API Key,不要分享给他人
Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

+51 -35
View File
@@ -8,9 +8,9 @@ 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,
build_openai_edit_files_from_sources,
calculate_exact_cost_gpt_image_2_from_usage,
calculate_token_price_2,
decode_response_images,
empty_image,
@@ -18,7 +18,6 @@ from ._image_gpt_common import (
format_credits_value,
format_rmb_range,
format_rmb_value,
mask_to_png_bytes,
post_with_retries,
resolve_gpt_image_2_size,
validate_gpt_image_2_size,
@@ -91,36 +90,55 @@ class PDOpenAIGPTImage2APIKey:
"mask": ("MASK", {
"tooltip": "Optional mask for inpainting (white areas will be replaced)"
}),
"files": ("GEMINI_INPUT_FILES", {
"tooltip": "Optional packed multi-image files input, for example from PD_comfyplus_image."
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "info")
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "info", "price")
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):
def generate_image(
self,
api_key,
prompt,
model,
seed,
quality,
background,
size,
custom_width,
custom_height,
num_images,
unique_id,
image=None,
mask=None,
files=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)
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)}")
return (empty_image(), f"Error: {str(e)}", "")
api_key = api_key.strip()
is_edit_mode = image is not None
is_edit_mode = image is not None or files 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 = build_openai_edit_files(image, mask)
request_files = build_openai_edit_files_from_sources(image=image, mask=mask, input_files=files)
data = {
"model": model,
@@ -137,7 +155,7 @@ class PDOpenAIGPTImage2APIKey:
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = post_with_retries(base_url, headers, data=data, files=files, timeout=300)
response = post_with_retries(base_url, headers, data=data, files=request_files, timeout=300)
else:
payload = {
"model": model,
@@ -160,14 +178,14 @@ class PDOpenAIGPTImage2APIKey:
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)
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)
return (empty_image(), error_msg, "")
final_image = torch.cat(image_tensors, dim=0)
duration = time.time() - start_time
@@ -176,13 +194,13 @@ class PDOpenAIGPTImage2APIKey:
size_note = resolve_gpt_image_2_size(size, custom_width, custom_height)
exact_cost = calculate_exact_cost_gpt_image_2_from_usage(result)
lines = [
mode_label = "编辑" if is_edit_mode else "生成"
info_lines = [
f"模型: {model}",
f"显示名称: OpenAI GPT Image 2",
f"模式: {'编辑' if is_edit_mode else '生成'}",
f"状态: 成功",
"状态: 成功",
f"模式: {mode_label}",
f"耗时: {duration:.2f} 秒",
f"图片数量: {len(image_tensors)}",
f"输出数量: {len(image_tensors)}",
f"尺寸预设: {size}",
f"实际尺寸: {size_note}",
f"质量: {quality}",
@@ -190,33 +208,31 @@ class PDOpenAIGPTImage2APIKey:
]
if exact_cost is not None:
lines.extend([
f"实际价格(USD): ${exact_cost['usd']:.4f}",
price_lines = [
f"Tokens: 图像:{exact_cost['image_tokens']} | 文本:{exact_cost['text_tokens']} | 输出:{exact_cost['output_tokens']}",
f"实际价格(USD): ${exact_cost['usd']:.6f}",
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)}",
])
price_lines = [
"Tokens: 未返回精确 usage,以下为估算",
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)
return (final_image, "\n".join(info_lines), "\n".join(price_lines))
except requests.exceptions.Timeout:
return (empty_image(), "Error: Request timeout (300s)")
return (empty_image(), "Error: Request timeout (300s)", "")
except requests.exceptions.ConnectionError as e:
return (empty_image(), f"Error: Connection failed - {str(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)}")
return (empty_image(), f"Error: {str(e)}", "")
NODE_CLASS_MAPPINGS = {
+49 -35
View File
@@ -8,9 +8,9 @@ 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,
build_openai_edit_files_from_sources,
calculate_exact_cost_gpt_image_2_from_usage,
calculate_token_price_2,
decode_response_images,
empty_image,
@@ -18,7 +18,6 @@ from ._image_gpt_common import (
format_credits_value,
format_rmb_range,
format_rmb_value,
mask_to_png_bytes,
post_with_retries,
resolve_gpt_image_2_size,
validate_gpt_image_2_size,
@@ -86,40 +85,57 @@ class PDOpenAIGPTImage2AuthToken:
"optional": {
"image": ("IMAGE", {"default": None, "tooltip": "Input image for editing"}),
"mask": ("MASK", {"default": None, "tooltip": "Mask for inpainting"}),
"files": ("GEMINI_INPUT_FILES", {"default": None, "tooltip": "Packed multi-image files input"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "info")
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "info", "price")
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):
def generate_image(
self,
auth_token,
prompt,
model,
quality,
background,
size,
custom_width,
custom_height,
num_images,
seed,
unique_id,
image=None,
mask=None,
files=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)
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)}")
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
is_edit_mode = image is not None or files is not None
start_time = time.time()
try:
if is_edit_mode:
url = f"{base_url}/proxy/openai/images/edits"
files = build_openai_edit_files(image, mask)
request_files = build_openai_edit_files_from_sources(image=image, mask=mask, input_files=files)
data = {
"model": model,
@@ -135,7 +151,7 @@ class PDOpenAIGPTImage2AuthToken:
"Connection": "close",
"User-Agent": "ComfyUI-PD-Node/2.1"
}
response = post_with_retries(url, headers, data=data, files=files, timeout=300)
response = post_with_retries(url, headers, data=data, files=request_files, timeout=300)
else:
url = f"{base_url}/proxy/openai/images/generations"
payload = {
@@ -158,14 +174,14 @@ class PDOpenAIGPTImage2AuthToken:
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)
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)
return (empty_image(), error_msg, "")
final_image = torch.cat(image_tensors, dim=0)
duration = time.time() - start_time
@@ -174,13 +190,13 @@ class PDOpenAIGPTImage2AuthToken:
size_note = resolve_gpt_image_2_size(size, custom_width, custom_height)
exact_cost = calculate_exact_cost_gpt_image_2_from_usage(result)
lines = [
mode_label = "编辑" if is_edit_mode else "生成"
info_lines = [
f"模型: {model}",
f"显示名称: OpenAI GPT Image 2",
f"模式: {'编辑' if is_edit_mode else '生成'}",
f"状态: 成功",
"状态: 成功",
f"模式: {mode_label}",
f"耗时: {duration:.2f} 秒",
f"图片数量: {len(image_tensors)}",
f"输出数量: {len(image_tensors)}",
f"尺寸预设: {size}",
f"实际尺寸: {size_note}",
f"质量: {quality}",
@@ -188,33 +204,31 @@ class PDOpenAIGPTImage2AuthToken:
]
if exact_cost is not None:
lines.extend([
f"实际价格(USD): ${exact_cost['usd']:.4f}",
price_lines = [
f"Tokens: 图像:{exact_cost['image_tokens']} | 文本:{exact_cost['text_tokens']} | 输出:{exact_cost['output_tokens']}",
f"实际价格(USD): ${exact_cost['usd']:.6f}",
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)}",
])
price_lines = [
"Tokens: 未返回精确 usage,以下为估算",
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)
return (final_image, "\n".join(info_lines), "\n".join(price_lines))
except requests.exceptions.Timeout:
return (empty_image(), "Error: Request timeout (300s)")
return (empty_image(), "Error: Request timeout (300s)", "")
except requests.exceptions.ConnectionError as e:
return (empty_image(), f"Error: Connection failed - {str(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)}")
return (empty_image(), f"Error: {str(e)}", "")
NODE_CLASS_MAPPINGS = {
+100 -76
View File
@@ -1,45 +1,43 @@
"""
PD: Nano Banana 2 (ComfyUI Key)
基于 Google Gemini 3.1 Flash Image 模型的图像生成/编辑节点
支持进度反馈、Token 成本计算、及其独立输出接口
Google Gemini 3.1 Flash Image node with limited retries and Chinese pricing output.
"""
import torch
import base64
import json
import time
import uuid
import requests
import numpy as np
import io
import time
import numpy as np
import requests
import torch
from PIL import Image
from typing import Optional, List
from server import PromptServer
import comfy.utils
# 默认系统提示词
from ._image_gpt_common import format_credits_value, format_rmb_value
GEMINI_IMAGE_SYS_PROMPT = (
"You are an expert image-generation engine. You must ALWAYS produce an image.\n"
"Interpret all user input—regardless of "
"format, intent, or abstraction—as literal visual directives for image composition.\n"
"Interpret all user input regardless of format, intent, or abstraction as literal visual directives for image composition.\n"
"If a prompt is conversational or lacks specific visual details, "
"you must creatively invent a concrete visual scenario that depicts the concept.\n"
"Prioritize generating the visual representation above any text, formatting, or conversational requests."
)
def tensor_to_base64(tensor):
"""将 Tensor 转换为 base64 字符串"""
if tensor.ndim == 4:
tensor = tensor[0]
array = np.clip(255. * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
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")
return base64.b64encode(buffered.getvalue()).decode("utf-8")
def bytes_to_tensor(data):
"""将图片字节数据转换为 Tensor"""
try:
img = Image.open(io.BytesIO(data)).convert("RGB")
image_np = np.array(img).astype(np.float32) / 255.0
@@ -48,14 +46,15 @@ def bytes_to_tensor(data):
print(f"[PD_NB2] Image decode error: {e}")
return torch.zeros((1, 512, 512, 3))
class PDGeminiNanoBanana2:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"comfy_api_key": ("STRING", {
"default": "",
"multiline": False,
"default": "",
"multiline": False,
"placeholder": "Paste your ComfyUI API Key here"
}),
"prompt": ("STRING", {
@@ -68,7 +67,7 @@ class PDGeminiNanoBanana2:
"seed": ("INT", {
"default": 42,
"min": 0,
"max": 0x7FFFFFFF,
"max": 0x7FFFFFFF,
}),
"aspect_ratio": (["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"], {
"default": "auto",
@@ -84,8 +83,8 @@ class PDGeminiNanoBanana2:
}),
},
"optional": {
"images": ("IMAGE", ),
"files": ("GEMINI_INPUT_FILES", ),
"images": ("IMAGE",),
"files": ("GEMINI_INPUT_FILES",),
"system_prompt": ("STRING", {
"multiline": True,
"default": GEMINI_IMAGE_SYS_PROMPT,
@@ -95,28 +94,39 @@ class PDGeminiNanoBanana2:
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "info", "price")
FUNCTION = "generate_image"
CATEGORY = "PD_Tools/Image_Generation"
def generate_image(self, comfy_api_key, prompt, model, seed, aspect_ratio, resolution, response_modalities, thinking_level, unique_id, images=None, files=None, system_prompt=""):
def generate_image(
self,
comfy_api_key,
prompt,
model,
seed,
aspect_ratio,
resolution,
response_modalities,
thinking_level,
unique_id,
images=None,
files=None,
system_prompt="",
):
pbar = comfy.utils.ProgressBar(100)
pbar.update(10)
if not comfy_api_key or not comfy_api_key.strip():
return (torch.zeros((1, 512, 512, 3)), "Error: Missing ComfyUI API Key", "$0.00")
return (torch.zeros((1, 512, 512, 3)), "错误: 缺少 ComfyUI API Key", "¥0.00")
api_key = comfy_api_key.strip()
api_model = "gemini-3.1-flash-image-preview"
self._send_status(unique_id, "准备请求...")
# 发送进度文字到前端
self._send_status(unique_id, "Preparing tokens...")
# 构建请求内容
parts = [{"text": prompt}]
if images is not None:
for i in range(min(images.shape[0], 14)):
b64_str = tensor_to_base64(images[i])
@@ -126,8 +136,7 @@ class PDGeminiNanoBanana2:
"data": b64_str
}
})
# Files 输入处理
if files:
for file_part in files:
if hasattr(file_part, "model_dump"):
@@ -135,8 +144,6 @@ class PDGeminiNanoBanana2:
else:
parts.append(file_part)
# 核心参数修正:匹配官方 Pydantic 模型的 camelCase 结构
# 且不发送 seed (官方 GeminiNanoBanana2.execute 并没有发送 seed 到 API)
image_config = {
"imageSize": resolution,
"imageOutputOptions": {"mimeType": "image/png"}
@@ -157,7 +164,7 @@ class PDGeminiNanoBanana2:
}],
"generationConfig": generation_config
}
if system_prompt and system_prompt.strip():
payload["systemInstruction"] = {
"parts": [{"text": system_prompt.strip()}]
@@ -167,34 +174,31 @@ class PDGeminiNanoBanana2:
"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",
}
base_url = f"https://api.comfy.org/proxy/vertexai/gemini/{api_model}"
self._send_status(unique_id, "Sending request to Google...")
self._send_status(unique_id, "正在发送请求...")
pbar.update(30)
start_time = time.time()
try:
# 打印发送的 Payload 调试 (可选)
# print(f"[PD_NB2] Payload: {json.dumps(payload)}")
response = requests.post(base_url, headers=headers, json=payload, timeout=120)
response = self._post_with_retries(base_url, headers, payload, unique_id)
pbar.update(80)
if response.status_code != 200:
error_body = response.text
error_msg = f"API Error {response.status_code}: {error_body}"
self._send_status(unique_id, "Error occurred")
self._send_status(unique_id, "请求失败")
return (torch.zeros((1, 512, 512, 3)), error_msg, "N/A")
result = response.json()
image_tensors = []
text_parts = []
self._send_status(unique_id, "Processing response...")
self._send_status(unique_id, "正在处理响应...")
candidates = result.get("candidates", [])
for cand in candidates:
content = cand.get("content", {})
@@ -206,49 +210,67 @@ class PDGeminiNanoBanana2:
if data_b64:
raw_data = base64.b64decode(data_b64)
image_tensors.append(bytes_to_tensor(raw_data))
if "text" in part and part["text"]:
text_parts.append(part["text"])
if not image_tensors:
return (torch.zeros((1, 512, 512, 3)), "Error: No images found in response", "N/A")
return (torch.zeros((1, 512, 512, 3)), "错误: 响应中没有图片", "N/A")
final_image = torch.cat(image_tensors, dim=0)
final_text = "\n".join(text_parts) if text_parts else "No description"
# 计算成本
final_text = "\n".join(text_parts) if text_parts else "无文字描述"
cost_info = self._calculate_cost(result, api_model, time.time() - start_time)
pbar.update(100)
self._send_status(unique_id, "Success")
self._send_status(unique_id, "成功")
return (final_image, final_text, cost_info)
except Exception as e:
self._send_status(unique_id, "Exception caught")
self._send_status(unique_id, "发生异常")
return (torch.zeros((1, 512, 512, 3)), f"Error: {str(e)}", "N/A")
def _send_status(self, node_id, text):
"""发送状态文字到节点下方显示"""
try:
PromptServer.instance.send_sync("progress_text", {"text": text, "node": node_id})
except:
except Exception:
pass
def _post_with_retries(self, url, headers, payload, unique_id, retries=3, timeout=90):
last_error = None
for attempt in range(retries):
try:
if attempt > 0:
self._send_status(unique_id, f"网络波动,正在重试 {attempt + 1}/{retries}...")
return requests.post(
url,
headers={**headers, "Connection": "close"},
json=payload,
timeout=timeout,
)
except (
requests.exceptions.SSLError,
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
) as exc:
last_error = exc
if attempt == retries - 1:
raise
time.sleep(min(4, attempt + 1))
raise last_error
def _calculate_cost(self, result, model, duration):
"""基于 API 响应计算成本"""
usage = result.get("usageMetadata", {})
prompt_tokens = usage.get("promptTokenCount", 0)
candidates_tokens = usage.get("candidatesTokenCount", 0)
details = usage.get("candidatesTokensDetails", [])
thoughts_tokens = usage.get("thoughtsTokenCount", 0)
# 价格配置 (3.1 Flash Image Preview)
prices = {"input": 0.5, "output_text": 3.0, "output_image": 60.0}
text_tokens = thoughts_tokens
img_tokens = 0
for d in details:
modality = d.get("modality")
count = d.get("tokenCount", 0)
@@ -256,24 +278,26 @@ class PDGeminiNanoBanana2:
img_tokens += count
else:
text_tokens += count
if not details and candidates_tokens > 0:
text_tokens += candidates_tokens
input_cost = (prompt_tokens / 1_000_000) * prices["input"]
text_cost = (text_tokens / 1_000_000) * prices["output_text"]
img_cost = (img_tokens / 1_000_000) * prices["output_image"]
total_cost = input_cost + text_cost + img_cost
info = [
f"Model: {model}",
f"Status: Success",
f"Time: {duration:.2f}s",
f"Tokens: In:{prompt_tokens:,} | Out Text:{text_tokens:,} | Out Img:{img_tokens:,}",
f"Est. Cost: ${total_cost:.6f}"
f"模型: {model}",
f"状态: 成功",
f"耗时: {duration:.2f} 秒",
f"Tokens: 输入:{prompt_tokens:,} | 输出文本:{text_tokens:,} | 输出图像:{img_tokens:,}",
f"实际价格(USD): ${total_cost:.6f}",
f"实际价格(RMB): {format_rmb_value(total_cost)}",
f"实际价格(Credits): {format_credits_value(total_cost)}",
]
return "\n".join(info)
# 注册
NODE_CLASS_MAPPINGS = {"PDGeminiNanoBanana2": PDGeminiNanoBanana2}
NODE_DISPLAY_NAME_MAPPINGS = {"PDGeminiNanoBanana2": "PD: Nano Banana 2 (ComfyUI Key)"}
+62
View File
@@ -110,6 +110,68 @@ def build_openai_edit_files(image, mask=None):
return files
def append_gemini_input_files_to_openai_files(files, input_files):
if not input_files:
return files
image_index = sum(1 for field_name, _ in files if field_name in ("image", "image[]"))
for file_part in input_files:
if not isinstance(file_part, dict):
continue
inline_data = file_part.get("inlineData") or file_part.get("inline_data")
if not isinstance(inline_data, dict):
continue
mime_type = inline_data.get("mimeType") or inline_data.get("mime_type") or "image/png"
if not isinstance(mime_type, str) or not mime_type.startswith("image/"):
continue
data_b64 = inline_data.get("data")
if not data_b64:
continue
image_bytes = base64.b64decode(data_b64)
field_name = "image" if image_index == 0 and len(files) == 0 else "image[]"
extension = mime_type.split("/")[-1] or "png"
files.append((field_name, (f"image_{image_index}.{extension}", image_bytes, mime_type)))
image_index += 1
return files
def build_openai_edit_files_from_sources(image=None, mask=None, input_files=None):
files = []
if image is not None:
files.extend(build_openai_edit_files(image, mask))
elif mask is not None:
raise ValueError("Mask editing requires an input image")
append_gemini_input_files_to_openai_files(files, input_files)
image_count = sum(1 for field_name, _ in files if field_name in ("image", "image[]"))
if image_count == 0:
raise ValueError("No valid images were provided")
if image_count > 1:
normalized_files = []
for field_name, file_value in files:
if field_name in ("image", "image[]"):
normalized_files.append(("image[]", file_value))
else:
normalized_files.append((field_name, file_value))
files = normalized_files
if mask is not None and image is None:
raise ValueError("Mask editing does not support files-only input")
if mask is not None and image_count != 1:
raise ValueError("Mask editing only supports a single input image")
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):