Files
yawiii-ComfyUI-Prompt-Assis…/node/image_caption_node.py
T

335 lines
14 KiB
Python
Raw 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 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 ..utils.common import format_api_error, format_model_with_thinking, generate_request_id, log_prepare, log_error, TASK_IMAGE_CAPTION, SOURCE_NODE
from ..services.thinking_control import build_thinking_suppression
from .base import VLMNodeBase
class ImageCaptionNode(VLMNodeBase):
"""
图像反推提示词节点
分析输入图像并生成描述性提示词
"""
@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():
# 过滤掉不在后端显示的规则
show_in = value.get('showIn', ["frontend", "node"])
if 'node' not in show_in:
continue
name = value.get('name', key)
category = value.get('category', '')
# 如果有分类,显示为 "类别/规则名称",否则直接显示规则名称
display_name = f"{category}/{name}" if category else name
prompt_template_options.append(display_name)
# 如果没有选项,添加一个默认选项
if not prompt_template_options:
prompt_template_options = ["默认中文反推提示词"]
# ---动态获取VLM服务/模型列表---
service_options = cls.get_vlm_service_options()
default_service = service_options[0] if service_options else "智谱"
return {
"required": {
"image": ("IMAGE",),
"rule": (prompt_template_options, {"default": prompt_template_options[0] if prompt_template_options else "默认中文反推提示词", "tooltip": "Choose a preset rule for image captioning"}),
"custom_rule": ("BOOLEAN", {"default": False, "label_on": "Enable", "label_off": "Disable", "tooltip": "Enable to use custom rule content below"}),
"custom_rule_content": ("STRING", {"multiline": True, "default": "", "placeholder": "在此输入临时规则,仅在启用'临时规则'时生效", "tooltip": "在此输入您的自定义规则内容"}),
"user_prompt": ("STRING", {"multiline": True, "default": "", "placeholder": "输入额外的具体要求,将与规则一起发送给模型", "tooltip": "输入额外的具体要求,将与规则一起发送给模型"}),
"vlm_service": (service_options, {"default": default_service, "tooltip": "Select VLM service and model"}),
# Ollama Automatic VRAM Unload
"ollama_auto_unload": ("BOOLEAN", {"default": True, "label_on": "Enable", "label_off": "Disable", "tooltip": "Auto unload Ollama model after generation"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("prompt_output", "prompt_list")
OUTPUT_IS_LIST = (False, True)
FUNCTION = "analyze_image"
CATEGORY = "✨Prompt Assistant"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, image=None, rule=None, custom_rule=None, custom_rule_content=None, user_prompt=None, vlm_service=None, ollama_auto_unload=None, seed=None, unique_id=None):
"""
只在输入内容真正变化时才触发重新执行
使用输入参数的哈希值作为判断依据
"""
# 导入图像哈希工具函数
from ..utils.image import compute_image_hash
# 计算图像的哈希值
img_hash = compute_image_hash(image)
# 组合所有输入的哈希值
input_hash = hash((
img_hash,
rule,
bool(custom_rule),
custom_rule_content,
user_prompt,
vlm_service,
bool(ollama_auto_unload),
seed
))
return input_hash
def _analyze_single_image(self, image_data, prompt_template, rule_name, service_id, service, provider_config, unique_id, frame_index=None):
"""
分析单张图像(内部方法)
Args:
image_data: base64编码的图像数据
prompt_template: 提示词模板
rule_name: 规则名称
service_id: 服务ID
service: 服务配置
provider_config: Provider配置
unique_id: 节点唯一ID
frame_index: 批次中的帧索引(用于日志标识)
Returns:
str: 图像描述结果
"""
# 创建请求ID(包含帧索引信息)
frame_suffix = f"_f{frame_index}" if frame_index is not None else ""
request_id = generate_request_id("icap", None, unique_id) + frame_suffix
# 检查是否关闭思维链
model_full_name = provider_config.get('model')
disable_thinking_enabled = service.get('disable_thinking', True)
thinking_extra = build_thinking_suppression(service_id, model_full_name) if disable_thinking_enabled else None
model_display = format_model_with_thinking(model_full_name, bool(thinking_extra))
# 获取服务显示名称
service_display_name = service.get('name', service_id)
# 准备阶段日志(包含帧信息)
frame_info = f" [帧 {frame_index + 1}]" if frame_index is not None else ""
log_prepare(TASK_IMAGE_CAPTION, request_id, SOURCE_NODE, service_display_name, model_display, rule_name + frame_info)
# 执行图像分析
result = self._run_vision_task(
VisionService.analyze_image,
service_id,
image_data=image_data,
request_id=request_id,
stream_callback=None,
prompt_content=prompt_template,
custom_provider=service_id,
custom_provider_config=provider_config,
task_type=TASK_IMAGE_CAPTION,
source=SOURCE_NODE
)
if result and result.get('success'):
description = result.get('data', {}).get('description', '').strip()
if not description:
error_msg = 'API返回结果为空,请检查API密钥、模型配置或网络连接'
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"分析失败: {error_msg}")
return description
else:
error_msg = result.get('error', '分析失败,未知错误') if result else '分析服务未返回结果'
if error_msg == "任务被中断":
raise InterruptProcessingException()
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"分析失败: {error_msg}")
def analyze_image(self, image, rule, custom_rule, custom_rule_content, user_prompt, vlm_service, ollama_auto_unload, seed=None, unique_id=None):
"""
分析图像并生成提示词(支持 batch 遍历)
Args:
image: 输入的图像数据,支持单张或 batch
rule: 选择的提示词模板
custom_rule: 是否启用临时规则
custom_rule_content: 临时规则的内容
user_prompt: 用户补充的提示词
vlm_service: 选择的视觉服务
ollama_auto_unload: Ollama 自动卸载开关
unique_id: 节点唯一ID
Returns:
tuple: 分析结果,batch 输入时结果用换行符分隔
"""
request_id = None # 初始化,用于异常处理
try:
# 检查输入
if image is None:
raise ValueError("输入图像不能为空")
# ---准备提示词模板---
prompt_template = None
rule_name = "Custom Rule" if (custom_rule and custom_rule_content) else rule
if custom_rule and custom_rule_content:
prompt_template = custom_rule_content
else:
from ..config_manager import config_manager
system_prompts = config_manager.get_system_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():
name = value.get('name', key)
category = value.get('category', '')
display_name = f"{category}/{name}" if category else name
if display_name == rule:
prompt_template = value.get('content')
template_found = True
break
if not template_found:
# 兼容旧格式
for key, value in vision_prompts.items():
if value.get('name') == rule or key == rule:
prompt_template = value.get('content')
template_found = True
break
if not template_found or not prompt_template:
prompt_template = "请详细描述这张图片的内容,包括主体、场景、风格、色彩等要素。"
rule_name = "Default Rule"
# 拼接用户提示词
if user_prompt and user_prompt.strip():
prompt_template = f"{prompt_template}\n\n用户补充要求:\n{user_prompt}"
# ---解析服务配置---
service_id, model_name = self.parse_service_model(vlm_service)
if not service_id:
raise ValueError(f"Invalid service selection: {vlm_service}")
from ..config_manager import config_manager
service = config_manager.get_service(service_id)
if not service:
raise ValueError(f"Service config not found: {vlm_service}")
# 构建 provider_config
vlm_models = service.get('vlm_models', [])
target_model = None
if model_name:
target_model = next((m for m in vlm_models if m.get('name') == model_name), None)
if not target_model:
target_model = next((m for m in vlm_models if m.get('is_default')),
vlm_models[0] if vlm_models else None)
if not target_model:
raise ValueError(f"Service {vlm_service} has no available models")
provider_config = {
'provider': service_id,
'model': target_model.get('name', ''),
'base_url': service.get('base_url', ''),
'api_key': service.get('api_key', ''),
'temperature': target_model.get('temperature', 0.7),
'max_tokens': target_model.get('max_tokens', 500),
'top_p': target_model.get('top_p', 0.9),
}
if service.get('type') == 'ollama':
provider_config['auto_unload'] = ollama_auto_unload
# ---处理 batch 输入---
# 检查是否为 4D tensor(batch 格式)
if len(image.shape) == 4 and image.shape[0] > 1:
# Batch 模式:逐帧处理
batch_size = image.shape[0]
results = []
for i in range(batch_size):
# 提取单帧并转换为 base64
single_frame = image[i:i+1] # 保持 4D 形状 [1, H, W, C]
image_data = self._image_to_base64(single_frame)
# 分析单张图像
description = self._analyze_single_image(
image_data=image_data,
prompt_template=prompt_template,
rule_name=rule_name,
service_id=service_id,
service=service,
provider_config=provider_config,
unique_id=unique_id,
frame_index=i
)
results.append(description)
# 输出:合并结果 + 列表
combined_result = "\n---\n".join(results)
return (combined_result, results)
else:
# 单张模式
image_data = self._image_to_base64(image)
description = self._analyze_single_image(
image_data=image_data,
prompt_template=prompt_template,
rule_name=rule_name,
service_id=service_id,
service=service,
provider_config=provider_config,
unique_id=unique_id,
frame_index=None
)
return (description, [description])
except InterruptProcessingException:
raise
except Exception as e:
error_msg = format_api_error(e, vlm_service)
if request_id:
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"分析异常: {error_msg}")
# 节点映射,用于向ComfyUI注册节点
NODE_CLASS_MAPPINGS = {
"ImageCaptionNode": ImageCaptionNode,
}
# 节点显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {
"ImageCaptionNode": "✨图像反推提示词",
}