Add wildcardsPromptMatrix Node:

1. wildcard-replaced prompt will be returned in order rather than randomly
2. will return the amount of probilities and amount of probilities each option or wildcard
3. the prompt can be selected by offset argument, the order of probilities is fixed
4. even if the offset exceeds the total, it will not stop, but will always return to the last probility, requiring additional nodes to control.
This commit is contained in:
rosmeowtis
2025-04-25 03:07:49 +08:00
parent 50ae13a993
commit 15cfeedf7b
4 changed files with 294 additions and 11 deletions
+25
View File
@@ -145,6 +145,31 @@
}
}
},
"easy wildcardsMatrix": {
"display_name": "Wildcard 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"
}
},
"outputs": {
"0": {
"name": "Prompt"
},
"1": {
"name": "Replaced Prompt"
},
"2": {
"name": "Total Count of the Probilities"
}
}
},
"easy prompt": {
"display_name": "Prompt",
"inputs": {
+25
View File
@@ -75,6 +75,31 @@
}
}
},
"easy wildcardsMatrix": {
"display_name": "通配符提示词矩阵",
"inputs": {
"Select to add LoRA": {
"name": "选择添加Lora"
},
"Select to add Wildcard": {
"name": "选择添加通配符"
},
"offset": {
"name": "偏移量"
}
},
"outputs": {
"0": {
"name": "提示词"
},
"1": {
"name": "通配填充词"
},
"2": {
"name": "可能性总数"
}
}
},
"easy prompt": {
"display_name": "提示词",
"inputs": {
+183 -5
View File
@@ -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,177 @@ 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: __dir/filename__ filename 不应添加后缀,会自动补全为 .txt
"""
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 个"
if n >= self.total():
return self.getn(self.total() - 1)
if n < 0:
return self.getn(0)
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 的设置导致预期的结果长度超过剩下的实际长度,则结果会被截断为实际长度。
"""
if offset + limit > self.total():
limit = self.total() - 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 按照顺序在模板中留下 {0}, {1} 等占位符,
即使遇到相同的 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:]
+61 -6
View File
@@ -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:
@@ -83,6 +86,56 @@ class wildcardsPrompt:
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)"}),
# 这个新节点无法通过下拉选单直接向 text 中添加 lora 或 wildcard,为什么?
# # 点击该属性从下拉列表中选择要添加的lora
# "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,),
# 本次返回第 n 种可能
"offset": ("INT", {"default": 0, "min": 0, "step": 1, "control_after_generate": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("STRING", "STRING", "INT", "INT")
# 提示词、替换后提示词、可能性的总数、各替换项的可能性数目
RETURN_NAMES = ("text", "populated_text", "total", "factors")
OUTPUT_IS_LIST = (True, True, True, True)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
def translate(self, text):
return text
def main(self, *args, **kwargs):
prompt = kwargs["prompt"] if "prompt" in kwargs else None
offset = kwargs["offset"]
# Clean loaded_objects
if prompt:
easyCache.update_loaded_objects(prompt)
text = kwargs['text']
text = self.translate(text)
p = WildcardProcessor(text)
populated_text = [p.getn(offset)]
text = [text]
return {"ui": {"value": [offset]}, "result": (text, populated_text, [p.total()], list(p.placeholder_choices.values()))}
# 负面提示词
class negativePrompt:
@@ -518,6 +571,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 +585,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",