Files
yawiii-ComfyUI-Prompt-Assis…/node/image_caption_node.py
T
yawiii 35d3b4a6a4 新增扩写节点
新增302ai服务
标签面板新增记忆功能
右键菜单支持快速切换服务
针对某些主流模型支持关闭思维链
优化反推和翻译节点
新增交流反馈入口徽标
修复下拉菜单bug
修复标签面板搜索标签无法插入bug
修复base_url裁剪错误,解决偶发性请求报错
2025-09-16 09:50:09 +08:00

321 lines
13 KiB
Python

import asyncio
import random
import time
import threading
import hashlib
import base64
from io import BytesIO
import os
import json
import torch
import numpy as np
from PIL import Image
from comfy.model_management import InterruptProcessingException
from ..services.vlm import VisionService
from ..services.error_util import format_api_error
# 定义ANSI颜色代码常量
GREEN = "\033[92m"
RESET = "\033[0m"
class ImageCaptionNode:
"""
图像反推提示词节点
分析输入图像并生成描述性提示词
"""
# 定义日志前缀(带绿色)
LOG_PREFIX = f"{GREEN}[PromptAssistant]{RESET}"
@classmethod
def INPUT_TYPES(cls):
# 从config_manager获取系统提示词配置
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
# 获取所有vision_prompts作为选项
vision_prompts = {}
if system_prompts and 'vision_prompts' in system_prompts:
vision_prompts = system_prompts['vision_prompts']
# 构建提示词模板选项
prompt_template_options = []
for key, value in vision_prompts.items():
name = value.get('name', key)
prompt_template_options.append(name)
# 如果没有选项,添加一个默认选项
if not prompt_template_options:
prompt_template_options = ["默认中文反推提示词"]
return {
"required": {
"图像": ("IMAGE",),
"规则模板": (prompt_template_options, {"default": prompt_template_options[0] if prompt_template_options else "默认中文反推提示词"}),
"临时规则": ("BOOLEAN", {"default": False, "label_on": "启用", "label_off": "禁用"}),
"临时规则内容": ("STRING", {"multiline": True, "default": "", "placeholder": "请输入临时规则内容,仅在启用“临时规则”时生效"}),
"视觉服务": (["智谱", "硅基流动", "自定义"], {"default": "智谱"}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("提示词输出",)
FUNCTION = "analyze_image"
CATEGORY = "✨提示词小助手"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, 图像, 规则模板, 临时规则, 临时规则内容, 视觉服务):
"""
只在输入内容真正变化时才触发重新执行
使用输入参数的哈希值作为判断依据
"""
# 计算图像的哈希值(只使用第一帧的部分数据,避免计算量过大)
img_hash = ""
if 图像 is not None:
try:
if len(图像.shape) == 4:
# 取第一帧的中心区域作为哈希计算依据
h, w = 图像.shape[1:3]
center_h, center_w = h // 2, w // 2
size = min(100, h // 4, w // 4) # 限制计算区域大小
img_data = 图像[0,
max(0, center_h - size):min(h, center_h + size),
max(0, center_w - size):min(w, center_w + size),
0].cpu().numpy().tobytes()
img_hash = hashlib.md5(img_data).hexdigest()
else:
# 如果不是4D张量,使用整个张量的哈希
img_data = 图像.cpu().numpy().tobytes()
img_hash = hashlib.md5(img_data).hexdigest()
except Exception:
img_hash = "0"
# 组合所有输入的哈希值
input_hash = hash((
img_hash,
规则模板,
bool(临时规则),
临时规则内容,
视觉服务
))
return input_hash
def analyze_image(self, 图像, 规则模板, 临时规则, 临时规则内容, 视觉服务):
"""
分析图像并生成提示词
Args:
图像: 输入的图像数据
规则模板: 选择的提示词模板
临时规则: 是否启用临时规则
临时规则内容: 临时规则的内容
视觉服务: 选择的视觉服务
Returns:
tuple: 分析结果
"""
try:
# 检查输入
if 图像 is None:
raise ValueError("输入图像不能为空")
# 将图像转换为base64编码
image_data = self._image_to_base64(图像)
# 获取提示词模板内容
prompt_template = None
if 临时规则 and 临时规则内容:
prompt_template = 临时规则内容
print(f"{self.LOG_PREFIX} 图像反推: 使用临时规则")
else:
# 从config_manager获取系统提示词配置
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
# 获取vision_prompts
vision_prompts = {}
if system_prompts and 'vision_prompts' in system_prompts:
vision_prompts = system_prompts['vision_prompts']
# 查找选定的提示词模板
template_found = False
for key, value in vision_prompts.items():
if value.get('name') == 规则模板:
prompt_template = value.get('content')
template_found = True
print(f"{self.LOG_PREFIX} 图像反推: 使用模板 '{规则模板}'")
break
if not template_found:
print(f"{self.LOG_PREFIX} 图像反推: 未找到模板 '{规则模板}',尝试直接匹配键名")
# 尝试直接匹配键名
for key, value in vision_prompts.items():
if key == 规则模板:
prompt_template = value.get('content')
template_found = True
print(f"{self.LOG_PREFIX} 图像反推: 使用模板 '{规则模板}'")
break
# 如果没有找到提示词模板,使用默认值
if not prompt_template:
prompt_template = "请详细描述这张图片的内容,包括主体、场景、风格、色彩等要素。"
print(f"{self.LOG_PREFIX} 图像反推: 未找到模板 '{规则模板}',使用默认提示词")
# 执行图像分析
print(f"{self.LOG_PREFIX} 开始图像反推分析: {视觉服务}")
# 映射视觉服务选项到provider
provider_map = {
"智谱": "zhipu",
"硅基流动": "siliconflow",
"自定义": "custom"
}
# 获取选定的provider
selected_provider = provider_map.get(视觉服务)
if not selected_provider:
raise ValueError(f"不支持的视觉服务: {视觉服务}")
# 获取对应provider的配置
from ..config_manager import config_manager
provider_config = self._get_provider_config(config_manager, selected_provider)
if not provider_config:
raise ValueError(f"未找到{视觉服务}的配置,请先完成API配置")
print(f"{self.LOG_PREFIX} 图像反推: 使用{视觉服务}服务, API: {provider_config.get('model')}")
# 执行图像分析
result = self._analyze_with_vision_service(image_data, prompt_template, selected_provider, provider_config)
if result and result.get('success'):
description = result.get('data', {}).get('description', '').strip()
if not description:
error_msg = 'API返回结果为空,请检查API密钥、模型配置或网络连接'
print(f"{self.LOG_PREFIX} 图像反推分析失败: {error_msg}")
raise RuntimeError(f"分析失败: {error_msg}")
print(f"{self.LOG_PREFIX} 图像反推分析完成,结果长度: {len(description)}")
return (description,)
else:
error_msg = result.get('error', '分析失败,未知错误') if result else '分析服务未返回结果'
print(f"{self.LOG_PREFIX} 图像反推分析失败: {error_msg}")
raise RuntimeError(f"分析失败: {error_msg}")
except InterruptProcessingException:
print(f"{self.LOG_PREFIX} 图像反推任务被用户取消")
raise
except Exception as e:
error_msg = format_api_error(e, 视觉服务)
print(f"{self.LOG_PREFIX} 图像反推节点异常: {error_msg}")
raise RuntimeError(f"分析异常: {error_msg}")
def _analyze_with_vision_service(self, image_data, prompt_template, provider, provider_config):
"""使用视觉服务分析图像"""
try:
# 创建请求ID
request_id = f"image_caption_{int(time.time())}_{random.randint(1000, 9999)}"
result_container = {}
# 在独立线程中运行图像分析
thread = threading.Thread(
target=self._run_async_vision_analysis,
args=(image_data, prompt_template, request_id, result_container, provider, provider_config)
)
thread.start()
# 等待分析完成,同时检查中断
while thread.is_alive():
# 检查是否被中断 - 这会抛出 InterruptProcessingException
try:
import nodes
nodes.before_node_execution()
except:
# 如果检查中断时出现异常,说明被中断了
print(f"{self.LOG_PREFIX} 检测到中断信号,正在终止分析任务...")
# 设置结果容器为中断状态,让线程知道要停止
result_container['interrupted'] = True
# 等待线程结束或超时
thread.join(timeout=1.0)
if thread.is_alive():
print(f"{self.LOG_PREFIX} 分析线程未能及时响应中断")
raise InterruptProcessingException()
time.sleep(0.1)
return result_container.get('result')
except Exception as e:
return {"success": False, "error": str(e)}
def _run_async_vision_analysis(self, image_data, prompt_template, request_id, result_container, provider, provider_config):
"""在独立线程中运行异步图像分析任务"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 检查是否在开始前就被中断了
if result_container.get('interrupted'):
result_container['result'] = {"success": False, "error": "任务被中断"}
return
result = loop.run_until_complete(
VisionService.analyze_image(image_data, request_id, None, prompt_template, provider, provider_config)
)
# 检查是否在执行过程中被中断了
if result_container.get('interrupted'):
result_container['result'] = {"success": False, "error": "任务被中断"}
return
result_container['result'] = result
except Exception as e:
if result_container.get('interrupted'):
result_container['result'] = {"success": False, "error": "任务被中断"}
else:
result_container['result'] = {"success": False, "error": str(e)}
finally:
loop.close()
def _get_provider_config(self, config_manager, provider):
"""获取指定provider的配置"""
vision_config = config_manager.get_vision_config()
if 'providers' in vision_config and provider in vision_config['providers']:
return vision_config['providers'][provider]
return None
def _image_to_base64(self, image_tensor):
"""将图像张量转换为base64编码"""
# 确保图像是正确的形状 [batch, height, width, channels]
if len(image_tensor.shape) == 4:
# 取第一张图片
image_tensor = image_tensor[0]
# 将图像转换为numpy数组并缩放到0-255范围
image_np = (image_tensor.cpu().numpy() * 255).astype(np.uint8)
# 创建PIL图像
image = Image.fromarray(image_np)
# 将图像转换为JPEG格式的字节流
buffer = BytesIO()
image.save(buffer, format="JPEG", quality=95)
# 将字节流转换为base64编码
encoded_image = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 返回带有MIME类型的data URL
return f"data:image/jpeg;base64,{encoded_image}"
# 节点映射,用于向ComfyUI注册节点
NODE_CLASS_MAPPINGS = {
"ImageCaptionNode": ImageCaptionNode,
}
# 节点显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageCaptionNode": "✨图像反推提示词",
}