Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d820842e39 | ||
|
|
2f78a523b3 | ||
|
|
b2a8666423 | ||
|
|
2c02a471d0 | ||
|
|
ea521e0303 | ||
|
|
9b5daac023 | ||
|
|
0de83f88dc | ||
|
|
342ce8ccad | ||
|
|
b7881d84b1 | ||
|
|
0f5ad38384 | ||
|
|
a3f487c822 | ||
|
|
d0f496adc1 | ||
|
|
665861ff35 | ||
|
|
61568e021c | ||
|
|
a2edc37d89 | ||
|
|
1c4cb43f7b | ||
|
|
66143b0e20 | ||
|
|
f0da5e25c9 | ||
|
|
ebf25b585f | ||
|
|
fb8968d438 | ||
|
|
15cfeedf7b | ||
|
|
50ae13a993 | ||
|
|
aedf917067 | ||
|
|
69ac5e52a0 | ||
|
|
368f7e508d | ||
|
|
44f0676323 | ||
|
|
98273b37f2 |
@@ -52,6 +52,12 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
**v1.3.0**
|
||||
|
||||
- 将循环节点设置为最大输入和输出数量为20
|
||||
- 添加 `uniform width` 方式到 `easy makeImageForICLora`
|
||||
- 增加 `wildcardsPromptMatrix` 通配符提示词矩阵,由 [Rosmeowtis](https://github.com/Rosmeowtis) 贡献
|
||||
|
||||
**v1.2.9**
|
||||
|
||||
- 修复 Imagechooser 会导致工作流处理取消
|
||||
|
||||
@@ -47,6 +47,12 @@ Double-click install.bat to install the required dependencies
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v1.3.0**
|
||||
|
||||
- Set loop nodes maximum number of inputs and outputs to 20
|
||||
- Add `uniform width` method to `easy makeImageForICLora`
|
||||
- Add `wildcardsPromptMatrix` Node by [Rosmeowtis](https://github.com/Rosmeowtis)
|
||||
|
||||
**v1.2.9**
|
||||
|
||||
- Fix ImageChooser causes workflow processing to cancel
|
||||
|
||||
+2
-30
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.2.9"
|
||||
__version__ = "1.3.0"
|
||||
|
||||
import yaml
|
||||
import json
|
||||
@@ -20,31 +20,6 @@ for module_name in nodes_list:
|
||||
imported_module = importlib.import_module(".py.nodes.{}".format(module_name), __name__)
|
||||
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}
|
||||
# transfer python nodes to locale file
|
||||
# for i in imported_module.NODE_CLASS_MAPPINGS:
|
||||
# locale[i] = {
|
||||
# "display_name": imported_module.NODE_DISPLAY_NAME_MAPPINGS[i] if i in imported_module.NODE_DISPLAY_NAME_MAPPINGS else i,
|
||||
# "inputs":{},
|
||||
# "outputs":{},
|
||||
# }
|
||||
# node_class = imported_module.NODE_CLASS_MAPPINGS[i]
|
||||
# input_types = node_class.INPUT_TYPES()
|
||||
# if "required" in input_types:
|
||||
# for j in input_types["required"]:
|
||||
# locale[i]['inputs'][j] = {"name": j}
|
||||
# if "optional" in input_types:
|
||||
# for j in input_types["optional"]:
|
||||
# locale[i]['inputs'][j] = {"name": j}
|
||||
# count = 0
|
||||
# if "RETURN_NAMES" in node_class.__dict__:
|
||||
# for j in node_class.RETURN_NAMES:
|
||||
# locale[i]['outputs'][str(count)] = {"name": j}
|
||||
# count+=1
|
||||
|
||||
# en_json_path = os.path.join(cwd_path,'locales/en/nodeDefs.json')
|
||||
# with open(en_json_path, 'w', encoding='utf-8') as f:
|
||||
# json.dump(locale, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
#Wildcards
|
||||
from .py.libs.wildcards import read_wildcard_dict
|
||||
@@ -86,11 +61,8 @@ if not os.path.exists(example_path):
|
||||
with open(example_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# get comfyui revision
|
||||
from .py.libs.utils import compare_revision
|
||||
|
||||
new_frontend_revision = 2546
|
||||
web_default_version = 'v2' if compare_revision(new_frontend_revision) else 'v1'
|
||||
web_default_version = 'v2'
|
||||
# web directory
|
||||
config_path = os.path.join(cwd_path, "config.yaml")
|
||||
if os.path.isfile(config_path):
|
||||
|
||||
@@ -145,6 +145,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy wildcardsMatrix": {
|
||||
"display_name": "Wildcards Matrix",
|
||||
"inputs": {
|
||||
"Select to add LoRA": {
|
||||
"name": "Select to add LoRA"
|
||||
},
|
||||
"Select to add Wildcard": {
|
||||
"name": "Select to add Wildcard"
|
||||
},
|
||||
"offset": {
|
||||
"name": "Offset in All Probilities"
|
||||
},
|
||||
"output_limit": {
|
||||
"name": "Output Limit",
|
||||
"tooltip": "Output n fill wildcards, -1 is output all possibilities (force offset to zero), the default value is 1"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Replaced Prompt"
|
||||
},
|
||||
"1": {
|
||||
"name": "Total Count of the Probilities"
|
||||
},
|
||||
"2": {
|
||||
"name": "Factors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy prompt": {
|
||||
"display_name": "Prompt",
|
||||
"inputs": {
|
||||
|
||||
@@ -75,6 +75,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy wildcardsMatrix": {
|
||||
"display_name": "通配符提示词矩阵",
|
||||
"inputs": {
|
||||
"Select to add LoRA": {
|
||||
"name": "选择添加Lora"
|
||||
},
|
||||
"Select to add Wildcard": {
|
||||
"name": "选择添加通配符"
|
||||
},
|
||||
"offset": {
|
||||
"name": "偏移量"
|
||||
},
|
||||
"output_limit": {
|
||||
"name": "输出个数限制",
|
||||
"tooltip": "输出n个填充后通配符, -1为输出所有可能性(偏移值失效),默认值为1"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "通配填充词"
|
||||
},
|
||||
"1": {
|
||||
"name": "可能性总数"
|
||||
},
|
||||
"2": {
|
||||
"name": "可能性总数(每通配符)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy prompt": {
|
||||
"display_name": "提示词",
|
||||
"inputs": {
|
||||
@@ -5691,6 +5720,9 @@
|
||||
},
|
||||
"pixels": {
|
||||
"name": "限制像素"
|
||||
},
|
||||
"method": {
|
||||
"name": "限制方式"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -57,8 +57,11 @@ class BizyAIRAPI:
|
||||
)
|
||||
|
||||
# joycaptionTwo
|
||||
def joyCaption2(self, payload, image):
|
||||
api_key = self.getAPIKey()
|
||||
def joyCaption2(self, payload, image, apikey_override=None):
|
||||
if apikey_override is not None:
|
||||
api_key = apikey_override
|
||||
else:
|
||||
api_key = self.getAPIKey()
|
||||
url = f"{self.base_url}/supernode/joycaption2"
|
||||
auth = f"Bearer {api_key}"
|
||||
headers = {
|
||||
|
||||
+29
-1
@@ -125,7 +125,35 @@ class ResizeMode(Enum):
|
||||
return 2
|
||||
assert False, "NOTREACHED"
|
||||
|
||||
|
||||
# credit by https://github.com/chflame163/ComfyUI_LayerStyle/blob/main/py/imagefunc.py#L591C1-L617C22
|
||||
def fit_resize_image(image: Image, target_width: int, target_height: int, fit: str, resize_sampler: str,
|
||||
background_color: str = '#000000') -> Image:
|
||||
image = image.convert('RGB')
|
||||
orig_width, orig_height = image.size
|
||||
if image is not None:
|
||||
if fit == 'letterbox':
|
||||
if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑
|
||||
fit_width = target_width
|
||||
fit_height = int(target_width / orig_width * orig_height)
|
||||
else: # 更瘦,左右留黑
|
||||
fit_height = target_height
|
||||
fit_width = int(target_height / orig_height * orig_width)
|
||||
fit_image = image.resize((fit_width, fit_height), resize_sampler)
|
||||
ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color)
|
||||
ret_image.paste(fit_image, box=((target_width - fit_width) // 2, (target_height - fit_height) // 2))
|
||||
elif fit == 'crop':
|
||||
if orig_width / orig_height > target_width / target_height: # 更宽,裁左右
|
||||
fit_width = int(orig_height * target_width / target_height)
|
||||
fit_image = image.crop(
|
||||
((orig_width - fit_width) // 2, 0, (orig_width - fit_width) // 2 + fit_width, orig_height))
|
||||
else: # 更瘦,裁上下
|
||||
fit_height = int(orig_width * target_height / target_width)
|
||||
fit_image = image.crop(
|
||||
(0, (orig_height - fit_height) // 2, orig_width, (orig_height - fit_height) // 2 + fit_height))
|
||||
ret_image = fit_image.resize((target_width, target_height), resize_sampler)
|
||||
else:
|
||||
ret_image = image.resize((target_width, target_height), resize_sampler)
|
||||
return ret_image
|
||||
|
||||
# CLIP反推
|
||||
import comfy.utils
|
||||
|
||||
+177
-5
@@ -1,9 +1,13 @@
|
||||
import re
|
||||
import random
|
||||
import os
|
||||
import folder_paths
|
||||
import yaml
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from math import prod
|
||||
|
||||
import yaml
|
||||
|
||||
import folder_paths
|
||||
|
||||
from .log import log_node_info
|
||||
|
||||
easy_wildcard_dict = {}
|
||||
@@ -302,3 +306,171 @@ def process_with_loras(wildcard_opt, model, clip, title="Positive", seed=None, c
|
||||
log_node_info("easy wildcards",f'{title}_decode: {pass1}')
|
||||
|
||||
return model, clip, pass2, pass1, show_wildcard_prompt, pipe_lora_stack
|
||||
|
||||
|
||||
def expand_wildcard(keyword: str) -> tuple[str]:
|
||||
"""传入文件通配符的关键词,从 easy_wildcard_dict 中获取通配符的所有选项。"""
|
||||
global easy_wildcard_dict
|
||||
if keyword in easy_wildcard_dict:
|
||||
return tuple(easy_wildcard_dict[keyword])
|
||||
elif '*' in keyword:
|
||||
subpattern = keyword.replace('*', '.*').replace('+', r"\+")
|
||||
total_pattern = []
|
||||
for k, v in easy_wildcard_dict.items():
|
||||
if re.match(subpattern, k) is not None:
|
||||
total_pattern.extend(v)
|
||||
if total_pattern:
|
||||
return tuple(total_pattern)
|
||||
elif '/' not in keyword:
|
||||
return expand_wildcard(f"*/{keyword}")
|
||||
|
||||
def expand_options(options: str) -> tuple[str]:
|
||||
"""传入去掉 {} 的选项。
|
||||
展开选项通配符,返回该选项中的每一项,这里的每一项都是一个替换项。
|
||||
不会对选项内容进行任何处理,即便存在空格或特殊符号,也会原样返回。"""
|
||||
return tuple(options.split("|"))
|
||||
|
||||
|
||||
def decimal_to_irregular(n, bases):
|
||||
"""
|
||||
将十进制数转换为不规则进制
|
||||
|
||||
:param n: 十进制数
|
||||
:param bases: 各位置的基数列表,从低位到高位
|
||||
:return: 不规则进制表示的列表,从低位到高位
|
||||
"""
|
||||
if n == 0:
|
||||
return [0] * len(bases) if bases else [0]
|
||||
|
||||
digits = []
|
||||
remaining = n
|
||||
|
||||
# 从低位到高位处理
|
||||
for base in bases:
|
||||
digit = remaining % base
|
||||
digits.append(digit)
|
||||
remaining = remaining // base
|
||||
|
||||
return digits
|
||||
|
||||
|
||||
class WildcardProcessor:
|
||||
"""通配符处理器
|
||||
|
||||
通配符格式:
|
||||
+ option : {a|b}
|
||||
+ wildcard: __keyword__ 通配符内容将从 Easy-Use 插件提供的 easy_wildcard_dict 中获取
|
||||
"""
|
||||
|
||||
RE_OPTIONS = re.compile(r"{([^{}]*?)}")
|
||||
RE_WILDCARD = re.compile(r"__([\w\s.\-+/*\\]+?)__")
|
||||
RE_REPLACER = re.compile(r"{([^{}]*?)}|__([\w\s.\-+/*\\]+?)__")
|
||||
|
||||
# 将输入的提示词转化成符合 python str.format 要求格式的模板,并将 option 和 wildcard 按照顺序在模板中留下 {0}, {1} 等占位符
|
||||
template: str
|
||||
# option、wildcard 的替换项列表,按照在模板中出现的顺序排列,相同的替换项列表只保留第一份
|
||||
replacers: dict[int, tuple[str]]
|
||||
# 占位符的编号和替换项列表的索引的映射,占位符编号按照在模板中出现的顺序排列,方便减少替换项的存储占用
|
||||
placeholder_mapping: dict[str, int] # placeholder_id => replacer_id
|
||||
# 各替换项列表的项数,按照在模板中出现的顺序排列,提前计算,方便后续使用
|
||||
placeholder_choices: dict[str, int] # placeholder_id => len(replacer)
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.__make_template(text)
|
||||
self.__total = None
|
||||
|
||||
def random(self, seed=None) -> str:
|
||||
"从所有可能性中随机获取一个"
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
return self.getn(random.randint(0, self.total() - 1))
|
||||
|
||||
def getn(self, n: int) -> str:
|
||||
"从所有可能性中获取第 n 个,以 self.total() 为周期循环"
|
||||
n = n % self.total()
|
||||
indice = decimal_to_irregular(n, self.placeholder_choices.values())
|
||||
replacements = {
|
||||
placeholder_id: self.replacers[self.placeholder_mapping[placeholder_id]][i]
|
||||
for placeholder_id, i in zip(self.placeholder_mapping.keys(), indice)
|
||||
}
|
||||
return self.template.format(**replacements)
|
||||
|
||||
def getmany(self, limit: int, offset: int = 0) -> list[str]:
|
||||
"""返回一组可能性组成的列表,为了避免结果太长导致内存占用超限,使用 limit 限制列表的长度,使用 offset 调整偏移。
|
||||
若 limit 和 offset 的设置导致预期的结果长度超过剩下的实际长度,则会回到开头。
|
||||
"""
|
||||
return [self.getn(n) for n in range(offset, offset + limit)]
|
||||
|
||||
def total(self) -> int:
|
||||
"计算可能性的数目"
|
||||
if self.__total is None:
|
||||
self.__total = prod(self.placeholder_choices.values())
|
||||
return self.__total
|
||||
|
||||
def __make_template(self, text: str):
|
||||
"""将输入的提示词转化成符合 python str.format 要求格式的模板,
|
||||
并将 option 和 wildcard 按照顺序在模板中留下 {r0}, {r1} 等占位符,
|
||||
即使遇到相同的 option 或 wildcard,留下的占位符编号也不同,从而使每项都独立变化。
|
||||
"""
|
||||
self.placeholder_mapping = {}
|
||||
placeholder_id = 0
|
||||
replacer_id = 0
|
||||
replacers_rev = {} # replacers => id
|
||||
blocks = []
|
||||
# 记录所处理过的通配符末尾在文本中的位置,用于拼接完整的模板
|
||||
tail = 0
|
||||
for match in self.RE_REPLACER.finditer(text):
|
||||
# 提取并展开通配符内容
|
||||
m = match.group(0)
|
||||
if m.startswith("{"):
|
||||
choices = expand_options(m[1:-1])
|
||||
elif m.startswith("__"):
|
||||
keyword = m[2:-2].lower()
|
||||
keyword = wildcard_normalize(keyword)
|
||||
choices = expand_wildcard(keyword)
|
||||
else:
|
||||
raise ValueError(f"{m!r} is not a wildcard or option")
|
||||
|
||||
# 记录通配符的替换项列表和ID,相同的通配符只保留第一个
|
||||
if choices not in replacers_rev:
|
||||
replacers_rev[choices] = replacer_id
|
||||
replacer_id += 1
|
||||
|
||||
# 拼接通配符前方文本
|
||||
start, end = match.span()
|
||||
blocks.append(text[tail:start])
|
||||
tail = end
|
||||
# 将通配符替换为占位符,并记录占位符和替换项列表的索引的映射
|
||||
blocks.append(f"{{r{placeholder_id}}}")
|
||||
self.placeholder_mapping[f"r{placeholder_id}"] = replacers_rev[choices]
|
||||
placeholder_id += 1
|
||||
|
||||
if tail < len(text):
|
||||
blocks.append(text[tail:])
|
||||
self.template = "".join(blocks)
|
||||
self.replacers = {v: k for k, v in replacers_rev.items()}
|
||||
self.placeholder_choices = {
|
||||
placeholder_id: len(self.replacers[replacer_id])
|
||||
for placeholder_id, replacer_id in self.placeholder_mapping.items()
|
||||
}
|
||||
|
||||
|
||||
def test_option():
|
||||
text = "{|a|b|c}"
|
||||
answer = ["", "a", "b", "c"]
|
||||
p = WildcardProcessor(text)
|
||||
assert p.total() == len(answer)
|
||||
assert p.getn(0) == answer[0]
|
||||
assert p.getmany(4) == answer
|
||||
assert p.getmany(4, 1) == answer[1:]
|
||||
|
||||
|
||||
def test_same():
|
||||
text = "{a|b},{a|b}"
|
||||
answer = ["a,a", "b,a", "a,b", "b,b"]
|
||||
p = WildcardProcessor(text)
|
||||
assert p.total() == len(answer)
|
||||
assert p.getn(0) == answer[0]
|
||||
assert p.getmany(4) == answer
|
||||
assert p.getmany(4, 1) == answer[1:]
|
||||
|
||||
|
||||
@@ -5,13 +5,21 @@ import os
|
||||
import types
|
||||
|
||||
import torch
|
||||
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
|
||||
try:
|
||||
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
|
||||
except:
|
||||
init_empty_weights, load_checkpoint_and_dispatch = None, None
|
||||
|
||||
import comfy
|
||||
|
||||
from .model import BrushNetModel, PowerPaintModel
|
||||
from .model_patch import add_model_patch_option, patch_model_function_wrapper
|
||||
from .powerpaint_utils import TokenizerWrapper, add_tokens
|
||||
try:
|
||||
from .model import BrushNetModel, PowerPaintModel
|
||||
from .model_patch import add_model_patch_option, patch_model_function_wrapper
|
||||
from .powerpaint_utils import TokenizerWrapper, add_tokens
|
||||
except:
|
||||
BrushNetModel, PowerPaintModel = None, None
|
||||
add_model_patch_option, patch_model_function_wrapper = None, None
|
||||
TokenizerWrapper, add_tokens = None, None
|
||||
|
||||
cwd_path = os.path.dirname(os.path.realpath(__file__))
|
||||
brushnet_config_file = os.path.join(cwd_path, 'config', 'brushnet.json')
|
||||
|
||||
@@ -9,7 +9,10 @@ from enum import Enum
|
||||
from comfy.utils import load_torch_file
|
||||
from comfy.conds import CONDRegular
|
||||
from comfy_extras.nodes_compositing import JoinImageWithAlpha
|
||||
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
|
||||
try:
|
||||
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
|
||||
except:
|
||||
ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel = None, None, None
|
||||
from .attension_sharing import AttentionSharingPatcher
|
||||
from ...config import LAYER_DIFFUSION, LAYER_DIFFUSION_DIR, LAYER_DIFFUSION_VAE
|
||||
from ...libs.utils import to_lora_patch_dict, get_local_filepath, get_sd_version
|
||||
@@ -51,7 +54,7 @@ class LayerDiffuse:
|
||||
|
||||
return (write_c_concat(cond), write_c_concat(uncond))
|
||||
|
||||
def apply_layer_diffusion(self, model: ModelPatcher, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
|
||||
def apply_layer_diffusion(self, model, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
|
||||
control_img: Optional[torch.TensorType] = None
|
||||
sd_version = get_sd_version(model)
|
||||
model_url = LAYER_DIFFUSION[method.value][sd_version]["model_url"]
|
||||
|
||||
+5
-1
@@ -101,6 +101,9 @@ class joyCaption2API:
|
||||
"multiline": True,
|
||||
},
|
||||
),
|
||||
},
|
||||
"optional":{
|
||||
"apikey_override": ("STRING", {"default": "", "forceInput": True, "tooltip":"Override the API key in the local config"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +126,7 @@ class joyCaption2API:
|
||||
extra_options,
|
||||
name_input,
|
||||
custom_prompt,
|
||||
apikey_override=None
|
||||
):
|
||||
pbar = comfy.utils.ProgressBar(100)
|
||||
pbar.update_absolute(10)
|
||||
@@ -145,7 +149,7 @@ class joyCaption2API:
|
||||
}
|
||||
|
||||
pbar.update_absolute(30)
|
||||
caption = bizyairAPI.joyCaption2(payload, image)
|
||||
caption = bizyairAPI.joyCaption2(payload, image, apikey_override)
|
||||
|
||||
pbar.update_absolute(100)
|
||||
return (caption,)
|
||||
|
||||
+55
-17
@@ -14,7 +14,7 @@ from torchvision.transforms.functional import to_pil_image
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple
|
||||
from ..libs.cache import cache, update_cache, remove_cache
|
||||
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image
|
||||
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image, fit_resize_image
|
||||
from ..libs.colorfix import adain_color_fix, wavelet_color_fix
|
||||
from ..libs.chooser import ChooserMessage, ChooserCancelled
|
||||
from ..config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR
|
||||
@@ -802,7 +802,20 @@ class imageConcat:
|
||||
elif image2 is None:
|
||||
return (image1,)
|
||||
if match_image_size:
|
||||
image2 = torch.nn.functional.interpolate(image2, size=(image1.shape[2], image1.shape[3]), mode="bilinear")
|
||||
# Convert tensor to PIL for proper aspect ratio resizing
|
||||
pil_image2 = tensor2pil(image2)
|
||||
if direction in ['right', 'left']:
|
||||
aspect_ratio = pil_image2.width / pil_image2.height
|
||||
new_height = image1.shape[1]
|
||||
new_width = int(aspect_ratio * new_height)
|
||||
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
|
||||
else: # 'up' or 'down'
|
||||
aspect_ratio = pil_image2.height / pil_image2.width
|
||||
new_width = image1.shape[2]
|
||||
new_height = int(aspect_ratio * new_width)
|
||||
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
|
||||
image2 = pil2tensor(pil_image2)
|
||||
|
||||
if direction == 'right':
|
||||
row = torch.cat((image1, image2), dim=2)
|
||||
elif direction == 'down':
|
||||
@@ -1924,6 +1937,7 @@ class makeImageForICRepaint:
|
||||
"image_1": ("IMAGE",),
|
||||
"direction": (["top-bottom", "left-right"], {"default": "left-right"}),
|
||||
"pixels": ("INT", {"default": 0, "max": MAX_RESOLUTION, "min": 0, "step": 8, "tooltip": "The pixel of the output image is not set when it is 0"}),
|
||||
"method": (["uniform height", "uniform width", "auto"],{"default": "auto"}),
|
||||
},
|
||||
"optional": {
|
||||
"image_2": ("IMAGE",),
|
||||
@@ -1950,26 +1964,52 @@ class makeImageForICRepaint:
|
||||
b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
|
||||
return torch.cat((r, g, b), dim=-1)
|
||||
|
||||
def make(self, image_1, direction, pixels=0, image_2=None, mask_1=None, mask_2=None):
|
||||
def resize_image_and_mask(self, image, mask, w, h ):
|
||||
ret_images = []
|
||||
ret_masks = []
|
||||
_mask = Image.new('L', size=(w, h), color='black')
|
||||
_image = Image.new('RGB', size=(w, h), color='black')
|
||||
if image is not None and len(image) > 0:
|
||||
for i in image:
|
||||
_image = tensor2pil(i).convert('RGB')
|
||||
_image = fit_resize_image(_image, w, h, 'fill', Image.LANCZOS, '#000000')
|
||||
ret_images.append(pil2tensor(_image))
|
||||
if mask is not None and len(mask) > 0:
|
||||
for m in mask:
|
||||
_mask = tensor2pil(m).convert('L')
|
||||
_mask = fit_resize_image(_mask, w, h, 'fill', Image.LANCZOS).convert('L')
|
||||
ret_masks.append(image2mask(_mask))
|
||||
|
||||
if len(ret_images) > 0 and len(ret_masks) > 0:
|
||||
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
|
||||
elif len(ret_images) > 0 and len(ret_masks) == 0:
|
||||
return (torch.cat(ret_images, dim=0), None,)
|
||||
elif len(ret_images) == 0 and len(ret_masks) > 0:
|
||||
return (None, torch.cat(ret_masks, dim=0),)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
def make(self, image_1, direction, pixels, method, image_2=None, mask_1=None, mask_2=None):
|
||||
if image_2 is None:
|
||||
image_2 = self.emptyImage(image_1.shape[2], image_1.shape[1])
|
||||
mask_2 = torch.full((1, image_1.shape[1], image_1.shape[2]), 1, dtype=torch.float32, device="cpu")
|
||||
|
||||
elif image_2 is not None and mask_2 is None:
|
||||
raise ValueError("mask_2 is required when image_2 is provided")
|
||||
mask_2 = torch.full((1, image_2.shape[1], image_2.shape[2]), 1, dtype=torch.float32, device="cpu")
|
||||
|
||||
if pixels > 0:
|
||||
_, img2_h, img2_w, _ = image_2.shape
|
||||
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
|
||||
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
|
||||
if method == "uniform height":
|
||||
h = pixels
|
||||
w = int(img2_w * (pixels / img2_h))
|
||||
elif method == "uniform width":
|
||||
w = pixels
|
||||
h = int(img2_h * (pixels / img2_w))
|
||||
else:
|
||||
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
|
||||
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
|
||||
|
||||
image_2 = image_2.movedim(-1, 1)
|
||||
image_2 = comfy.utils.common_upscale(image_2, w, h, 'bicubic', 'disabled')
|
||||
image_2 = image_2.movedim(1, -1)
|
||||
|
||||
orig_image_2 = tensor2pil(image_2)
|
||||
orig_mask_2 = tensor2pil(mask_2).convert('L')
|
||||
orig_mask_2 = orig_mask_2.resize(orig_image_2.size)
|
||||
mask_2 = pil2tensor(orig_mask_2)
|
||||
image_2, mask_2 = self.resize_image_and_mask(image_2, mask_2, w, h)
|
||||
|
||||
_, img1_h, img1_w, _ = image_1.shape
|
||||
_, img2_h, img2_w, _ = image_2.shape
|
||||
@@ -1986,9 +2026,7 @@ class makeImageForICRepaint:
|
||||
scale_factor = img2_w / img1_w
|
||||
height = round(img1_h * scale_factor)
|
||||
|
||||
image_1 = image_1.movedim(-1, 1)
|
||||
image_1 = comfy.utils.common_upscale(image_1, width, height, 'bicubic', 'disabled')
|
||||
image_1 = image_1.movedim(1, -1)
|
||||
image_1, mask_1 = self.resize_image_and_mask(image_1, mask_1, width, height)
|
||||
|
||||
if mask_1 is None:
|
||||
mask_1 = torch.full((1, image_1.shape[1], image_1.shape[2]), 0, dtype=torch.float32, device="cpu")
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import comfy.utils
|
||||
import folder_paths
|
||||
|
||||
DEFAULT_FLOW_NUM = 2
|
||||
MAX_FLOW_NUM = 10
|
||||
MAX_FLOW_NUM = 20
|
||||
lazy_options = {"lazy": True} if compare_revision(2543) else {}
|
||||
|
||||
any_type = AlwaysEqualProxy("*")
|
||||
|
||||
+56
-12
@@ -1,12 +1,15 @@
|
||||
import os
|
||||
import json
|
||||
import folder_paths
|
||||
import os
|
||||
from urllib.request import urlopen
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.wildcards import get_wildcard_list, process
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
from ..config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE
|
||||
|
||||
import folder_paths
|
||||
|
||||
from .. import easyCache
|
||||
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
|
||||
from ..libs.log import log_node_info
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
|
||||
|
||||
|
||||
# 正面提示词
|
||||
class positivePrompt:
|
||||
@@ -40,7 +43,7 @@ class wildcardsPrompt:
|
||||
def INPUT_TYPES(s):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
|
||||
@@ -56,9 +59,6 @@ class wildcardsPrompt:
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def translate(self, text):
|
||||
return text
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
seed = kwargs["seed"]
|
||||
@@ -73,16 +73,58 @@ class wildcardsPrompt:
|
||||
_text = []
|
||||
text = text.split("\n")
|
||||
for t in text:
|
||||
t = self.translate(t)
|
||||
_text.append(t)
|
||||
populated_text.append(process(t, seed))
|
||||
text = _text
|
||||
else:
|
||||
text = self.translate(text)
|
||||
populated_text = [process(text, seed)]
|
||||
text = [text]
|
||||
return {"ui": {"value": [seed]}, "result": (text, populated_text)}
|
||||
|
||||
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
|
||||
class wildcardsPromptMatrix:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
wildcard_list = get_wildcard_list()
|
||||
return {"required": {
|
||||
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
|
||||
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
|
||||
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
|
||||
"offset": ("INT", {"default": 0, "min": 0, "step": 1, "control_after_generate": True}),
|
||||
},
|
||||
"optional":{
|
||||
"output_limit": ("INT", {"default": 1, "min": -1, "step": 1, "tooltip": "Output All Probilities"})
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "INT")
|
||||
RETURN_NAMES = ("populated_text", "total", "factors")
|
||||
OUTPUT_IS_LIST = (True, False, True)
|
||||
FUNCTION = "main"
|
||||
|
||||
CATEGORY = "EasyUse/Prompt"
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
prompt = kwargs["prompt"] if "prompt" in kwargs else None
|
||||
offset = kwargs["offset"]
|
||||
output_limit = kwargs.get("output_limit", 1)
|
||||
# Clean loaded_objects
|
||||
if prompt:
|
||||
easyCache.update_loaded_objects(prompt)
|
||||
|
||||
text = kwargs['text']
|
||||
p = WildcardProcessor(text)
|
||||
total = p.total()
|
||||
limit = total if output_limit > total or output_limit == -1 else output_limit
|
||||
offset = 0 if output_limit == -1 else offset
|
||||
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
|
||||
return {"ui": {"value": [offset]}, "result": (populated_text, p.total(), list(p.placeholder_choices.values()))}
|
||||
|
||||
# 负面提示词
|
||||
class negativePrompt:
|
||||
|
||||
@@ -518,6 +560,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy positive": positivePrompt,
|
||||
"easy negative": negativePrompt,
|
||||
"easy wildcards": wildcardsPrompt,
|
||||
"easy wildcardsMatrix": wildcardsPromptMatrix,
|
||||
"easy prompt": prompt,
|
||||
"easy promptList": promptList,
|
||||
"easy promptLine": promptLine,
|
||||
@@ -531,6 +574,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy positive": "Positive",
|
||||
"easy negative": "Negative",
|
||||
"easy wildcards": "Wildcards",
|
||||
"easy wildcardsMatrix": "Wildcards Matrix",
|
||||
"easy prompt": "Prompt",
|
||||
"easy promptList": "PromptList",
|
||||
"easy promptLine": "PromptLine",
|
||||
|
||||
@@ -389,6 +389,12 @@ class samplerFull:
|
||||
|
||||
"loader_settings": {
|
||||
**pipe["loader_settings"],
|
||||
"steps": steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": denoise,
|
||||
"add_noise": add_noise,
|
||||
"spent_time": spent_time
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,29 +146,6 @@ async def getModelsList(request):
|
||||
else:
|
||||
return web.Response(status=400)
|
||||
|
||||
# get models thumbnails
|
||||
@PromptServer.instance.routes.get("/easyuse/models/thumbnail")
|
||||
async def getModelsThumbnail(request):
|
||||
limit = 500
|
||||
if "limit" in request.rel_url.query:
|
||||
limit = request.rel_url.query.get("limit")
|
||||
limit = int(limit)
|
||||
checkpoints = folder_paths.get_filename_list("checkpoints_thumb")
|
||||
loras = folder_paths.get_filename_list("loras_thumb")
|
||||
checkpoints_full = []
|
||||
loras_full = []
|
||||
if len(checkpoints) + len(loras) >= limit:
|
||||
return web.Response(status=400)
|
||||
for index, i in enumerate(checkpoints):
|
||||
full_path = folder_paths.get_full_path('checkpoints_thumb', str(i))
|
||||
if full_path:
|
||||
checkpoints_full.append(full_path)
|
||||
for index, i in enumerate(loras):
|
||||
full_path = folder_paths.get_full_path('loras_thumb', str(i))
|
||||
if full_path:
|
||||
loras_full.append(full_path)
|
||||
return web.json_response(checkpoints_full + loras_full)
|
||||
|
||||
@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}")
|
||||
async def save_notes(request):
|
||||
name = request.match_info["name"]
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-easy-use"
|
||||
description = "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes."
|
||||
version = "1.2.9"
|
||||
version = "1.3.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["diffusers", "accelerate", "clip_interrogator>=0.6.0", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python", "matplotlib", "peft"]
|
||||
|
||||
|
||||
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
Reference in New Issue
Block a user