Files
2026-02-24 15:37:34 +08:00

436 lines
21 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.
import os
import json
import google.generativeai as genai
import random
import re
import concurrent.futures
import threading
import time # 添加time模块用于超时处理
from collections import deque
class ComfyUITimeoutError(Exception):
pass
class ComfyUIAPIError(Exception):
pass
# 提示词模板常量 - 便于用户自定义修改
PROMPT_TEMPLATES = {
# 基础提示词模板(无图像)
"base_with_memory": "Generate me a prompt for image generator. The theme of the prompt is {theme}. You already created those prompts: {history}. Make sure you generate original prompt.",
"base_without_memory": "Generate me a prompt for image generator. The theme of the prompt is {theme}.",
# 图像提示词模板(有图像)
"image_with_memory": "Generate me a prompt for image generator based on the provided image(s). The theme of the prompt is {theme}. You already created those prompts: {history}. Make sure you generate original prompt that describes and expands on what you see in the image(s).",
"image_without_memory": "Generate me a prompt for image generator based on the provided image(s). The theme of the prompt is {theme}.",
# 自定义提示词模板前缀
"custom_with_images": "The theme is {theme}. I'm providing you with image(s) for reference. Please generate a response strictly following the instruction: {custom_prompt}.",
"custom_without_images": "The theme is {theme}. Please generate a response strictly following the instruction: {custom_prompt}.",
# 限制性后缀
"length_restriction": " You must keep the length of your generated prompt around {length} words.",
"no_length_restriction": "",
"general_restriction": " **Restrictions: Do not explain your prompt; just output the prompt directly. Do not output any other non-prompt text; only output the prompt itself, and do not include anything like 'Here is the prompt' or similar words.**",
"english_only_restriction": " Your answer must be in English only."
}
# 使用deque替代list,并设置最大长度为15
prompt_history = deque(maxlen=15) # 将memory重命名为prompt_history
last_memory_mode = None # 添加一个变量来跟踪上一次的内存模式
# 新增函数: 从 config.json 文件中读取 API 密钥
def get_gemini_api_key():
try:
config_path = os.path.join(os.path.dirname(__file__), 'config.json') # 确保路径是相对于当前文件的
with open(config_path, 'r') as f:
config = json.load(f)
api_key = config.get("GEMINI_API_KEY", "").strip() # 安全获取并去除多余空格
if not api_key:
raise ValueError("API key not found in config.json.")
return api_key
except Exception as e:
print(f"Error: Unable to read API key. {str(e)}")
return ""
# 将图像转换为Pillow图像的辅助函数
def images_to_pillow(image):
from PIL import Image
import numpy as np
if image is None:
return []
# 转换张量为PIL图像
result = []
if len(image.shape) == 4: # 批量图像
for i in range(image.shape[0]):
img = image[i].numpy()
img = (img * 255).astype(np.uint8)
img = Image.fromarray(img)
result.append(img)
else: # 单个图像
img = image.numpy()
img = (img * 255).astype(np.uint8)
img = Image.fromarray(img)
result.append(img)
return result
class GeminiPromptGeneratorJT:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"theme": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "输入想要生成的提示词主题"
}),
"override_system_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "自定义提示词。您可以使用以下变量:\n{theme} - 当前主题\n{prompt_history}或{memory} - 历史提示记录(当Memory设置为Disable时将替换为空列表[])\n{prompt_length} - 提示长度设置\n{seed} - 当前种子值\n例如:'为{theme}生成一个图像提示词,历史记录:{prompt_history}'"
}),
"model": (["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-flash-preview"], {
"tooltip": "模型名称 + 免费层速率限制(官方配额会动态调整,以 AI Studio 为准):\n• gemini-2.5-flash-lite:10 RPM / 250K TPM / 20 RPD\n• gemini-2.5-flash:5 RPM / 250K TPM / 20 RPD\n• gemini-2.5-pro:2 RPM / 125K TPM / 50 RPD\n• gemini-2.0-flash-lite:30 RPM / 1M TPM / 200 RPD\n• gemini-2.0-flash:15 RPM / 1M TPM / 200 RPD\n• gemini-3-flash-preview:5 RPM / 250K TPM / 20 RPD\n参数:RPM=每分钟请求数,TPM=每分钟Token数,RPD=每日请求数"
}),
"enable_memory": ("BOOLEAN", {
"default": False, # 修改默认值为False
"tooltip": "启用或禁用历史记忆功能。启用时会记住之前生成的提示,避免重复;禁用时每次生成独立提示"
}),
"english_only": ("BOOLEAN", {
"default": False,
"tooltip": "启用后将强制生成的提示词只使用英文。会在发送给Gemini的指令中添加英文限制要求"
}),
"prompt_length": ("INT", {
"default": 120, # 修改默认值为120
"min": 0,
"max": 5000,
"tooltip": "控制生成提示词的长度(单词数)。设为0表示不限制长度"
}),
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff,
"tooltip": "随机种子值,相同的种子会产生相似的结果"
}),
"timeout": ("INT", {
"default": 15, # 修改默认值为15
"min": 0,
"max": 6000,
"tooltip": "API请求超时时间(秒)。如果在指定时间内未收到响应,将中断请求"
})
},
"optional": {
"image_1": ("IMAGE", ),
"image_2": ("IMAGE", ),
"image_3": ("IMAGE", )
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate_prompt"
CATEGORY = "text/generation"
def generate_prompt(self, theme, override_system_prompt, model, enable_memory, english_only, prompt_length, seed, timeout, image_1=None, image_2=None, image_3=None):
# 将布尔值转换为原来的字符串格式以保持兼容性
memory = "Enable" if enable_memory else "Disable"
# Use the seed to initialize the random number generator
random.seed(seed)
# 跟踪内存模式的变化并清除历史记录
global last_memory_mode
if last_memory_mode is None:
# 第一次运行
if memory == "Disable":
# 如果第一次运行时就是Disable模式,确保历史记录为空
prompt_history.clear()
print("First run with Memory Disable mode. History cleared.")
elif last_memory_mode != memory:
# 当模式发生变化时
if memory == "Enable":
# 当从Disable切换到Enable模式时,清空历史记录
prompt_history.clear()
print("Memory mode changed from Disable to Enable. History cleared.")
else:
# 当从Enable切换到Disable模式时,打印通知但不清空历史记录(因为disable模式不使用历史记录)
print("Memory mode changed from Enable to Disable. New prompts will not be recorded.")
# 更新当前的内存模式
last_memory_mode = memory
# 通过 config.json 获取 API 密钥
api_key = get_gemini_api_key()
if not api_key:
raise ComfyUIAPIError("API key is required. Please check config.json.")
# 使用线程级别的取消标志
cancel_event = threading.Event()
result = [None] # 使用列表来存储结果
error = [None] # 使用列表来存储错误
def generate_content_worker():
try:
# 配置API并创建模型
genai.configure(api_key=api_key)
# 设置安全配置以避免被审核过滤
safety_settings = {
"harassment": "block_none",
"hate_speech": "block_none",
"sexually_explicit": "block_none",
"dangerous_content": "block_none"
}
# 创建生成模型实例并设置生成参数
generation_config = {
"temperature": 0.9,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 2048,
"response_mime_type": "text/plain"
}
try:
# 尝试使用新版API格式
gemini_model = genai.GenerativeModel(
model_name=model,
generation_config=generation_config,
safety_settings=safety_settings
)
except Exception as e:
# print(f"Warning: Failed to initialize with new API format: {str(e)}")
# print("Falling back to legacy API format")
# 降级使用传统API格式 - 一些较旧版本的库使用不同的安全设置格式
safety_settings_legacy = [
{
"category": "HARM_CATEGORY_HARASSMENT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_HATE_SPEECH",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE",
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE",
},
]
gemini_model = genai.GenerativeModel(
model_name=model,
generation_config=generation_config,
safety_settings=safety_settings_legacy
)
# 处理图像输入
images_to_send = []
for img in [image_1, image_2, image_3]:
if img is not None:
images_to_send.extend(images_to_pillow(img))
# 是否包含图像
has_images = len(images_to_send) > 0
# 根据 prompt_length 动态调整 input_prompt - 使用提示词模板
if not override_system_prompt:
# 选择基础模板
if memory == "Enable":
# 处理历史记录:如果为空则使用空字符串,否则使用历史记录列表的字符串表示
history_str = "" if len(prompt_history) == 0 else str(list(prompt_history))
if has_images:
input_prompt = PROMPT_TEMPLATES["image_with_memory"].format(
theme=theme,
history=history_str
)
else:
input_prompt = PROMPT_TEMPLATES["base_with_memory"].format(
theme=theme,
history=history_str
)
else: # memory == "Disable"
if has_images:
input_prompt = PROMPT_TEMPLATES["image_without_memory"].format(theme=theme)
else:
input_prompt = PROMPT_TEMPLATES["base_without_memory"].format(theme=theme)
# 添加长度限制
if prompt_length > 0:
input_prompt += PROMPT_TEMPLATES["length_restriction"].format(length=prompt_length)
else:
input_prompt += PROMPT_TEMPLATES["no_length_restriction"]
# 添加通用限制
input_prompt += PROMPT_TEMPLATES["general_restriction"]
# 添加英文限制(如果启用)
if english_only:
input_prompt += PROMPT_TEMPLATES["english_only_restriction"]
else:
# 处理自定义提示词中的变量替换
custom_prompt = override_system_prompt
# 支持多种变量格式的替换
replacements = {
"{theme}": theme,
"{prompt_length}": str(prompt_length),
"{seed}": str(seed)
}
# 根据memory状态决定历史记录的值
if memory == "Enable":
# 启用历史记录时,如果历史记录为空则替换为空字符串,否则正常替换
if len(prompt_history) == 0:
# 历史记录为空时,使用空字符串而不是"[]"
replacements.update({
"{prompt_history}": "",
"{list(prompt_history)}": "",
"{memory}": ""
})
else:
# 历史记录不为空时,正常替换历史记录变量
replacements.update({
"{prompt_history}": str(list(prompt_history)),
"{list(prompt_history)}": str(list(prompt_history)),
"{memory}": str(list(prompt_history))
})
else:
# 禁用历史记录时,历史记录变量替换为空字符串
replacements.update({
"{prompt_history}": "",
"{list(prompt_history)}": "",
"{memory}": ""
})
# 替换所有支持的变量
for placeholder, value in replacements.items():
custom_prompt = custom_prompt.replace(placeholder, value)
# 打印调试信息
print(f"Memory mode: {memory}")
if memory == "Disable":
print("历史记录变量被替换为空字符串")
elif memory == "Enable" and len(prompt_history) == 0:
print("Memory已启用但历史记录为空,历史记录变量被替换为空字符串")
# 使用自定义提示词模板
if has_images:
input_prompt = PROMPT_TEMPLATES["custom_with_images"].format(
theme=theme,
custom_prompt=custom_prompt
)
else:
input_prompt = PROMPT_TEMPLATES["custom_without_images"].format(
theme=theme,
custom_prompt=custom_prompt
)
# 添加长度限制
if prompt_length > 0:
input_prompt += PROMPT_TEMPLATES["length_restriction"].format(length=prompt_length)
else:
input_prompt += PROMPT_TEMPLATES["no_length_restriction"]
# 添加通用限制
input_prompt += PROMPT_TEMPLATES["general_restriction"]
# 添加英文限制(如果启用)
if english_only:
input_prompt += PROMPT_TEMPLATES["english_only_restriction"]
# 检查是否取消
if cancel_event.is_set():
return
# 发送请求到API
try:
if has_images:
# 发送文本和图像
content_list = [input_prompt] + images_to_send
response = gemini_model.generate_content(content_list)
else:
# 只发送文本
response = gemini_model.generate_content(input_prompt)
except Exception as api_error:
error_msg = str(api_error)
if "400 Bad Request" in error_msg:
raise ComfyUIAPIError(f"API rejected the request due to invalid input. Details: {error_msg}")
elif "403 Forbidden" in error_msg:
raise ComfyUIAPIError(f"API access denied. Please check your API key and permissions. Details: {error_msg}")
elif "429 Too Many Requests" in error_msg:
raise ComfyUIAPIError(f"API rate limit exceeded. Please wait before trying again. Details: {error_msg}")
elif "500 Internal Server Error" in error_msg:
raise ComfyUIAPIError(f"Google API server error. Please try again later. Details: {error_msg}")
elif "Connection" in error_msg:
raise ComfyUIAPIError(f"Connection error. Please check your internet connection. Details: {error_msg}")
else:
raise ComfyUIAPIError(f"Failed to generate content. Details: {error_msg}")
# 检查是否取消
if cancel_event.is_set():
return
# 处理响应
if response and hasattr(response, 'text'):
generated_prompt = response.text.strip()
print("----INPUT----")
print(input_prompt)
if has_images:
print(f"(包含 {len(images_to_send)} 张图片)")
print("----OUTPUT----")
print(generated_prompt)
# 使用deque的append方法,当达到最大长度时会自动移除最早的元素
if memory == "Enable": # 仅在启用memory时添加到历史记录
prompt_history.append(generated_prompt)
print("-------------")
result[0] = generated_prompt
else:
error[0] = ComfyUIAPIError("Received empty response from Gemini API.")
except Exception as e:
error[0] = ComfyUIAPIError(f"Failed to generate prompt. Details: {str(e)}")
# 创建线程执行API调用
worker_thread = threading.Thread(target=generate_content_worker)
worker_thread.daemon = True
worker_thread.start()
# 等待线程完成或超时
start_time = time.time()
while worker_thread.is_alive():
if time.time() - start_time > timeout:
cancel_event.set() # 通知线程取消操作
error_message = f"Prompt generation timed out after {timeout} seconds. Please check your network connection or increase the timeout value."
raise ComfyUITimeoutError(error_message)
time.sleep(0.1) # 短暂休眠以避免CPU占用
# 检查是否有错误
if error[0]:
raise error[0]
# 检查是否成功获得结果
if result[0] is None:
raise ComfyUIAPIError("Failed to generate prompt. No result was returned.")
return (result[0],)
# Node class mappings
NODE_CLASS_MAPPINGS = {
"GeminiPromptGeneratorJT": GeminiPromptGeneratorJT
}
# Node display name mappings
NODE_DISPLAY_NAME_MAPPINGS = {
"GeminiPromptGeneratorJT": "Gemini Prompt Generator-JT"
}