diff --git a/__init__.py b/__init__.py index fa1ddef..fd560a3 100644 --- a/__init__.py +++ b/__init__.py @@ -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__", diff --git a/nodes/tools/load_video_frames.py b/nodes/tools/load_video_frames.py new file mode 100644 index 0000000..a8f90ba --- /dev/null +++ b/nodes/tools/load_video_frames.py @@ -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)" +} + diff --git a/nodes/tools/show_any.py b/nodes/tools/show_any.py new file mode 100644 index 0000000..3068036 --- /dev/null +++ b/nodes/tools/show_any.py @@ -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)" +} diff --git a/pyproject.toml b/pyproject.toml index d43e59d..d29f8fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/web/show_any.js b/web/show_any.js new file mode 100644 index 0000000..15ef29e --- /dev/null +++ b/web/show_any.js @@ -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); + } + }; + } + } +});