update GPT
update GPT
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"newFileLocation": "root",
|
||||
"newFileFolderPath": "",
|
||||
"attachmentFolderPath": "./image"
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
Vendored
+33
@@ -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
|
||||
}
|
||||
Vendored
+196
@@ -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 |
@@ -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)
|
||||

|
||||
|
||||
@@ -54,23 +75,11 @@ pip install -r comfyui-PD_comfy-api-node/requirements.txt
|
||||
|
||||

|
||||
|
||||
|
||||
**优点**:
|
||||
- ✅ 无需申请 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
@@ -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
@@ -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
@@ -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)"}
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user