v0.1 结构重构、节点分类、工具节点合并、完善README和.gitignore

This commit is contained in:
Cyber Dick Lang
2025-06-06 19:46:09 +08:00
parent 5f7bce277c
commit 0242ff558b
8 changed files with 334 additions and 163 deletions
+42 -16
View File
@@ -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/
+47 -54
View File
@@ -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
MIT
+26 -4
View File
@@ -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": "空单元生成器",
}
"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"]
+2
View File
@@ -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
+144
View File
@@ -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
+71
View File
@@ -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,)
+2 -1
View File
@@ -1,2 +1,3 @@
Pillow
numpy
numpy>=1.24.0
torch>=2.0.0
-88
View File
@@ -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)