2.0.1
This commit is contained in:
+129
-77
@@ -74,8 +74,9 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt_output",)
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt_output", "prompt_list")
|
||||
OUTPUT_IS_LIST = (False, True)
|
||||
FUNCTION = "analyze_image"
|
||||
CATEGORY = "✨Prompt Assistant"
|
||||
OUTPUT_NODE = False
|
||||
@@ -86,6 +87,10 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
只在输入内容真正变化时才触发重新执行
|
||||
使用输入参数的哈希值作为判断依据
|
||||
"""
|
||||
# 检查是否包含强制刷新符号 [R]
|
||||
if cls._check_is_changed_bypass(rule, custom_rule_content, user_prompt):
|
||||
return float("nan")
|
||||
|
||||
# 导入图像哈希工具函数
|
||||
from ..utils.image import compute_image_hash
|
||||
|
||||
@@ -105,54 +110,111 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
|
||||
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, 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: 分析结果
|
||||
tuple: 分析结果,batch 输入时结果用换行符分隔
|
||||
"""
|
||||
request_id = None # 初始化,用于异常处理
|
||||
|
||||
try:
|
||||
# 检查输入
|
||||
if image is None:
|
||||
raise ValueError("输入图像不能为空")
|
||||
|
||||
# 将图像转换为base64编码
|
||||
image_data = self._image_to_base64(image)
|
||||
|
||||
# 获取提示词模板内容
|
||||
# ---准备提示词模板---
|
||||
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:
|
||||
# 从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():
|
||||
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')
|
||||
@@ -160,14 +222,13 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
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"
|
||||
@@ -176,35 +237,30 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
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---
|
||||
# 查找指定的模型或默认模型
|
||||
# 构建 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', ''),
|
||||
@@ -215,62 +271,58 @@ class ImageCaptionNode(VLMNodeBase):
|
||||
'top_p': target_model.get('top_p', 0.9),
|
||||
}
|
||||
|
||||
# Ollama特殊处理:添加auto_unload配置
|
||||
if service.get('type') == 'ollama':
|
||||
provider_config['auto_unload'] = ollama_auto_unload
|
||||
|
||||
# 创建请求ID
|
||||
request_id = generate_request_id("icap", None, unique_id)
|
||||
|
||||
# 检查是否关闭思维链
|
||||
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)
|
||||
|
||||
# 准备阶段日志
|
||||
log_prepare(TASK_IMAGE_CAPTION, request_id, SOURCE_NODE, service_display_name, model_display, rule_name)
|
||||
|
||||
# 执行图像分析
|
||||
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,)
|
||||
# ---处理 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:
|
||||
error_msg = result.get('error', '分析失败,未知错误') if result else '分析服务未返回结果'
|
||||
# 如果是中断错误,直接抛出InterruptProcessingException,不打印日志(由基类打印)
|
||||
if error_msg == "任务被中断":
|
||||
raise InterruptProcessingException()
|
||||
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
|
||||
raise RuntimeError(f"分析失败: {error_msg}")
|
||||
# 单张模式
|
||||
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)
|
||||
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
|
||||
if request_id:
|
||||
log_error(TASK_IMAGE_CAPTION, request_id, error_msg, source=SOURCE_NODE)
|
||||
raise RuntimeError(f"分析异常: {error_msg}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user