diff --git a/.gitignore b/.gitignore index 50ec79d..3d9e47a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,50 @@ # Python __pycache__/ *.py[cod] -*$py.class *.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg *.egg +*.egg-info/ +dist/ +build/ +.eggs/ + +# Jupyter +.ipynb_checkpoints + +# VSCode +.vscode/ + +# PyCharm +.idea/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# ComfyUI +*.ckpt +*.safetensors +*.pth +*.pt +*.onnx +*.npz +*.npz +*.log +*.tmp + +# Node +node_modules/ + +# Others +*.swp +*.bak +*.tmp +*.old +*.orig + +# Custom +web/__pycache__/ # Virtual Environment venv/ diff --git a/README.md b/README.md index eeb05b2..ba5979b 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,63 @@ # ComfyUI-UniversalToolkit +## 版本 + +- 当前版本:**v0.1** + +## 更新日志 + +### v0.1 +- 项目结构模块化重构,所有节点分为 image_nodes、tool_nodes 两大类,便于维护和扩展。 +- 工具类节点(ShowInt、ShowFloat、ShowList、ShowText、PreviewMask)合并为 tool_nodes_utk.py。 +- 生成/分析类节点(EmptyUnitGenerator、ImageRatioDetector)合并为 image_nodes_utk.py。 +- 删除所有冗余和历史遗留节点文件,保持 nodes/ 目录整洁。 +- 统一节点命名后缀(UTK),便于识别。 +- 完善空输入兜底逻辑,所有节点均支持安全桥接。 + ## 简介 -本插件为 ComfyUI 提供多尺寸空白单元(image/mask/latent)生成节点,结构完全遵循 ComfyUI 插件标准。 +本插件为 ComfyUI 提供通用工具节点,当前实现了"空白单元生成"节点,可批量生成指定分辨率、颜色的 image、mask、latent。 -## 目录结构 -``` -ComfyUI-UniversalToolkit/ -├── main.py -├── image_utils.py -├── requirements.txt -├── README.md -└── nodes/ - ├── __init__.py - └── image_nodes.py -``` - -## 安装方法 -1. 将本目录放入 ComfyUI 的 custom_nodes 目录下。 +## 安装 +1. 将本插件文件夹放入 ComfyUI 的 `custom_nodes/` 目录下。 2. 安装依赖: - ```bash + ``` pip install -r requirements.txt ``` -3. 重启 ComfyUI。 ## 节点说明 -### 空单元生成器(EmptyUnitGenerator) -- 输入参数: - - 图像大小:small/medium/large - - 构图比例:1:1/3:4/16:9 - - 颜色:white/black/gray/red/green/blue -- 输出: - - image:空白图像 - - mask:空白蒙版 - - latent:空白潜空间 -## 依赖 -- Pillow -- numpy +### Universal Blank Unit +- **output_type**: 选择输出类型(image/mask/latent) +- **ratio_type**: 分辨率类型(standard/social_media) +- **ratio**: 具体分辨率 +- **batch**: 批量数 +- **image_color**: 颜色(仅 image/mask 有效) -## 功能特点 -- 模块化设计,各功能独立封装 -- 支持多种基础数据类型处理 -- 提供直观的节点界面 -- 完善的错误处理和参数验证 +#### 分辨率预设 +- **Standard**: + - 1:1 (512x512) + - 16:9 (896x512) + - 4:5 (512x640) + - 3:2 (768x512) + - 2:3 (512x768) -## 已实现功能 -- 多尺寸、多比例空白图像/mask/latent 一键生成 - - 支持 small(sd1.5)、medium(flux/sdxl)、large(high resolution)三种尺寸 - - 支持 1:1、3:4、16:9 等常见比例 - - 支持自定义颜色 +- **Social Media**: + - Instagram Post (1080x1080) + - Instagram Story (1080x1920) + - Twitter Post (1600x900) + - Facebook Cover (820x312) -## 使用方法 -1. 启动 ComfyUI -2. 在节点菜单中找到 "UniversalToolkit" 分类 -3. 选择需要的节点并配置参数 -4. 连接节点并运行工作流 +#### 颜色预设 +- White +- Black +- Gray +- Red +- Green +- Blue -## 开发计划 -- [ ] 添加更多图像处理节点 -- [ ] 添加数据类型转换节点 -- [ ] 添加逻辑处理节点 -- [ ] 优化节点界面 -- [ ] 添加更多示例工作流 - -## 贡献指南 -欢迎提交 Issue 和 Pull Request! +## 兼容性 +- 结构与 ComfyUI 官方插件规范兼容。 +- 支持多分辨率、批量输出,参数面板友好。 ## 许可证 -MIT License \ No newline at end of file +MIT \ No newline at end of file diff --git a/__init__.py b/__init__.py index 59a9117..dc327a9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,31 @@ -from .universal_toolkit_node import EmptyUnitGenerator +from .nodes import ( + EmptyUnitGenerator_UTK, + ImageRatioDetector_UTK, + ShowInt_UTK, + ShowFloat_UTK, + ShowList_UTK, + ShowText_UTK, + PreviewMask_UTK, +) NODE_CLASS_MAPPINGS = { - "EmptyUnitGenerator": EmptyUnitGenerator, + "EmptyUnitGenerator_UTK": EmptyUnitGenerator_UTK, + "ImageRatioDetector_UTK": ImageRatioDetector_UTK, + "ShowInt_UTK": ShowInt_UTK, + "ShowFloat_UTK": ShowFloat_UTK, + "ShowList_UTK": ShowList_UTK, + "ShowText_UTK": ShowText_UTK, + "PreviewMask_UTK": PreviewMask_UTK, } NODE_DISPLAY_NAME_MAPPINGS = { - "EmptyUnitGenerator": "空单元生成器", -} \ No newline at end of file + "EmptyUnitGenerator_UTK": "Empty Unit Generator (UTK)", + "ImageRatioDetector_UTK": "Image Ratio Detector (UTK)", + "ShowInt_UTK": "Show Int (UTK)", + "ShowFloat_UTK": "Show Float (UTK)", + "ShowList_UTK": "Show List (UTK)", + "ShowText_UTK": "Show Text (UTK)", + "PreviewMask_UTK": "Preview Mask (UTK)", +} + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..cf0b109 --- /dev/null +++ b/nodes/__init__.py @@ -0,0 +1,2 @@ +from .image_nodes_utk import EmptyUnitGenerator_UTK, ImageRatioDetector_UTK +from .tool_nodes_utk import ShowInt_UTK, ShowFloat_UTK, ShowList_UTK, ShowText_UTK, PreviewMask_UTK \ No newline at end of file diff --git a/nodes/image_nodes_utk.py b/nodes/image_nodes_utk.py new file mode 100644 index 0000000..ed63727 --- /dev/null +++ b/nodes/image_nodes_utk.py @@ -0,0 +1,144 @@ +import torch +import numpy as np +import re +import math + +class EmptyUnitGenerator_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + ratio_options = [ + "custom", + "SD1.5 - 1:1 square 512x512", + "SD1.5 - 2:3 portrait 512x768", + "SD1.5 - 3:4 portrait 512x682", + "SD1.5 - 3:2 landscape 768x512", + "SD1.5 - 4:3 landscape 682x512", + "SD1.5 - 16:9 cinema 910x512", + "SD1.5 - 1.85:1 cinema 952x512", + "SD1.5 - 2:1 cinema 1024x512", + "SDXL - 1:1 square 1024x1024", + "SDXL - 3:4 portrait 896x1152", + "SDXL - 5:8 portrait 832x1216", + "SDXL - 9:16 portrait 768x1344", + "SDXL - 9:21 portrait 640x1536", + "SDXL - 4:3 landscape 1152x896", + "SDXL - 3:2 landscape 1216x832", + "SDXL - 16:9 landscape 1344x768", + "SDXL - 21:9 landscape 1536x640", + ] + latent_type_options = ["standard", "sd3", "hunyuan", "ltx"] + return { + "required": { + "width": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 8, "label": "Width (custom only)"}), + "height": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 8, "label": "Height (custom only)"}), + "ratio": (ratio_options, {"default": ratio_options[9], "label": "Resolution/Ratio"}), + "scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 8.0, "step": 0.1, "label": "Scale (放大倍数)"}), + "divisor": ("INT", {"default": 8, "min": 1, "max": 512, "step": 1, "label": "Divisor (整除裁切)"}), + "image_color": (["white", "black", "gray", "red", "green", "blue"], {"default": "white"}), + "batch": ("INT", {"default": 1, "min": 1, "max": 16, "label": "Batch 数量"}), + "latent_type": (latent_type_options, {"default": "standard", "label": "Latent类型"}), + }, + "optional": {}, + } + + RETURN_TYPES = ("IMAGE", "MASK", "LATENT", "INT", "INT") + RETURN_NAMES = ("image", "mask", "latent", "width", "height") + FUNCTION = "generate" + + def generate(self, width, height, ratio, scale, divisor, image_color, batch, latent_type): + if ratio == "custom": + w = width + h = height + else: + m = re.search(r"(\d+)x(\d+)", ratio) + if m: + w, h = int(m.group(1)), int(m.group(2)) + else: + w, h = 1024, 1024 + w = max(1, int(round(w * scale))) + h = max(1, int(round(h * scale))) + if divisor > 1: + w = (w // divisor) * divisor + h = (h // divisor) * divisor + COLOR_OPTIONS = { + "white": (255, 255, 255), + "black": (0, 0, 0), + "gray": (128, 128, 128), + "red": (255, 0, 0), + "green": (0, 255, 0), + "blue": (0, 0, 255), + } + color_rgb = COLOR_OPTIONS[image_color] + images = [] + for _ in range(batch): + img = torch.from_numpy(np.array(Image.new("RGB", (w, h), color_rgb))).float() / 255.0 + img = img.permute(2, 0, 1) + images.append(img) + images = torch.stack(images, dim=0) + mask_value = color_rgb[0] / 255.0 + masks = torch.ones([batch, 1, h, w], dtype=torch.float32) * mask_value + latent_channels = { + "standard": 4, + "sd3": 8, + "hunyuan": 8, + "ltx": 16, + }.get(latent_type, 4) + latent = { + "samples": torch.zeros([batch, latent_channels, h // 8, w // 8], dtype=torch.float32), + "batch_index_list": None + } + return images, masks, latent, w, h + +class ImageRatioDetector_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"image": ("IMAGE",)}} + RETURN_TYPES = ("STRING", "INT", "INT", "STRING") + RETURN_NAMES = ("ratio_str", "width", "height", "approx_ratio_str") + FUNCTION = "detect" + def detect(self, image): + if hasattr(image, 'dim') and image.dim() == 4: + img = image[0] + else: + img = image + shape = img.shape + if len(shape) == 3: + if shape[0] <= 4: + _, h, w = shape + else: + h, w, _ = shape + elif len(shape) == 2: + h, w = shape + else: + return "?", 0, 0, "N/A" + h = int(h) + w = int(w) + if w == 0 or h == 0: + ratio_str = "0:0" + approx_ratio_str = "N/A" + return ratio_str, w, h, approx_ratio_str + gcd = math.gcd(w, h) + ratio_str = f"{w//gcd}:{h//gcd}" + std_ratios = { + "1:1": 1.0, + "16:9": 16/9, + "4:3": 4/3, + "3:2": 3/2, + "2:3": 2/3, + "3:4": 3/4, + "9:16": 9/16, + "5:4": 5/4, + "7:5": 7/5, + "21:9": 21/9, + "5:3": 5/3, + "3:1": 3/1, + "1:2": 1/2, + "2:1": 2/1, + "1:1.85": 1/1.85, + "1:2.35": 1/2.35, + } + wh_ratio = float(w) / float(h) + approx_ratio_str = min(std_ratios.keys(), key=lambda k: abs(std_ratios[k] - wh_ratio)) + return ratio_str, w, h, approx_ratio_str \ No newline at end of file diff --git a/nodes/tool_nodes_utk.py b/nodes/tool_nodes_utk.py new file mode 100644 index 0000000..1f03a98 --- /dev/null +++ b/nodes/tool_nodes_utk.py @@ -0,0 +1,71 @@ +import torch + +class ShowInt_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"int_val": ("INT",)}} + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("int_val",) + FUNCTION = "show" + IS_PREVIEW = True + def show(self, int_val=None): + if int_val is None: + int_val = 0 + return (int_val,) + +class ShowFloat_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"float_val": ("FLOAT",)}} + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("float_val",) + FUNCTION = "show" + IS_PREVIEW = True + def show(self, float_val=None): + if float_val is None: + float_val = 0.0 + return (float_val,) + +class ShowList_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"list_val": ("LIST",)}} + RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list_val",) + FUNCTION = "show" + IS_PREVIEW = True + def show(self, list_val=None): + if list_val is None: + list_val = [] + return (list_val,) + +class ShowText_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"text": ("STRING",)}} + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "show" + IS_PREVIEW = True + def show(self, text=None): + if text is None: + text = "" + return (text,) + +class PreviewMask_UTK: + CATEGORY = "UniversalToolkit" + @classmethod + def INPUT_TYPES(cls): + return {"required": {"mask": ("MASK",)}} + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "show" + IS_PREVIEW = True + def show(self, mask=None): + if mask is None: + mask = torch.zeros([1, 1, 64, 64], dtype=torch.float32) + return (mask,) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 62bb017..8323421 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Pillow -numpy \ No newline at end of file +numpy>=1.24.0 +torch>=2.0.0 \ No newline at end of file diff --git a/universal_toolkit_node.py b/universal_toolkit_node.py deleted file mode 100644 index b33cc86..0000000 --- a/universal_toolkit_node.py +++ /dev/null @@ -1,88 +0,0 @@ -from PIL import Image -import numpy as np -import torch -import re - -class EmptyUnitGenerator: - CATEGORY = "UniversalToolkit" - @classmethod - def INPUT_TYPES(cls): - # 标准比例选项 - STANDARD_RATIOS = [ - ("1:1", [(1024, 1024), (2048, 2048)]), - ("3:2", [(1200, 800), (800, 1200)]), - ("4:3", [(1600, 1200), (1200, 1600)]), - ("8:5", [(1280, 800), (800, 1280)]), - ("16:9", [(1920, 1080), (1080, 1920)]), - ("21:9", [(2520, 1080), (1080, 2520)]), - ] - standard_options = [] - for ratio, sizes in STANDARD_RATIOS: - for w, h in sizes: - orientation = "横向" if w >= h else "纵向" - standard_options.append(f"{ratio} {orientation} ({w}x{h})") - - # 社交媒体分辨率 - social_options = [ - "Instagram Portrait - 1080x1350", - "Instagram Square - 1080x1080", - "Instagram Landscape - 1080x608", - "Instagram Stories/Reels - 1080x1920", - "Facebook Landscape - 1080x1350", - "Facebook Marketplace - 1200x1200", - "Facebook Stories - 1080x1920", - "TikTok - 1080x1920", - "YouTube Banner - 2560x1440", - "LinkedIn Profile Banner - 1584x396", - "LinkedIn Page Cover - 1128x191", - "LinkedIn Post - 1200x627", - "Pinterest Pin Image - 1000x1500", - "CivitAI Cover - 1600x400", - "OpenArt App - 1500x1000", - ] - - return { - "required": { - "ratio_type": (["standard", "social media"], {"default": "standard", "label": "比例类型"}), - "ratio": (standard_options, {"default": standard_options[0], "label": "尺寸/比例", "dynamic": True, "depends_on": ["ratio_type"]}), - "image_color": (["white", "black", "gray", "red", "green", "blue"], {"default": "white", "label": "Image Color"}), - "batch": ("INT", {"default": 1, "min": 1, "max": 16, "step": 1, "label": "输出组数(batch)"}), - }, - "optional": {}, - "dynamic": { - "ratio": lambda params: standard_options if params.get("ratio_type", "standard") == "standard" else social_options - } - } - - RETURN_TYPES = ("IMAGE", "MASK", "LATENT") - RETURN_NAMES = ("image", "mask", "latent") - FUNCTION = "generate" - - def generate(self, ratio_type, ratio, image_color, batch): - # 解析分辨率 - if ratio_type == "standard": - m = re.search(r"\((\d+)x(\d+)\)", ratio) - width, height = int(m.group(1)), int(m.group(2)) - else: - m = re.search(r"(\d+)x(\d+)", ratio) - width, height = int(m.group(1)), int(m.group(2)) - COLOR_OPTIONS = { - "white": (255, 255, 255), - "black": (0, 0, 0), - "gray": (128, 128, 128), - "red": (255, 0, 0), - "green": (0, 255, 0), - "blue": (0, 0, 255), - } - color_rgb = COLOR_OPTIONS[image_color] - images = [] - masks = [] - latents = [] - for _ in range(batch): - image = torch.from_numpy(np.array(Image.new("RGB", (width, height), color_rgb))).float() / 255.0 - mask = torch.from_numpy(np.array(Image.new("L", (width, height), 0))).unsqueeze(-1).float() / 255.0 - latent = torch.from_numpy(np.zeros((height, width, 4), dtype=np.float32)) - images.append(image) - masks.append(mask) - latents.append(latent) - return tuple(images), tuple(masks), tuple(latents) \ No newline at end of file