feat: 新增Show Any (UTK)节点和Extract Video Frames (UTK)节点
- 新增Show Any (UTK)节点:支持显示任意类型数据,参考comfyui-easy-use实现 - 新增Extract Video Frames (UTK)节点:支持从视频或图片序列中智能抽取帧 - 更新版本号至1.4.9 - 添加WEB_DIRECTORY导出以支持JavaScript文件加载
This commit is contained in:
+35
-2
@@ -8,13 +8,22 @@ A comprehensive toolkit for ComfyUI that provides various utility nodes for imag
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = "1.4.8"
|
||||
__version__ = "1.4.9"
|
||||
__author__ = "CyberDickLang"
|
||||
__email__ = "286878701@qq.com"
|
||||
__url__ = "https://github.com/whmc76"
|
||||
|
||||
# 更新日志
|
||||
CHANGELOG = {
|
||||
"1.4.9": [
|
||||
"新增Show Any (UTK)节点:",
|
||||
"- 参考comfyui-easy-use的showAnything节点实现",
|
||||
"- 支持显示任意类型的数据(string、int、float、json、list、torch.Tensor等)",
|
||||
"- 在节点内部文本框中显示数据类型和内容",
|
||||
"- 支持数据透传,不做任何修改",
|
||||
"- 支持工作流保存和加载时恢复显示内容",
|
||||
"- 分类:UniversalToolkit/Tools",
|
||||
],
|
||||
"1.4.8": [
|
||||
"版本更新和代码优化:",
|
||||
"- 更新插件版本号为 1.4.8",
|
||||
@@ -777,6 +786,14 @@ try:
|
||||
NODE_CLASS_MAPPINGS as GET_IMAGE_RANGE_MAPPINGS
|
||||
from .nodes.tools.get_image_range_from_batch import \
|
||||
NODE_DISPLAY_NAME_MAPPINGS as GET_IMAGE_RANGE_DISPLAY
|
||||
from .nodes.tools.load_video_frames import \
|
||||
NODE_CLASS_MAPPINGS as EXTRACT_VIDEO_FRAMES_MAPPINGS
|
||||
from .nodes.tools.load_video_frames import \
|
||||
NODE_DISPLAY_NAME_MAPPINGS as EXTRACT_VIDEO_FRAMES_DISPLAY
|
||||
from .nodes.tools.show_any import \
|
||||
NODE_CLASS_MAPPINGS as SHOW_ANY_MAPPINGS
|
||||
from .nodes.tools.show_any import \
|
||||
NODE_DISPLAY_NAME_MAPPINGS as SHOW_ANY_DISPLAY
|
||||
from .nodes.tools.optimal_context_window_node import \
|
||||
NODE_CLASS_MAPPINGS as BEST_CONTEXT_WINDOW_MAPPINGS
|
||||
from .nodes.tools.optimal_context_window_node import \
|
||||
@@ -789,9 +806,14 @@ try:
|
||||
NODE_CLASS_MAPPINGS as RESIZE_VER_KJ_MAPPINGS
|
||||
from .nodes.image.resize_image_ver_kj import \
|
||||
NODE_DISPLAY_NAME_MAPPINGS as RESIZE_VER_KJ_DISPLAY
|
||||
except ImportError:
|
||||
except ImportError as e:
|
||||
print(f"[UniversalToolkit] 导入错误: {e}")
|
||||
GET_IMAGE_RANGE_MAPPINGS = {}
|
||||
GET_IMAGE_RANGE_DISPLAY = {}
|
||||
EXTRACT_VIDEO_FRAMES_MAPPINGS = {}
|
||||
EXTRACT_VIDEO_FRAMES_DISPLAY = {}
|
||||
SHOW_ANY_MAPPINGS = {}
|
||||
SHOW_ANY_DISPLAY = {}
|
||||
BEST_CONTEXT_WINDOW_MAPPINGS = {}
|
||||
BEST_CONTEXT_WINDOW_DISPLAY = {}
|
||||
BLOCKIFY_MASK_MAPPINGS = {}
|
||||
@@ -839,6 +861,8 @@ NODE_CLASS_MAPPINGS.update(COLOR_TO_MASK_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(LAZY_SWITCH_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(TEXT_TRANSLATOR_API_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(GET_IMAGE_RANGE_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(EXTRACT_VIDEO_FRAMES_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(SHOW_ANY_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(BEST_CONTEXT_WINDOW_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(BLOCKIFY_MASK_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(RESIZE_VER_KJ_MAPPINGS)
|
||||
@@ -882,6 +906,8 @@ NODE_DISPLAY_NAME_MAPPINGS.update(COLOR_TO_MASK_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LAZY_SWITCH_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_TRANSLATOR_API_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(GET_IMAGE_RANGE_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(EXTRACT_VIDEO_FRAMES_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(SHOW_ANY_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(BEST_CONTEXT_WINDOW_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(BLOCKIFY_MASK_DISPLAY)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(RESIZE_VER_KJ_DISPLAY)
|
||||
@@ -926,16 +952,23 @@ NODE_CATEGORIES = {
|
||||
"APIImageGenerator_UTK",
|
||||
"TextTranslatorAPI_UTK",
|
||||
"GetImageRangeFromBatch_UTK",
|
||||
"Extract_Video_Frames_UTK",
|
||||
"ShowAny_UTK",
|
||||
"BestContextWindow_UTK",
|
||||
"BlockifyMask_UTK",
|
||||
"ResizeImageVerKJ_UTK",
|
||||
]
|
||||
}
|
||||
|
||||
# 导出WEB_DIRECTORY以便ComfyUI加载JavaScript文件
|
||||
import os
|
||||
WEB_DIRECTORY = os.path.join(os.path.dirname(__file__), "web")
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
"NODE_CATEGORIES",
|
||||
"WEB_DIRECTORY",
|
||||
"__version__",
|
||||
"__author__",
|
||||
"__email__",
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Load Video Frames Node for ComfyUI Universal Toolkit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Load frames from video files or image sequences and intelligently sample frames
|
||||
based on target frame count and sampling mode.
|
||||
|
||||
:copyright: (c) 2024 by May
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import os
|
||||
import cv2
|
||||
import torch
|
||||
from typing import Optional, Tuple, List
|
||||
from PIL import Image
|
||||
|
||||
from ..image.image_converters import pil2tensor
|
||||
|
||||
|
||||
class Extract_Video_Frames_UTK:
|
||||
"""
|
||||
从视频文件或图片序列中智能抽取帧
|
||||
|
||||
支持多种抽取模式:
|
||||
- 平均抽取:均匀分布在整个视频/序列中
|
||||
- 前面较多:前半部分抽取更多帧
|
||||
- 后面较多:后半部分抽取更多帧
|
||||
- 中间较多:中间部分抽取更多帧
|
||||
- 两端较多:开头和结尾抽取更多帧
|
||||
"""
|
||||
|
||||
CATEGORY = "UniversalToolkit/Tools"
|
||||
RETURN_TYPES = ("IMAGE", "INT")
|
||||
RETURN_NAMES = ("images", "frames_count")
|
||||
FUNCTION = "load_frames"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"video_path": ("STRING", {
|
||||
"default": "",
|
||||
"tooltip": "视频文件路径(支持mp4, avi, mov等格式)"
|
||||
}),
|
||||
"target_frames": ("INT", {
|
||||
"default": 8,
|
||||
"min": 1,
|
||||
"max": 1000,
|
||||
"step": 1,
|
||||
"tooltip": "目标抽取的帧数"
|
||||
}),
|
||||
"mode": (["average", "front_heavy", "back_heavy", "middle_heavy", "ends_heavy"], {
|
||||
"default": "average",
|
||||
"tooltip": "Frame extraction mode"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"images": ("IMAGE", {
|
||||
"tooltip": "图片序列输入(如果提供,将优先使用图片序列而不是视频)"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
def calculate_frame_indices(self, total_frames: int, target_frames: int, mode: str) -> List[int]:
|
||||
"""
|
||||
根据模式和目标帧数计算要抽取的帧索引
|
||||
|
||||
Args:
|
||||
total_frames: 总帧数
|
||||
target_frames: 目标抽取帧数
|
||||
mode: 抽取模式
|
||||
|
||||
Returns:
|
||||
帧索引列表
|
||||
"""
|
||||
if total_frames <= 0:
|
||||
return []
|
||||
|
||||
if target_frames >= total_frames:
|
||||
# 如果目标帧数大于等于总帧数,返回所有帧
|
||||
return list(range(total_frames))
|
||||
|
||||
indices = []
|
||||
|
||||
if mode == "average":
|
||||
# 均匀分布
|
||||
step = total_frames / target_frames
|
||||
indices = [int(i * step) for i in range(target_frames)]
|
||||
# 确保最后一个索引不超过总帧数
|
||||
indices[-1] = min(indices[-1], total_frames - 1)
|
||||
|
||||
elif mode == "front_heavy":
|
||||
# 前半部分抽取60%,后半部分抽取40%
|
||||
front_count = int(target_frames * 0.6)
|
||||
back_count = target_frames - front_count
|
||||
|
||||
# 前半部分均匀抽取
|
||||
if front_count > 0:
|
||||
front_step = (total_frames // 2) / front_count
|
||||
front_indices = [int(i * front_step) for i in range(front_count)]
|
||||
else:
|
||||
front_indices = []
|
||||
|
||||
# 后半部分均匀抽取
|
||||
if back_count > 0:
|
||||
back_start = total_frames // 2
|
||||
back_step = (total_frames - back_start) / back_count
|
||||
back_indices = [back_start + int(i * back_step) for i in range(back_count)]
|
||||
else:
|
||||
back_indices = []
|
||||
|
||||
indices = front_indices + back_indices
|
||||
|
||||
elif mode == "back_heavy":
|
||||
# 前半部分抽取40%,后半部分抽取60%
|
||||
front_count = int(target_frames * 0.4)
|
||||
back_count = target_frames - front_count
|
||||
|
||||
# 前半部分均匀抽取
|
||||
if front_count > 0:
|
||||
front_step = (total_frames // 2) / front_count
|
||||
front_indices = [int(i * front_step) for i in range(front_count)]
|
||||
else:
|
||||
front_indices = []
|
||||
|
||||
# 后半部分均匀抽取
|
||||
if back_count > 0:
|
||||
back_start = total_frames // 2
|
||||
back_step = (total_frames - back_start) / back_count
|
||||
back_indices = [back_start + int(i * back_step) for i in range(back_count)]
|
||||
else:
|
||||
back_indices = []
|
||||
|
||||
indices = front_indices + back_indices
|
||||
|
||||
elif mode == "middle_heavy":
|
||||
# 开头20%,中间60%,结尾20%
|
||||
start_count = int(target_frames * 0.2)
|
||||
middle_count = int(target_frames * 0.6)
|
||||
end_count = target_frames - start_count - middle_count
|
||||
|
||||
# 开头部分
|
||||
if start_count > 0:
|
||||
start_step = (total_frames // 4) / max(start_count, 1)
|
||||
start_indices = [int(i * start_step) for i in range(start_count)]
|
||||
else:
|
||||
start_indices = []
|
||||
|
||||
# 中间部分
|
||||
if middle_count > 0:
|
||||
middle_start = total_frames // 4
|
||||
middle_end = total_frames * 3 // 4
|
||||
middle_step = (middle_end - middle_start) / max(middle_count, 1)
|
||||
middle_indices = [middle_start + int(i * middle_step) for i in range(middle_count)]
|
||||
else:
|
||||
middle_indices = []
|
||||
|
||||
# 结尾部分
|
||||
if end_count > 0:
|
||||
end_start = total_frames * 3 // 4
|
||||
end_step = (total_frames - end_start) / max(end_count, 1)
|
||||
end_indices = [end_start + int(i * end_step) for i in range(end_count)]
|
||||
else:
|
||||
end_indices = []
|
||||
|
||||
indices = start_indices + middle_indices + end_indices
|
||||
|
||||
elif mode == "ends_heavy":
|
||||
# 开头40%,中间20%,结尾40%
|
||||
start_count = int(target_frames * 0.4)
|
||||
middle_count = int(target_frames * 0.2)
|
||||
end_count = target_frames - start_count - middle_count
|
||||
|
||||
# 开头部分
|
||||
if start_count > 0:
|
||||
start_step = (total_frames // 3) / max(start_count, 1)
|
||||
start_indices = [int(i * start_step) for i in range(start_count)]
|
||||
else:
|
||||
start_indices = []
|
||||
|
||||
# 中间部分
|
||||
if middle_count > 0:
|
||||
middle_start = total_frames // 3
|
||||
middle_end = total_frames * 2 // 3
|
||||
middle_step = (middle_end - middle_start) / max(middle_count, 1)
|
||||
middle_indices = [middle_start + int(i * middle_step) for i in range(middle_count)]
|
||||
else:
|
||||
middle_indices = []
|
||||
|
||||
# 结尾部分
|
||||
if end_count > 0:
|
||||
end_start = total_frames * 2 // 3
|
||||
end_step = (total_frames - end_start) / max(end_count, 1)
|
||||
end_indices = [end_start + int(i * end_step) for i in range(end_count)]
|
||||
else:
|
||||
end_indices = []
|
||||
|
||||
indices = start_indices + middle_indices + end_indices
|
||||
|
||||
# 去重并排序
|
||||
indices = sorted(list(set(indices)))
|
||||
# 确保索引在有效范围内
|
||||
indices = [idx for idx in indices if 0 <= idx < total_frames]
|
||||
|
||||
# 如果去重后数量不足,补充帧
|
||||
while len(indices) < target_frames and len(indices) < total_frames:
|
||||
# 找到最大的间隔并补充
|
||||
if len(indices) == 0:
|
||||
indices.append(0)
|
||||
elif len(indices) == 1:
|
||||
if indices[0] < total_frames - 1:
|
||||
indices.append(total_frames - 1)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
max_gap = 0
|
||||
insert_pos = 0
|
||||
for i in range(len(indices) - 1):
|
||||
gap = indices[i + 1] - indices[i]
|
||||
if gap > max_gap:
|
||||
max_gap = gap
|
||||
insert_pos = i + 1
|
||||
insert_value = (indices[i] + indices[i + 1]) // 2
|
||||
|
||||
if max_gap > 1:
|
||||
indices.insert(insert_pos, insert_value)
|
||||
indices.sort()
|
||||
else:
|
||||
# 如果没有大间隔,在两端补充
|
||||
if indices[0] > 0:
|
||||
indices.insert(0, indices[0] - 1)
|
||||
elif indices[-1] < total_frames - 1:
|
||||
indices.append(min(indices[-1] + 1, total_frames - 1))
|
||||
else:
|
||||
break
|
||||
|
||||
return indices[:target_frames]
|
||||
|
||||
def load_video_frames(self, video_path: str, indices: List[int]) -> List[torch.Tensor]:
|
||||
"""
|
||||
从视频文件中加载指定索引的帧
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
indices: 要加载的帧索引列表
|
||||
|
||||
Returns:
|
||||
帧张量列表
|
||||
"""
|
||||
# 路径预处理
|
||||
video_path = video_path.strip().strip('"').strip("'")
|
||||
video_path = video_path.replace("\\", "/")
|
||||
|
||||
if not os.path.isfile(video_path):
|
||||
raise FileNotFoundError(f"视频文件不存在: {video_path}")
|
||||
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"无法打开视频文件: {video_path}")
|
||||
|
||||
# 为了保持原始顺序,先按索引顺序读取并存储到字典中
|
||||
frame_dict = {}
|
||||
sorted_indices = sorted(set(indices)) # 去重并排序以提高效率
|
||||
|
||||
for target_idx in sorted_indices:
|
||||
# 跳转到目标帧
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, target_idx)
|
||||
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print(f"警告: 无法读取第 {target_idx} 帧")
|
||||
continue
|
||||
|
||||
# 转换BGR到RGB
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
# 转换为PIL图像
|
||||
pil_image = Image.fromarray(frame_rgb)
|
||||
# 转换为tensor
|
||||
tensor = pil2tensor(pil_image)
|
||||
frame_dict[target_idx] = tensor
|
||||
|
||||
cap.release()
|
||||
|
||||
if len(frame_dict) == 0:
|
||||
raise RuntimeError("未能从视频中加载任何帧")
|
||||
|
||||
# 按照原始indices顺序返回帧
|
||||
frames = [frame_dict[idx] for idx in indices if idx in frame_dict]
|
||||
|
||||
return frames
|
||||
|
||||
def load_image_sequence_frames(self, images: torch.Tensor, indices: List[int]) -> List[torch.Tensor]:
|
||||
"""
|
||||
从图片序列中提取指定索引的帧
|
||||
|
||||
Args:
|
||||
images: 图片批次张量 [batch, height, width, channels]
|
||||
indices: 要提取的帧索引列表
|
||||
|
||||
Returns:
|
||||
帧张量列表
|
||||
"""
|
||||
frames = []
|
||||
for idx in indices:
|
||||
if 0 <= idx < len(images):
|
||||
frames.append(images[idx])
|
||||
else:
|
||||
print(f"警告: 索引 {idx} 超出图片序列范围 [0, {len(images)-1}]")
|
||||
|
||||
return frames
|
||||
|
||||
def load_frames(
|
||||
self,
|
||||
video_path: str,
|
||||
target_frames: int,
|
||||
mode: str,
|
||||
images: Optional[torch.Tensor] = None
|
||||
) -> Tuple[torch.Tensor]:
|
||||
"""
|
||||
加载并抽取帧
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
target_frames: 目标帧数
|
||||
mode: 抽取模式
|
||||
images: 可选的图片序列输入
|
||||
|
||||
Returns:
|
||||
抽取的帧批次张量
|
||||
"""
|
||||
# 优先使用图片序列输入
|
||||
if images is not None:
|
||||
total_frames = len(images)
|
||||
print(f"📸 从图片序列中抽取帧: 总帧数={total_frames}, 目标帧数={target_frames}, 模式={mode}")
|
||||
|
||||
indices = self.calculate_frame_indices(total_frames, target_frames, mode)
|
||||
print(f"📊 计算得到的帧索引: {indices}")
|
||||
|
||||
# 按照indices的顺序提取帧(保持计算出的顺序)
|
||||
frame_tensors = self.load_image_sequence_frames(images, indices)
|
||||
|
||||
elif video_path and video_path.strip():
|
||||
# 使用视频文件
|
||||
video_path = video_path.strip().strip('"').strip("'")
|
||||
|
||||
# 先获取视频总帧数
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"无法打开视频文件: {video_path}")
|
||||
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
cap.release()
|
||||
|
||||
print(f"🎬 从视频中抽取帧: 文件={video_path}, 总帧数={total_frames}, FPS={fps:.2f}, 目标帧数={target_frames}, 模式={mode}")
|
||||
|
||||
indices = self.calculate_frame_indices(total_frames, target_frames, mode)
|
||||
print(f"📊 计算得到的帧索引: {indices}")
|
||||
|
||||
frame_tensors = self.load_video_frames(video_path, indices)
|
||||
|
||||
else:
|
||||
raise ValueError("必须提供视频路径或图片序列输入")
|
||||
|
||||
if len(frame_tensors) == 0:
|
||||
raise RuntimeError("未能加载任何帧")
|
||||
|
||||
# 将所有帧堆叠成批次
|
||||
batch_tensor = torch.stack(frame_tensors, dim=0)
|
||||
frames_count = len(frame_tensors)
|
||||
|
||||
print(f"✅ 成功加载 {frames_count} 帧,输出形状: {batch_tensor.shape}")
|
||||
|
||||
return (batch_tensor, frames_count)
|
||||
|
||||
|
||||
# 节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Extract_Video_Frames_UTK": Extract_Video_Frames_UTK
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Extract_Video_Frames_UTK": "Extract Video Frames (UTK)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Show Any Node for ComfyUI Universal Toolkit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Display any type of input data in a text field, showing data type and content.
|
||||
Acts as a passthrough node for debugging and inspection.
|
||||
|
||||
Based on comfyui-easy-use's showAnything node implementation.
|
||||
|
||||
:copyright: (c) 2024 by May
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import json
|
||||
import torch
|
||||
import numpy as np
|
||||
from typing import Any, List
|
||||
|
||||
# Import AnyType for accepting any input type
|
||||
try:
|
||||
from comfy.comfy_types.node_typing import IO
|
||||
ANY_TYPE = IO.ANY
|
||||
except ImportError:
|
||||
try:
|
||||
from comfy_extras.nodes_custom_sampler import AnyType
|
||||
ANY_TYPE = AnyType("*")
|
||||
except ImportError:
|
||||
from .any_type import AnyType
|
||||
ANY_TYPE = AnyType("*")
|
||||
|
||||
|
||||
class ShowAny_UTK:
|
||||
"""
|
||||
显示任意类型数据的节点
|
||||
|
||||
功能:
|
||||
- 接受任何类型的输入数据
|
||||
- 在文本框中显示数据类型和数据内容
|
||||
- 直接输出输入的数据(不做任何修改)
|
||||
- 用于调试和查看数据流
|
||||
|
||||
参考实现:comfyui-easy-use 的 showAnything 节点
|
||||
"""
|
||||
|
||||
CATEGORY = "UniversalToolkit/Tools"
|
||||
RETURN_TYPES = (ANY_TYPE,)
|
||||
RETURN_NAMES = ("data",)
|
||||
FUNCTION = "show_any"
|
||||
INPUT_IS_LIST = True
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"data": (ANY_TYPE, {
|
||||
"tooltip": "输入任意类型的数据"
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
}
|
||||
}
|
||||
|
||||
def format_data(self, data: Any) -> str:
|
||||
"""
|
||||
格式化数据为可读的字符串
|
||||
|
||||
Args:
|
||||
data: 要格式化的数据
|
||||
|
||||
Returns:
|
||||
格式化后的字符串
|
||||
"""
|
||||
# 获取数据类型
|
||||
data_type = type(data).__name__
|
||||
|
||||
# 根据不同类型格式化数据
|
||||
if data is None:
|
||||
return f"Type: NoneType\nValue: None"
|
||||
|
||||
elif isinstance(data, str):
|
||||
return f"Type: string\nValue: {data}"
|
||||
|
||||
elif isinstance(data, (int, float)):
|
||||
return f"Type: {data_type}\nValue: {data}"
|
||||
|
||||
elif isinstance(data, bool):
|
||||
return f"Type: boolean\nValue: {data}"
|
||||
|
||||
elif isinstance(data, (list, tuple)):
|
||||
# 列表或元组
|
||||
try:
|
||||
# 尝试转换为JSON格式
|
||||
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
return f"Type: {data_type}\nLength: {len(data)}\nValue:\n{json_str}"
|
||||
except (TypeError, ValueError):
|
||||
# 如果无法序列化为JSON,显示repr
|
||||
return f"Type: {data_type}\nLength: {len(data)}\nValue: {repr(data)}"
|
||||
|
||||
elif isinstance(data, dict):
|
||||
# 字典
|
||||
try:
|
||||
json_str = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
return f"Type: dict\nKeys: {len(data)}\nValue:\n{json_str}"
|
||||
except (TypeError, ValueError):
|
||||
return f"Type: dict\nKeys: {len(data)}\nValue: {repr(data)}"
|
||||
|
||||
elif isinstance(data, torch.Tensor):
|
||||
# PyTorch Tensor
|
||||
shape = list(data.shape)
|
||||
dtype = str(data.dtype)
|
||||
device = str(data.device)
|
||||
min_val = float(data.min().item()) if data.numel() > 0 else None
|
||||
max_val = float(data.max().item()) if data.numel() > 0 else None
|
||||
|
||||
info = f"Type: torch.Tensor\nShape: {shape}\nDtype: {dtype}\nDevice: {device}"
|
||||
if min_val is not None and max_val is not None:
|
||||
info += f"\nMin: {min_val:.6f}\nMax: {max_val:.6f}"
|
||||
info += f"\nNumel: {data.numel()}"
|
||||
|
||||
return info
|
||||
|
||||
elif isinstance(data, np.ndarray):
|
||||
# NumPy Array
|
||||
shape = data.shape
|
||||
dtype = str(data.dtype)
|
||||
min_val = float(data.min()) if data.size > 0 else None
|
||||
max_val = float(data.max()) if data.size > 0 else None
|
||||
|
||||
info = f"Type: numpy.ndarray\nShape: {shape}\nDtype: {dtype}"
|
||||
if min_val is not None and max_val is not None:
|
||||
info += f"\nMin: {min_val:.6f}\nMax: {max_val:.6f}"
|
||||
info += f"\nSize: {data.size}"
|
||||
|
||||
return info
|
||||
|
||||
else:
|
||||
# 其他类型,尝试使用repr
|
||||
try:
|
||||
repr_str = repr(data)
|
||||
# 限制长度,避免过长
|
||||
if len(repr_str) > 500:
|
||||
repr_str = repr_str[:500] + "..."
|
||||
return f"Type: {data_type}\nValue: {repr_str}"
|
||||
except Exception:
|
||||
return f"Type: {data_type}\nValue: <无法显示>"
|
||||
|
||||
def show_any(self, unique_id=None, extra_pnginfo=None, **kwargs) -> dict:
|
||||
"""
|
||||
显示任意类型的数据
|
||||
|
||||
Args:
|
||||
unique_id: 节点的唯一ID(用于保存工作流)
|
||||
extra_pnginfo: 额外的PNG信息(用于保存工作流)
|
||||
**kwargs: 输入的数据(任意类型)
|
||||
|
||||
Returns:
|
||||
dict: 包含ui显示和结果的字典
|
||||
"""
|
||||
values = []
|
||||
if "data" in kwargs:
|
||||
for val in kwargs['data']:
|
||||
try:
|
||||
if isinstance(val, str):
|
||||
values.append(val)
|
||||
elif isinstance(val, list):
|
||||
values = val
|
||||
elif isinstance(val, (int, float, bool)):
|
||||
values.append(str(val))
|
||||
elif isinstance(val, torch.Tensor):
|
||||
# 处理torch.Tensor(IMAGE类型)
|
||||
shape = list(val.shape)
|
||||
values.append(f"torch.Tensor(shape={shape}, dtype={val.dtype})")
|
||||
else:
|
||||
val = json.dumps(val)
|
||||
values.append(str(val))
|
||||
except Exception:
|
||||
values.append(str(val))
|
||||
pass
|
||||
|
||||
# 保存到工作流中(用于加载工作流时恢复显示)
|
||||
if not extra_pnginfo:
|
||||
pass
|
||||
elif not isinstance(extra_pnginfo, list) or len(extra_pnginfo) == 0:
|
||||
pass
|
||||
elif (not isinstance(extra_pnginfo[0], dict) or "workflow" not in extra_pnginfo[0]):
|
||||
pass
|
||||
else:
|
||||
workflow = extra_pnginfo[0]["workflow"]
|
||||
if unique_id and isinstance(unique_id, list) and len(unique_id) > 0:
|
||||
node = next((x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])), None)
|
||||
if node:
|
||||
node["widgets_values"] = [values]
|
||||
|
||||
# 返回结果(完全按照comfyui-easy-use格式)
|
||||
if isinstance(values, list) and len(values) == 1:
|
||||
return {"ui": {"text": values}, "result": (values[0],)}
|
||||
else:
|
||||
return {"ui": {"text": values}, "result": (values,)}
|
||||
|
||||
|
||||
# 节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ShowAny_UTK": ShowAny_UTK
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ShowAny_UTK": "Show Any (UTK)"
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "universaltoolkit"
|
||||
description = "A comprehensive toolkit based on ComfyUI, providing image, mask, audio, and tools nodes, fully modular and v3 compatible."
|
||||
version = "1.4.8"
|
||||
version = "1.4.9"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"torch>=1.9.0",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ShowAny_UTK",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "ShowAny_UTK") {
|
||||
function populate(text) {
|
||||
if (!text || !Array.isArray(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex((w) => w.name === "text");
|
||||
if (pos !== -1) {
|
||||
for (let i = pos; i < this.widgets.length; i++) {
|
||||
this.widgets[i].onRemove?.();
|
||||
}
|
||||
this.widgets.length = pos;
|
||||
}
|
||||
}
|
||||
|
||||
for (const list of text) {
|
||||
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
|
||||
w.inputEl.readOnly = true;
|
||||
w.inputEl.style.opacity = 0.6;
|
||||
w.value = list;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
|
||||
// When the node is executed we will be sent the input text, display this in the widget
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
if (message && message.text) {
|
||||
populate.call(this, message.text);
|
||||
}
|
||||
};
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigure?.apply(this, arguments);
|
||||
if (this.widgets_values?.length) {
|
||||
populate.call(this, this.widgets_values);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user