Files
2026-03-20 00:25:25 +08:00

311 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
PD: Gemini Pro Image Generation Node (ComfyUI Auth Token)
Nano Banana Pro - 基于 Gemini 3 Pro Image
支持高分辨率图像生成(1K/2K/4K)
"""
import torch
import requests
import time
import io
import base64
import numpy as np
from PIL import Image
# 默认系统提示词(来自 ComfyUI 官方 API 节点)
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"
"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)
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
return torch.from_numpy(image_np).unsqueeze(0)
except Exception as e:
print(f"[GEMINI_PRO_AUTHTOKEN] Image decode error: {e}")
return torch.zeros((1, 512, 512, 3))
class PDGeminiProImageGenAuthToken:
"""
PD: Gemini Pro Image Generation (Auth Token)
Nano Banana Pro - 使用 ComfyUI Auth Token 调用 Gemini 3 Pro Image API
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"auth_token": ("STRING", {
"default": "",
"multiline": False,
"placeholder": "Paste your Auth Token here"
}),
"prompt": ("STRING", {
"multiline": True,
"default": "A futuristic city with flying cars",
"tooltip": "Text prompt describing the image to generate or the edits to apply"
}),
"model": (["gemini-3-pro-image-preview"], {
"default": "gemini-3-pro-image-preview"
}),
"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",
"tooltip": "If set to 'auto', matches your input image's aspect ratio"
}),
"resolution": (["1K", "2K", "4K"], {
"default": "1K",
"tooltip": "Target output resolution. For 2K/4K the native Gemini upscaler is used"
}),
"response_modalities": (["IMAGE+TEXT", "IMAGE"], {
"default": "IMAGE+TEXT",
"tooltip": "Choose 'IMAGE' for image-only output, or 'IMAGE+TEXT' for both"
}),
"seed": ("INT", {
"default": 42,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF
}),
},
"optional": {
"images": ("IMAGE", {
"tooltip": "Optional reference image(s) (up to 14)"
}),
"files": ("GEMINI_INPUT_FILES", ),
"system_prompt": ("STRING", {
"multiline": True,
"default": GEMINI_IMAGE_SYS_PROMPT,
"tooltip": "Foundational instructions that dictate an AI's behavior"
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "text")
FUNCTION = "generate_image"
CATEGORY = "PD_Tools/Image_Generation"
def generate_image(self, auth_token, prompt, model, aspect_ratio, resolution, response_modalities, seed, unique_id, images=None, files=None, system_prompt=""):
"""生成图片"""
print(f"[GEMINI_PRO_AUTHTOKEN] ========== 开始执行 ==========")
print(f"[GEMINI_PRO_AUTHTOKEN] Prompt: {prompt[:50]}...")
print(f"[GEMINI_PRO_AUTHTOKEN] Model: {model}")
# 验证 Token
if not auth_token or not auth_token.strip():
error_msg = "Error: Auth Token is required"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
# 清理 Token
auth_token = auth_token.strip()
if auth_token.lower().startswith("bearer "):
auth_token = auth_token[7:].strip()
# 构建 API URL
base_url = f"https://api.comfy.org/proxy/vertexai/gemini/{model}"
# 构建请求内容
parts = []
# 添加用户提示词
parts.append({"text": prompt})
# 添加参考图片(如果有,最多14张)
if images is not None:
num_images = min(images.shape[0], 14)
for i in range(num_images):
b64_str = tensor_to_base64(images[i])
parts.append({
"inlineData": {
"mimeType": "image/png",
"data": b64_str
}
})
print(f"[GEMINI_PRO_AUTHTOKEN] Added reference image {i+1}/{num_images}")
if images.shape[0] > 14:
print(f"[GEMINI_PRO_AUTHTOKEN] Warning: Only first 14 images will be used (provided {images.shape[0]})")
# Files 输入处理
if files:
for file_part in files:
if hasattr(file_part, "model_dump"):
parts.append(file_part.model_dump(exclude_none=True))
else:
parts.append(file_part)
print(f"[GEMINI_PRO_AUTHTOKEN] Added file part")
# 生成配置
generation_config = {
"responseModalities": ["IMAGE"] if response_modalities == "IMAGE" else ["TEXT", "IMAGE"]
}
# 图片配置
image_config = {
"imageSize": resolution
}
if aspect_ratio != "auto":
image_config["aspectRatio"] = aspect_ratio
generation_config["imageConfig"] = image_config
# 系统指令
system_instruction = None
if system_prompt and system_prompt.strip():
system_instruction = {
"parts": [{"text": system_prompt.strip()}]
}
# 完整请求体
payload = {
"contents": [{
"role": "user",
"parts": parts
}],
"generationConfig": generation_config
}
if system_instruction:
payload["systemInstruction"] = system_instruction
# 请求头
headers = {
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json",
"User-Agent": "ComfyUI-PD-Node/2.0"
}
start_time = time.time()
try:
print(f"[GEMINI_PRO_AUTHTOKEN] Sending request to {model}")
print(f"[GEMINI_PRO_AUTHTOKEN] Aspect Ratio: {aspect_ratio}, Resolution: {resolution}")
response = requests.post(base_url, headers=headers, json=payload, timeout=180)
# 检查响应
if response.status_code != 200:
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
result = response.json()
# 提取图片和文本
image_tensors = []
text_parts = []
candidates = result.get("candidates", [])
if not candidates:
error_msg = "Error: No candidates in response"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
for cand in candidates:
content = cand.get("content", {})
parts = content.get("parts", [])
for part in parts:
# 提取图片
inline_data = part.get("inline_data") or part.get("inlineData")
if inline_data:
mime = inline_data.get("mime_type") or inline_data.get("mimeType")
data_b64 = inline_data.get("data")
if mime in ["image/png", "image/jpeg"] and data_b64:
raw_data = base64.b64decode(data_b64)
image_tensors.append(bytes_to_tensor(raw_data))
print(f"[GEMINI_PRO_AUTHTOKEN] Extracted image ({mime})")
# 提取文本
if "text" in part and part["text"]:
text_parts.append(part["text"])
if not image_tensors:
error_msg = f"Error: No images found in response"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
print(f"[GEMINI_PRO_AUTHTOKEN] Response: {str(result)[:500]}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
final_image = torch.cat(image_tensors, dim=0)
final_text = "\n".join(text_parts) if text_parts else "No text output"
# 计算耗时和成本
end_time = time.time()
duration = end_time - start_time
# 估算成本(Gemini 3 Pro Image)
# $2/1M input tokens, $12/1M output text tokens, $120/1M output image tokens
cost_per_img = 0.12 # 估算
total_cost = cost_per_img * len(image_tensors)
info_str = (
f"Model: {model}\n"
f"Status: Success\n"
f"Time: {duration:.2f}s\n"
f"Images: {len(image_tensors)}\n"
f"Aspect Ratio: {aspect_ratio}\n"
f"Resolution: {resolution}\n"
f"Est. Cost: ${total_cost:.4f}\n"
f"---\n"
f"{final_text}"
)
print(f"[GEMINI_PRO_AUTHTOKEN] Success! Generated {len(image_tensors)} images in {duration:.2f}s")
return (final_image, info_str)
except requests.exceptions.Timeout:
error_msg = "Error: Request timeout (180s)"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
except requests.exceptions.ConnectionError as e:
error_msg = f"Error: Connection failed - {str(e)}"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
except Exception as e:
error_msg = f"Error: {str(e)}"
print(f"[GEMINI_PRO_AUTHTOKEN] {error_msg}")
import traceback
traceback.print_exc()
return (torch.zeros((1, 512, 512, 3)), error_msg)
# 节点注册
NODE_CLASS_MAPPINGS = {
"PDGeminiProImageGenAuthToken": PDGeminiProImageGenAuthToken
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PDGeminiProImageGenAuthToken": "PD: Gemini Pro Image (ComfyUI AuthToken)"
}