* Added Wildcard Concat node for selecting and concatenating wildcards.

* Refactor functions to resolve wildcard and extra-network mapping folder paths.
This commit is contained in:
Antonio Cordero Balcazar
2026-04-06 13:32:35 +02:00
parent b460565605
commit 12d5d40440
4 changed files with 281 additions and 39 deletions
+3
View File
@@ -17,6 +17,7 @@ from .ppp_comfyui import (
PromptPostProcessorSTNOptionsComfyUINode,
PromptPostProcessorCleanupOptionsComfyUINode,
PromptPostProcessorSelectVariableComfyUINode,
PromptPostProcessorWildcardConcatComfyUINode,
)
NODE_CLASS_MAPPINGS = {
@@ -26,6 +27,7 @@ NODE_CLASS_MAPPINGS = {
"ACBPPPSendToNegativeOptions": PromptPostProcessorSTNOptionsComfyUINode,
"ACBPPPCleanupOptions": PromptPostProcessorCleanupOptionsComfyUINode,
"ACBPPPSelectVariable": PromptPostProcessorSelectVariableComfyUINode,
"ACBPPPWildcardConcat": PromptPostProcessorWildcardConcatComfyUINode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ACBPromptPostProcessor": "ACB Prompt Post Processor",
@@ -34,6 +36,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ACBPPPSendToNegativeOptions": "ACB PPP Send-To-Negative Options",
"ACBPPPCleanupOptions": "ACB PPP Cleanup Options",
"ACBPPPSelectVariable": "ACB PPP Select Variable",
"ACBPPPWildcardConcat": "ACB PPP Wildcard Concat",
}
WEB_DIRECTORY = "./web"
+45 -5
View File
@@ -12,7 +12,11 @@ The model variants now support regular expressions instead of a list of strings
## ComfyUI
### ACB Prompt Post Processor node inputs
### ACB Prompt Post Processor node
The main node that processes the prompt.
Inputs:
* **model**: Connect here the MODEL or a string with the model class name used by *ComfyUI*. Needed for the model kind system variables.
* **modelname**: Name of the model. Needed for the detection of model variants.
@@ -31,16 +35,46 @@ The model variants now support regular expressions instead of a list of strings
The options nodes are optional. If you don't need to change any of the default values then you don't need to use them.
### ACB Prompt Post Processor node outputs
Outputs:
The outputs are the final positive and negative prompt and a variables dictionary.
* **pos_prompt**: the resulting positive prompt
* **neg_prompt**: the resulting negative prompt
* **variables**: the dictionary of variables set or echoed.
You can use the "**ACB PPP Select Variable**" node to choose one and output its value. You can use this to send only part of the prompt to, for example, a detailer node. For example:
### ACB PPP Select Variable node
With this prompt: `__quality__, 1girl, ${head:__eyes__, __hair__, __expression__}, __body__, __clothes__, __background__, __style__` then you extract the `head` variable and use `${head}` as prompt for the head/face detailer.
Lets you extract the variables used from the output (or just one of them). You can use this to send only part of the prompt to, for example, a detailer node. For example:
With this prompt: `__quality__, 1girl, ${head:__eyes__, __hair__, __expression__}, __body__, __clothes__, __background__, __style__` then you extract the `head` variable and use it as prompt for the head/face detailer.
Inputs:
* **variables**: the variables dictionary from the main node output.
* **name**: optional name of a variable.
Output:
* **value**: the resulting content, either all the variables (one per line, in "name: value" format) or just the content of the chosen one.
### ACB PPP Wildcards Concat node
This node lets you select up to 10 wildcards that will be concatenated with a chosen separator.
Inputs:
* **previous_prompt**: An optional text that will be prepended to the wildcards. Lets you chain multiple nodes or other string nodes.
* **filter**: a string to filter the identifiers of the wildcards. It matches the start of the identifiers.
* **separator**: a separator string.
* **wildcard_<n>**: the wildcards to concatenate.
Output:
* **prompt**: concatenated result.
### ACB PPP Wildcard Options node
Options for wildcard processing, in case you want to change them from the defaults.
* **folders**: You can enter multiple folders separated by commas. You can leave it empty (the default) and add a `ppp_wildcards` or `wildcards` entry in the **extra_model_paths.yaml** file (recommended).
* **definitions**: Wildcards definitions (in yaml or json format). Direct input added to the ones found in the wildcards folders. Allows wildcards to be included in the workflow.
* **if_wildcards**: Select what do you want to do with any found wildcards/choices (when process wildcards is off or after the processing).
@@ -53,11 +87,15 @@ With this prompt: `__quality__, 1girl, ${head:__eyes__, __hair__, __expression__
### ACB PPP Send-To-Negative Options node
Options for sent to negative commands, in case you want to change them from the defaults.
* **separator**: You can specify the separator used when adding to the negative prompt (by default it's ", ").
* **ignore_repeats**: It ignores repeated content to avoid repetitions in the negative prompt.
### ACB PPP Cleanup Options node
Options for cleanup processing, in case you want to change them from the defaults.
* **extra_spaces**: Removes other unnecessary spaces.
* **empty_constructs**: Removes attention/scheduling/alternation constructs when they are invalid.
* **extra_separators**: Removes unnecessary separators. This applies to the configured separator and regular commas.
@@ -75,6 +113,8 @@ Please note that *ComfyUI* does not natively support the `BREAK` and `AND` const
### ACB PPP ExtraNetwork Mapping Options node
Options for extranetworks mapping, in case you want to change them from the defaults.
* **folders**: You can enter multiple folders separated by commas. You can leave it empty (the default) and add a `ppp_extranetworkmappings` entry in the **extra_model_paths.yaml** file (recommended).
* **definitions**: Extranetwork Mappings definitions (in yaml format). Direct input added to the ones found in the extranetwork mappings folders. Allows the mappings to be included in the workflow.
+174 -34
View File
@@ -14,6 +14,48 @@ if __name__ == "__main__":
raise SystemExit("This script must be run from ComfyUI")
def _resolve_wildcards_folders(override: str = "") -> list[str]:
"""Return the resolved list of wildcard folder paths."""
folders_str = override
if folders_str == "":
try:
fp1 = folder_paths.get_folder_paths("ppp_wildcards")
except Exception: # pylint: disable=W0718
fp1 = None
try:
fp2 = folder_paths.get_folder_paths("wildcards")
except Exception: # pylint: disable=W0718
fp2 = None
folders_str = ",".join(fp1 or fp2 or [])
if folders_str == "":
folders_str = os.getenv("WILDCARD_DIR", PPPWildcards.DEFAULT_WILDCARDS_FOLDER)
return [
(f if os.path.isabs(f) else os.path.abspath(os.path.join(folder_paths.models_dir, f)))
for f in folders_str.split(",")
if f.strip() != ""
]
def _resolve_enmappings_folders(override: str = "") -> list[str]:
"""Return the resolved list of extra-network mapping folder paths."""
folders_str = override
if folders_str == "":
try:
fp3 = folder_paths.get_folder_paths("ppp_extranetworkmappings")
except Exception: # pylint: disable=W0718
fp3 = None
folders_str = ",".join(fp3 or [])
if folders_str == "":
folders_str = os.getenv(
"EXTRANETWORKMAPPINGS_DIR", PPPExtraNetworkMappings.DEFAULT_ENMAPPINGS_FOLDER
)
return [
(f if os.path.isabs(f) else os.path.abspath(os.path.join(folder_paths.models_dir, f)))
for f in folders_str.split(",")
if f.strip() != ""
]
class PromptPostProcessorComfyUINode:
"""
Node for processing prompts.
@@ -220,40 +262,8 @@ class PromptPostProcessorComfyUINode:
"model_class": modelclass,
"property_base": None,
}
wc_wildcards_folders = wc_options["wc_wildcards_folders"] if wc_options else ""
if wc_wildcards_folders == "":
try:
fp1 = folder_paths.get_folder_paths("ppp_wildcards")
except Exception: # pylint: disable=W0718
fp1 = None
try:
fp2 = folder_paths.get_folder_paths("wildcards")
except Exception: # pylint: disable=W0718
fp2 = None
wc_wildcards_folders = ",".join(fp1 or fp2 or [])
if wc_wildcards_folders == "":
wc_wildcards_folders = os.getenv("WILDCARD_DIR", PPPWildcards.DEFAULT_WILDCARDS_FOLDER)
wildcards_folders = [
(f if os.path.isabs(f) else os.path.abspath(os.path.join(folder_paths.models_dir, f)))
for f in wc_wildcards_folders.split(",")
if f.strip() != ""
]
en_mappings_folders = en_options["en_mappings_folders"] if en_options else ""
if en_mappings_folders == "":
try:
fp3 = folder_paths.get_folder_paths("ppp_extranetworkmappings")
except Exception: # pylint: disable=W0718
fp3 = None
en_mappings_folders = ",".join(fp3 or [])
if en_mappings_folders == "":
en_mappings_folders = os.getenv(
"EXTRANETWORKMAPPINGS_DIR", PPPExtraNetworkMappings.DEFAULT_ENMAPPINGS_FOLDER
)
enmappings_folders = [
(f if os.path.isabs(f) else os.path.abspath(os.path.join(folder_paths.models_dir, f)))
for f in en_mappings_folders.split(",")
if f.strip() != ""
]
wildcards_folders = _resolve_wildcards_folders(wc_options["wc_wildcards_folders"] if wc_options else "")
enmappings_folders = _resolve_enmappings_folders(en_options["en_mappings_folders"] if en_options else "")
options = {
"debug_level": debug_level,
@@ -743,3 +753,133 @@ class PromptPostProcessorSelectVariableComfyUINode:
elif name in variables:
value = variables[name]
return (value,)
class PromptPostProcessorWildcardConcatComfyUINode:
"""
Node for selecting and concatenating wildcards as a prompt.
"""
NONE_OPTION = "(none)"
_wildcards_obj = None
_wildcards_log = None
@classmethod
def _ensure_wildcards(cls):
if cls._wildcards_obj is None:
lf = PromptPostProcessorLogFactory(SUPPORTED_APPS.comfyui)
cls._wildcards_log = lf.log
cls._wildcards_obj = PPPWildcards(cls._wildcards_log)
cls._wildcards_obj.refresh_wildcards(DEBUG_LEVEL.minimal, _resolve_wildcards_folders())
return cls._wildcards_obj
@classmethod
def get_wildcard_keys(cls, filter_prefix=""):
wc_obj = cls._ensure_wildcards()
keys = sorted(wc_obj.wildcards.keys())
if filter_prefix:
keys = [k for k in keys if k.startswith(filter_prefix)]
return [cls.NONE_OPTION] + keys
@classmethod
def INPUT_TYPES(cls):
wc_list = cls.get_wildcard_keys()
return {
"required": {
"filter": (
"STRING",
{
"default": "build",
"multiline": False,
"dynamicPrompts": False,
"tooltip": "Filter wildcards by prefix (changing this refreshes the dropdowns)",
},
),
"separator": (
"STRING",
{
"default": ", ",
"multiline": False,
"dynamicPrompts": False,
"tooltip": "Separator used to concatenate the selected wildcard references",
},
),
"wildcard_1": (wc_list, {"tooltip": "Wildcard 1"}),
"wildcard_2": (wc_list, {"tooltip": "Wildcard 2"}),
"wildcard_3": (wc_list, {"tooltip": "Wildcard 3"}),
"wildcard_4": (wc_list, {"tooltip": "Wildcard 4"}),
"wildcard_5": (wc_list, {"tooltip": "Wildcard 5"}),
"wildcard_6": (wc_list, {"tooltip": "Wildcard 6"}),
"wildcard_7": (wc_list, {"tooltip": "Wildcard 7"}),
"wildcard_8": (wc_list, {"tooltip": "Wildcard 8"}),
"wildcard_9": (wc_list, {"tooltip": "Wildcard 9"}),
"wildcard_10": (wc_list, {"tooltip": "Wildcard 10"}),
},
"optional": {
"previous_prompt": (
"STRING",
{
"forceInput": True,
"tooltip": "Connect to the prompt output of another Wildcard Concat node to chain more than 10 wildcards",
},
),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "process"
CATEGORY = "ACB"
def process(
self,
filter, # noqa: A002 # pylint: disable=redefined-builtin, unused-argument
separator,
wildcard_1,
wildcard_2,
wildcard_3,
wildcard_4,
wildcard_5,
wildcard_6,
wildcard_7,
wildcard_8,
wildcard_9,
wildcard_10,
previous_prompt=None,
):
parts = [
f"__{w}__"
for w in [
wildcard_1,
wildcard_2,
wildcard_3,
wildcard_4,
wildcard_5,
wildcard_6,
wildcard_7,
wildcard_8,
wildcard_9,
wildcard_10,
]
if w != self.NONE_OPTION
]
result = separator.join(parts)
if previous_prompt:
result = previous_prompt + separator + result if result else previous_prompt
return (result,)
try:
from server import PromptServer # type: ignore # pylint: disable=import-error
from aiohttp import web as _aiohttp_web # type: ignore # pylint: disable=import-error
@PromptServer.instance.routes.get("/acb_ppp/wildcards")
async def _acb_ppp_get_wildcards(request):
filter_prefix = request.rel_url.query.get("filter", "")
wc_list = PromptPostProcessorWildcardConcatComfyUINode.get_wildcard_keys(filter_prefix)
return _aiohttp_web.json_response({"wildcards": wc_list})
except Exception: # pylint: disable=broad-except
pass
+59
View File
@@ -0,0 +1,59 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "ACB.PPP.WildcardConcat",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "ACBPPPWildcardConcat") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
const self = this;
const refreshWildcards = async () => {
const filterWidget = self.widgets?.find((w) => w.name === "filter");
if (!filterWidget) return;
try {
const resp = await api.fetchApi(
`/acb_ppp/wildcards?filter=${encodeURIComponent(filterWidget.value ?? "")}`
);
const data = await resp.json();
const wildcards = data.wildcards ?? [];
for (let i = 1; i <= 10; i++) {
const widget = self.widgets?.find((w) => w.name === `wildcard_${i}`);
if (widget) {
const current = widget.value;
widget.options.values = wildcards;
widget.value = wildcards.includes(current)
? current
: wildcards[0] ?? "(none)";
}
}
app.graph.setDirtyCanvas(true, false);
} catch (err) {
console.error("[ACB PPP] Failed to refresh wildcard list:", err);
}
};
// Hook the filter widget so that committing a new value (Enter / blur)
// immediately refreshes all 10 wildcard dropdowns without running the workflow.
const filterWidget = this.widgets?.find((w) => w.name === "filter");
if (filterWidget) {
const origCallback = filterWidget.callback;
filterWidget.callback = async function (...args) {
if (origCallback) origCallback.apply(this, args);
await refreshWildcards();
};
}
return result;
};
},
});