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

292 lines
10 KiB
Python

"""
PD: Gemini Image Generation Node (ComfyUI Auth Token)
基于 gemini_image_api 的 Auth Token 认证方式
支持文生图和图生图
"""
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_AUTHTOKEN] Image decode error: {e}")
return torch.zeros((1, 512, 512, 3))
class PDGeminiImageGenAuthToken:
"""
PD: Gemini Image Generation (Auth Token)
使用 ComfyUI Auth Token 调用 Gemini 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": "Describe what you want to generate"
}),
"model": ([
"gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview"
], {
"default": "gemini-2.5-flash-image"
}),
"aspect_ratio": ([
"auto", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21"
], {
"default": "auto"
}),
"resolution": (["auto", "1K", "2K", "4K"], {
"default": "1K"
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF
}),
},
"optional": {
"images": ("IMAGE", {
"tooltip": "Reference image(s) for image-to-image generation"
}),
"files": ("GEMINI_INPUT_FILES", ),
"system_prompt": ("STRING", {
"multiline": True,
"default": GEMINI_IMAGE_SYS_PROMPT,
"tooltip": "Optional system instructions"
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "info")
FUNCTION = "generate_image"
CATEGORY = "PD_Tools/Image_Generation"
def generate_image(self, auth_token, prompt, model, aspect_ratio, resolution, seed, unique_id, images=None, files=None, system_prompt=""):
"""生成图片"""
print(f"[GEMINI_AUTHTOKEN] ========== 开始执行 ==========")
print(f"[GEMINI_AUTHTOKEN] Prompt: {prompt[:50]}...")
print(f"[GEMINI_AUTHTOKEN] Model: {model}")
# 验证 Token
if not auth_token or not auth_token.strip():
error_msg = "Error: Auth Token is required"
print(f"[GEMINI_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 (不需要添加 :generateContent 后缀,API 会自动处理)
base_url = f"https://api.comfy.org/proxy/vertexai/gemini/{model}"
# 构建请求内容
parts = []
# 添加系统提示词(如果有)
if system_prompt and system_prompt.strip():
parts.append({"text": system_prompt.strip()})
# 添加用户提示词
parts.append({"text": prompt})
# 添加参考图片(如果有)
if images is not None:
for i in range(images.shape[0]):
b64_str = tensor_to_base64(images[i])
parts.append({
"inlineData": {
"mimeType": "image/png",
"data": b64_str
}
})
print(f"[GEMINI_AUTHTOKEN] Added reference image {i+1}")
# 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_AUTHTOKEN] Added file part")
# 生成配置
generation_config = {
"responseModalities": ["IMAGE"]
}
# 图片配置
image_config = {}
if aspect_ratio != "auto":
image_config["aspectRatio"] = aspect_ratio
if resolution != "auto":
image_config["resolution"] = resolution
if image_config:
generation_config["imageConfig"] = image_config
# 完整请求体
payload = {
"contents": [{
"role": "user",
"parts": parts
}],
"generationConfig": generation_config
}
# 请求头
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_AUTHTOKEN] Sending request to {model}")
print(f"[GEMINI_AUTHTOKEN] Aspect Ratio: {aspect_ratio}, Resolution: {resolution}")
response = requests.post(base_url, headers=headers, json=payload, timeout=120)
# 检查响应
if response.status_code != 200:
error_msg = f"API Error {response.status_code}: {response.text[:200]}"
print(f"[GEMINI_AUTHTOKEN] {error_msg}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
result = response.json()
# 提取图片
image_tensors = []
candidates = result.get("candidates", [])
if not candidates:
error_msg = "Error: No candidates in response"
print(f"[GEMINI_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_AUTHTOKEN] Extracted image ({mime})")
if not image_tensors:
error_msg = f"Error: No images found in response"
print(f"[GEMINI_AUTHTOKEN] {error_msg}")
print(f"[GEMINI_AUTHTOKEN] Response: {str(result)[:500]}")
return (torch.zeros((1, 512, 512, 3)), error_msg)
final_image = torch.cat(image_tensors, dim=0)
# 计算耗时和成本
end_time = time.time()
duration = end_time - start_time
# 估算成本(根据模型)
cost_per_img = 0.04
if "pro" in model.lower():
cost_per_img = 0.08
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}"
)
print(f"[GEMINI_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 (120s)"
print(f"[GEMINI_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_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_AUTHTOKEN] {error_msg}")
import traceback
traceback.print_exc()
return (torch.zeros((1, 512, 512, 3)), error_msg)
# 节点注册
NODE_CLASS_MAPPINGS = {
"PDGeminiImageGenAuthToken": PDGeminiImageGenAuthToken
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PDGeminiImageGenAuthToken": "PD: Gemini Image (ComfyUI AuthToken)"
}