Upgrade to v1.0.4

This commit is contained in:
yolain
2024-01-28 00:49:17 +08:00
parent 4e064fb845
commit e4289a2d96
11 changed files with 969 additions and 68 deletions
+143 -2
View File
@@ -127,6 +127,7 @@
"inputs": {
"optional_lora_stack": "Lora堆(可选)",
"model_override": "模型(可选)",
"vae_override": "VAE(可选)",
"clip_override": "CLIP(可选)"
},
"outputs": {
@@ -265,7 +266,9 @@
"widgets": {
"control_net_name": "ControlNet",
"strength": "强度",
"start_percent": "开始时间"
"start_percent": "开始时间",
"end_percent": "结束时间",
"scale_soft_weights": "缩放柔和权重"
},
"inputs": {
"pipe": "节点束",
@@ -1112,6 +1115,45 @@
"image": "图像"
}
},
"easy imageScaleDown": {
"title": "图像缩小",
"inputs": {
"images": "图像"
},
"widgets": {
"width": "宽度",
"height": "高度",
"crop": "裁切"
},
"outputs": {
"IMAGE": "图像"
}
},
"easy imageScaleDownBy": {
"title": "图像缩小(按比例)",
"inputs": {
"images": "图像"
},
"widgets": {
"scale_by": "缩小比例"
},
"outputs": {
"IMAGE": "图像"
}
},
"easy imageScaleDownToSize": {
"title": "图像缩小(按边)",
"inputs": {
"images": "图像"
},
"widgets": {
"size": "尺寸",
"mode": "模式"
},
"outputs": {
"IMAGE": "图像"
}
},
"easy imagePixelPerfect": {
"title": "图像完美像素",
"widgets": {
@@ -1124,7 +1166,28 @@
"image": "图像"
},
"outputs": {
"resolution": "分辨率(整数)"
"resolution": "分辨率(整数)"
}
},
"easy imageSave": {
"title": "图像保存",
"inputs":{
"images": "图像"
},
"widgets": {
"filename_prefix": "图片名前缀"
}
},
"easy joinImageBatch": {
"title": "合并图像批次",
"inputs": {
"images": "图像"
},
"widgets": {
"mode": "合并模式"
},
"outputs": {
"image": "图像"
}
},
"easy poseEditor": {
@@ -1143,5 +1206,83 @@
"inputs": {
"pipe": "节点束"
}
},
"easy string": {
"title": "字符串",
"widgets": {
"value": "值"
}
},
"easy int": {
"title": "整数",
"widgets": {
"value": "值"
}
},
"easy rangeInt": {
"title": "整数(范围)",
"widgets": {
"range_mode": "范围模式",
"start": "开始",
"stop": "终止",
"step": "步进",
"end_mode": "结束模式",
"num_steps": "步进数量"
}
},
"easy float": {
"title": "浮点数",
"widgets": {
"value": "值"
}
},
"easy rangeFloat": {
"title": "浮点数(范围)",
"widgets": {
"range_mode": "范围模式",
"start": "开始",
"stop": "终止",
"step": "步进",
"end_mode": "结束模式",
"num_steps": "步进数量"
}
},
"easy boolean": {
"title": "布尔",
"widgets": {
"value": "值"
}
},
"easy compare": {
"title": "比较",
"widgets": {
"comparison": "比较方式"
}
},
"easy if": {
"title": "If判断",
"inputs": {
"any": "输入任何"
}
},
"easy xyAny": {
"widgets": {
"direction": "方向"
}
},
"easy convertAnything": {
"title": "转换任何",
"inputs": {
"anything": "输入任何"
},
"widgets": {
"output_type": "输出类型"
}
},
"easy showAnything": {
"title": "展示任何",
"inputs": {
"anything": "输入任何"
}
}
}
+8
View File
@@ -29,6 +29,14 @@ After installing the node package, the UI interface will be automatically switch
## Changelog
**v1.0.4(2024-01-28)**
- Added **Logic** nodes
- Added `easy imageSave` - Image saving node with date conversion and aspect and height formatting
- Added `easy joinImageBatch`
- Fixed `easy XYInputs: ControlNet` Error
- Fixed `easy loraStack` error when **toggle** is disabled
**2024-01-22**
- Fixed `easy XYInputs: Sampler/Scheduler` Error
+16 -4
View File
@@ -37,17 +37,27 @@
## 更新日志
**v1.0.4(2024-01-28)**
- 增加 **Logic** 逻辑类节点
- 增加 `easy imageSave` - 带日期转换和宽高格式化的图像保存节点
- 增加 `easy joinImageBatch` - 合并图像批次
- 修复 `easy XYInputs: ControlNet` 报错
- 修复 `easy loraStack` **toggle** 为 disabled 时报错
**2024-01-22**
- 修复 `easy XYInputs: Sampler/Scheduler` 报错
- 修复 右侧菜单 点击按钮时老是跑位的问题
- 调整UI主题,分为官方默认背景和深黑色背景两套样式,可在设置里调色板里切换(调整原因:深黑色背景对亮度较暗的用户和UP主不太友好)
**2024-01-21**
<details>
<summary>2024-01-21</summary>>
- 修改 styles 路径以兼容其他环境
- 修复 `easy comfyLoader` 读取错误
- 修复 xyPlot 在连接 zero123 时报错
</details>
**v1.0.3(2024-01-19)**
@@ -215,6 +225,7 @@
| dynamicThresholdingFull | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull |
| easy imageInsetCrop | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | ImageInsetCrop |
| easy poseEditor | [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) | poseNode |
| easy if | [ComfyUI-Logic](https://github.com/theUpsider/ComfyUI-Logic) | IfExecute |
## 示例
@@ -240,9 +251,10 @@
[tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes) - 管道节点(节点束)让用户减少了不必要的连接
[ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) - diffus3的获取与设置点让用户可以分离工作流构成
[ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) - diffus3的获取与设置点让用户可以分离工作流构成
[ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack) - 常规整合包1
[ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) - 常规整合包2
[ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) - 常规整合包2
[ComfyUI-Logic](https://github.com/theUpsider/ComfyUI-Logic) - ComfyUI逻辑运算
+2 -1
View File
@@ -10,7 +10,8 @@ node_list = [
"api",
"easyNodes",
"image",
"lllite"
"lllite",
"logic"
]
NODE_CLASS_MAPPINGS = {}
+67 -52
View File
@@ -18,11 +18,6 @@ import latent_preview
import comfy.model_base
import comfy.model_management
from comfy.sd import CLIP, VAE
import comfy.clip_vision
from comfy.clip_vision import ClipVisionModel
import comfy.clip_model
import comfy.sd1_clip
from comfy.sd1_clip import escape_important, token_weights, unescape_important
from pathlib import Path
from urllib.request import urlopen
from collections import defaultdict
@@ -31,9 +26,6 @@ from PIL import Image, ImageDraw, ImageFont
from comfy.model_patcher import ModelPatcher
from comfy_extras.chainner_models import model_loading
from typing import Dict, List, Optional, Tuple, Union, Any
from transformers import CLIPImageProcessor
from transformers.image_utils import PILImageResampling
from itertools import zip_longest
from .adv_encode import advanced_encode, advanced_encode_XL
from server import PromptServer
@@ -496,9 +488,7 @@ class easyXYPlot:
value_label = f"{value_type}: {value}"
if "ControlNet" in value_type:
if "," in value:
line = value.split(',')
value_label = f"{value_type}: {line[2]}"
value_label = f"ControlNet {index + 1}"
if value_type in ["ModelMergeBlocks"]:
if ":" in value:
@@ -757,6 +747,9 @@ class easyXYPlot:
w_max=1.0,
apply_to_pooled="enable")
positive = [[positive, {"pooled_output": positive_pooled}]]
if "positive_cond" in plot_image_vars:
positive = positive + plot_image_vars["positive_cond"]
if "Negative" in self.x_type or "Negative" in self.y_type:
if self.x_type == 'Negative Prompt S/R' or self.y_type == 'Negative Prompt S/R':
negative = x_value if self.x_type == "Negative Prompt S/R" else y_value
@@ -779,6 +772,8 @@ class easyXYPlot:
w_max=1.0,
apply_to_pooled="enable")
negative = [[negative, {"pooled_output": negative_pooled}]]
if "negative_cond" in plot_image_vars:
positive = positive + plot_image_vars["negative_cond"]
# ControlNet
if "ControlNet" in self.x_type or "ControlNet" in self.y_type:
@@ -794,20 +789,15 @@ class easyXYPlot:
}
cnet = plot_image_vars["cnet"] if "cnet" in plot_image_vars else None
if cnet:
strength, start_percent, end_percent = x_value.split(',') if "ControlNet" in self.x_type else y_value.split(',')
strength = float(strength)
start_percent = float(start_percent)
end_percent = float(end_percent)
for index, item in enumerate(cnet):
control_net_names = item[0]
index = x_value if "ControlNet" in self.x_type else y_value
controlnet = cnet[index]
for index, item in enumerate(controlnet):
control_net_name = item[0]
image = item[1]
for idx, control_net_name in enumerate(control_net_names):
# print(control_net_name)
_pipe, = controlnetAdvanced().controlnetApply(_pipe, image, control_net_name, None, strength, start_percent,
end_percent)
positive = _pipe['positive']
negative = _pipe['negative']
strength = item[2]
start_percent = item[3]
end_percent = item[4]
_pipe, positive, negative = controlnetAdvanced().controlnetApply(_pipe, image, control_net_name, None, strength, start_percent, end_percent, 1)
del _pipe
@@ -1382,7 +1372,7 @@ class stylesPromptSelector:
CATEGORY = 'EasyUse/Prompt'
FUNCTION = 'run'
OUTPUT_MODE = True
OUTPUT_NODE = True
def replace_repeat(self, prompt):
@@ -1727,7 +1717,7 @@ class latentCompositeMaskedWithCond:
RETURN_TYPES = ("PIPE_LINE", "LATENT", "CONDITIONING")
RETURN_NAMES = ("pipe", "latent", "conditioning",)
FUNCTION = "run"
OUTPUT_MODE = True
OUTPUT_NODE = True
CATEGORY = "EasyUse/Latent"
@@ -2363,7 +2353,7 @@ class loraStackLoader:
def stack(self, toggle, mode, num_loras, lora_stack=None, **kwargs):
if (toggle in [False, None, "False"]) or not kwargs:
return None
return (None,)
loras = []
@@ -2387,12 +2377,45 @@ class loraStackLoader:
loras.append((lora_name, model_strength, clip_strength))
return (loras,)
class controlnetNameStack:
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
controlnets = ["None"] + get_file_list(folder_paths.get_filename_list("controlnet"))
@classmethod
def INPUT_TYPES(s):
return {"required": {},
"optional": {
"switch_1": (["Off", "On"],),
"controlnet_1": (s.controlnets,),
"controlnet_strength_1": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_1": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_1": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"switch_2": (["Off", "On"],),
"controlnet_2": (s.controlnets,),
"controlnet_strength_2": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_2": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_2": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"switch_3": (["Off", "On"],),
"controlnet_3": (s.controlnets,),
"controlnet_strength_3": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"start_percent_3": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent_3": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"image_1": ("IMAGE",),
"image_2": ("IMAGE",),
"image_3": ("IMAGE",),
"controlnet_stack": ("CONTROL_NET_STACK",)
},
}
# controlnet
class controlnetSimple:
@classmethod
def INPUT_TYPES(s):
def get_file_list(filenames):
return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file]
return {
"required": {
"pipe": ("PIPE_LINE",),
@@ -2516,7 +2539,7 @@ class controlnetAdvanced:
CATEGORY = "EasyUse/Loaders"
def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1, scale_soft_weights=0):
def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1, scale_soft_weights=1):
if control_net is None:
if scale_soft_weights < 1:
if "ScaledSoftControlNetWeights" in ALL_NODE_CLASS_MAPPINGS:
@@ -4615,9 +4638,8 @@ class pipeXYPlotAdvanced:
x_values = []
cnet = []
for index, value in enumerate(x_value):
cnet_stack, image, strength, start_percent, end_percent = value
cnet.append((cnet_stack, image,), )
x_values.append((",".join((str(strength), str(start_percent), str(end_percent),))),)
cnet.append(value)
x_values.append(str(index))
x_values = "; ".join(x_values)
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
@@ -4629,9 +4651,8 @@ class pipeXYPlotAdvanced:
y_values = []
cnet = []
for index, value in enumerate(y_value):
cnet_stack, image, strength, start_percent, end_percent = value
cnet.append((cnet_stack, image,),)
y_values.append((",".join((str(strength), str(start_percent), str(end_percent),))),)
cnet.append(value)
y_values.append(str(index))
y_values = "; ".join(y_values)
new_pipe['loader_settings'] = {
**pipe['loader_settings'],
@@ -5021,49 +5042,43 @@ class XYplot_Control_Net:
axis, = None,
# If cnet_stack is provided, extend each inner array with its content
# if cnet_stack:
# for inner_list in values:
# inner_list.extend(cnet_stack)
cnet_stack = [control_net_name]
values = []
if target_parameter == "strength":
axis = "advanced: ControlNetStrength"
values.append((cnet_stack, image, first_strength, start_percent, end_percent))
values.append([(control_net_name, image, first_strength, start_percent, end_percent)])
strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0
for i in range(1, batch_count - 1):
values.append((cnet_stack, image, first_strength + i * strength_increment, start_percent,
end_percent))
values.append([(control_net_name, image, first_strength + i * strength_increment, start_percent,
end_percent)])
if batch_count > 1:
values.append((cnet_stack, image, last_strength, start_percent, end_percent))
values.append([(control_net_name, image, last_strength, start_percent, end_percent)])
elif target_parameter == "start_percent":
axis = "advanced: ControlNetStart%"
percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append((cnet_stack, image, strength, first_start_percent, end_percent))
values.append([(control_net_name, image, strength, first_start_percent, end_percent)])
for i in range(1, batch_count - 1):
values.append((cnet_stack, image, strength, first_start_percent + i * percent_increment,
end_percent))
values.append([(control_net_name, image, strength, first_start_percent + i * percent_increment,
end_percent)])
# Always add the last start_percent if batch_count is more than 1.
if batch_count > 1:
values.append((cnet_stack, image, strength, last_start_percent, end_percent))
values.append((control_net_name, image, strength, last_start_percent, end_percent))
elif target_parameter == "end_percent":
axis = "advanced: ControlNetEnd%"
percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append((cnet_stack, image, strength, start_percent, first_end_percent))
values.append([(control_net_name, image, image, strength, start_percent, first_end_percent)])
for i in range(1, batch_count - 1):
values.append((cnet_stack, image, strength, start_percent,
first_end_percent + i * percent_increment))
values.append([(control_net_name, image, strength, start_percent,
first_end_percent + i * percent_increment)])
if batch_count > 1:
values.append((cnet_stack, image, strength, start_percent, last_end_percent))
values.append([(control_net_name, image, strength, start_percent, last_end_percent)])
return ({"axis": axis, "values": values},)
+221
View File
@@ -1,5 +1,9 @@
from PIL import Image
from PIL.PngImagePlugin import PngInfo
from enum import Enum
import datetime
import re
import json
import os
import hashlib
import folder_paths
@@ -218,6 +222,109 @@ class imageSizeByLongerSide:
result = (0,)
return {"ui": {"text": str(result[0])}, "result": result}
# 图像缩放
class imageScaleDown:
crop_methods = ["disabled", "center"]
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"width": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1},
),
"height": (
"INT",
{"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1},
),
"crop": (s.crop_methods,),
}
}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "EasyUse/Image"
FUNCTION = "image_scale_down"
def image_scale_down(self, images, width, height, crop):
if crop == "center":
old_width = images.shape[2]
old_height = images.shape[1]
old_aspect = old_width / old_height
new_aspect = width / height
x = 0
y = 0
if old_aspect > new_aspect:
x = round((old_width - old_width * (new_aspect / old_aspect)) / 2)
elif old_aspect < new_aspect:
y = round((old_height - old_height * (old_aspect / new_aspect)) / 2)
s = images[:, y: old_height - y, x: old_width - x, :]
else:
s = images
results = []
for image in s:
img = tensor2pil(image).convert("RGB")
img = img.resize((width, height), Image.LANCZOS)
results.append(pil2tensor(img))
return (torch.cat(results, dim=0),)
# 图像缩放比例
class imageScaleDownBy(imageScaleDown):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"scale_by": (
"FLOAT",
{"default": 0.5, "min": 0.01, "max": 1.0, "step": 0.01},
),
}
}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "EasyUse/Image"
FUNCTION = "image_scale_down_by"
def image_scale_down_by(self, images, scale_by):
width = images.shape[2]
height = images.shape[1]
new_width = int(width * scale_by)
new_height = int(height * scale_by)
return self.image_scale_down(images, new_width, new_height, "center")
# 图像缩放尺寸
class imageScaleDownToSize(imageScaleDownBy):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"size": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),
"mode": ("BOOLEAN", {"default": True, "label_on": "max", "label_off": "min"}),
}
}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "EasyUse/Image"
FUNCTION = "image_scale_down_to_size"
def image_scale_down_to_size(self, images, size, mode):
width = images.shape[2]
height = images.shape[1]
if mode:
scale_by = size / max(width, height)
else:
scale_by = size / min(width, height)
scale_by = min(scale_by, 1.0)
return self.image_scale_down_by(images, scale_by)
# 图像完美像素
class imagePixelPerfect:
@classmethod
@@ -296,6 +403,110 @@ class imageToMask:
image = pil2tensor(image)
return (image.squeeze().mean(2),)
# 图像保存 (简易)
from comfy.cli_args import args
class imageSaveSimple:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "EasyUse/Image"
@staticmethod
def _format_date(text: str, date: datetime.datetime) -> str:
"""Format the date according to specific patterns."""
date_formats = {
'd': lambda d: d.day,
'dd': lambda d: '{:02d}'.format(d.day),
'M': lambda d: d.month,
'MM': lambda d: '{:02d}'.format(d.month),
'h': lambda d: d.hour,
'hh': lambda d: '{:02d}'.format(d.hour),
'm': lambda d: d.minute,
'mm': lambda d: '{:02d}'.format(d.minute),
's': lambda d: d.second,
'ss': lambda d: '{:02d}'.format(d.second),
'y': lambda d: d.year,
'yy': lambda d: str(d.year)[2:],
'yyy': lambda d: str(d.year)[1:],
'yyyy': lambda d: d.year,
}
# We need to sort the keys in reverse order to ensure we match the longest formats first
for format_str in sorted(date_formats.keys(), key=len, reverse=True):
if format_str in text:
text = text.replace(format_str, str(date_formats[format_str](date)))
return text
def save(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
filename_prefix = re.sub(r'%date:(.*?)%', lambda m: self._format_date(m.group(1), datetime.datetime.now()),
filename_prefix)
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
for image in images:
img = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
filename = filename.replace("%width%", str(img.size[0])).replace("%height%", str(img.size[1]))
metadata = None
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
file = f"{filename}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return { "ui": { "images": results } }
# 图像批次合并
class JoinImageBatch:
"""Turns an image batch into one big image."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"mode": (("horizontal", "vertical"), {"default": "horizontal"}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "join"
CATEGORY = "EasyUse/Image"
def join(self, images, mode):
n, h, w, c = images.shape
image = None
if mode == "vertical":
# for vertical we can just reshape
image = images.reshape(1, n * h, w, c)
elif mode == "horizontal":
# for horizontal we have to swap axes
image = torch.transpose(torch.transpose(images, 1, 2).reshape(1, n * w, h, c), 1, 2)
return (image,)
# 姿势编辑器
class poseEditor:
@@ -345,7 +556,12 @@ NODE_CLASS_MAPPINGS = {
"easy imageSizeBySide": imageSizeBySide,
"easy imageSizeByLongerSide": imageSizeByLongerSide,
"easy imagePixelPerfect": imagePixelPerfect,
"easy imageScaleDown": imageScaleDown,
"easy imageScaleDownBy": imageScaleDownBy,
"easy imageScaleDownToSize": imageScaleDownToSize,
"easy imageToMask": imageToMask,
"easy imageSave": imageSaveSimple,
"easy joinImageBatch": JoinImageBatch,
"easy poseEditor": poseEditor
}
@@ -355,7 +571,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy imageSizeBySide": "ImageSize (Side)",
"easy imageSizeByLongerSide": "ImageSize (LongerSide)",
"easy imagePixelPerfect": "ImagePixelPerfect",
"easy imageScaleDown": "Image Scale Down",
"easy imageScaleDownBy": "Image Scale Down By",
"easy imageScaleDownToSize": "Image Scale Down To Size",
"easy imageToMask": "ImageToMask",
"easy imageHSVMask": "ImageHSVMask",
"easy imageSave": "SaveImage (Simple)",
"easy joinImageBatch": "JoinImageBatch",
"easy poseEditor": "PoseEditor"
}
+424
View File
@@ -0,0 +1,424 @@
from typing import Iterator, List, Tuple, Dict, Any, Union, Optional
from _decimal import Context, getcontext
from decimal import Decimal
import numpy as np
def validate_list_args(args: Dict[str, List[Any]]) -> Tuple[bool, Optional[str], Optional[str]]:
"""
Checks that if there are multiple arguments, they are all the same length or 1
:param args:
:return: Tuple (Status, mismatched_key_1, mismatched_key_2)
"""
# Only have 1 arg
if len(args) == 1:
return True, None, None
len_to_match = None
matched_arg_name = None
for arg_name, arg in args.items():
if arg_name == 'self':
# self is in locals()
continue
if len(arg) != 1:
if len_to_match is None:
len_to_match = len(arg)
matched_arg_name = arg_name
elif len(arg) != len_to_match:
return False, arg_name, matched_arg_name
return True, None, None
def error_if_mismatched_list_args(args: Dict[str, List[Any]]) -> None:
is_valid, failed_key1, failed_key2 = validate_list_args(args)
if not is_valid:
assert failed_key1 is not None
assert failed_key2 is not None
raise ValueError(
f"Mismatched list inputs received. {failed_key1}({len(args[failed_key1])}) !== {failed_key2}({len(args[failed_key2])})"
)
def zip_with_fill(*lists: Union[List[Any], None]) -> Iterator[Tuple[Any, ...]]:
"""
Zips lists together, but if a list has 1 element, it will be repeated for each element in the other lists.
If a list is None, None will be used for that element.
(Not intended for use with lists of different lengths)
:param lists:
:return: Iterator of tuples of length len(lists)
"""
max_len = max(len(lst) if lst is not None else 0 for lst in lists)
for i in range(max_len):
yield tuple(None if lst is None else (lst[0] if len(lst) == 1 else lst[i]) for lst in lists)
# ---------------------------------------------------------------类型 开始----------------------------------------------------------------------#
# 字符串
class String:
@classmethod
def INPUT_TYPES(s):
return {
"required": {"value": ("STRING", {"default": ""})},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = "execute"
CATEGORY = "EasyUse/Logic/Type"
def execute(self, value):
return (value,)
# 整数
class Int:
@classmethod
def INPUT_TYPES(s):
return {
"required": {"value": ("INT", {"default": 0})},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("int",)
FUNCTION = "execute"
CATEGORY = "EasyUse/Logic/Type"
def execute(self, value):
return (value,)
# 整数范围
class RangeInt:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s) -> Dict[str, Dict[str, Any]]:
return {
"required": {
"range_mode": (["step", "num_steps"], {"default": "step"}),
"start": ("INT", {"default": 0, "min": -4096, "max": 4096, "step": 1}),
"stop": ("INT", {"default": 0, "min": -4096, "max": 4096, "step": 1}),
"step": ("INT", {"default": 0, "min": -4096, "max": 4096, "step": 1}),
"num_steps": ("INT", {"default": 0, "min": -4096, "max": 4096, "step": 1}),
"end_mode": (["Inclusive", "Exclusive"], {"default": "Inclusive"}),
},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("range", "range_sizes")
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, True)
FUNCTION = "build_range"
CATEGORY = "EasyUse/Logic/Type"
def build_range(
self, range_mode, start, stop, step, num_steps, end_mode
) -> Tuple[List[int], List[int]]:
error_if_mismatched_list_args(locals())
ranges = []
range_sizes = []
for range_mode, e_start, e_stop, e_num_steps, e_step, e_end_mode in zip_with_fill(
range_mode, start, stop, num_steps, step, end_mode
):
if range_mode == 'step':
if e_end_mode == "Inclusive":
e_stop += 1
vals = list(range(e_start, e_stop, e_step))
ranges.extend(vals)
range_sizes.append(len(vals))
elif range_mode == 'num_steps':
direction = 1 if e_stop > e_start else -1
if e_end_mode == "Exclusive":
e_stop -= direction
vals = (np.rint(np.linspace(e_start, e_stop, e_num_steps)).astype(int).tolist())
ranges.extend(vals)
range_sizes.append(len(vals))
return ranges, range_sizes
# 浮点数
class Float:
@classmethod
def INPUT_TYPES(s):
return {
"required": {"value": ("FLOAT", {"default": 0, "step": 0.01})},
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("float",)
FUNCTION = "execute"
CATEGORY = "EasyUse/Logic/Type"
def execute(self, value):
return (value,)
# 浮点数范围
class RangeFloat:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s) -> Dict[str, Dict[str, Any]]:
return {
"required": {
"range_mode": (["step", "num_steps"], {"default": "step"}),
"start": ("FLOAT", {"default": 0, "min": -4096, "max": 4096, "step": 0.1}),
"stop": ("FLOAT", {"default": 0, "min": -4096, "max": 4096, "step": 0.1}),
"step": ("FLOAT", {"default": 0, "min": -4096, "max": 4096, "step": 0.1}),
"num_steps": ("INT", {"default": 0, "min": -4096, "max": 4096, "step": 1}),
"end_mode": (["Inclusive", "Exclusive"], {"default": "Inclusive"}),
},
}
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("range", "range_sizes")
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, True)
FUNCTION = "build_range"
CATEGORY = "EasyUse/Logic/Type"
@staticmethod
def _decimal_range(
range_mode: String, start: Decimal, stop: Decimal, step: Decimal, num_steps: Int, inclusive: bool
) -> Iterator[float]:
if range_mode == 'step':
ret_val = start
if inclusive:
stop = stop + step
direction = 1 if step > 0 else -1
while (ret_val - stop) * direction < 0:
yield float(ret_val)
ret_val += step
elif range_mode == 'num_steps':
step = (stop - start) / (num_steps - 1)
direction = 1 if step > 0 else -1
ret_val = start
for _ in range(num_steps):
if (ret_val - stop) * direction > 0: # Ensure we don't exceed the 'stop' value
break
yield float(ret_val)
ret_val += step
def build_range(
self,
range_mode,
start,
stop,
step,
num_steps,
end_mode,
) -> Tuple[List[float], List[int]]:
error_if_mismatched_list_args(locals())
getcontext().prec = 12
start = [Decimal(s) for s in start]
stop = [Decimal(s) for s in stop]
step = [Decimal(s) for s in step]
ranges = []
range_sizes = []
for range_mode, e_start, e_stop, e_step, e_num_steps, e_end_mode in zip_with_fill(
range_mode, start, stop, step, num_steps, end_mode
):
vals = list(
self._decimal_range(range_mode, e_start, e_stop, e_step, e_num_steps, e_end_mode == 'Inclusive')
)
ranges.extend(vals)
range_sizes.append(len(vals))
return ranges, range_sizes
# 布尔
class Boolean:
@classmethod
def INPUT_TYPES(s):
return {
"required": {"value": ("BOOLEAN", {"default": False})},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("boolean",)
FUNCTION = "execute"
CATEGORY = "EasyUse/Logic/Type"
def execute(self, value):
return (value,)
# ---------------------------------------------------------------运算 开始----------------------------------------------------------------------#
COMPARE_FUNCTIONS = {
"a == b": lambda a, b: a == b,
"a != b": lambda a, b: a != b,
"a < b": lambda a, b: a < b,
"a > b": lambda a, b: a > b,
"a <= b": lambda a, b: a <= b,
"a >= b": lambda a, b: a >= b,
}
class AlwaysEqualProxy(str):
def __eq__(self, _):
return True
def __ne__(self, _):
return False
# 比较
class Compare:
@classmethod
def INPUT_TYPES(s):
s.compare_functions = list(COMPARE_FUNCTIONS.keys())
return {
"required": {
"a": (AlwaysEqualProxy("*"), {"default": 0}),
"b": (AlwaysEqualProxy("*"), {"default": 0}),
"comparison": (s.compare_functions, {"default": "a == b"}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("boolean",)
FUNCTION = "compare"
CATEGORY = "EasyUse/Logic/Math"
def compare(self, a, b, comparison):
return (COMPARE_FUNCTIONS[comparison](a, b),)
# 判断
class If:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"any": (AlwaysEqualProxy("*"),),
"if": (AlwaysEqualProxy("*"),),
"else": (AlwaysEqualProxy("*"),),
},
}
RETURN_TYPES = (AlwaysEqualProxy("*"),)
RETURN_NAMES = ("?",)
FUNCTION = "execute"
CATEGORY = "EasyUse/Logic/Math"
def execute(self, *args, **kwargs):
return (kwargs['if'] if kwargs['any'] else kwargs['else'],)
#xy矩阵
class xyAny:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"X": (AlwaysEqualProxy("*"), {}),
"Y": (AlwaysEqualProxy("*"), {}),
"direction": (["horizontal", "vertical"], {"default": "horizontal"})
}
}
RETURN_TYPES = (AlwaysEqualProxy("*"), AlwaysEqualProxy("*"))
RETURN_NAMES = ("X", "Y")
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, True)
CATEGORY = "EasyUse/Logic"
FUNCTION = "to_xy"
def to_xy(self, X, Y, direction):
new_x = list()
new_y = list()
if direction[0] == "horizontal":
for y in Y:
for x in X:
new_x.append(x)
new_y.append(y)
else:
for x in X:
for y in Y:
new_x.append(x)
new_y.append(y)
return (new_x, new_y)
# 转换所有类型
class ConvertAnything:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"anything": (AlwaysEqualProxy("*"),),
"output_type": (["string", "int", "float", "boolean"], {"default": "string"}),
}}
RETURN_TYPES = (AlwaysEqualProxy("*"),),
RETURN_NAMES = ('*',)
OUTPUT_NODE = True
FUNCTION = "convert"
CATEGORY = "EasyUse/Logic"
def convert(self, *args, **kwargs):
print(kwargs)
anything = kwargs['anything']
output_type = kwargs['output_type']
params = None
if output_type == 'string':
params = str(anything)
elif output_type == 'int':
params = int(anything)
elif output_type == 'float':
params = float(anything)
elif output_type == 'boolean':
params = bool(anything)
return (params,)
# 将所有类型的内容都转成字符串输出
class ShowAnything:
@classmethod
def INPUT_TYPES(s):
return {"required": {}, "optional": {"anything": (AlwaysEqualProxy("*"), {}), },
"hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO",
}}
RETURN_TYPES = ()
INPUT_IS_LIST = True
OUTPUT_NODE = True
FUNCTION = "log_input"
CATEGORY = "EasyUse/Logic"
def log_input(self, unique_id=None, extra_pnginfo=None, **kwargs):
print(kwargs['anything'])
if unique_id and extra_pnginfo and "workflow" in extra_pnginfo[0]:
workflow = extra_pnginfo[0]["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id[0]), None)
if node:
node["widgets_values"] = [kwargs['anything']]
return {"ui": {"text": kwargs['anything']}}
NODE_CLASS_MAPPINGS = {
"easy string": String,
"easy int": Int,
"easy rangeInt": RangeInt,
"easy float": Float,
"easy rangeFloat": RangeFloat,
"easy boolean": Boolean,
"easy compare": Compare,
"easy if": If,
"easy xyAny": xyAny,
"easy convertAnything": ConvertAnything,
"easy showAnything": ShowAnything,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy string": "String",
"easy int": "Int",
"easy rangeInt": "Range(Int)",
"easy float": "Float",
"easy rangeFloat": "Range(Float)",
"easy boolean": "Boolean",
"easy compare": "Compare",
"easy if": "If",
"easy xyAny": "XYAny",
"easy convertAnything": "ConvertAnything",
"easy showAnything": "ShowAnything"
}
+3 -1
View File
@@ -384,7 +384,9 @@ app.registerExtension({
e.currentTarget.parentNode.draggable = 'true';
})
text_group_title.addEventListener('mouseleave',e=>{
isGroupMapcanMove = true
setTimeout(_=>{
isGroupMapcanMove = true
},150)
})
group_item.append(text_group_title)
// 按钮组
+78 -2
View File
@@ -1,5 +1,7 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
import { ComfyWidgets } from "/scripts/widgets.js";
let origProps = {};
@@ -149,6 +151,17 @@ function widgetLogic(node, widget) {
updateNodeHeight(node)
}
if (widget.name == 'range_mode'){
if(widget.value == 'step'){
toggleWidget(node, findWidgetByName(node, 'step'), true)
toggleWidget(node, findWidgetByName(node, 'num_steps'))
}else if(widget.value == 'num_steps'){
toggleWidget(node, findWidgetByName(node, 'step'))
toggleWidget(node, findWidgetByName(node, 'num_steps'), true)
}
updateNodeHeight(node)
}
if (widget.name === 'toggle') {
widget.type = 'toggle'
widget.options = {on: 'Enabled', off: 'Disabled'}
@@ -329,6 +342,8 @@ app.registerExtension({
case "easy XYInputs: Sampler/Scheduler":
case "easy XYInputs: PromptSR":
case "easy XYInputs: ControlNet":
case "easy rangeInt":
case "easy rangeFloat":
getSetters(node)
break
case "easy wildcards":
@@ -509,7 +524,6 @@ app.registerExtension({
accept: "text/csv",
style: "display: none",
onchange: async (event) => {
if (fileInput.files.length) {
await uploadFile(fileInput.files[0], true);
event.target.value = ''
@@ -646,6 +660,68 @@ app.registerExtension({
},1)
}
}
if(nodeData.name == 'easy showAnything'){
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === "text");
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i].onRemove?.();
}
this.widgets.length = pos;
}
}
for (const list of text) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
}
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
app.graph.setDirtyCanvas(true, false);
});
}
// When the node is executed we will be sent the input text, display this in the widget
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigure?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
}
if(nodeData.name == 'easy convertAnything'){
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
const type_control = this.widgets[this.widgets.findIndex((w) => w.name === "output_type")]
let _this = this
type_control.callback = _ => {
_this.outputs[0].type = (type_control.value).toUpperCase()
_this.outputs[0].name = type_control.value
}
}
}
}
});
@@ -655,7 +731,7 @@ const getSetWidgets = ['rescale_after_model', 'rescale', 'image_output',
'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method',
'image_output', 'add_noise', 'info', 'sampler_name',
'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name',
'num_loras', 'mode', 'toggle', 'resolution', 'target_parameter', 'input_count', 'replace_count', "downscale_mode"]
'num_loras', 'mode', 'toggle', 'resolution', 'target_parameter', 'input_count', 'replace_count', 'downscale_mode', 'range_mode']
function getSetters(node) {
if (node.widgets)
+3 -2
View File
@@ -54,15 +54,16 @@ app.registerExtension({
name: 'comfy.easyUse.quick',
init() {
const keybindListener = async function (event) {
const modifierPressed = event.altKey;
let modifierPressed = event.altKey;
const isEnabled = getEnableNodeTemplateShortcut()
if(isEnabled){
const mac_alt_nums = ['¡','™','£','¢','∞','§','¶','•','ª']
const nums = ['1','2','3','4','5','6','7','8','9']
let key = event.key
if(mac_alt_nums.includes(key)){
const idx = mac_alt_nums.findIndex(key)
const idx = mac_alt_nums.findIndex(cate=> cate == key)
key = nums[idx]
modifierPressed = true
}
if(['1','2','3','4','5','6','7','8','9'].includes(key) && modifierPressed) {
const template = loadTemplate()
+4 -4
View File
@@ -210,7 +210,7 @@ app.registerExtension({
this.addOutput("*", '*');
this.onConnectionsChange = function(
slotType, //0 = output, 1 = input
slot, //self-explanatory
@@ -224,13 +224,13 @@ app.registerExtension({
},1)
}
this.setName = function(name) {
node.widgets[0].value = name;
node.onRename();
node.serialize();
}
this.onRename = function() {
const setter = this.findSetter(node.graph);
@@ -256,7 +256,7 @@ app.registerExtension({
node.graph.removeLink(linkId)
}
})
}
}
}
this.setType = function(type) {