v0.1 结构重构、节点分类、工具节点合并、完善README和.gitignore
This commit is contained in:
+42
-16
@@ -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/
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -1,2 +1,3 @@
|
||||
Pillow
|
||||
numpy
|
||||
numpy>=1.24.0
|
||||
torch>=2.0.0
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user