Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1616dd6602 | ||
|
|
282eedfea6 | ||
|
|
17b163e234 | ||
|
|
501d97bb5c | ||
|
|
de92038f88 | ||
|
|
530333d72d | ||
|
|
041f49540c | ||
|
|
71c7865d2d | ||
|
|
c7fbf05970 | ||
|
|
2986a01469 | ||
|
|
fa7c5d8b4d | ||
|
|
1d8db7510b | ||
|
|
7ef0612ce7 | ||
|
|
7ff4790493 | ||
|
|
640ef31625 | ||
|
|
e4ac947d96 | ||
|
|
d287e28e5c | ||
|
|
f33c17f762 | ||
|
|
e07b8cc7bf | ||
|
|
6abe07bb79 | ||
|
|
7fbd03bda7 | ||
|
|
9cc2ac02da | ||
|
|
2f2a3035a2 | ||
|
|
5d8f0a3b0a | ||
|
|
8aadd72494 | ||
|
|
fceec754a4 | ||
|
|
419b7c985c | ||
|
|
ce62fc73da | ||
|
|
4f31641da3 | ||
|
|
deec62ab76 | ||
|
|
5c8cdb58c7 | ||
|
|
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 |
+6
-1
@@ -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 会导致工作流处理取消
|
||||
@@ -509,7 +515,6 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
|
||||
**Comfyui-Easy-Use** 是一个 GPL 许可的开源项目。为了项目取得更好、可持续的发展,我希望能够获得更多的支持。 如果我的自定义节点为您的一天增添了价值,请考虑喝杯咖啡来进一步补充能量! 💖感谢您的支持,每一杯咖啡都是我创作的动力!
|
||||
|
||||
- [BiliBili充电](https://space.bilibili.com/1840885116)
|
||||
- [爱发电](https://afdian.com/a/yolain)
|
||||
- [Wechat/Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
|
||||
|
||||
感谢您的捐助,我将用这些费用来租用 GPU 或购买其他 GPT 服务,以便更好地调试和完善 ComfyUI-Easy-Use 功能
|
||||
|
||||
@@ -47,6 +47,18 @@ Double-click install.bat to install the required dependencies
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v1.3.1**
|
||||
|
||||
- Rewrite drawNodeWidget and fix the GroupNode preview issue.
|
||||
- Updated some features of XYPlot by [mekinney](https://github.com/mekinney)
|
||||
- Add `easy seedList` node (It's useful for in loops)
|
||||
|
||||
**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
|
||||
@@ -493,7 +505,6 @@ If my custom nodes has added value to your day, consider indulging in a coffee t
|
||||
💖You can support me in any of the following ways:
|
||||
|
||||
- [BiliBili](https://space.bilibili.com/1840885116)
|
||||
- [Afdian](https://afdian.com/a/yolain)
|
||||
- [Wechat / Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
|
||||
|
||||
## 🌟Stargazers
|
||||
|
||||
+2
-30
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.2.9"
|
||||
__version__ = "1.3.1"
|
||||
|
||||
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": {
|
||||
@@ -1727,6 +1756,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy seedList": {
|
||||
"display_name": "随机种列表",
|
||||
"description": "可用于for循环的随机数种子列表,通过与easy forLoopStart节点的索引与easy indexAny节点相连接可实现在循环中使用不同种子值进行采样",
|
||||
"inputs": {
|
||||
"min_num": {
|
||||
"name": "最小值"
|
||||
},
|
||||
"max_num": {
|
||||
"name": "最大值"
|
||||
},
|
||||
"method": {
|
||||
"name": "生成方式"
|
||||
},
|
||||
"total": {
|
||||
"name": "总量"
|
||||
},
|
||||
"seed": {
|
||||
"name": "列表序号"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "随机种"
|
||||
},
|
||||
"1": {
|
||||
"name": "总量"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy globalSeed": {
|
||||
"display_name": "全局随机种",
|
||||
"inputs": {
|
||||
@@ -5691,6 +5749,9 @@
|
||||
},
|
||||
"pixels": {
|
||||
"name": "限制像素"
|
||||
},
|
||||
"method": {
|
||||
"name": "限制方式"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -6333,7 +6394,7 @@
|
||||
"name": "高度"
|
||||
},
|
||||
"scale": {
|
||||
"name": "缩放洗漱"
|
||||
"name": "缩放系数"
|
||||
},
|
||||
"flip_w/h": {
|
||||
"name": "翻转宽高"
|
||||
@@ -6676,4 +6737,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-1
@@ -351,7 +351,7 @@ class easyLoader:
|
||||
lora_path = None
|
||||
|
||||
if lora_path is not None:
|
||||
log_node_info("Load LORA",f"{lora_name}: {model_strength}, {clip_strength}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
|
||||
log_node_info("Load LORA",f"{lora_name}: model={model_strength:.3f}, clip={clip_strength:.3f}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
|
||||
if lbw:
|
||||
lbw = lora["lbw"]
|
||||
lbw_a = lora["lbw_a"]
|
||||
@@ -432,10 +432,13 @@ class easyLoader:
|
||||
clip_vision = None
|
||||
lora_stack = []
|
||||
|
||||
# Check for model override
|
||||
can_load_lora = True
|
||||
# 判断是否存在 模型或Lora叠加xyplot, 若存在优先缓存第一个模型
|
||||
# Determine whether there is a model or Lora overlapping xyplot, and if there is, prioritize caching the first model.
|
||||
xy_model_id = next((x for x in prompt if str(prompt[x]["class_type"]) in ["easy XYInputs: ModelMergeBlocks",
|
||||
"easy XYInputs: Checkpoint"]), None)
|
||||
# This will find nodes that aren't actively connected to anything, and skip loading lora's for them.
|
||||
xy_lora_id = next((x for x in prompt if str(prompt[x]["class_type"]) == "easy XYInputs: Lora"), None)
|
||||
if xy_lora_id is not None:
|
||||
can_load_lora = False
|
||||
@@ -461,6 +464,7 @@ class easyLoader:
|
||||
|
||||
if optional_lora_stack is not None and can_load_lora:
|
||||
for lora in optional_lora_stack:
|
||||
# This is a subtle bit of code because it uses the model created by the last call, and passes it to the next call.
|
||||
lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1],
|
||||
"clip_strength": lora[2]}
|
||||
model, clip = self.load_lora(lora)
|
||||
|
||||
+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:]
|
||||
|
||||
|
||||
+90
-15
@@ -8,6 +8,7 @@ from .log import log_node_warn
|
||||
from ..modules.layer_diffuse import LayerDiffuse
|
||||
from ..config import RESOURCES_DIR
|
||||
from nodes import CLIPTextEncode
|
||||
import pprint
|
||||
try:
|
||||
from comfy_extras.nodes_flux import FluxGuidance
|
||||
except:
|
||||
@@ -52,7 +53,7 @@ class easyXYPlot():
|
||||
|
||||
plot_image_vars[value_type] = value
|
||||
if value_type in ["seed", "Seeds++ Batch"]:
|
||||
value_label = f"{value}"
|
||||
value_label = f"seed: {value}"
|
||||
else:
|
||||
value_label = f"{value_type}: {value}"
|
||||
|
||||
@@ -63,7 +64,9 @@ class easyXYPlot():
|
||||
arr = value.split(',')
|
||||
model_name = os.path.basename(os.path.splitext(arr[0])[0])
|
||||
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr[3]) > 2 else ''
|
||||
value_label = f"{model_name}{trigger_words}"
|
||||
lora_weight = float(arr[1]) if value_type == 'Lora' and len(arr) > 1 else 0
|
||||
lora_weight_desc = f"({lora_weight:.2f})" if lora_weight > 0 else ''
|
||||
value_label = f"{model_name[:30]}{lora_weight_desc} {trigger_words}"
|
||||
|
||||
if value_type in ["ModelMergeBlocks"]:
|
||||
if ":" in value:
|
||||
@@ -118,24 +121,32 @@ class easyXYPlot():
|
||||
|
||||
def calculate_background_dimensions(self):
|
||||
border_size = int((self.max_width // 8) * 1.5) if self.y_type != "None" or self.x_type != "None" else 0
|
||||
|
||||
bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * (
|
||||
self.y_type != "None")
|
||||
bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * (
|
||||
self.x_type != "None")
|
||||
|
||||
# Add space at the bottom of the image for common informaiton about the image
|
||||
bg_height = bg_height + (border_size*2)
|
||||
# print(f"Grid Size: width = {bg_width} height = {bg_height} border_size = {border_size}")
|
||||
|
||||
x_offset_initial = border_size if self.y_type != "None" else 0
|
||||
y_offset = border_size if self.x_type != "None" else 0
|
||||
|
||||
return bg_width, bg_height, x_offset_initial, y_offset
|
||||
|
||||
|
||||
def adjust_font_size(self, text, initial_font_size, label_width):
|
||||
font = self.get_font(initial_font_size, self.custom_font)
|
||||
text_width = font.getbbox(text)
|
||||
# pprint.pp(f"Initial font size: {initial_font_size}, text: {text}, text_width: {text_width}")
|
||||
if text_width and text_width[2]:
|
||||
text_width = text_width[2]
|
||||
|
||||
scaling_factor = 0.9
|
||||
if text_width > (label_width * scaling_factor):
|
||||
# print(f"Adjusting font size from {initial_font_size} to fit text width {text_width} into label width {label_width} scaling_factor {scaling_factor}")
|
||||
return int(initial_font_size * (label_width / text_width) * scaling_factor)
|
||||
else:
|
||||
return initial_font_size
|
||||
@@ -144,15 +155,22 @@ class easyXYPlot():
|
||||
_, _, width, height = d.textbbox((0, 0), text=text, font=font)
|
||||
return width, height
|
||||
|
||||
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10):
|
||||
label_width = img.width if is_x_label else img.height
|
||||
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10, label_width=0, label_height=0):
|
||||
|
||||
# if the label_width is specified, leave it along. Otherwise do the old logic.
|
||||
if label_width == 0:
|
||||
label_width = img.width if is_x_label else img.height
|
||||
|
||||
text_lines = text.split('\n')
|
||||
longest_line = max(text_lines, key=len)
|
||||
|
||||
# Adjust font size
|
||||
font_size = self.adjust_font_size(text, initial_font_size, label_width)
|
||||
font_size = self.adjust_font_size(longest_line, initial_font_size, label_width)
|
||||
font_size = min(max_font_size, font_size) # Ensure font isn't too large
|
||||
font_size = max(min_font_size, font_size) # Ensure font isn't too small
|
||||
|
||||
label_height = int(font_size * 1.5) if is_x_label else font_size
|
||||
if label_height == 0:
|
||||
label_height = int(font_size * 1.5) if is_x_label else font_size
|
||||
|
||||
label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0))
|
||||
d = ImageDraw.Draw(label_bg)
|
||||
@@ -166,7 +184,7 @@ class easyXYPlot():
|
||||
text = text + '...'
|
||||
|
||||
# Compute text width and height for multi-line text
|
||||
text_lines = text.split('\n')
|
||||
|
||||
text_widths, text_heights = zip(*[self.textsize(d, line, font=font) for line in text_lines])
|
||||
max_text_width = max(text_widths)
|
||||
total_text_height = sum(text_heights)
|
||||
@@ -195,8 +213,7 @@ class easyXYPlot():
|
||||
clip = clip if clip is not None else plot_image_vars["clip"]
|
||||
steps = plot_image_vars['steps'] if "steps" in plot_image_vars else 1
|
||||
|
||||
sd_version = get_sd_version(plot_image_vars['model'])
|
||||
|
||||
sd_version = get_sd_version(plot_image_vars['model'])
|
||||
# 高级用法
|
||||
if plot_image_vars["x_node_type"] == "advanced" or plot_image_vars["y_node_type"] == "advanced":
|
||||
if self.x_type == "Seeds++ Batch" or self.y_type == "Seeds++ Batch":
|
||||
@@ -347,17 +364,24 @@ class easyXYPlot():
|
||||
|
||||
# Lora
|
||||
if self.x_type == "Lora" or self.y_type == "Lora":
|
||||
# print(f"Lora: {x_value} {y_value}")
|
||||
model = model if model is not None else plot_image_vars["model"]
|
||||
clip = clip if clip is not None else plot_image_vars["clip"]
|
||||
|
||||
xy_values = x_value if self.x_type == "Lora" else y_value
|
||||
lora_name, lora_model_strength, lora_clip_strength, _ = xy_values.split(",")
|
||||
lora_stack = [{"lora_name": lora_name, "model": model, "clip" :clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}]
|
||||
|
||||
# print(f"new_lora_stack: {new_lora_stack}")
|
||||
|
||||
|
||||
if 'lora_stack' in plot_image_vars:
|
||||
lora_stack = lora_stack + plot_image_vars['lora_stack']
|
||||
|
||||
|
||||
if lora_stack is not None and lora_stack != []:
|
||||
for lora in lora_stack:
|
||||
# Each generation of the model, must use the reference to previously created model / clip objects.
|
||||
lora['model'] = model
|
||||
lora['clip'] = clip
|
||||
model, clip = self.easyCache.load_lora(lora)
|
||||
|
||||
# 提示词
|
||||
@@ -464,6 +488,7 @@ class easyXYPlot():
|
||||
plot_image_vars['negative_weight_interpretation'], w_max=1.0,
|
||||
apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps)
|
||||
|
||||
|
||||
model = model if model is not None else plot_image_vars["model"]
|
||||
vae = vae if vae is not None else plot_image_vars["vae"]
|
||||
positive = positive if positive is not None else plot_image_vars["positive_cond"]
|
||||
@@ -582,11 +607,10 @@ class easyXYPlot():
|
||||
|
||||
return self.latents_plot
|
||||
|
||||
def plot_images_and_labels(self):
|
||||
# Calculate the background dimensions
|
||||
def plot_images_and_labels(self, plot_image_vars):
|
||||
|
||||
bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions()
|
||||
|
||||
# Create the white background image
|
||||
background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
|
||||
|
||||
output_image = []
|
||||
@@ -618,4 +642,55 @@ class easyXYPlot():
|
||||
|
||||
y_offset += img.height + self.grid_spacing
|
||||
|
||||
return (self.sampler.pil2tensor(background), output_image)
|
||||
# lookup used models in the image
|
||||
common_label = ""
|
||||
# Update to add a function to do the heavy lifting. Parameters are plot_image_vars name, label to use, names of the axis,
|
||||
|
||||
# pprint.pp(plot_image_vars)
|
||||
|
||||
# We don't process LORAs here because there can be multiple of them.
|
||||
labels = [
|
||||
{"id": "ckpt_name", "id_desc": "ckpt", "axis_type" : "Checkpoint"},
|
||||
{"id": "vae_name", "id_desc": '', "axis_type" : "vae_name"},
|
||||
{"id": "sampler_name", "id_desc": "sampler", "axis_type" : "Sampler"},
|
||||
{"id": "scheduler", "id_desc": '', "axis_type" : "Scheduler"},
|
||||
{"id": "steps", "id_desc": '', "axis_type" : "Steps"},
|
||||
{"id": "Flux Guidance", "id_desc": 'guidance', "axis_type" : "Flux Guidance"},
|
||||
{"id": "seed", "id_desc": '', "axis_type" : "Seeds++ Batch"}
|
||||
]
|
||||
|
||||
for item in labels:
|
||||
# Only add the label if it's not one of the axis
|
||||
# print(f"Checking item: {item['id']} axis_type {item['axis_type']} x_type: {self.x_type} y_type: {self.y_type}")
|
||||
if self.x_type != item['axis_type'] and self.y_type != item['axis_type']:
|
||||
common_label += self.add_common_label(item['id'], plot_image_vars, item['id_desc'])
|
||||
common_label += f"\n"
|
||||
|
||||
if plot_image_vars['lora_stack'] is not None and plot_image_vars['lora_stack'] != []:
|
||||
# print(f"lora_stack: {plot_image_vars['lora_stack']}")
|
||||
for lora in plot_image_vars['lora_stack']:
|
||||
|
||||
lora_name = lora['lora_name']
|
||||
lora_weight = lora['model_strength']
|
||||
if lora_name is not None and len(lora_name) > 0 and lora_weight > 0:
|
||||
common_label += f"LORA: {lora_name} weight: {lora_weight:.2f} \n"
|
||||
|
||||
common_label = common_label.strip()
|
||||
|
||||
if len(common_label) > 0:
|
||||
label_height = background.height - y_offset
|
||||
label_bg = self.create_label(background, common_label, int(48 * background.width / 512), label_width=background.width, label_height=label_height)
|
||||
label_x = (background.width - label_bg.width) // 2
|
||||
label_y = y_offset
|
||||
# print(f"Adding common label: {common_label} x = {label_x} y = {label_y}")
|
||||
background.alpha_composite(label_bg, (label_x, label_y))
|
||||
|
||||
return (self.sampler.pil2tensor(background), output_image)
|
||||
|
||||
def add_common_label(self, tag, plot_image_vars, description = ''):
|
||||
label = ''
|
||||
if description == '': description = tag
|
||||
if tag in plot_image_vars and plot_image_vars[tag] is not None and plot_image_vars[tag] != 'None':
|
||||
label += f"{description}: {plot_image_vars[tag]} "
|
||||
# print(f"add_common_label: {tag} description: {description} label: {label}" )
|
||||
return label
|
||||
|
||||
@@ -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,)
|
||||
|
||||
+71
-26
@@ -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':
|
||||
@@ -1044,7 +1057,12 @@ class imageChooser(PreviewImage):
|
||||
images_in = torch.cat(kwargs.pop('images'))
|
||||
self.batch = images_in.shape[0]
|
||||
for x in kwargs: kwargs[x] = kwargs[x][0]
|
||||
result = self.save_images(images=images_in, prompt=prompt)
|
||||
|
||||
try:
|
||||
pnginfo = extra_pnginfo[0]
|
||||
except:
|
||||
pnginfo = None
|
||||
result = self.save_images(images=images_in, prompt=prompt, extra_pnginfo=pnginfo)
|
||||
|
||||
images = result['ui']['images']
|
||||
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
|
||||
@@ -1924,6 +1942,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 +1969,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 ,fit='fill'):
|
||||
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, fit, 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, fit, 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
|
||||
@@ -1977,18 +2022,18 @@ class makeImageForICRepaint:
|
||||
image, mask, context_mask = None, None, None
|
||||
|
||||
# resize
|
||||
if img1_h != img2_h and img1_w != img2_w:
|
||||
if img1_h != img2_h or img1_w != img2_w:
|
||||
width, height = img2_w, img2_h
|
||||
if direction == 'left-right' and img1_h != img2_h:
|
||||
scale_factor = img2_h / img1_h
|
||||
width = round(img1_w * scale_factor)
|
||||
elif direction == 'top-bottom' and img1_w != img2_w:
|
||||
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)
|
||||
fit = 'crop'
|
||||
if method != 'uniform width':
|
||||
if direction == 'left-right' and img1_h != img2_h:
|
||||
scale_factor = img2_h / img1_h
|
||||
width = round(img1_w * scale_factor)
|
||||
elif direction == 'top-bottom' and img1_w != img2_w:
|
||||
scale_factor = img2_w / img1_w
|
||||
height = round(img1_h * scale_factor)
|
||||
fit = 'fill'
|
||||
image_1, mask_1 = self.resize_image_and_mask(image_1, mask_1, width, height, fit)
|
||||
|
||||
if mask_1 is None:
|
||||
mask_1 = torch.full((1, image_1.shape[1], image_1.shape[2]), 0, dtype=torch.float32, device="cpu")
|
||||
|
||||
+18
-16
@@ -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("*")
|
||||
@@ -166,7 +166,7 @@ class Float:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"value": ("FLOAT", {"default": 0, "step": 0.01, "min": -999999, "max": 999999, })},
|
||||
"required": {"value": ("FLOAT", {"default": 0, "step": 0.01, "min":-0xffffffffffffffff, "max": 0xffffffffffffffff, })},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
@@ -175,7 +175,7 @@ class Float:
|
||||
CATEGORY = "EasyUse/Logic/Type"
|
||||
|
||||
def execute(self, value):
|
||||
return (value,)
|
||||
return (round(value, 3),)
|
||||
|
||||
|
||||
# 浮点数范围
|
||||
@@ -239,9 +239,9 @@ class RangeFloat:
|
||||
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]
|
||||
start = [round(Decimal(s),2) for s in start]
|
||||
stop = [round(Decimal(s),2) for s in stop]
|
||||
step = [round(Decimal(s),2) for s in step]
|
||||
|
||||
ranges = []
|
||||
range_sizes = []
|
||||
@@ -573,17 +573,17 @@ class mathFloatOperation:
|
||||
|
||||
def float_math_operation(self, a, b, operation):
|
||||
if operation == "add":
|
||||
return (a + b,)
|
||||
return (round(a + b,3),)
|
||||
elif operation == "subtract":
|
||||
return (a - b,)
|
||||
return (round(a - b,3),)
|
||||
elif operation == "multiply":
|
||||
return (a * b,)
|
||||
return (round(a * b,3),)
|
||||
elif operation == "divide":
|
||||
return (a / b,)
|
||||
return (round(a / b,3),)
|
||||
elif operation == "modulo":
|
||||
return (a % b,)
|
||||
return (round(a % b,3),)
|
||||
elif operation == "power":
|
||||
return (a ** b,)
|
||||
return (round(a ** b,3),)
|
||||
|
||||
|
||||
class mathStringOperation:
|
||||
@@ -1607,8 +1607,10 @@ class saveText:
|
||||
if not os.path.exists(output_file_path):
|
||||
os.makedirs(output_file_path)
|
||||
|
||||
if not overwrite:
|
||||
pass
|
||||
if overwrite:
|
||||
file_mode = "w"
|
||||
else:
|
||||
file_mode = "a"
|
||||
|
||||
log_node_info("Save Text", f"Saving to {filepath}")
|
||||
|
||||
@@ -1617,13 +1619,13 @@ class saveText:
|
||||
for i in text.split("\n"):
|
||||
text_list.append(i.strip())
|
||||
|
||||
with open(filepath, "w", newline="", encoding='utf-8') as csv_file:
|
||||
with open(filepath, file_mode, newline="", encoding='utf-8') as csv_file:
|
||||
csv_writer = csv.writer(csv_file)
|
||||
# Write each line as a separate row in the CSV file
|
||||
for line in text_list:
|
||||
csv_writer.writerow([line])
|
||||
else:
|
||||
with open(filepath, "w", newline="", encoding='utf-8') as text_file:
|
||||
with open(filepath, file_mode, newline="", encoding='utf-8') as text_file:
|
||||
for line in text:
|
||||
text_file.write(line)
|
||||
|
||||
|
||||
+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
|
||||
}
|
||||
}
|
||||
@@ -507,7 +513,7 @@ class samplerFull:
|
||||
|
||||
samp_samples = {"samples": latents_plot}
|
||||
|
||||
images, image_list = sampleXYplot.plot_images_and_labels()
|
||||
images, image_list = sampleXYplot.plot_images_and_labels(plot_image_vars)
|
||||
|
||||
# Generate output_images
|
||||
output_images = torch.stack([tensor.squeeze() for tensor in image_list])
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from ..config import MAX_SEED_NUM
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
class easySeed:
|
||||
@classmethod
|
||||
@@ -19,6 +21,53 @@ class easySeed:
|
||||
def doit(self, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
|
||||
return seed,
|
||||
|
||||
class seedList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"min_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
|
||||
"max_num": ("INT", {"default": MAX_SEED_NUM, "min": 0 }),
|
||||
"method": (["random", "increment", "decrement"], {"default": "random"}),
|
||||
"total": ("INT", {"default": 1, "min": 1, "max": 100000}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM,}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("seed", "total")
|
||||
FUNCTION = "doit"
|
||||
DESCRIPTION = "Random number seed that can be used in a for loop, by connecting index and easy indexAny node to realize different seed values in the loop."
|
||||
|
||||
CATEGORY = "EasyUse/Seed"
|
||||
|
||||
def doit(self, min_num, max_num, method, total, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
|
||||
random.seed(seed)
|
||||
|
||||
seed_list = []
|
||||
if min_num > max_num:
|
||||
min_num, max_num = max_num, min_num
|
||||
for i in range(total):
|
||||
if method == 'random':
|
||||
s = random.randint(min_num, max_num)
|
||||
elif method == 'increment':
|
||||
s = min_num + i
|
||||
if s > max_num:
|
||||
s = max_num
|
||||
elif method == 'decrement':
|
||||
s = max_num - i
|
||||
if s < min_num:
|
||||
s = min_num
|
||||
seed_list.append(s)
|
||||
return seed_list, total
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, seed, **kwargs):
|
||||
m = hashlib.sha256()
|
||||
m.update(seed)
|
||||
return m.digest().hex()
|
||||
|
||||
# 全局随机种
|
||||
class globalSeed:
|
||||
@classmethod
|
||||
@@ -46,10 +95,12 @@ class globalSeed:
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy seed": easySeed,
|
||||
"easy seedList": seedList,
|
||||
"easy globalSeed": globalSeed,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy seed": "EasySeed",
|
||||
"easy seedList": "EasySeedList",
|
||||
"easy globalSeed": "EasyGlobalSeed",
|
||||
}
|
||||
+20
-1
@@ -106,6 +106,23 @@ class setControlName:
|
||||
|
||||
def set_name(self, controlnet_name):
|
||||
return (controlnet_name,)
|
||||
|
||||
class setLoraName:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {
|
||||
"lora_name": (folder_paths.get_filename_list("loras"),),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (AlwaysEqualProxy('*'),)
|
||||
RETURN_NAMES = ("lora_name",)
|
||||
FUNCTION = "set_name"
|
||||
CATEGORY = "EasyUse/Util"
|
||||
|
||||
def set_name(self, lora_name):
|
||||
return (lora_name,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
@@ -113,6 +130,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"easy sliderControl": sliderControl,
|
||||
"easy ckptNames": setCkptName,
|
||||
"easy controlnetNames": setControlName,
|
||||
"easy loraNames": setLoraName,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -120,4 +138,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy sliderControl": "Easy Slider Control",
|
||||
"easy ckptNames": "Ckpt Names",
|
||||
"easy controlnetNames": "ControlNet Names",
|
||||
}
|
||||
"easy loraNames": "Lora Names",
|
||||
}
|
||||
|
||||
@@ -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.1"
|
||||
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