refactory mode
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ share/python-wheels/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
/tmp/
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
# ComfyUI-KYNode
|
||||
For comfyui custom nodes utils
|
||||
- ## Load images from URL/Path/Base64
|
||||
|
||||
## 功能节点
|
||||
|
||||
### 图像处理
|
||||
- Load images from URL/Path/Base64
|
||||

|
||||
|
||||
### 视频处理
|
||||
- Video Compare: 对比两个视频(支持URL和视频文件)并在预览窗口中显示
|
||||
- 支持两种输入模式:URL和视频文件对象
|
||||
- 可以通过拖拽中间的分隔线来对比两个视频
|
||||
- 提供滑块控制对比比例
|
||||
- 可以在预览窗口中动态更新视频URL
|
||||
- 自动处理ComfyUI的视频文件路径(input/output文件夹)
|
||||
|
||||
- Video File Compare: 直接从input/output文件夹中选择视频文件进行对比
|
||||
- 下拉菜单选择input或output文件夹中的视频文件
|
||||
- 无需连接其他节点,直接预览对比
|
||||
|
||||
- KY Load Video: 仿造ComfyUI官方Load Video节点
|
||||
- 从input或output文件夹加载视频文件
|
||||
- 下拉菜单选择视频文件
|
||||
- 输出视频对象和预览URL
|
||||
|
||||
- KY Save Video: 保存图像序列为支持alpha通道的视频文件
|
||||
- 支持将IMAGE张量序列保存为MOV/AVI/MP4格式视频
|
||||
- 支持alpha通道(通过MASK输入)
|
||||
- 多种编码器选择:ProRes 4444、PNG、QuickTime RLE
|
||||
- 可调节视频质量、帧率等参数
|
||||
- 支持添加音频轨道
|
||||
- 自动生成预览URL
|
||||
|
||||
- KY Save Image Sequence: 保存图像序列为独立的图片文件
|
||||
- 支持PNG、JPG、TIFF、WebP格式
|
||||
- 支持alpha通道保存(PNG/TIFF/WebP)
|
||||
- 可设置图片质量和帧范围
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# 单个检测结果处理修复说明
|
||||
|
||||
## 问题描述
|
||||
API返回了单个检测结果的字典格式,而不是预期的字典列表格式:
|
||||
|
||||
```
|
||||
解析到的bbox数据: {'bbox_2d': [108, 184, 359, 845], 'label': 'yellow elephant'}
|
||||
错误: bbox_data不是列表类型,是: dict
|
||||
```
|
||||
|
||||
## 问题原因
|
||||
不同的MiniCPM-V API实现可能返回不同的数据格式:
|
||||
- **单个检测结果**: `{'bbox_2d': [x, y, w, h], 'label': 'object'}`
|
||||
- **多个检测结果**: `[{'bbox_2d': [x, y, w, h], 'label': 'object1'}, {'bbox_2d': [x, y, w, h], 'label': 'object2'}]`
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 兼容性处理
|
||||
```python
|
||||
# 处理单个检测结果(字典)或多个检测结果(字典列表)
|
||||
if isinstance(bbox_data, dict):
|
||||
# 单个检测结果,转换为列表
|
||||
bbox_list = [bbox_data]
|
||||
debug_info += f"检测到单个结果,转换为列表处理\n"
|
||||
elif isinstance(bbox_data, list):
|
||||
# 多个检测结果
|
||||
bbox_list = bbox_data
|
||||
debug_info += f"检测到{len(bbox_list)}个结果\n"
|
||||
else:
|
||||
debug_info += f"错误: bbox_data类型不支持,是: {type(bbox_data).__name__}\n"
|
||||
return ([], None, None, None, json.dumps(bbox_data, ensure_ascii=False), debug_info)
|
||||
```
|
||||
|
||||
### 统一处理逻辑
|
||||
修复后的代码现在可以处理:
|
||||
|
||||
1. **单个检测结果**:
|
||||
```json
|
||||
{'bbox_2d': [108, 184, 359, 845], 'label': 'yellow elephant'}
|
||||
```
|
||||
|
||||
2. **多个检测结果**:
|
||||
```json
|
||||
[
|
||||
{'bbox_2d': [108, 184, 359, 845], 'label': 'yellow elephant'},
|
||||
{'bbox_2d': [500, 300, 200, 400], 'label': 'blue car'}
|
||||
]
|
||||
```
|
||||
|
||||
## 预期的debug_info输出
|
||||
|
||||
修复后,你应该看到类似这样的输出:
|
||||
|
||||
```
|
||||
原始尺寸: (1536, 1024)
|
||||
解析到的bbox数据: {'bbox_2d': [108, 184, 359, 845], 'label': 'yellow elephant'}
|
||||
检测到单个结果,转换为列表处理
|
||||
处理第1个item: {'bbox_2d': [108, 184, 359, 845], 'label': 'yellow elephant'}
|
||||
原始bbox: [108, 184, 359, 845]
|
||||
还原bbox: [实际转换后的坐标]
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
在ComfyUI中重新运行节点,现在应该能够正确处理这个单个检测结果,并输出正确的坐标转换信息。
|
||||
+46
-40
@@ -1,45 +1,51 @@
|
||||
from .KY_Files import FILE_CLASS_MAPPINGS, FILE_NAME_MAPPINGS
|
||||
from .LLM import LLM_CLASS_MAPPINGS, LLM_NAME_MAPPINGS
|
||||
from .KY_Image import IMG_CLASS_MAPPINGS, IMG_NAME_MAPPINGS
|
||||
from .KY_VideoCompare import VIDEO_COMPARE_CLASS_MAPPINGS, VIDEO_COMPARE_NAME_MAPPINGS
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
python = sys.executable
|
||||
|
||||
print('=====================================')
|
||||
|
||||
def get_ext_dir(subpath=None, mkdir=False):
|
||||
dir = os.path.dirname(__file__)
|
||||
if subpath is not None:
|
||||
dir = os.path.join(dir, subpath)
|
||||
|
||||
dir = os.path.abspath(dir)
|
||||
|
||||
if mkdir and not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
return dir
|
||||
|
||||
def serialize(obj):
|
||||
if isinstance(obj, (str, int, float, bool, list, dict, type(None))):
|
||||
return obj
|
||||
return str(obj) # 转为字符串
|
||||
|
||||
|
||||
py = get_ext_dir("py")
|
||||
files = os.listdir(py)
|
||||
print(files)
|
||||
all_nodes = {}
|
||||
for file in files:
|
||||
if not file.endswith(".py"):
|
||||
continue
|
||||
name = os.path.splitext(file)[0]
|
||||
imported_module = importlib.import_module(".py.{}".format(name), __name__)
|
||||
try:
|
||||
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
|
||||
serialized_CLASS_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_CLASS_MAPPINGS.items()}
|
||||
serialized_DISPLAY_NAME_MAPPINGS = {k: serialize(v) for k, v in imported_module.NODE_DISPLAY_NAME_MAPPINGS.items()}
|
||||
all_nodes[file]={"NODE_CLASS_MAPPINGS": serialized_CLASS_MAPPINGS, "NODE_DISPLAY_NAME_MAPPINGS": serialized_DISPLAY_NAME_MAPPINGS}
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
from .UtilNode import UTIL_NODE_CLASS_MAPPINGS, UTIL_NODE_NAME_MAPPINGS
|
||||
|
||||
from .bbox import BBOX_NODE_CLASS_MAPPINGS, BBOX_NODE_NAME_MAPPINGS
|
||||
|
||||
# 将节点映射到NODE_CLASS_MAPPINGS和NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(FILE_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(FILE_NAME_MAPPINGS)
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(IMG_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(IMG_NAME_MAPPINGS)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(LLM_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(LLM_NAME_MAPPINGS)
|
||||
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(UTIL_NODE_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(UTIL_NODE_NAME_MAPPINGS)
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(BBOX_NODE_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(BBOX_NODE_NAME_MAPPINGS)
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(VIDEO_COMPARE_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(VIDEO_COMPARE_NAME_MAPPINGS)
|
||||
|
||||
from .KY_load_video import VIDEO_LOAD_CLASS_MAPPINGS, VIDEO_LOAD_NAME_MAPPINGS
|
||||
NODE_CLASS_MAPPINGS.update(VIDEO_LOAD_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(VIDEO_LOAD_NAME_MAPPINGS)
|
||||
|
||||
# 添加上传节点
|
||||
from .KY_Uploads import UPLOAD_NODE_CLASS_MAPPINGS, UPLOAD_NODE_NAME_MAPPINGS
|
||||
NODE_CLASS_MAPPINGS.update(UPLOAD_NODE_CLASS_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(UPLOAD_NODE_NAME_MAPPINGS)
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
@@ -0,0 +1,113 @@
|
||||
import types
|
||||
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
from asyncio import sleep, run
|
||||
import json, re, traceback
|
||||
|
||||
remove_type_name = re.compile(r"(\{.*\})", re.I | re.M)
|
||||
|
||||
# Hack: string type that is always equal in not equal comparisons, thanks pythongosssss
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
PY_CODE = AnyType("*")
|
||||
IDEs_DICT = {}
|
||||
|
||||
|
||||
# - Thank you very much for the class -> Trung0246 -
|
||||
# - https://github.com/Trung0246/ComfyUI-0246/blob/main/utils.py#L51
|
||||
class TautologyStr(str):
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
|
||||
class ByPassTypeTuple(tuple):
|
||||
def __getitem__(self, index):
|
||||
if index > 0:
|
||||
index = 0
|
||||
item = super().__getitem__(index)
|
||||
if isinstance(item, str):
|
||||
return TautologyStr(item)
|
||||
return item
|
||||
# ---------------------------
|
||||
|
||||
|
||||
class KY_Eval_Python:
|
||||
def __init__(self):
|
||||
self.js_complete = False
|
||||
self.js_result = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"pycode": (
|
||||
"PYCODE",
|
||||
{
|
||||
"default": """import re, json, os, traceback
|
||||
from time import strftime
|
||||
|
||||
def runCode():
|
||||
nowDataTime = strftime("%Y-%m-%d %H:%M:%S")
|
||||
return f"Hello ComfyUI with us today {nowDataTime}!"
|
||||
r0_str = runCode() + unique_id
|
||||
"""
|
||||
},
|
||||
),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ByPassTypeTuple((PY_CODE,))
|
||||
RETURN_NAMES = ("r0_str",)
|
||||
FUNCTION = "exec_py"
|
||||
DESCRIPTION = "IDE Node is an node that allows you to run code written in Python or Javascript directly in the node."
|
||||
CATEGORY = "KYNode/Code"
|
||||
|
||||
def exec_py(self, pycode, unique_id, extra_pnginfo, **kwargs):
|
||||
if unique_id not in IDEs_DICT:
|
||||
IDEs_DICT[unique_id] = self
|
||||
|
||||
|
||||
outputs = {unique_id: unique_id}
|
||||
if extra_pnginfo and 'workflow' in extra_pnginfo and extra_pnginfo['workflow']:
|
||||
for node in extra_pnginfo['workflow']['nodes']:
|
||||
if node['id'] == int(unique_id):
|
||||
outputs_valid = [ouput for ouput in node.get('outputs', []) if ouput.get('name','') != '' and ouput.get('type','') != '']
|
||||
outputs = {ouput['name']: None for ouput in outputs_valid}
|
||||
self.RETURN_TYPES = ByPassTypeTuple(out["type"] for out in outputs_valid)
|
||||
self.RETURN_NAMES = tuple(name for name in outputs.keys())
|
||||
my_namespace = types.SimpleNamespace()
|
||||
# 从 prompt 对象中提取 prompt_id
|
||||
# if extra_data and 'extra_data' in extra_data and 'prompt_id' in extra_data['extra_data']:
|
||||
# prompt_id = prompt['extra_data']['prompt_id']
|
||||
# outputs['p0_str'] = p0_str
|
||||
|
||||
|
||||
my_namespace.__dict__.update(outputs)
|
||||
my_namespace.__dict__.update({prop: kwargs[prop] for prop in kwargs})
|
||||
# my_namespace.__dict__.setdefault("r0_str", "The r0 variable is not assigned")
|
||||
|
||||
try:
|
||||
exec(pycode, my_namespace.__dict__)
|
||||
except Exception as e:
|
||||
err=traceback.format_exc()
|
||||
mc = re.search(r'line (\d+), in <module>([\w\W]+)$', err, re.MULTILINE)
|
||||
msg = mc[1] + ':'+mc[2]
|
||||
my_namespace.r0 = f"Error Line{msg}"
|
||||
|
||||
new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])}
|
||||
return (*new_dict.values(),)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_Eval_Python": KY_Eval_Python
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_Eval_Python": "Eval Python Code"
|
||||
}
|
||||
@@ -107,12 +107,12 @@ class FileSequenceAnalyzer:
|
||||
return (patterns[0], numbers[0], patterns[1], numbers[1])
|
||||
|
||||
|
||||
FILE_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_FilePathAnalyzer-": FilePathAnalyzer,
|
||||
"KY_FileSequenceAnalyzer": FileSequenceAnalyzer,
|
||||
}
|
||||
|
||||
FILE_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_FilePathAnalyzer-": "FilePath Analyzer",
|
||||
"KY_FileSequenceAnalyzer": "File Sequence Analyzer",
|
||||
}
|
||||
@@ -442,7 +442,7 @@ class CropImageByXYWH:
|
||||
return (cropped_image,)
|
||||
|
||||
|
||||
IMG_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_ReadImage": ReadImage,
|
||||
"KY_LoadImagesFromFolder": LoadImagesFromFolder,
|
||||
"KY_SaveImageToPath": KY_SaveImageToPath,
|
||||
@@ -450,7 +450,7 @@ IMG_CLASS_MAPPINGS = {
|
||||
"KY_CropImageByXYWH": CropImageByXYWH
|
||||
}
|
||||
|
||||
IMG_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_ReadImage": "Read Image from Path",
|
||||
"KY_LoadImagesFromFolder": "Load Images From Folder",
|
||||
"KY_SaveImageToPath": "Save Images To Path with sequence number",
|
||||
@@ -167,10 +167,10 @@ class KY_Uploads:
|
||||
return None
|
||||
|
||||
# 注册节点
|
||||
UPLOAD_NODE_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_Uploads": KY_Uploads,
|
||||
}
|
||||
|
||||
UPLOAD_NODE_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_Uploads": "KY Upload Files",
|
||||
}
|
||||
@@ -344,12 +344,12 @@ class KY_ToVideoUrl:
|
||||
|
||||
|
||||
# 节点映射
|
||||
VIDEO_COMPARE_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_VideoCompare": VideoCompareNode,
|
||||
"KY_ToVideoUrl": KY_ToVideoUrl,
|
||||
}
|
||||
|
||||
VIDEO_COMPARE_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_VideoCompare": "Video Compare",
|
||||
"KY_ToVideoUrl": "Video Object To String",
|
||||
}
|
||||
@@ -373,12 +373,12 @@ class CreateVideoObjectFromPath:
|
||||
return (video_object,)
|
||||
|
||||
|
||||
VIDEO_LOAD_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_LoadVideoByPath": LoadVideoByPath,
|
||||
"KY_CreateVideoObjectFromPath": CreateVideoObjectFromPath,
|
||||
}
|
||||
|
||||
VIDEO_LOAD_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_LoadVideoByPath": "load video from path",
|
||||
"KY_CreateVideoObjectFromPath": "create video object from path",
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
# KY_save_video 保存视频的 ComfyUI节点
|
||||
# 用于将包含alpha 通道的PNG图片转换保存为 mov 格式的视频,保证输出视频包含alpha 通道
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import folder_paths
|
||||
from .utils.logger import logger
|
||||
from .utils.video_utils import ffmpeg_path, ENCODE_ARGS, BIGMAX, DIMMAX
|
||||
|
||||
|
||||
class KY_SaveVideo:
|
||||
"""保存图像序列为支持alpha通道的视频文件"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "KY_video"}),
|
||||
"fps": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 60.0, "step": 0.1}),
|
||||
"quality": ("INT", {"default": 18, "min": 0, "max": 51, "step": 1}),
|
||||
"codec": (["prores_ks", "png", "qtrle"], {"default": "prores_ks"}),
|
||||
"format": (["mov", "avi", "mp4"], {"default": "mov"}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",),
|
||||
"audio": ("AUDIO",),
|
||||
"start_frame": ("INT", {"default": 0, "min": 0, "max": BIGMAX}),
|
||||
"end_frame": ("INT", {"default": -1, "min": -1, "max": BIGMAX}),
|
||||
"loop_count": ("INT", {"default": 0, "min": 0, "max": 100}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("video_path", "preview_url")
|
||||
FUNCTION = "save_video"
|
||||
CATEGORY = "KY_Video"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def save_video(self, images, filename_prefix, fps, quality, codec, format,
|
||||
mask=None, audio=None, start_frame=0, end_frame=-1, loop_count=0):
|
||||
"""
|
||||
保存图像序列为视频文件
|
||||
|
||||
Args:
|
||||
images: 输入图像张量 (B, H, W, C)
|
||||
filename_prefix: 文件名前缀
|
||||
fps: 帧率
|
||||
quality: 视频质量 (CRF值,越小质量越高)
|
||||
codec: 视频编码器
|
||||
format: 输出格式
|
||||
mask: 可选的alpha通道蒙版
|
||||
audio: 可选的音频数据
|
||||
start_frame: 起始帧
|
||||
end_frame: 结束帧 (-1表示到最后)
|
||||
loop_count: 循环次数
|
||||
"""
|
||||
|
||||
if ffmpeg_path is None:
|
||||
raise Exception("FFmpeg not found. Please install FFmpeg to use video save functionality.")
|
||||
|
||||
# 处理帧范围
|
||||
total_frames = len(images)
|
||||
if end_frame == -1:
|
||||
end_frame = total_frames
|
||||
else:
|
||||
end_frame = min(end_frame, total_frames)
|
||||
|
||||
if start_frame >= end_frame:
|
||||
raise ValueError(f"Invalid frame range: start_frame={start_frame}, end_frame={end_frame}")
|
||||
|
||||
# 选择帧范围
|
||||
selected_images = images[start_frame:end_frame]
|
||||
selected_mask = None
|
||||
if mask is not None:
|
||||
selected_mask = mask[start_frame:end_frame]
|
||||
|
||||
logger.info(f"Saving {len(selected_images)} frames as {format} video with {codec} codec")
|
||||
|
||||
# 创建输出目录
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
timestamp = int(time.time())
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
output_filename = f"{filename_prefix}_{timestamp}_{unique_id}.{format}"
|
||||
output_path = os.path.join(output_dir, output_filename)
|
||||
|
||||
try:
|
||||
# 根据编码器选择不同的保存方式
|
||||
if codec == "png":
|
||||
self._save_with_png_codec(selected_images, selected_mask,
|
||||
output_path, fps, audio, loop_count)
|
||||
else:
|
||||
self._save_with_video_codec(selected_images, selected_mask,
|
||||
output_path, fps, quality, codec, format, audio, loop_count)
|
||||
|
||||
# 生成预览URL
|
||||
preview_url = f"/view?filename={output_filename}&type=output&format=video/{format}"
|
||||
|
||||
logger.info(f"Video saved successfully: {output_path}")
|
||||
return (output_path, preview_url)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save video: {str(e)}")
|
||||
raise e
|
||||
|
||||
def _save_with_png_codec(self, images, mask, output_path, fps, audio, loop_count):
|
||||
"""使用PNG编码器保存,适合保留alpha通道"""
|
||||
|
||||
# 创建临时目录存储PNG序列
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 保存每一帧为PNG
|
||||
for i, img_tensor in enumerate(images):
|
||||
frame_path = os.path.join(temp_dir, f"frame_{i:06d}.png")
|
||||
|
||||
# 转换张量到PIL图像
|
||||
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
|
||||
img_pil = Image.fromarray(img_np, mode='RGB')
|
||||
|
||||
# 如果有mask,添加alpha通道
|
||||
if mask is not None:
|
||||
alpha_np = (mask[i].cpu().numpy() * 255).astype(np.uint8)
|
||||
img_pil = img_pil.convert('RGBA')
|
||||
img_pil.putalpha(Image.fromarray(alpha_np, mode='L'))
|
||||
|
||||
img_pil.save(frame_path, "PNG")
|
||||
|
||||
# 使用ffmpeg将PNG序列转换为视频
|
||||
input_pattern = os.path.join(temp_dir, "frame_%06d.png")
|
||||
self._ffmpeg_encode(input_pattern, output_path, fps, "png", None, "mov", audio, loop_count, is_sequence=True)
|
||||
|
||||
def _save_with_video_codec(self, images, mask, output_path, fps, quality, codec, format, audio, loop_count):
|
||||
"""使用视频编码器保存"""
|
||||
|
||||
# 创建临时目录
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# 如果有alpha通道,合并到图像中
|
||||
if mask is not None:
|
||||
# 将RGB图像和alpha mask合并为RGBA
|
||||
rgba_images = []
|
||||
for i in range(len(images)):
|
||||
rgb = images[i].cpu().numpy() # (H, W, 3)
|
||||
alpha = mask[i].cpu().numpy() # (H, W)
|
||||
rgba = np.concatenate([rgb, alpha[..., np.newaxis]], axis=-1) # (H, W, 4)
|
||||
rgba_images.append(rgba)
|
||||
|
||||
# 保存为带alpha的临时视频
|
||||
temp_raw_path = os.path.join(temp_dir, "temp_rgba.raw")
|
||||
self._save_raw_rgba(rgba_images, temp_raw_path)
|
||||
|
||||
# 使用ffmpeg编码
|
||||
height, width = rgba_images[0].shape[:2]
|
||||
self._ffmpeg_encode_raw_rgba(temp_raw_path, output_path, width, height, fps, quality, codec, format, audio, loop_count)
|
||||
else:
|
||||
# 只有RGB,保存为临时PNG序列然后编码
|
||||
for i, img_tensor in enumerate(images):
|
||||
frame_path = os.path.join(temp_dir, f"frame_{i:06d}.png")
|
||||
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
|
||||
img_pil = Image.fromarray(img_np, mode='RGB')
|
||||
img_pil.save(frame_path, "PNG")
|
||||
|
||||
input_pattern = os.path.join(temp_dir, "frame_%06d.png")
|
||||
self._ffmpeg_encode(input_pattern, output_path, fps, codec, quality, format, audio, loop_count, is_sequence=True)
|
||||
|
||||
def _save_raw_rgba(self, rgba_images, output_path):
|
||||
"""保存RGBA图像序列为原始数据"""
|
||||
with open(output_path, 'wb') as f:
|
||||
for rgba in rgba_images:
|
||||
# 转换为uint8并写入
|
||||
rgba_uint8 = (rgba * 255).astype(np.uint8)
|
||||
f.write(rgba_uint8.tobytes())
|
||||
|
||||
def _ffmpeg_encode_raw_rgba(self, input_path, output_path, width, height, fps, quality, codec, format, audio, loop_count):
|
||||
"""从原始RGBA数据编码视频"""
|
||||
|
||||
cmd = [ffmpeg_path]
|
||||
|
||||
# 输入参数
|
||||
cmd.extend([
|
||||
'-f', 'rawvideo',
|
||||
'-pixel_format', 'rgba',
|
||||
'-video_size', f'{width}x{height}',
|
||||
'-framerate', str(fps),
|
||||
'-i', input_path
|
||||
])
|
||||
|
||||
# 添加音频输入
|
||||
if audio is not None:
|
||||
audio_path = self._save_temp_audio(audio)
|
||||
cmd.extend(['-i', audio_path])
|
||||
|
||||
# 编码参数
|
||||
if codec == "prores_ks":
|
||||
cmd.extend(['-c:v', 'prores_ks', '-profile:v', '4444', '-pix_fmt', 'yuva444p10le'])
|
||||
elif codec == "qtrle":
|
||||
cmd.extend(['-c:v', 'qtrle', '-pix_fmt', 'rgba'])
|
||||
|
||||
if quality is not None and codec not in ["png", "qtrle"]:
|
||||
cmd.extend(['-crf', str(quality)])
|
||||
|
||||
# 循环参数
|
||||
if loop_count > 0:
|
||||
cmd.extend(['-loop', str(loop_count)])
|
||||
|
||||
# 音频参数
|
||||
if audio is not None:
|
||||
cmd.extend(['-c:a', 'aac'])
|
||||
|
||||
# 输出
|
||||
cmd.extend(['-y', output_path])
|
||||
|
||||
logger.info(f"FFmpeg command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, check=True)
|
||||
logger.info("FFmpeg encoding completed successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr.decode(*ENCODE_ARGS) if e.stderr else "Unknown FFmpeg error"
|
||||
raise Exception(f"FFmpeg encoding failed: {error_msg}")
|
||||
|
||||
def _ffmpeg_encode(self, input_path, output_path, fps, codec, quality, format, audio, loop_count, is_sequence=False):
|
||||
"""使用ffmpeg编码视频"""
|
||||
|
||||
cmd = [ffmpeg_path]
|
||||
|
||||
# 输入参数
|
||||
if is_sequence:
|
||||
cmd.extend(['-framerate', str(fps), '-i', input_path])
|
||||
else:
|
||||
cmd.extend(['-i', input_path])
|
||||
|
||||
# 添加音频输入
|
||||
if audio is not None:
|
||||
audio_path = self._save_temp_audio(audio)
|
||||
cmd.extend(['-i', audio_path])
|
||||
|
||||
# 编码参数
|
||||
if codec == "prores_ks":
|
||||
cmd.extend(['-c:v', 'prores_ks', '-profile:v', '4444'])
|
||||
if format == "mov":
|
||||
cmd.extend(['-pix_fmt', 'yuva444p10le']) # 支持alpha
|
||||
elif codec == "png":
|
||||
cmd.extend(['-c:v', 'png'])
|
||||
elif codec == "qtrle":
|
||||
cmd.extend(['-c:v', 'qtrle'])
|
||||
|
||||
if quality is not None and codec not in ["png", "qtrle"]:
|
||||
cmd.extend(['-crf', str(quality)])
|
||||
|
||||
# 帧率
|
||||
cmd.extend(['-r', str(fps)])
|
||||
|
||||
# 循环参数
|
||||
if loop_count > 0:
|
||||
cmd.extend(['-loop', str(loop_count)])
|
||||
|
||||
# 音频参数
|
||||
if audio is not None:
|
||||
cmd.extend(['-c:a', 'aac'])
|
||||
|
||||
# 输出
|
||||
cmd.extend(['-y', output_path])
|
||||
|
||||
logger.info(f"FFmpeg command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, check=True)
|
||||
logger.info("FFmpeg encoding completed successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr.decode(*ENCODE_ARGS) if e.stderr else "Unknown FFmpeg error"
|
||||
raise Exception(f"FFmpeg encoding failed: {error_msg}")
|
||||
|
||||
def _save_temp_audio(self, audio):
|
||||
"""保存临时音频文件"""
|
||||
# 这里假设audio是一个包含音频数据的字典
|
||||
# 实际实现需要根据ComfyUI的音频格式进行调整
|
||||
temp_audio_path = os.path.join(tempfile.gettempdir(), f"temp_audio_{uuid.uuid4().hex}.wav")
|
||||
# TODO: 实现音频保存逻辑
|
||||
return temp_audio_path
|
||||
|
||||
|
||||
class KY_SaveImageSequence:
|
||||
"""保存图像序列为PNG文件"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "KY_sequence"}),
|
||||
"format": (["png", "jpg", "tiff", "webp"], {"default": "png"}),
|
||||
"quality": ("INT", {"default": 95, "min": 1, "max": 100}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",),
|
||||
"start_frame": ("INT", {"default": 0, "min": 0, "max": BIGMAX}),
|
||||
"end_frame": ("INT", {"default": -1, "min": -1, "max": BIGMAX}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT")
|
||||
RETURN_NAMES = ("output_dir", "frame_count")
|
||||
FUNCTION = "save_images"
|
||||
CATEGORY = "KY_Video"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def save_images(self, images, filename_prefix, format, quality, mask=None, start_frame=0, end_frame=-1):
|
||||
"""保存图像序列"""
|
||||
|
||||
# 处理帧范围
|
||||
total_frames = len(images)
|
||||
if end_frame == -1:
|
||||
end_frame = total_frames
|
||||
else:
|
||||
end_frame = min(end_frame, total_frames)
|
||||
|
||||
if start_frame >= end_frame:
|
||||
raise ValueError(f"Invalid frame range: start_frame={start_frame}, end_frame={end_frame}")
|
||||
|
||||
# 选择帧范围
|
||||
selected_images = images[start_frame:end_frame]
|
||||
selected_mask = None
|
||||
if mask is not None:
|
||||
selected_mask = mask[start_frame:end_frame]
|
||||
|
||||
# 创建输出目录
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
timestamp = int(time.time())
|
||||
sequence_dir = os.path.join(output_dir, f"{filename_prefix}_{timestamp}")
|
||||
os.makedirs(sequence_dir, exist_ok=True)
|
||||
|
||||
logger.info(f"Saving {len(selected_images)} images to {sequence_dir}")
|
||||
|
||||
# 保存每一帧
|
||||
for i, img_tensor in enumerate(selected_images):
|
||||
frame_filename = f"{filename_prefix}_{i:06d}.{format}"
|
||||
frame_path = os.path.join(sequence_dir, frame_filename)
|
||||
|
||||
# 转换张量到PIL图像
|
||||
img_np = (img_tensor.cpu().numpy() * 255).astype(np.uint8)
|
||||
img_pil = Image.fromarray(img_np, mode='RGB')
|
||||
|
||||
# 如果有mask且格式支持alpha,添加alpha通道
|
||||
if mask is not None and selected_mask is not None and format in ['png', 'tiff', 'webp']:
|
||||
alpha_np = (selected_mask[i].cpu().numpy() * 255).astype(np.uint8)
|
||||
img_pil = img_pil.convert('RGBA')
|
||||
img_pil.putalpha(Image.fromarray(alpha_np, mode='L'))
|
||||
|
||||
# 保存图像
|
||||
if format in ['jpg', 'jpeg']:
|
||||
img_pil = img_pil.convert('RGB') # JPEG不支持alpha
|
||||
img_pil.save(frame_path, format.upper(), quality=quality)
|
||||
elif format == 'png':
|
||||
img_pil.save(frame_path, "PNG")
|
||||
elif format == 'webp':
|
||||
img_pil.save(frame_path, "WEBP", quality=quality)
|
||||
elif format == 'tiff':
|
||||
img_pil.save(frame_path, "TIFF")
|
||||
|
||||
logger.info(f"Saved {len(selected_images)} images successfully")
|
||||
return (sequence_dir, len(selected_images))
|
||||
|
||||
|
||||
# 节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_SaveVideo": KY_SaveVideo,
|
||||
"KY_SaveImageSequence": KY_SaveImageSequence,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_SaveVideo": "KY Save Video (Alpha Support)",
|
||||
"KY_SaveImageSequence": "KY Save Image Sequence",
|
||||
}
|
||||
|
||||
+29
-16
@@ -43,7 +43,11 @@ class OpenAICaptionImage:
|
||||
),
|
||||
"caption_prompt": (
|
||||
"STRING",
|
||||
{"default": "Describe this image without any speculations"},
|
||||
{"multiline": True, "default": "Describe this image without any speculations"},
|
||||
),
|
||||
"response_format": (
|
||||
["text", "json_object"],
|
||||
{"default": "text"},
|
||||
),
|
||||
"max_tokens": ("INT", {"default": 200}),
|
||||
"temperature": ("FLOAT", {"default": 0.5}),
|
||||
@@ -69,6 +73,7 @@ class OpenAICaptionImage:
|
||||
ollama_model,
|
||||
system_prompt,
|
||||
caption_prompt,
|
||||
response_format,
|
||||
max_tokens,
|
||||
temperature,
|
||||
top_p,
|
||||
@@ -97,12 +102,12 @@ class OpenAICaptionImage:
|
||||
return self._caption_openai(
|
||||
pil_image, custom_model, system_prompt, caption_prompt,
|
||||
max_tokens, temperature, top_p, frequency_penalty,
|
||||
presence_penalty, base_url, api_key
|
||||
presence_penalty, base_url, api_key, response_format
|
||||
)
|
||||
|
||||
def _caption_openai(self, pil_image, model, system_prompt, caption_prompt,
|
||||
max_tokens, temperature, top_p, frequency_penalty,
|
||||
presence_penalty, base_url, api_key):
|
||||
presence_penalty, base_url, api_key, response_format="text"):
|
||||
# Convert PIL Image to base64
|
||||
buffered = io.BytesIO()
|
||||
pil_image.save(buffered, format="PNG")
|
||||
@@ -111,10 +116,10 @@ class OpenAICaptionImage:
|
||||
# Set up OpenAI client
|
||||
client = openai.OpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
# Make API call to OpenAI
|
||||
response = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
# Prepare API call parameters
|
||||
params = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{
|
||||
"role": "user",
|
||||
@@ -127,13 +132,21 @@ class OpenAICaptionImage:
|
||||
],
|
||||
},
|
||||
],
|
||||
timeout=30,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
frequency_penalty=frequency_penalty,
|
||||
presence_penalty=presence_penalty,
|
||||
)
|
||||
"timeout": 30,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
"top_p": top_p,
|
||||
"frequency_penalty": frequency_penalty,
|
||||
"presence_penalty": presence_penalty,
|
||||
}
|
||||
|
||||
# Add response_format if json_object is selected
|
||||
if response_format == "json_object":
|
||||
params["response_format"] = {"type": "json_object"}
|
||||
|
||||
# Make API call to OpenAI
|
||||
response = client.chat.completions.create(**params)
|
||||
|
||||
if response.choices[0].message.content is None:
|
||||
raise ValueError("No content in response")
|
||||
|
||||
@@ -493,13 +506,13 @@ class OpenAIChat:
|
||||
raise ValueError(f"Ollama API error: {str(e)}")
|
||||
|
||||
|
||||
LLM_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_OpenAICaptionImage": OpenAICaptionImage,
|
||||
"KY_OpenAICaptionImages": OpenAICaptionImages,
|
||||
"KY_OpenAIChat": OpenAIChat,
|
||||
}
|
||||
|
||||
LLM_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_OpenAICaptionImage": "KY Caption Image by openai-protocol local LLM services",
|
||||
"KY_OpenAICaptionImages": "KY Caption Images Batch by openai-protocol local LLM services",
|
||||
"KY_OpenAIChat": "KY Chat with openai-protocol local LLM services",
|
||||
@@ -785,7 +785,7 @@ class KY_MergeToJSON:
|
||||
return result
|
||||
|
||||
|
||||
UTIL_NODE_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_JoinToString": KY_JoinToString,
|
||||
"KY_RegexReplace": KY_RegexReplace,
|
||||
"KY_RegexExtractor": KY_RegexExtractor,
|
||||
@@ -797,7 +797,7 @@ UTIL_NODE_CLASS_MAPPINGS = {
|
||||
"KY_First_NOT_EMPTY": KY_First_NOT_EMPTY,
|
||||
}
|
||||
|
||||
UTIL_NODE_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_JoinToString": "Join any into string",
|
||||
"KY_RegexReplace": "Replace text by regex",
|
||||
"KY_RegexExtractor": "Extract text by regex",
|
||||
@@ -428,7 +428,7 @@ bboxes input will override the shape and width/height parameters.
|
||||
outstack = torch.cat(out, dim=0)
|
||||
return (outstack, 1.0 - outstack,)
|
||||
|
||||
BBOX_NODE_CLASS_MAPPINGS = {
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KY_BBoxesToSAM2": BBoxesToSAM2,
|
||||
"KY_restoreBBox": restoreBBoxFrom,
|
||||
"KY_toBBox": XYWHtoBBox,
|
||||
@@ -439,7 +439,7 @@ BBOX_NODE_CLASS_MAPPINGS = {
|
||||
"KY_BBoxPosition": BBoxPosition,
|
||||
}
|
||||
|
||||
BBOX_NODE_NAME_MAPPINGS = {
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"KY_BBoxesToSAM2": "Prepare BBoxes for SAM2",
|
||||
"KY_restoreBBox": "restore bbox from VLM scale",
|
||||
"KY_toBBox": "Convert X Y WH into box",
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
import "./../ace.js"; ace.define("ace/snippets/python.snippets",["require","exports","module"],function(e,t,n){n.exports='snippet #!\n #!/usr/bin/env python\nsnippet imp\n import ${1:module}\nsnippet from\n from ${1:package} import ${2:module}\n# Module Docstring\nsnippet docs\n \'\'\'\n File: ${1:FILENAME:file_name}\n Author: ${2:author}\n Description: ${3}\n \'\'\'\nsnippet wh\n while ${1:condition}:\n ${2:# TODO: write code...}\n# dowh - does the same as do...while in other languages\nsnippet dowh\n while True:\n ${1:# TODO: write code...}\n if ${2:condition}:\n break\nsnippet with\n with ${1:expr} as ${2:var}:\n ${3:# TODO: write code...}\n# New Class\nsnippet cl\n class ${1:ClassName}(${2:object}):\n """${3:docstring for $1}"""\n def __init__(self, ${4:arg}):\n ${5:super($1, self).__init__()}\n self.$4 = $4\n ${6}\n# New Function\nsnippet def\n def ${1:fname}(${2:`indent(\'.\') ? \'self\' : \'\'`}):\n """${3:docstring for $1}"""\n ${4:# TODO: write code...}\nsnippet deff\n def ${1:fname}(${2:`indent(\'.\') ? \'self\' : \'\'`}):\n ${3:# TODO: write code...}\n# New Method\nsnippet defs\n def ${1:mname}(self, ${2:arg}):\n ${3:# TODO: write code...}\n# New Property\nsnippet property\n def ${1:foo}():\n doc = "${2:The $1 property.}"\n def fget(self):\n ${3:return self._$1}\n def fset(self, value):\n ${4:self._$1 = value}\n# Ifs\nsnippet if\n if ${1:condition}:\n ${2:# TODO: write code...}\nsnippet el\n else:\n ${1:# TODO: write code...}\nsnippet ei\n elif ${1:condition}:\n ${2:# TODO: write code...}\n# For\nsnippet for\n for ${1:item} in ${2:items}:\n ${3:# TODO: write code...}\n# Encodes\nsnippet cutf8\n # -*- coding: utf-8 -*-\nsnippet clatin1\n # -*- coding: latin-1 -*-\nsnippet cascii\n # -*- coding: ascii -*-\n# Lambda\nsnippet ld\n ${1:var} = lambda ${2:vars} : ${3:action}\nsnippet .\n self.\nsnippet try Try/Except\n try:\n ${1:# TODO: write code...}\n except ${2:Exception}, ${3:e}:\n ${4:raise $3}\nsnippet try Try/Except/Else\n try:\n ${1:# TODO: write code...}\n except ${2:Exception}, ${3:e}:\n ${4:raise $3}\n else:\n ${5:# TODO: write code...}\nsnippet try Try/Except/Finally\n try:\n ${1:# TODO: write code...}\n except ${2:Exception}, ${3:e}:\n ${4:raise $3}\n finally:\n ${5:# TODO: write code...}\nsnippet try Try/Except/Else/Finally\n try:\n ${1:# TODO: write code...}\n except ${2:Exception}, ${3:e}:\n ${4:raise $3}\n else:\n ${5:# TODO: write code...}\n finally:\n ${6:# TODO: write code...}\n# if __name__ == \'__main__\':\nsnippet ifmain\n if __name__ == \'__main__\':\n ${1:main()}\n# __magic__\nsnippet _\n __${1:init}__${2}\n# python debugger (pdb)\nsnippet pdb\n import pdb; pdb.set_trace()\n# ipython debugger (ipdb)\nsnippet ipdb\n import ipdb; ipdb.set_trace()\n# ipython debugger (pdbbb)\nsnippet pdbbb\n import pdbpp; pdbpp.set_trace()\nsnippet pprint\n import pprint; pprint.pprint(${1})${2}\nsnippet "\n """\n ${1:doc}\n """\n# test function/method\nsnippet test\n def test_${1:description}(${2:self}):\n ${3:# TODO: write code...}\n# test case\nsnippet testcase\n class ${1:ExampleCase}(unittest.TestCase):\n \n def test_${2:description}(self):\n ${3:# TODO: write code...}\nsnippet fut\n from __future__ import ${1}\n#getopt\nsnippet getopt\n try:\n # Short option syntax: "hv:"\n # Long option syntax: "help" or "verbose="\n opts, args = getopt.getopt(sys.argv[1:], "${1:short_options}", [${2:long_options}])\n \n except getopt.GetoptError, err:\n # Print debug info\n print str(err)\n ${3:error_action}\n\n for option, argument in opts:\n if option in ("-h", "--help"):\n ${4}\n elif option in ("-v", "--verbose"):\n verbose = argument\n'}),ace.define("ace/snippets/python",["require","exports","module","ace/snippets/python.snippets"],function(e,t,n){"use strict";t.snippetText=e("./python.snippets"),t.scope="python"}); (function() {
|
||||
ace.require(["ace/snippets/python"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import "./ace.js";
|
||||
ace.define("ace/theme/monokai-css",["require","exports","module"],function(e,t,n){n.exports=".ace-monokai .ace_gutter {\n background: #2F3129;\n color: #8F908A\n}\n\n.ace-monokai .ace_print-margin {\n width: 1px;\n background: #555651\n}\n\n.ace-monokai {\n background-color: #272822;\n color: #F8F8F2\n}\n\n.ace-monokai .ace_cursor {\n color: #F8F8F0\n}\n\n.ace-monokai .ace_marker-layer .ace_selection {\n background: #49483E\n}\n\n.ace-monokai.ace_multiselect .ace_selection.ace_start {\n box-shadow: 0 0 3px 0px #272822;\n}\n\n.ace-monokai .ace_marker-layer .ace_step {\n background: rgb(102, 82, 0)\n}\n\n.ace-monokai .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #49483E\n}\n\n.ace-monokai .ace_marker-layer .ace_active-line {\n background: #202020\n}\n\n.ace-monokai .ace_gutter-active-line {\n background-color: #272727\n}\n\n.ace-monokai .ace_marker-layer .ace_selected-word {\n border: 1px solid #49483E\n}\n\n.ace-monokai .ace_invisible {\n color: #52524d\n}\n\n.ace-monokai .ace_entity.ace_name.ace_tag,\n.ace-monokai .ace_keyword,\n.ace-monokai .ace_meta.ace_tag,\n.ace-monokai .ace_storage {\n color: #F92672\n}\n\n.ace-monokai .ace_punctuation,\n.ace-monokai .ace_punctuation.ace_tag {\n color: #fff\n}\n\n.ace-monokai .ace_constant.ace_character,\n.ace-monokai .ace_constant.ace_language,\n.ace-monokai .ace_constant.ace_numeric,\n.ace-monokai .ace_constant.ace_other {\n color: #AE81FF\n}\n\n.ace-monokai .ace_invalid {\n color: #F8F8F0;\n background-color: #F92672\n}\n\n.ace-monokai .ace_invalid.ace_deprecated {\n color: #F8F8F0;\n background-color: #AE81FF\n}\n\n.ace-monokai .ace_support.ace_constant,\n.ace-monokai .ace_support.ace_function {\n color: #66D9EF\n}\n\n.ace-monokai .ace_fold {\n background-color: #A6E22E;\n border-color: #F8F8F2\n}\n\n.ace-monokai .ace_storage.ace_type,\n.ace-monokai .ace_support.ace_class,\n.ace-monokai .ace_support.ace_type {\n font-style: italic;\n color: #66D9EF\n}\n\n.ace-monokai .ace_entity.ace_name.ace_function,\n.ace-monokai .ace_entity.ace_other,\n.ace-monokai .ace_entity.ace_other.ace_attribute-name,\n.ace-monokai .ace_variable {\n color: #A6E22E\n}\n\n.ace-monokai .ace_variable.ace_parameter {\n font-style: italic;\n color: #FD971F\n}\n\n.ace-monokai .ace_string {\n color: #E6DB74\n}\n\n.ace-monokai .ace_comment {\n color: #75715E\n}\n\n.ace-monokai .ace_indent-guide {\n background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y\n}\n\n.ace-monokai .ace_indent-guide-active {\n background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQIW2PQ1dX9zzBz5sz/ABCcBFFentLlAAAAAElFTkSuQmCC) right repeat-y;\n}\n"}),ace.define("ace/theme/monokai",["require","exports","module","ace/theme/monokai-css","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-monokai",t.cssText=e("./monokai-css");var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass,!1)}); (function() {
|
||||
ace.require(["ace/theme/monokai"], function(m) {
|
||||
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||
module.exports = m;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
// Make modal window
|
||||
function makeModal({
|
||||
title = "Message",
|
||||
text = "No text",
|
||||
type = "info",
|
||||
parent = null,
|
||||
stylePos = "fixed",
|
||||
classes = [],
|
||||
} = {}) {
|
||||
const overlay = document.createElement("div");
|
||||
Object.assign(overlay.style, {
|
||||
display: "none",
|
||||
position: stylePos,
|
||||
background: "rgba(0 0 0 / 0.8)",
|
||||
opacity: 0,
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
bottom: "0",
|
||||
zIndex: "500",
|
||||
transition: "all .8s",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
const boxModal = document.createElement("div");
|
||||
Object.assign(boxModal.style, {
|
||||
transition: "all 0.5s",
|
||||
opacity: 0,
|
||||
display: "none",
|
||||
position: stylePos,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%,-50%)",
|
||||
background: "#525252",
|
||||
minWidth: "300px",
|
||||
fontFamily: "sans-serif",
|
||||
zIndex: "501",
|
||||
border: "1px solid rgb(255 255 255 / 45%)",
|
||||
});
|
||||
|
||||
boxModal.className = "alekpet_modal_window";
|
||||
boxModal.classList.add(...classes);
|
||||
|
||||
const boxModalBody = document.createElement("div");
|
||||
Object.assign(boxModalBody.style, {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
textAlign: "center",
|
||||
});
|
||||
|
||||
boxModalBody.className = "alekpet_modal_body";
|
||||
|
||||
const boxModalHtml = `
|
||||
<div class="alekpet_modal_header" style="display: flex; align-items: center; background: #222; width: 100%;justify-content: center;">
|
||||
<div class="alekpet_modal_title" style="flex-basis: 85%; text-align: center;padding: 5px;">${title}</div>
|
||||
<div class="alekpet_modal_close">✕</div>
|
||||
</div>
|
||||
<div class="alekpet_modal_description" style="padding: 8px;">${text}</div>`;
|
||||
boxModalBody.innerHTML = boxModalHtml;
|
||||
|
||||
const alekpet_modal_header = boxModalBody.querySelector(
|
||||
".alekpet_modal_header"
|
||||
);
|
||||
Object.assign(alekpet_modal_header.style, {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
const close = boxModalBody.querySelector(".alekpet_modal_close");
|
||||
Object.assign(close.style, {
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
let parentElement = document.body;
|
||||
if (parent && parent.nodeType === 1) {
|
||||
parentElement = parent;
|
||||
}
|
||||
|
||||
boxModal.append(boxModalBody);
|
||||
parentElement.append(overlay, boxModal);
|
||||
|
||||
const removeEvent = new Event("removeElements");
|
||||
const remove = () => {
|
||||
animateTransitionProps(boxModal, { opacity: 0 }).then(() =>
|
||||
animateTransitionProps(overlay, { opacity: 0 }).then(() => {
|
||||
parentElement.removeChild(boxModal);
|
||||
parentElement.removeChild(overlay);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
boxModal.addEventListener("removeElements", remove);
|
||||
overlay.addEventListener("removeElements", remove);
|
||||
|
||||
animateTransitionProps(overlay)
|
||||
.then(() => {
|
||||
overlay.addEventListener("click", () => {
|
||||
overlay.dispatchEvent(removeEvent);
|
||||
});
|
||||
animateTransitionProps(boxModal);
|
||||
})
|
||||
.then(() =>
|
||||
boxModal
|
||||
.querySelector(".alekpet_modal_close")
|
||||
.addEventListener("click", () => boxModal.dispatchEvent(removeEvent))
|
||||
);
|
||||
}
|
||||
|
||||
function findWidget(node, value, attr = "name", func = "find") {
|
||||
return node?.widgets
|
||||
? node.widgets[func]((w) =>
|
||||
Array.isArray(value) ? value.includes(w[attr]) : w[attr] === value
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
function animateTransitionProps(
|
||||
el,
|
||||
props = { opacity: 1 },
|
||||
preStyles = { display: "block" }
|
||||
) {
|
||||
Object.assign(el.style, preStyles);
|
||||
|
||||
el.style.transition =
|
||||
!el.style.transition ||
|
||||
!window.getComputedStyle(el).getPropertyValue("transition")
|
||||
? "all .8s"
|
||||
: el.style.transition;
|
||||
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
Object.assign(el.style, props);
|
||||
|
||||
const transstart = () => (el.isAnimating = true);
|
||||
const transchancel = () => (el.isAnimating = false);
|
||||
el.addEventListener("transitionstart", transstart);
|
||||
el.addEventListener("transitioncancel", transchancel);
|
||||
|
||||
el.addEventListener("transitionend", function transend() {
|
||||
el.isAnimating = false;
|
||||
el.removeEventListener("transitionend", transend);
|
||||
el.removeEventListener("transitionend", transchancel);
|
||||
el.removeEventListener("transitionend", transstart);
|
||||
res(el);
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function animateClick(target, params = {}) {
|
||||
const { opacityVal = 0.9, callback = () => {} } = params;
|
||||
if (target?.isAnimating) return;
|
||||
|
||||
const hide = +target.style.opacity === 0;
|
||||
return animateTransitionProps(target, {
|
||||
opacity: hide ? opacityVal : 0,
|
||||
}).then((el) => {
|
||||
const isHide = hide || el.style.display === "none";
|
||||
showHide({ elements: [target], hide: !hide });
|
||||
callback();
|
||||
return isHide;
|
||||
});
|
||||
}
|
||||
|
||||
function showHide({ elements = [], hide = null, displayProp = "block" } = {}) {
|
||||
Array.from(elements).forEach((el) => {
|
||||
if (hide !== null) {
|
||||
el.style.display = !hide ? displayProp : "none";
|
||||
} else {
|
||||
el.style.display =
|
||||
!el.style.display || el.style.display === "none" ? displayProp : "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
if (!obj) return true;
|
||||
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
}
|
||||
|
||||
function makeElement(tag, attrs = {}) {
|
||||
if (!tag) tag = "div";
|
||||
const element = document.createElement(tag);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
const currValue = attrs[key];
|
||||
if (key === "class") {
|
||||
if (Array.isArray(currValue)) {
|
||||
element.classList.add(...currValue);
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element.className = currValue;
|
||||
}
|
||||
} else if (key === "dataset") {
|
||||
try {
|
||||
if (Array.isArray(currValue)) {
|
||||
currValue.forEach((datasetArr) => {
|
||||
const [prop, propval] = Object.entries(datasetArr)[0];
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
} else {
|
||||
Object.entries(currValue).forEach((datasetArr) => {
|
||||
const [prop, propval] = datasetArr;
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
} else if (key === "style") {
|
||||
if (
|
||||
typeof currValue === "object" &&
|
||||
!Array.isArray(currValue) &&
|
||||
Object.keys(currValue).length
|
||||
) {
|
||||
Object.assign(element[key], currValue);
|
||||
} else if (
|
||||
typeof currValue === "object" &&
|
||||
Array.isArray(currValue) &&
|
||||
currValue.length
|
||||
) {
|
||||
element[key] = [...currValue];
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element[key] = currValue;
|
||||
}
|
||||
} else if (["for"].includes(key)) {
|
||||
element.setAttribute(key, currValue);
|
||||
} else if (key === "children") {
|
||||
element.append(...(currValue instanceof Array ? currValue : [currValue]));
|
||||
} else if (key === "parent") {
|
||||
currValue.append(element);
|
||||
} else {
|
||||
element[key] = currValue;
|
||||
}
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
function isValidStyle(opt, strColor) {
|
||||
let op = new Option().style;
|
||||
if (!op.hasOwnProperty(opt))
|
||||
return { result: false, color: "", color_hex: "" };
|
||||
|
||||
op[opt] = strColor;
|
||||
|
||||
return {
|
||||
result: op[opt] !== "",
|
||||
color_rgb: op[opt],
|
||||
color_hex: rgbToHex(op[opt]),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
const regEx = new RegExp(/\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
|
||||
if (regEx.test(rgb)) {
|
||||
let [, r, g, b] = regEx.exec(rgb);
|
||||
r = parseInt(r).toString(16);
|
||||
g = parseInt(g).toString(16);
|
||||
b = parseInt(b).toString(16);
|
||||
|
||||
r = r.length === 1 ? r + "0" : r;
|
||||
g = g.length === 1 ? g + "0" : g;
|
||||
b = b.length === 1 ? b + "0" : b;
|
||||
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDataJSON(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
} catch (err) {
|
||||
return new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function deepMerge(target, source) {
|
||||
if (source?.nodeType) return;
|
||||
for (let key in source) {
|
||||
if (source[key] instanceof Object && key in target) {
|
||||
Object.assign(source[key], deepMerge(target[key], source[key]));
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(target || {}, source);
|
||||
return target;
|
||||
}
|
||||
|
||||
const THEME_MODAL_WINDOW_BASE = {
|
||||
stylesTitle: {
|
||||
background: "auto",
|
||||
padding: "5px",
|
||||
borderRadius: "6px",
|
||||
marginBottom: "5px",
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
stylesWrapper: {
|
||||
display: "none",
|
||||
opacity: 0,
|
||||
minWidth: "220px",
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
opacity: 0,
|
||||
transition: "all .8s",
|
||||
fontFamily: "monospace",
|
||||
zIndex: 99999,
|
||||
},
|
||||
stylesBox: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "#0e0e0e",
|
||||
padding: "6px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
textAlign: "center",
|
||||
borderRadius: "6px",
|
||||
color: "white",
|
||||
border: "2px solid silver",
|
||||
boxShadow: "2px 2px 4px silver",
|
||||
maxWidth: "300px",
|
||||
},
|
||||
stylesClose: {
|
||||
position: "absolute",
|
||||
top: "-10px",
|
||||
right: "-10px",
|
||||
background: "silver",
|
||||
borderRadius: "50%",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
fontSize: "0.8rem",
|
||||
},
|
||||
};
|
||||
|
||||
const THEMES_MODAL_WINDOW = {
|
||||
error: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#8f210f",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#3b2222",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #f91b1b",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#3b2222",
|
||||
},
|
||||
},
|
||||
warning: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#e99818",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#594e32",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #e99818",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#594e32",
|
||||
},
|
||||
},
|
||||
normal: {
|
||||
stylesTitle: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "#108f0f",
|
||||
},
|
||||
stylesBox: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
background: "#223b2a",
|
||||
boxShadow: "3px 3px 6px #141414",
|
||||
border: "1px solid #108f0f",
|
||||
},
|
||||
stylesWrapper: { ...THEME_MODAL_WINDOW_BASE.stylesWrapper },
|
||||
stylesClose: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
background: "#223b2a",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
auto: {
|
||||
autohide: false,
|
||||
autoshow: false,
|
||||
autoremove: false,
|
||||
propStyles: { opacity: 0 },
|
||||
propPreStyles: {},
|
||||
timewait: 2000,
|
||||
},
|
||||
overlay: {
|
||||
overlay_enabled: false,
|
||||
overlayClasses: [],
|
||||
overlayStyles: {},
|
||||
},
|
||||
close: { closeRemove: false, showClose: true },
|
||||
parent: null,
|
||||
};
|
||||
|
||||
function createWindowModal({
|
||||
textTitle = "Message",
|
||||
textBody = "Hello world!",
|
||||
textFooter = null,
|
||||
classesWrapper = [],
|
||||
stylesWrapper = {},
|
||||
classesBox = [],
|
||||
stylesBox = {},
|
||||
classesTitle = [],
|
||||
stylesTitle = {},
|
||||
classesBody = [],
|
||||
stylesBody = {},
|
||||
classesClose = [],
|
||||
stylesClose = {},
|
||||
classesFooter = [],
|
||||
stylesFooter = {},
|
||||
options = defaultOptions,
|
||||
} = {}) {
|
||||
// Check all options exist
|
||||
const _options = deepMerge(
|
||||
JSON.parse(JSON.stringify(defaultOptions)),
|
||||
options
|
||||
);
|
||||
|
||||
const {
|
||||
parent,
|
||||
overlay: { overlay_enabled, overlayClasses, overlayStyles },
|
||||
close: { closeRemove, showClose },
|
||||
auto: {
|
||||
autohide,
|
||||
autoshow,
|
||||
autoremove,
|
||||
timewait,
|
||||
propStyles,
|
||||
propPreStyles,
|
||||
},
|
||||
} = _options;
|
||||
|
||||
// Function past text(html)
|
||||
function addText(text, parent) {
|
||||
if (!parent) return;
|
||||
|
||||
switch (typeof text) {
|
||||
case "string":
|
||||
if (/^\<.*\/?\>$/.test(text)) {
|
||||
parent.innerHTML = text;
|
||||
} else {
|
||||
parent.textContent = text;
|
||||
}
|
||||
break;
|
||||
case "object":
|
||||
default:
|
||||
if (Array.isArray(text)) {
|
||||
text.forEach(
|
||||
(element) =>
|
||||
(element.nodeType === 1 || element.nodeType === 3) &&
|
||||
parent.append(element)
|
||||
);
|
||||
} else if (text.nodeType === 1 || text.nodeType === 3)
|
||||
parent.append(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay
|
||||
let overlayElement = null;
|
||||
if (overlay_enabled) {
|
||||
overlayElement = makeElement("div", {
|
||||
class: [...overlayClasses],
|
||||
style: {
|
||||
display: "none",
|
||||
position: "fixed",
|
||||
background: "rgba(0 0 0 / 0.8)",
|
||||
opacity: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 99999,
|
||||
transition: "all .8s",
|
||||
cursor: "pointer",
|
||||
...overlayStyles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper
|
||||
const wrapper_settings = makeElement("div", {
|
||||
class: ["alekpet__wrapper__window", ...classesWrapper],
|
||||
});
|
||||
|
||||
Object.assign(wrapper_settings.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesWrapper,
|
||||
...stylesWrapper,
|
||||
});
|
||||
|
||||
// Box
|
||||
const box__settings = makeElement("div", {
|
||||
class: ["alekpet__window__box", ...classesBox],
|
||||
});
|
||||
Object.assign(box__settings.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
...stylesBox,
|
||||
});
|
||||
|
||||
// Title
|
||||
let box_settings_title = "";
|
||||
if (textTitle) {
|
||||
box_settings_title = makeElement("div", {
|
||||
class: ["alekpet__window__title", ...classesTitle],
|
||||
});
|
||||
|
||||
Object.assign(box_settings_title.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
...stylesTitle,
|
||||
});
|
||||
|
||||
// Add text (html) to title
|
||||
addText(textTitle, box_settings_title);
|
||||
}
|
||||
// Body
|
||||
let box_settings_body = "";
|
||||
if (textBody) {
|
||||
box_settings_body = makeElement("div", {
|
||||
class: ["alekpet__window__body", ...classesBody],
|
||||
});
|
||||
|
||||
Object.assign(box_settings_body.style, {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "5px",
|
||||
textWrap: "wrap",
|
||||
...stylesBody,
|
||||
});
|
||||
|
||||
// Add text (html) to body
|
||||
addText(textBody, box_settings_body);
|
||||
}
|
||||
|
||||
// Close button
|
||||
const close__box__button = makeElement("div", {
|
||||
class: ["close__box__button", ...classesClose],
|
||||
textContent: "✖",
|
||||
});
|
||||
|
||||
Object.assign(close__box__button.style, {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesClose,
|
||||
...stylesClose,
|
||||
});
|
||||
|
||||
if (!showClose) close__box__button.style.display = "none";
|
||||
|
||||
const closeEvent = new Event("closeModal");
|
||||
const closeModalWindow = function () {
|
||||
overlay_enabled
|
||||
? animateTransitionProps(overlayElement, {
|
||||
opacity: 0,
|
||||
})
|
||||
.then(() =>
|
||||
animateTransitionProps(wrapper_settings, {
|
||||
opacity: 0,
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
if (closeRemove) {
|
||||
parent.removeChild(wrapper_settings);
|
||||
parent.removeChild(overlayElement);
|
||||
} else {
|
||||
showHide({ elements: [wrapper_settings, overlayElement] });
|
||||
}
|
||||
})
|
||||
: animateTransitionProps(wrapper_settings, {
|
||||
opacity: 0,
|
||||
}).then(() => {
|
||||
showHide({ elements: [wrapper_settings] });
|
||||
});
|
||||
};
|
||||
|
||||
close__box__button.addEventListener("closeModal", closeModalWindow);
|
||||
|
||||
close__box__button.addEventListener("click", () =>
|
||||
close__box__button.dispatchEvent(closeEvent)
|
||||
);
|
||||
|
||||
close__box__button.onmouseenter = () => {
|
||||
close__box__button.style.opacity = 0.8;
|
||||
};
|
||||
|
||||
close__box__button.onmouseleave = () => {
|
||||
close__box__button.style.opacity = 1;
|
||||
};
|
||||
|
||||
box__settings.append(box_settings_title, box_settings_body);
|
||||
|
||||
// Footer
|
||||
if (textFooter) {
|
||||
const box_settings_footer = makeElement("div", {
|
||||
class: [...classesFooter],
|
||||
});
|
||||
Object.assign(box_settings_footer.style, {
|
||||
...stylesFooter,
|
||||
});
|
||||
|
||||
// Add text (html) to body
|
||||
addText(textFooter, box_settings_footer);
|
||||
|
||||
box__settings.append(box_settings_footer);
|
||||
}
|
||||
|
||||
wrapper_settings.append(close__box__button, box__settings);
|
||||
|
||||
if (parent && parent.nodeType === 1) {
|
||||
if (overlay_enabled) parent.append(overlayElement);
|
||||
parent.append(wrapper_settings);
|
||||
|
||||
if (autoshow) {
|
||||
overlay_enabled
|
||||
? animateClick(overlayElement).then(() =>
|
||||
animateClick(wrapper_settings).then(
|
||||
() =>
|
||||
autohide &&
|
||||
setTimeout(
|
||||
() =>
|
||||
animateTransitionProps(
|
||||
wrapper_settings,
|
||||
{ ...propStyles },
|
||||
{ ...propPreStyles }
|
||||
)
|
||||
.then(() =>
|
||||
animateTransitionProps(
|
||||
overlayElement,
|
||||
{ ...propStyles },
|
||||
{ ...propPreStyles }
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
if (autoremove) {
|
||||
parent.removeChild(wrapper_settings);
|
||||
parent.removeChild(overlayElement);
|
||||
}
|
||||
}),
|
||||
timewait
|
||||
)
|
||||
)
|
||||
)
|
||||
: animateClick(wrapper_settings).then(
|
||||
() =>
|
||||
autohide &&
|
||||
setTimeout(
|
||||
() =>
|
||||
animateTransitionProps(
|
||||
wrapper_settings,
|
||||
{ ...propStyles },
|
||||
{ ...propPreStyles }
|
||||
).then(
|
||||
() => autoremove && parent.removeChild(wrapper_settings)
|
||||
),
|
||||
timewait
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper_settings;
|
||||
}
|
||||
|
||||
// Prompt
|
||||
async function comfyuiDesktopPrompt(title, message, defaultValue) {
|
||||
try {
|
||||
return await app.extensionManager.dialog.prompt({
|
||||
title,
|
||||
message,
|
||||
defaultValue,
|
||||
});
|
||||
} catch (err) {
|
||||
return prompt(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Alert
|
||||
function comfyuiDesktopAlert(message) {
|
||||
try {
|
||||
app.extensionManager.toast.addAlert(message);
|
||||
} catch (err) {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm
|
||||
function confirmModal({ title, message }) {
|
||||
return new Promise((res) => {
|
||||
const overlay = makeElement("div", {
|
||||
class: ["alekpet_confOverlay"],
|
||||
style: {
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
position: "fixed",
|
||||
background: "rgba(0 0 0 / 0.8)",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
userSelect: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const modal = makeElement("div", {
|
||||
class: ["alekpet_confModal"],
|
||||
style: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesBox,
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
fontFamily: "monospace",
|
||||
background: "rgb(92 186 255 / 20%)",
|
||||
transform: "translate(-50%, -50%)",
|
||||
borderColor: "rgba(92, 186, 255, 0.63)",
|
||||
boxShadow: "rgba(92, 186, 255, 0.63) 2px 2px 4px",
|
||||
},
|
||||
});
|
||||
|
||||
const titleEl = makeElement("div", {
|
||||
class: ["alekpet_confTitle"],
|
||||
style: {
|
||||
...THEME_MODAL_WINDOW_BASE.stylesTitle,
|
||||
background: "rgba(92, 186, 255, 0.63)",
|
||||
},
|
||||
textContent: title,
|
||||
});
|
||||
|
||||
const messageEl = makeElement("div", {
|
||||
class: ["alekpet_confMessage"],
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "5px",
|
||||
textWrap: "wrap",
|
||||
},
|
||||
textContent: message,
|
||||
});
|
||||
|
||||
const action_box = makeElement("div", {
|
||||
class: ["alekpet_confActions"],
|
||||
style: {
|
||||
display: "flex",
|
||||
gap: "5px",
|
||||
width: "100%",
|
||||
padding: "4px",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
});
|
||||
|
||||
const remove = () => {
|
||||
modal.remove();
|
||||
overlay.remove();
|
||||
};
|
||||
|
||||
const ok = makeElement("div", {
|
||||
class: ["alekpet_confButtons", "alekpet_confButtonOk"],
|
||||
style: {
|
||||
background: "linear-gradient(45deg, green, limegreen) rgb(21, 100, 6)",
|
||||
},
|
||||
textContent: "Ok",
|
||||
onclick: (e) => {
|
||||
res(true);
|
||||
remove();
|
||||
},
|
||||
});
|
||||
|
||||
const Cancel = makeElement("div", {
|
||||
class: ["alekpet_confButtons", "alekpet_confButtonCancel"],
|
||||
style: {
|
||||
background: "linear-gradient(45deg, #b64396, #a52a8b) rgb(135 3 161)",
|
||||
},
|
||||
textContent: "Cancel",
|
||||
onclick: (e) => {
|
||||
res(false);
|
||||
remove();
|
||||
},
|
||||
});
|
||||
|
||||
action_box.append(ok, Cancel);
|
||||
modal.append(titleEl, messageEl, action_box);
|
||||
overlay.append(modal);
|
||||
document.body.append(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
async function comfyuiDesktopConfirm(message) {
|
||||
try {
|
||||
const result = await confirmModal({
|
||||
title: "Confirm",
|
||||
message: message,
|
||||
});
|
||||
|
||||
// Wait update comfyui frontend! Confirm Cancel not return value! Fixed in ComfyUI_frontend ver. v1.10.8
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2649
|
||||
// const result = await app.extensionManager.dialog.confirm({
|
||||
// title: "Confirm",
|
||||
// message: message,
|
||||
// });
|
||||
return result;
|
||||
} catch (err) {
|
||||
return confirm(message);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
makeModal,
|
||||
createWindowModal,
|
||||
animateTransitionProps,
|
||||
animateClick,
|
||||
showHide,
|
||||
makeElement,
|
||||
getDataJSON,
|
||||
isEmptyObject,
|
||||
isValidStyle,
|
||||
rgbToHex,
|
||||
findWidget,
|
||||
THEMES_MODAL_WINDOW,
|
||||
//
|
||||
comfyuiDesktopConfirm,
|
||||
comfyuiDesktopPrompt,
|
||||
comfyuiDesktopAlert,
|
||||
};
|
||||
@@ -0,0 +1,420 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import "./ace/ace.js";
|
||||
import {
|
||||
createWindowModal,
|
||||
makeElement,
|
||||
findWidget,
|
||||
THEMES_MODAL_WINDOW,
|
||||
comfyuiDesktopConfirm,
|
||||
comfyuiDesktopPrompt,
|
||||
} from "./ace/utils.js";
|
||||
|
||||
ace.config.set("basePath", new URL("./ace/", import.meta.url).toString());
|
||||
|
||||
// Constants
|
||||
const MAX_CHAR_VARNAME = 50;
|
||||
const LIST_THEMES = ["monokai"];
|
||||
const varTypes = [
|
||||
"int",
|
||||
"boolean",
|
||||
"string",
|
||||
"float",
|
||||
"json",
|
||||
"list",
|
||||
"dict",
|
||||
];
|
||||
const typeMap = {
|
||||
int: 'int',
|
||||
boolean: 'bool',
|
||||
string: 'str',
|
||||
float: 'float',
|
||||
json: 'json',
|
||||
list: 'list',
|
||||
dict: 'dict',
|
||||
}
|
||||
const DEFAULT_TEMPLATES = {
|
||||
py: `import re, json, os, traceback
|
||||
from time import strftime
|
||||
def runCode():
|
||||
nowDataTime = strftime("%Y-%m-%d %H:%M:%S")
|
||||
return f"Hello ComfyUI with us today {nowDataTime}!"
|
||||
r0 = runCode()
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
function getPostition(node, ctx, w_width, y, n_height) {
|
||||
const margin = 5;
|
||||
|
||||
const rect = ctx.canvas.getBoundingClientRect();
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y);
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||
|
||||
return {
|
||||
transformOrigin: "0 0",
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e + rect.left}px`,
|
||||
top: `${transform.d + transform.f + rect.top}px`,
|
||||
maxWidth: `${w_width - margin * 2}px`,
|
||||
maxHeight: `${n_height - margin * 2 - y - 15}px`,
|
||||
width: `${w_width - margin * 2}px`,
|
||||
height: "90%",
|
||||
position: "absolute",
|
||||
scrollbarColor: "var(--descrip-text) var(--bg-color)",
|
||||
scrollbarWidth: "thin",
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
};
|
||||
}
|
||||
|
||||
// Create editor code
|
||||
function codeEditor(node, inputName, inputData) {
|
||||
const widget = {
|
||||
type: "pycode",
|
||||
name: inputName,
|
||||
options: { hideOnZoom: true },
|
||||
value:
|
||||
inputData[1]?.default ||
|
||||
`def my(a, b=1):
|
||||
return a * b<br>
|
||||
|
||||
r0 = str(my(23, 9))`,
|
||||
draw(ctx, node, widget_width, y, widget_height) {
|
||||
const hidden =
|
||||
node.flags?.collapsed ||
|
||||
(!!widget.options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||
widget.type === "converted-widget" ||
|
||||
widget.type === "hidden";
|
||||
|
||||
widget.codeElement.hidden = hidden;
|
||||
|
||||
if (hidden) {
|
||||
widget.options.onHide?.(widget);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(
|
||||
this.codeElement.style,
|
||||
getPostition(node, ctx, widget_width, y, node.size[1])
|
||||
);
|
||||
},
|
||||
computeSize(...args) {
|
||||
return [500, 250];
|
||||
},
|
||||
};
|
||||
|
||||
widget.codeElement = makeElement("pre", {
|
||||
innerHTML: widget.value,
|
||||
});
|
||||
|
||||
widget.editor = ace.edit(widget.codeElement);
|
||||
widget.editor.setTheme("ace/theme/monokai");
|
||||
widget.editor.session.setMode("ace/mode/python");
|
||||
widget.editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
fontFamily: "monospace",
|
||||
});
|
||||
widget.codeElement.hidden = true;
|
||||
|
||||
document.body.appendChild(widget.codeElement);
|
||||
|
||||
const collapse = node.collapse;
|
||||
node.collapse = function () {
|
||||
collapse.apply(this, arguments);
|
||||
if (this.flags?.collapsed) {
|
||||
widget.codeElement.hidden = true;
|
||||
} else {
|
||||
if (this.flags?.collapsed === false) {
|
||||
widget.codeElement.hidden = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
// Save data to workflow forced!
|
||||
function saveValue() {
|
||||
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState();
|
||||
}
|
||||
|
||||
// Register extensions
|
||||
app.registerExtension({
|
||||
name: "KYNode.KY_Eval_Python",
|
||||
getCustomWidgets(app) {
|
||||
return {
|
||||
PYCODE: (node, inputName, inputData, app) => {
|
||||
const widget = codeEditor(node, inputName, inputData);
|
||||
|
||||
widget.editor.getSession().on("change", function (e) {
|
||||
widget.value = widget.editor.getValue();
|
||||
saveValue();
|
||||
});
|
||||
|
||||
const varTypeList = node.addWidget(
|
||||
"combo",
|
||||
"select_type",
|
||||
"string",
|
||||
(v) => {
|
||||
// widget.editor.setTheme(`ace/theme/${varTypeList.value}`);
|
||||
},
|
||||
{
|
||||
values: varTypes,
|
||||
serialize: false,
|
||||
}
|
||||
);
|
||||
|
||||
// 6. 使用 addDOMWidget 将容器添加到节点上
|
||||
// - 第一个参数是 widget 的名称,在节点内部需要是唯一的。
|
||||
// - 第二个参数是 widget 的类型,对于自定义 DOM 元素,通常是 "div"。
|
||||
// - 第三个参数是您创建的 DOM 元素。
|
||||
// - 第四个参数是一个选项对象,可以用来配置 widget。
|
||||
// node.addDOMWidget("rowOfButtons", "div", container, {
|
||||
// });
|
||||
node.addWidget(
|
||||
"button",
|
||||
"Add Input variable",
|
||||
"add_input_variable",
|
||||
async () => {
|
||||
// Input name variable and check
|
||||
let nameInput = node?.inputs?.length
|
||||
? `p${node.inputs.length - 1}`
|
||||
: "p0";
|
||||
|
||||
const currentWidth = node.size[0];
|
||||
let tp = varTypeList.value
|
||||
nameInput = nameInput + '_' + typeMap[tp]
|
||||
node.addInput(nameInput, "*");
|
||||
node.setSize([currentWidth, node.size[1]]);
|
||||
let cv = widget.editor.getValue();
|
||||
if(tp == 'json') {
|
||||
cv = cv + '\n' + nameInput + ' = json.loads('+ nameInput + ')'
|
||||
} else if(tp == 'list') {
|
||||
cv = cv + '\n' + nameInput + ' = []'
|
||||
} else if(tp == 'dict') {
|
||||
cv = cv + '\n' + nameInput + ' = {}'
|
||||
} else {
|
||||
cv = cv + '\n' + nameInput + ' = ' + typeMap[tp] + '('+ nameInput + ')'
|
||||
}
|
||||
widget.editor.setValue(cv)
|
||||
saveValue();
|
||||
}
|
||||
);
|
||||
|
||||
node.addWidget(
|
||||
"button",
|
||||
"Add Output variable",
|
||||
"add_output_variable",
|
||||
async () => {
|
||||
const currentWidth = node.size[0];
|
||||
// Output name variable
|
||||
let nameOutput = node?.outputs?.length
|
||||
? `r${node.outputs.length}`
|
||||
: "r0";
|
||||
let tp = varTypeList.value
|
||||
nameOutput = nameOutput + '_' + typeMap[tp]
|
||||
node.addOutput(nameOutput, tp);
|
||||
node.setSize([currentWidth, node.size[1]]);
|
||||
let cv = widget.editor.getValue();
|
||||
if(tp == 'json') {
|
||||
cv = cv + '\n' + nameOutput + ' = json.dumps('+ nameOutput + ')'
|
||||
} else if(tp == 'list') {
|
||||
cv = cv + '\n' + nameOutput + ' = []'
|
||||
} else if(tp == 'dict') {
|
||||
cv = cv + '\n' + nameOutput + ' = {}'
|
||||
} else {
|
||||
cv = cv + '\n' + nameOutput + ' = ' + typeMap[tp] + '('+ nameOutput + ')'
|
||||
}
|
||||
widget.editor.setValue(cv)
|
||||
saveValue();
|
||||
}
|
||||
);
|
||||
|
||||
node.onRemoved = function () {
|
||||
for (const w of node?.widgets) {
|
||||
if (w?.codeElement) w.codeElement.remove();
|
||||
}
|
||||
};
|
||||
|
||||
node.addCustomWidget(widget);
|
||||
|
||||
return widget;
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// --- IDENode
|
||||
if (nodeData.name === "KY_Eval_Python") {
|
||||
// Node Created
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = async function () {
|
||||
const ret = onNodeCreated
|
||||
? onNodeCreated.apply(this, arguments)
|
||||
: undefined;
|
||||
|
||||
const node_title = await this.getTitle();
|
||||
const nodeName = `${nodeData.name}_${this.id}`;
|
||||
|
||||
console.log(`Create ${nodeData.name}: ${nodeName}`);
|
||||
this.name = nodeName;
|
||||
|
||||
// Create default inputs, when first create node
|
||||
if (this?.inputs?.length<2) {
|
||||
["p0_str"].forEach((inputName) => {
|
||||
const currentWidth = this.size[0];
|
||||
this.addInput(inputName, "*");
|
||||
this.setSize([currentWidth, this.size[1]]);
|
||||
});
|
||||
}
|
||||
|
||||
const widgetEditor = findWidget(this, "pycode", "type");
|
||||
|
||||
this.setSize([530, this.size[1]]);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const onDrawForeground = nodeType.prototype.onDrawForeground;
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = onDrawForeground?.apply?.(this, arguments);
|
||||
|
||||
// if (this.flags?.collapsed) return r;
|
||||
|
||||
if (this?.outputs?.length) {
|
||||
for (let o = 0; o < this.outputs.length; o++) {
|
||||
const { name, type } = this.outputs[o];
|
||||
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
|
||||
const nameSize = ctx.measureText(name);
|
||||
const typeSize = ctx.measureText(
|
||||
`[${type === "*" ? "any" : type.toLowerCase()}]`
|
||||
);
|
||||
|
||||
ctx.fillStyle = colorType === "" ? "#AAA" : colorType;
|
||||
ctx.font = "12px Arial, sans-serif";
|
||||
ctx.textAlign = "right";
|
||||
ctx.fillText(
|
||||
`[${type === "*" ? "any" : type.toLowerCase()}]`,
|
||||
this.size[0] - nameSize.width - typeSize.width,
|
||||
o * 20 + 19
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this?.inputs?.length) {
|
||||
const not_showing = ["select_type", "pycode"];
|
||||
for (let i = 1; i < this.inputs.length; i++) {
|
||||
const { name, type } = this.inputs[i];
|
||||
if (not_showing.includes(name)) continue;
|
||||
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
|
||||
const nameSize = ctx.measureText(name);
|
||||
|
||||
ctx.fillStyle = !colorType || colorType === "" ? "#AAA" : colorType;
|
||||
ctx.font = "12px Arial, sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(
|
||||
`[${type === "*" ? "any" : type.toLowerCase()}]`,
|
||||
nameSize.width + 25,
|
||||
i * 20
|
||||
);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
// Node Configure
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function (node) {
|
||||
onConfigure?.apply(this, arguments);
|
||||
if (node?.widgets_values?.length) {
|
||||
const widget_code_id = findWidget(
|
||||
this,
|
||||
"pycode",
|
||||
"type",
|
||||
"findIndex"
|
||||
);
|
||||
const widget_theme_id = findWidget(
|
||||
this,
|
||||
"varTypeList",
|
||||
"name",
|
||||
"findIndex"
|
||||
);
|
||||
const widget_language_id = findWidget(
|
||||
this,
|
||||
"language",
|
||||
"name",
|
||||
"findIndex"
|
||||
);
|
||||
|
||||
const editor = this.widgets[widget_code_id]?.editor;
|
||||
|
||||
if (editor) {
|
||||
// editor.setTheme(
|
||||
// `ace/theme/${this.widgets_values[widget_theme_id]}`
|
||||
// );
|
||||
// editor.session.setMode(
|
||||
// `ace/mode/${this.widgets_values[widget_language_id]}`
|
||||
// );
|
||||
editor.setValue(this.widgets_values[widget_code_id]);
|
||||
editor.clearSelection();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ExtraMenuOptions
|
||||
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
getExtraMenuOptions?.apply(this, arguments);
|
||||
|
||||
const past_index = options.length - 1;
|
||||
const past = options[past_index];
|
||||
|
||||
if (!!past) {
|
||||
// Inputs remove
|
||||
for (const input_idx in this.inputs) {
|
||||
const input = this.inputs[input_idx];
|
||||
|
||||
if (["language", "select_type"].includes(input.name)) continue;
|
||||
|
||||
options.splice(past_index + 1, 0, {
|
||||
content: `Remove Input ${input.name}`,
|
||||
callback: (e) => {
|
||||
const currentWidth = this.size[0];
|
||||
if (input.link) {
|
||||
app.graph.removeLink(input.link);
|
||||
}
|
||||
this.removeInput(input_idx);
|
||||
this.setSize([80, this.size[1]]);
|
||||
saveValue();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Output remove
|
||||
for (const output_idx in this.outputs) {
|
||||
const output = this.outputs[output_idx];
|
||||
|
||||
if (output.name === "r0") continue;
|
||||
|
||||
options.splice(past_index + 1, 0, {
|
||||
content: `Remove Output ${output.name}`,
|
||||
callback: (e) => {
|
||||
const currentWidth = this.size[0];
|
||||
if (output.link) {
|
||||
app.graph.removeLink(output.link);
|
||||
}
|
||||
this.removeOutput(output_idx);
|
||||
this.setSize([currentWidth, this.size[1]]);
|
||||
saveValue();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// end - ExtraMenuOptions
|
||||
}
|
||||
},
|
||||
});
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
export function makeElement(tag, attrs = {}) {
|
||||
if (!tag) tag = "div";
|
||||
const element = document.createElement(tag);
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
const currValue = attrs[key];
|
||||
if (key === "class") {
|
||||
if (Array.isArray(currValue)) {
|
||||
element.classList.add(...currValue);
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element.className = currValue;
|
||||
}
|
||||
} else if (key === "dataset") {
|
||||
try {
|
||||
if (Array.isArray(currValue)) {
|
||||
currValue.forEach((datasetArr) => {
|
||||
const [prop, propval] = Object.entries(datasetArr)[0];
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
} else {
|
||||
Object.entries(currValue).forEach((datasetArr) => {
|
||||
const [prop, propval] = datasetArr;
|
||||
element.dataset[prop] = propval;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
} else if (key === "style") {
|
||||
if (
|
||||
typeof currValue === "object" &&
|
||||
!Array.isArray(currValue) &&
|
||||
Object.keys(currValue).length
|
||||
) {
|
||||
Object.assign(element[key], currValue);
|
||||
} else if (
|
||||
typeof currValue === "object" &&
|
||||
Array.isArray(currValue) &&
|
||||
currValue.length
|
||||
) {
|
||||
element[key] = [...currValue];
|
||||
} else if (currValue instanceof String || typeof currValue === "string") {
|
||||
element[key] = currValue;
|
||||
}
|
||||
} else if (["for"].includes(key)) {
|
||||
element.setAttribute(key, currValue);
|
||||
} else if (key === "children") {
|
||||
element.append(...(currValue instanceof Array ? currValue : [currValue]));
|
||||
} else if (key === "parent") {
|
||||
currValue.append(element);
|
||||
} else {
|
||||
element[key] = currValue;
|
||||
}
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
export function CUSTOM_INT(node, inputName, val, func, config = {}) {
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"number",
|
||||
inputName,
|
||||
val,
|
||||
func,
|
||||
Object.assign({}, { min: 0, max: 4096, step: 640, precision: 0 }, config)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function CUSTOM_COMBO(node, inputName, val, func, config = {}) {
|
||||
return {
|
||||
widget: node.addWidget(
|
||||
"combo",
|
||||
inputName,
|
||||
val,
|
||||
func,
|
||||
Object.assign({}, config)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function recursiveLinkUpstream(node, type, depth, index=null) {
|
||||
depth += 1
|
||||
let connections = []
|
||||
const inputList = (index !== null) ? [index] : [...Array(node.inputs.length).keys()]
|
||||
if (inputList.length === 0) { return }
|
||||
for (let i of inputList) {
|
||||
const link = node.inputs[i].link
|
||||
if (link) {
|
||||
const nodeID = node.graph.links[link].origin_id
|
||||
const slotID = node.graph.links[link].origin_slot
|
||||
const connectedNode = node.graph._nodes_by_id[nodeID]
|
||||
|
||||
if (connectedNode.outputs[slotID].type === type) {
|
||||
|
||||
connections.push([connectedNode.id, depth])
|
||||
|
||||
if (connectedNode.inputs) {
|
||||
const index = (connectedNode.type === "LatentComposite") ? 0 : null
|
||||
connections = connections.concat(recursiveLinkUpstream(connectedNode, type, depth, index))
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return connections
|
||||
}
|
||||
|
||||
export function transformFunc(widget, value, node, index) {
|
||||
const s = widget.options.step / 10;
|
||||
widget.value = Math.round(value / s) * s;
|
||||
node.properties["values"][node.widgets[node.index].value][index] = widget.value
|
||||
if (node.widgets_values) {
|
||||
node.widgets_values[2] = node.properties["values"].join()
|
||||
}
|
||||
}
|
||||
|
||||
export function swapInputs(node, indexA, indexB) {
|
||||
const linkA = node.inputs[indexA].link
|
||||
let origin_slotA = null
|
||||
let node_IDA = null
|
||||
let connectedNodeA = null
|
||||
let labelA = node.inputs[indexA].label || null
|
||||
|
||||
const linkB = node.inputs[indexB].link
|
||||
let origin_slotB = null
|
||||
let node_IDB = null
|
||||
let connectedNodeB = null
|
||||
let labelB = node.inputs[indexB].label || null
|
||||
|
||||
if (linkA) {
|
||||
node_IDA = node.graph.links[linkA].origin_id
|
||||
origin_slotA = node.graph.links[linkA].origin_slot
|
||||
connectedNodeA = node.graph._nodes_by_id[node_IDA]
|
||||
|
||||
node.disconnectInput(indexA)
|
||||
}
|
||||
|
||||
if (linkB) {
|
||||
node_IDB = node.graph.links[linkB].origin_id
|
||||
origin_slotB = node.graph.links[linkB].origin_slot
|
||||
connectedNodeB = node.graph._nodes_by_id[node_IDB]
|
||||
|
||||
node.disconnectInput(indexB)
|
||||
}
|
||||
|
||||
if (linkA) {
|
||||
connectedNodeA.connect(origin_slotA, node, indexB)
|
||||
}
|
||||
|
||||
if (linkB) {
|
||||
connectedNodeB.connect(origin_slotB, node, indexA)
|
||||
}
|
||||
|
||||
node.inputs[indexA].label = labelB
|
||||
node.inputs[indexB].label = labelA
|
||||
|
||||
}
|
||||
|
||||
export function renameNodeInputs(node, name, offset=0) {
|
||||
let index=0;
|
||||
for (let i=offset; i < node.inputs.length; i++) {
|
||||
if(node.inputs[i].widget){
|
||||
continue;
|
||||
}
|
||||
node.inputs[i].name = `${name}${index}`
|
||||
if(node.inputs[i].label){
|
||||
node.inputs[i].label = `${name}${index}`
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
export function swapOutputs(node, indexA, indexB) {
|
||||
const linkAs = JSON.parse(JSON.stringify(node.outputs[indexA].links))
|
||||
let origin_slotA = null
|
||||
let node_IDA = null
|
||||
let connectedNodeA = null
|
||||
let labelA = node.outputs[indexA].label || null
|
||||
|
||||
const linkBs = JSON.parse(JSON.stringify(node.outputs[indexB].links))
|
||||
let origin_slotB = null
|
||||
let node_IDB = null
|
||||
let connectedNodeB = null
|
||||
let labelB = node.outputs[indexB].label || null
|
||||
|
||||
if(linkAs&&linkAs.length>0){
|
||||
for(let linkA of linkAs){
|
||||
node_IDA = node.graph.links[linkA].target_id
|
||||
origin_slotA = node.graph.links[linkA].target_slot
|
||||
connectedNodeA = node.graph._nodes_by_id[node_IDA]
|
||||
node.connect(indexB,connectedNodeA,origin_slotA)
|
||||
}
|
||||
node.disconnectOutput(indexA)
|
||||
}
|
||||
|
||||
|
||||
if(linkBs&&linkBs.length>0){
|
||||
for(let linkB of linkBs){
|
||||
node_IDB = node.graph.links[linkB].target_id
|
||||
origin_slotB = node.graph.links[linkB].target_slot
|
||||
connectedNodeB = node.graph._nodes_by_id[node_IDB]
|
||||
node.connect(indexA,connectedNodeB,origin_slotB)
|
||||
}
|
||||
node.disconnectOutput(indexB)
|
||||
}
|
||||
|
||||
node.outputs[indexA].label = labelB
|
||||
node.outputs[indexB].label = labelA
|
||||
|
||||
}
|
||||
export function renameNodeOutputs(node, name, offset=0) {
|
||||
for (let i=offset; i < node.outputs.length; i++) {
|
||||
node.outputs[i].name = `${name}${i-offset}`
|
||||
if(node.outputs[i].label){
|
||||
node.outputs[i].label = `${name}${i-offset}`
|
||||
}
|
||||
}
|
||||
}
|
||||
export function swapInputsNot01(node, indexesToRemove){
|
||||
for (let i=0;i<indexesToRemove.length;i++) {
|
||||
if(node.inputs[indexesToRemove[i]].widget){
|
||||
for(let j=0; j<node.inputs.length; j++){
|
||||
if(!indexesToRemove.includes(j)&&j>indexesToRemove[i]){
|
||||
swapInputs(node, indexesToRemove[i], j)
|
||||
indexesToRemove[i]=j
|
||||
swapInputsNot01(node, indexesToRemove)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function removeNodeInputs(node, indexesToRemove, offset=0) {
|
||||
swapInputsNot01(node, indexesToRemove)
|
||||
indexesToRemove.sort((a, b) => b - a);
|
||||
for (let i of indexesToRemove) {
|
||||
if ((node.inputs.length-offset) <= 2) { console.log("too short"); continue } // if only 2 left
|
||||
node.removeInput(i)
|
||||
node.properties["values"]&&node.properties.values.splice(i-offset, 1)
|
||||
}
|
||||
if(node.properties["values"]){
|
||||
const inputLenght = node.properties["values"].length-1
|
||||
|
||||
node.widgets[node.index].options.max = inputLenght
|
||||
if (node.widgets[node.index].value > inputLenght) {
|
||||
node.widgets[node.index].value = inputLenght
|
||||
}
|
||||
}
|
||||
node?.onResize?.();
|
||||
}
|
||||
|
||||
export function swapOutputsNot01(node, indexesToRemove){
|
||||
for (let i=0;i<indexesToRemove.length;i++) {
|
||||
if(node.outputs[indexesToRemove[i]].widget){
|
||||
for(let j=0; j<node.outputs.length; j++){
|
||||
if(!indexesToRemove.includes(j)&&j>indexesToRemove[i]){
|
||||
swapOutputs(node, indexesToRemove[i], j)
|
||||
indexesToRemove[i]=j
|
||||
swapOutputsNot01(node, indexesToRemove)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function removeNodeOutputs(node, indexesToRemove, offset=0) {
|
||||
swapOutputsNot01(node, indexesToRemove)
|
||||
indexesToRemove.sort((a, b) => b - a);
|
||||
for (let i of indexesToRemove) {
|
||||
if ((node.outputs.length-offset) <= 1) { console.log("too short"); continue } // if only 2 left
|
||||
node.removeOutput(i)
|
||||
}
|
||||
node.onResize(node.size)
|
||||
}
|
||||
|
||||
export function getDrawColor(percent, alpha) {
|
||||
let h = 360*percent
|
||||
let s = 50;
|
||||
let l = 50;
|
||||
l /= 100;
|
||||
const a = s * Math.min(l, 1 - l) / 100;
|
||||
const f = n => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}${alpha}`;
|
||||
}
|
||||
|
||||
export function hexToRgba(hex, opacity) {
|
||||
return hex && hex.replace(/\s+/g, '').length === 7 ? 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' +
|
||||
parseInt('0x' + hex.slice(3, 5)) + ',' +
|
||||
parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')' : ''
|
||||
}
|
||||
|
||||
export function computeCanvasSize(node, size) {
|
||||
if (node.widgets[0].last_y == null) return;
|
||||
|
||||
const MIN_SIZE = 200;
|
||||
|
||||
let y = LiteGraph.NODE_WIDGET_HEIGHT * Math.max(node.inputs.length, node.outputs.length) + 5;
|
||||
let freeSpace = size[1] - y;
|
||||
|
||||
// Compute the height of all non customtext widgets
|
||||
let widgetHeight = 0;
|
||||
for (let i = 0; i < node.widgets.length; i++) {
|
||||
const w = node.widgets[i];
|
||||
if (w.type !== "customLamCanvas") {
|
||||
if (w.computeSize) {
|
||||
widgetHeight += w.computeSize()[1] + 4;
|
||||
} else {
|
||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See how large the canvas can be
|
||||
freeSpace -= widgetHeight;
|
||||
|
||||
// There isnt enough space for all the widgets, increase the size of the node
|
||||
if (freeSpace < MIN_SIZE) {
|
||||
freeSpace = MIN_SIZE;
|
||||
node.size[1] = y + widgetHeight + freeSpace;
|
||||
if(node.graph) node.graph.setDirtyCanvas(true);
|
||||
}
|
||||
|
||||
// Position each of the widgets
|
||||
for (const w of node.widgets) {
|
||||
w.y = y;
|
||||
if (w.type === "customLamCanvas") {
|
||||
y += freeSpace;
|
||||
} else if (w.computeSize) {
|
||||
y += w.computeSize()[1] + 4;
|
||||
} else {
|
||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||
}
|
||||
}
|
||||
|
||||
node.canvasHeight = freeSpace;
|
||||
}
|
||||
Reference in New Issue
Block a user