|
import re |
|
import random |
|
import os |
|
import nodes |
|
import folder_paths |
|
import yaml |
|
import numpy as np |
|
import threading |
|
from impact import utils |
|
from impact import config |
|
|
|
|
|
wildcards_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "wildcards")) |
|
|
|
RE_WildCardQuantifier = re.compile(r"(?P<quantifier>\d+)#__(?P<keyword>[\w.\-+/*\\]+?)__", re.IGNORECASE) |
|
wildcard_lock = threading.Lock() |
|
wildcard_dict = {} |
|
|
|
|
|
def get_wildcard_list(): |
|
with wildcard_lock: |
|
return [f"__{x}__" for x in wildcard_dict.keys()] |
|
|
|
|
|
def get_wildcard_dict(): |
|
global wildcard_dict |
|
with wildcard_lock: |
|
return wildcard_dict |
|
|
|
|
|
def wildcard_normalize(x): |
|
return x.replace("\\", "/").replace(' ', '-').lower() |
|
|
|
|
|
def read_wildcard(k, v): |
|
if isinstance(v, list): |
|
k = wildcard_normalize(k) |
|
wildcard_dict[k] = v |
|
elif isinstance(v, dict): |
|
for k2, v2 in v.items(): |
|
new_key = f"{k}/{k2}" |
|
new_key = wildcard_normalize(new_key) |
|
read_wildcard(new_key, v2) |
|
elif isinstance(v, str): |
|
k = wildcard_normalize(k) |
|
wildcard_dict[k] = [v] |
|
|
|
|
|
def read_wildcard_dict(wildcard_path): |
|
global wildcard_dict |
|
for root, directories, files in os.walk(wildcard_path, followlinks=True): |
|
for file in files: |
|
if file.endswith('.txt'): |
|
file_path = os.path.join(root, file) |
|
rel_path = os.path.relpath(file_path, wildcard_path) |
|
key = wildcard_normalize(os.path.splitext(rel_path)[0]) |
|
|
|
try: |
|
with open(file_path, 'r', encoding="ISO-8859-1") as f: |
|
lines = f.read().splitlines() |
|
wildcard_dict[key] = [x for x in lines if not x.strip().startswith('#')] |
|
except yaml.reader.ReaderError: |
|
with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f: |
|
lines = f.read().splitlines() |
|
wildcard_dict[key] = [x for x in lines if not x.strip().startswith('#')] |
|
elif file.endswith('.yaml'): |
|
file_path = os.path.join(root, file) |
|
|
|
try: |
|
with open(file_path, 'r', encoding="ISO-8859-1") as f: |
|
yaml_data = yaml.load(f, Loader=yaml.FullLoader) |
|
except yaml.reader.ReaderError as e: |
|
with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f: |
|
yaml_data = yaml.load(f, Loader=yaml.FullLoader) |
|
|
|
for k, v in yaml_data.items(): |
|
read_wildcard(k, v) |
|
|
|
return wildcard_dict |
|
|
|
|
|
def process_comment_out(text): |
|
lines = text.split('\n') |
|
|
|
lines0 = [] |
|
flag = False |
|
for line in lines: |
|
if line.lstrip().startswith('#'): |
|
flag = True |
|
continue |
|
|
|
if len(lines0) == 0: |
|
lines0.append(line) |
|
elif flag: |
|
lines0[-1] += ' ' + line |
|
flag = False |
|
else: |
|
lines0.append(line) |
|
|
|
return '\n'.join(lines0) |
|
|
|
|
|
def process(text, seed=None): |
|
text = process_comment_out(text) |
|
|
|
if seed is not None: |
|
random.seed(seed) |
|
random_gen = np.random.default_rng(seed) |
|
|
|
local_wildcard_dict = get_wildcard_dict() |
|
|
|
def replace_options(string): |
|
replacements_found = False |
|
|
|
def replace_option(match): |
|
nonlocal replacements_found |
|
options = match.group(1).split('|') |
|
|
|
multi_select_pattern = options[0].split('$$') |
|
select_range = None |
|
select_sep = ' ' |
|
range_pattern = r'(\d+)(-(\d+))?' |
|
range_pattern2 = r'-(\d+)' |
|
wildcard_pattern = r"__([\w.\-+/*\\]+?)__" |
|
|
|
if len(multi_select_pattern) > 1: |
|
r = re.match(range_pattern, options[0]) |
|
|
|
if r is None: |
|
r = re.match(range_pattern2, options[0]) |
|
a = '1' |
|
b = r.group(1).strip() |
|
else: |
|
a = r.group(1).strip() |
|
b = r.group(3) |
|
if b is not None: |
|
b = b.strip() |
|
|
|
if r is not None: |
|
if b is not None and is_numeric_string(a) and is_numeric_string(b): |
|
|
|
select_range = int(a), int(b) |
|
elif is_numeric_string(a): |
|
|
|
x = int(a) |
|
select_range = (x, x) |
|
|
|
if select_range is not None and len(multi_select_pattern) == 2: |
|
|
|
matches = re.findall(wildcard_pattern, multi_select_pattern[1]) |
|
if len(options) == 1 and matches: |
|
|
|
options = get_wildcard_options(multi_select_pattern[1]) |
|
else: |
|
|
|
options[0] = multi_select_pattern[1] |
|
elif select_range is not None and len(multi_select_pattern) == 3: |
|
|
|
select_sep = multi_select_pattern[1] |
|
options[0] = multi_select_pattern[2] |
|
|
|
adjusted_probabilities = [] |
|
|
|
total_prob = 0 |
|
|
|
for option in options: |
|
parts = option.split('::', 1) |
|
if len(parts) == 2 and is_numeric_string(parts[0].strip()): |
|
config_value = float(parts[0].strip()) |
|
else: |
|
config_value = 1 |
|
|
|
adjusted_probabilities.append(config_value) |
|
total_prob += config_value |
|
|
|
normalized_probabilities = [prob / total_prob for prob in adjusted_probabilities] |
|
|
|
if select_range is None: |
|
select_count = 1 |
|
else: |
|
select_count = random_gen.integers(low=select_range[0], high=select_range[1]+1, size=1) |
|
|
|
if select_count > len(options): |
|
random_gen.shuffle(options) |
|
selected_items = options |
|
else: |
|
selected_items = random_gen.choice(options, p=normalized_probabilities, size=select_count, replace=False) |
|
|
|
selected_items2 = [re.sub(r'^\s*[0-9.]+::', '', x, 1) for x in selected_items] |
|
replacement = select_sep.join(selected_items2) |
|
if '::' in replacement: |
|
pass |
|
|
|
replacements_found = True |
|
return replacement |
|
|
|
pattern = r'{([^{}]*?)}' |
|
replaced_string = re.sub(pattern, replace_option, string) |
|
|
|
return replaced_string, replacements_found |
|
|
|
def get_wildcard_options(string): |
|
pattern = r"__([\w.\-+/*\\]+?)__" |
|
matches = re.findall(pattern, string) |
|
|
|
options = [] |
|
|
|
for match in matches: |
|
keyword = match.lower() |
|
keyword = wildcard_normalize(keyword) |
|
if keyword in local_wildcard_dict: |
|
options.extend(local_wildcard_dict[keyword]) |
|
elif '*' in keyword: |
|
subpattern = keyword.replace('*', '.*').replace('+', '\\+') |
|
total_patterns = [] |
|
found = False |
|
for k, v in local_wildcard_dict.items(): |
|
if re.match(subpattern, k) is not None or re.match(subpattern, k+'/') is not None: |
|
total_patterns += v |
|
found = True |
|
|
|
if found: |
|
options.extend(total_patterns) |
|
elif '/' not in keyword: |
|
string_fallback = string.replace(f"__{match}__", f"__*/{match}__", 1) |
|
options.extend(get_wildcard_options(string_fallback)) |
|
|
|
return options |
|
|
|
def replace_wildcard(string): |
|
pattern = r"__([\w.\-+/*\\]+?)__" |
|
matches = re.findall(pattern, string) |
|
|
|
replacements_found = False |
|
|
|
for match in matches: |
|
keyword = match.lower() |
|
keyword = wildcard_normalize(keyword) |
|
if keyword in local_wildcard_dict: |
|
replacement = random_gen.choice(local_wildcard_dict[keyword]) |
|
replacements_found = True |
|
string = string.replace(f"__{match}__", replacement, 1) |
|
elif '*' in keyword: |
|
subpattern = keyword.replace('*', '.*').replace('+', '\\+') |
|
total_patterns = [] |
|
found = False |
|
for k, v in local_wildcard_dict.items(): |
|
if re.match(subpattern, k) is not None or re.match(subpattern, k+'/') is not None: |
|
total_patterns += v |
|
found = True |
|
|
|
if found: |
|
replacement = random_gen.choice(total_patterns) |
|
replacements_found = True |
|
string = string.replace(f"__{match}__", replacement, 1) |
|
elif '/' not in keyword: |
|
string_fallback = string.replace(f"__{match}__", f"__*/{match}__", 1) |
|
string, replacements_found = replace_wildcard(string_fallback) |
|
|
|
return string, replacements_found |
|
|
|
replace_depth = 100 |
|
stop_unwrap = False |
|
while not stop_unwrap and replace_depth > 1: |
|
replace_depth -= 1 |
|
|
|
option_quantifier = [e.groupdict() for e in RE_WildCardQuantifier.finditer(text)] |
|
for match in option_quantifier: |
|
keyword = match['keyword'].lower() |
|
quantifier = int(match['quantifier']) if match['quantifier'] else 1 |
|
replacement = '__|__'.join([keyword,] * quantifier) |
|
wilder_keyword = keyword.replace('*', '\\*') |
|
RE_TEMP = re.compile(fr"(?P<quantifier>\d+)#__(?P<keyword>{wilder_keyword})__", re.IGNORECASE) |
|
text = RE_TEMP.sub(f"__{replacement}__", text) |
|
|
|
|
|
pass1, is_replaced1 = replace_options(text) |
|
|
|
while is_replaced1: |
|
pass1, is_replaced1 = replace_options(pass1) |
|
|
|
|
|
text, is_replaced2 = replace_wildcard(pass1) |
|
stop_unwrap = not is_replaced1 and not is_replaced2 |
|
|
|
return text |
|
|
|
|
|
def is_numeric_string(input_str): |
|
return re.match(r'^-?(\d*\.?\d+|\d+\.?\d*)$', input_str) is not None |
|
|
|
|
|
def safe_float(x): |
|
if is_numeric_string(x): |
|
return float(x) |
|
else: |
|
return 1.0 |
|
|
|
|
|
def extract_lora_values(string): |
|
pattern = r'<lora:([^>]+)>' |
|
matches = re.findall(pattern, string) |
|
|
|
def touch_lbw(text): |
|
return re.sub(r'LBW=[A-Za-z][A-Za-z0-9_-]*:', r'LBW=', text) |
|
|
|
items = [touch_lbw(match.strip(':')) for match in matches] |
|
|
|
added = set() |
|
result = [] |
|
for item in items: |
|
item = item.split(':') |
|
|
|
lora = None |
|
a = None |
|
b = None |
|
lbw = None |
|
lbw_a = None |
|
lbw_b = None |
|
|
|
if len(item) > 0: |
|
lora = item[0] |
|
|
|
for sub_item in item[1:]: |
|
if is_numeric_string(sub_item): |
|
if a is None: |
|
a = float(sub_item) |
|
elif b is None: |
|
b = float(sub_item) |
|
elif sub_item.startswith("LBW="): |
|
for lbw_item in sub_item[4:].split(';'): |
|
if lbw_item.startswith("A="): |
|
lbw_a = safe_float(lbw_item[2:].strip()) |
|
elif lbw_item.startswith("B="): |
|
lbw_b = safe_float(lbw_item[2:].strip()) |
|
elif lbw_item.strip() != '': |
|
lbw = lbw_item |
|
|
|
if a is None: |
|
a = 1.0 |
|
if b is None: |
|
b = a |
|
|
|
if lora is not None and lora not in added: |
|
result.append((lora, a, b, lbw, lbw_a, lbw_b)) |
|
added.add(lora) |
|
|
|
return result |
|
|
|
|
|
def remove_lora_tags(string): |
|
pattern = r'<lora:[^>]+>' |
|
result = re.sub(pattern, '', string) |
|
|
|
return result |
|
|
|
|
|
def resolve_lora_name(lora_name_cache, name): |
|
if os.path.exists(name): |
|
return name |
|
else: |
|
if len(lora_name_cache) == 0: |
|
lora_name_cache.extend(folder_paths.get_filename_list("loras")) |
|
|
|
for x in lora_name_cache: |
|
if x.endswith(name): |
|
return x |
|
|
|
|
|
def process_with_loras(wildcard_opt, model, clip, clip_encoder=None, seed=None, processed=None): |
|
""" |
|
process wildcard text including loras |
|
|
|
:param wildcard_opt: wildcard text |
|
:param model: model |
|
:param clip: clip |
|
:param clip_encoder: you can pass custom encoder such as adv_cliptext_encode |
|
:param seed: seed for populating |
|
:param processed: output variable - [pass1, pass2, pass3] will be saved into passed list |
|
:return: model, clip, conditioning |
|
""" |
|
|
|
lora_name_cache = [] |
|
|
|
pass1 = process(wildcard_opt, seed) |
|
loras = extract_lora_values(pass1) |
|
pass2 = remove_lora_tags(pass1) |
|
|
|
for lora_name, model_weight, clip_weight, lbw, lbw_a, lbw_b in loras: |
|
lora_name_ext = lora_name.split('.') |
|
if ('.'+lora_name_ext[-1]) not in folder_paths.supported_pt_extensions: |
|
lora_name = lora_name+".safetensors" |
|
|
|
orig_lora_name = lora_name |
|
lora_name = resolve_lora_name(lora_name_cache, lora_name) |
|
|
|
if lora_name is not None: |
|
path = folder_paths.get_full_path("loras", lora_name) |
|
else: |
|
path = None |
|
|
|
if path is not None: |
|
print(f"LOAD LORA: {lora_name}: {model_weight}, {clip_weight}, LBW={lbw}, A={lbw_a}, B={lbw_b}") |
|
|
|
def default_lora(): |
|
return nodes.LoraLoader().load_lora(model, clip, lora_name, model_weight, clip_weight) |
|
|
|
if lbw is not None: |
|
if 'LoraLoaderBlockWeight //Inspire' not in nodes.NODE_CLASS_MAPPINGS: |
|
utils.try_install_custom_node( |
|
'https://github.com/ltdrdata/ComfyUI-Inspire-Pack', |
|
"To use 'LBW=' syntax in wildcards, 'Inspire Pack' extension is required.") |
|
|
|
print(f"'LBW(Lora Block Weight)' is given, but the 'Inspire Pack' is not installed. The LBW= attribute is being ignored.") |
|
model, clip = default_lora() |
|
else: |
|
cls = nodes.NODE_CLASS_MAPPINGS['LoraLoaderBlockWeight //Inspire'] |
|
model, clip, _ = cls().doit(model, clip, lora_name, model_weight, clip_weight, False, 0, lbw_a, lbw_b, "", lbw) |
|
else: |
|
model, clip = default_lora() |
|
else: |
|
print(f"LORA NOT FOUND: {orig_lora_name}") |
|
|
|
pass3 = [x.strip() for x in pass2.split("BREAK")] |
|
pass3 = [x for x in pass3 if x != ''] |
|
|
|
if len(pass3) == 0: |
|
pass3 = [''] |
|
|
|
pass3_str = [f'[{x}]' for x in pass3] |
|
print(f"CLIP: {str.join(' + ', pass3_str)}") |
|
|
|
result = None |
|
|
|
for prompt in pass3: |
|
if clip_encoder is None: |
|
cur = nodes.CLIPTextEncode().encode(clip, prompt)[0] |
|
else: |
|
cur = clip_encoder.encode(clip, prompt)[0] |
|
|
|
if result is not None: |
|
result = nodes.ConditioningConcat().concat(result, cur)[0] |
|
else: |
|
result = cur |
|
|
|
if processed is not None: |
|
processed.append(pass1) |
|
processed.append(pass2) |
|
processed.append(pass3) |
|
|
|
return model, clip, result |
|
|
|
|
|
def starts_with_regex(pattern, text): |
|
regex = re.compile(pattern) |
|
return regex.match(text) |
|
|
|
|
|
def split_to_dict(text): |
|
pattern = r'\[([A-Za-z0-9_. ]+)\]([^\[]+)(?=\[|$)' |
|
matches = re.findall(pattern, text) |
|
|
|
result_dict = {key: value.strip() for key, value in matches} |
|
|
|
return result_dict |
|
|
|
|
|
class WildcardChooser: |
|
def __init__(self, items, randomize_when_exhaust): |
|
self.i = 0 |
|
self.items = items |
|
self.randomize_when_exhaust = randomize_when_exhaust |
|
|
|
def get(self, seg): |
|
if self.i >= len(self.items): |
|
self.i = 0 |
|
if self.randomize_when_exhaust: |
|
random.shuffle(self.items) |
|
|
|
item = self.items[self.i] |
|
self.i += 1 |
|
|
|
return item |
|
|
|
|
|
class WildcardChooserDict: |
|
def __init__(self, items): |
|
self.items = items |
|
|
|
def get(self, seg): |
|
text = "" |
|
if 'ALL' in self.items: |
|
text = self.items['ALL'] |
|
|
|
if seg.label in self.items: |
|
text += self.items[seg.label] |
|
|
|
return text |
|
|
|
|
|
def split_string_with_sep(input_string): |
|
sep_pattern = r'\[SEP(?:\:\w+)?\]' |
|
|
|
substrings = re.split(sep_pattern, input_string) |
|
|
|
result_list = [None] |
|
matches = re.findall(sep_pattern, input_string) |
|
for i, substring in enumerate(substrings): |
|
result_list.append(substring) |
|
if i < len(matches): |
|
if matches[i] == '[SEP]': |
|
result_list.append(None) |
|
elif matches[i] == '[SEP:R]': |
|
result_list.append(random.randint(0, 1125899906842624)) |
|
else: |
|
try: |
|
seed = int(matches[i][5:-1]) |
|
except: |
|
seed = None |
|
result_list.append(seed) |
|
|
|
iterable = iter(result_list) |
|
return list(zip(iterable, iterable)) |
|
|
|
|
|
def process_wildcard_for_segs(wildcard): |
|
if wildcard.startswith('[LAB]'): |
|
raw_items = split_to_dict(wildcard) |
|
|
|
items = {} |
|
for k, v in raw_items.items(): |
|
v = v.strip() |
|
if v != '': |
|
items[k] = v |
|
|
|
return 'LAB', WildcardChooserDict(items) |
|
|
|
else: |
|
match = starts_with_regex(r"\[(ASC-SIZE|DSC-SIZE|ASC|DSC|RND)\]", wildcard) |
|
|
|
if match: |
|
mode = match[1] |
|
items = split_string_with_sep(wildcard[len(match[0]):]) |
|
|
|
if mode == 'RND': |
|
random.shuffle(items) |
|
return mode, WildcardChooser(items, True) |
|
else: |
|
return mode, WildcardChooser(items, False) |
|
|
|
else: |
|
return None, WildcardChooser([(None, wildcard)], False) |
|
|
|
|
|
def wildcard_load(): |
|
global wildcard_dict |
|
wildcard_dict = {} |
|
|
|
with wildcard_lock: |
|
read_wildcard_dict(wildcards_path) |
|
|
|
try: |
|
read_wildcard_dict(config.get_config()['custom_wildcards']) |
|
except Exception as e: |
|
print(f"[Impact Pack] Failed to load custom wildcards directory.") |
|
|
|
print(f"[Impact Pack] Wildcards loading done.") |
|
|