* Moved known models and their detection to the configuration file.

* Updated `ppp_config.yaml.defaults` to enhance documentation and structure for model detection and configuration.
* Improved validation of configuration files.
* Added a new `tests` application in `SUPPORTED_APPS` for testing purposes.
* Support for variables in list of values comparisons.
This commit is contained in:
Antonio Cordero Balcazar
2026-04-03 15:53:10 +02:00
parent acdf337354
commit 6ceb23b84b
11 changed files with 567 additions and 213 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"python.testing.unittestArgs": [
"-v",
"-s",
"./tests",
".",
"-p",
"test*.py"
],
+3 -3
View File
@@ -4,11 +4,11 @@
The extension supports a configuration file `ppp_config.yaml` with some settings that don't usually change.
By default this configuration is read from the file `ppp_config.yaml.defaults` in the extension folder (that file must not be modified). If you want to personalize any settings you should first copy this file as `ppp_config.yaml` in the same folder, or in the ComfyUI user folder (preferred, but only possible in ComfyUI). The options in this file will take precedence over those in the defaults file.
By default this configuration is read from the file `ppp_config.yaml.defaults` in the extension folder. That file must not be modified. If you want to personalize any settings you should first copy it as `ppp_config.yaml` in the same folder, or in the ComfyUI user folder (preferred, but only possible in ComfyUI). The options in this new file will take precedence over those in the defaults file.
This file contains some options for how the host applications (WebUIs) should act in certain operations, and also options for the supported models, which currently include only the model variants definitions. Hosts and models names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options.
This file contains some options for how the host applications (WebUIs) should act in certain operations, and also define the supported models, including how to detect them and the model variants definitions. Host names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options.
The model variants now support regular expressions instead of a list of strings to detect the variant. If you used a non default value in previous versions you should re-do it in this file with the new format. As before, the default defines variants for *Pony* and *Illustrious* models.
The model variants now support regular expressions instead of a list of strings to detect the variant. If you used a non default value in previous versions you should create a configuration file and add them with the new format. As before, the default file defines variants for *Pony* and *Illustrious* models.
## ComfyUI
+3 -2
View File
@@ -8,6 +8,7 @@ WC_NAME_PLAIN: /(?:(?!__|\$\$|\$\{|<ppp:|[('"])[a-zA-Z0-9\-.\\\/_#\*\?\[!\
INDEX: INT | IDENTIFIER
IDENTIFIER: CNAME
SIMPLEVALUE: STRING | SIGNED_NUMBER | BOOLEAN
COMPVALUE: SIMPLEVALUE | IDENTIFIER
// plain text and weights
?plain: /((?!__|\bAND\b|\${)[^\\()\[\]:<>${]|\\.)+/s // exclude only the starting ones
@@ -112,9 +113,9 @@ operation_and: condition _WHITESPACE "and" _WHITESPACE condition
operation_or: condition _WHITESPACE "or" _WHITESPACE condition
operation_not: "not" ( ( _WHITESPACE ungrouped_condition ) | ( _WHITESPACE? grouped_condition ) )
truthy_operand: IDENTIFIER
comparison_simple_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /eq|ne|gt|lt|ge|le|contains/ _WHITESPACE ( SIMPLEVALUE | IDENTIFIER )
comparison_simple_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /eq|ne|gt|lt|ge|le|contains/ _WHITESPACE COMPVALUE
comparison_list_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /contains|in/ _WHITESPACE listvalue
listvalue.9: "(" _WHITESPACE? SIMPLEVALUE ( _WHITESPACE? "," _WHITESPACE? SIMPLEVALUE )* _WHITESPACE? ")"
listvalue.9: "(" _WHITESPACE? COMPVALUE ( _WHITESPACE? "," _WHITESPACE? COMPVALUE )* _WHITESPACE? ")"
// command: set
commandset: "<ppp:set" _WHITESPACE IDENTIFIER [ commandsetmodifiers ] _WHITESPACE? ">" commandsetcontent "<ppp:/set>"
+327 -81
View File
@@ -96,33 +96,6 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
UNPROCESSED_STOP = "UNPROCESSED CONSTRUCTS!\nBREAK "
INVALID_CONTENT_STOP = "INVALID CONTENT! {0}\nBREAK "
KNOWN_MODELS = [
"sd1", # Stable Diffusion 1
"sd2", # Stable Diffusion 2
"ssd", # Segmind Stable Diffusion 1B
"sdxl", # Stable Diffusion XL
"sd3", # Stable Diffusion 3
"flux", # Flux 1
"auraflow", # AuraFlow
"pixart", # PixArt
"lumina2", # Lumina2 & ZImage
"ltxv", # LTXV
"cosmos", # Cosmos
"cosmospredict2", # CosmosPredict2
"genmomochi", # GenmoMochi
"hunyuan", # Hunyuan
"hunyuanvideo", # HunyuanVideo
"hunyuan3d", # Hunyuan3D
"wan", # Wan21 & Wan22
"hidream", # HiDream
"qwenimage", # QwenImage & QwenImageEdit
"chroma", # Chroma
"omnigen2", # Omnigen 2
"flux2", # Flux 2
"kandinsky5", # Kandinsky 5
"anima", # Anima
]
def __init__(
self,
logger: logging.Logger,
@@ -152,14 +125,28 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
self.env_info = env_info
self.wildcard_obj = wildcards_obj
self.extranetwork_mappings_obj = extranetwork_mappings_obj
default_config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ppp_config.yaml.defaults")
with open(default_config_file, "r", encoding="utf-8") as f:
self.config: dict[str, Any] = yaml.safe_load(f)
try:
with open(default_config_file, "r", encoding="utf-8") as f:
self.config: dict[str, Any] = yaml.safe_load(f)
except Exception as exc: # pylint: disable=broad-exception-caught
self.config = {}
raise PPPInterrupt(f"Failed to load default configuration from '{default_config_file}'.") from exc
validate_def_cfg = self.__validate_normalize_configuration(self.config, "default configuration file")
if validate_def_cfg != 0:
errmsg = "Default configuration file has errors. Please restore the default configuration file and, per instructions, use a copy to adapt it."
if validate_def_cfg == 2:
raise PPPInterrupt(errmsg)
else:
self.logger.warning(errmsg)
user_config_file = self.env_info.get("ppp_config", "")
user_config: dict[str, Any] = {}
if isinstance(user_config_file, dict):
user_config = user_config_file
self.__validate_normalize_configuration(user_config, "forced configuration")
else:
user_config = {}
if user_config_file == "":
if self.env_info.get("app", "") == SUPPORTED_APPS.comfyui.value:
try:
@@ -169,70 +156,102 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
if user_dir and os.path.isdir(user_dir):
user_config_file = os.path.join(user_dir, "default", "ppp_config.yaml")
except Exception: # pylint: disable=broad-exception-caught
pass
self.logger.warning("Failed to get user directory for PPP config.")
if not user_config_file or not os.path.exists(user_config_file):
user_config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ppp_config.yaml")
if user_config_file and os.path.exists(user_config_file):
with open(user_config_file, "r", encoding="utf-8") as f:
user_config = yaml.safe_load(f)
# Merge user config into default config
if len(user_config):
for k in ("hosts", "models"):
if k in user_config:
for key, value in user_config[k].items():
if key in self.config[k] and isinstance(self.config[k][key], list):
self.config[k][key].extend(value)
else:
self.config[k][key] = value
self.host_config: dict[str, Any] = (self.config.get("hosts") or {}).get(self.env_info.get("app", "")) or {}
self.__validate_normalize_configuration(user_config, "user configuration")
self.__merge_configuration(user_config)
self.models_config: dict[str, dict[str, Any] | None] = self.config.get("models") or {}
self.known_models: list[str] = list(self.models_config.keys())
# Patch for tests (copy comfyui)
if self.env_info.get("app", "") == "tests":
self.config.setdefault("hosts", {}).setdefault("tests", {})
for m in self.known_models:
self.models_config.setdefault(m, {}).setdefault("detect", {})["tests"] = (
self.models_config[m].get("detect", {}).get("comfyui", None)
)
self.host_config: dict[str, Any] = (self.config.get("hosts") or {}).get(self.env_info.get("app", ""))
if self.host_config is None:
raise PPPInterrupt(
f"No host configuration found for app '{self.env_info.get('app', '')}'. Please check your configuration."
)
# Update env_info with model detection
prop_base = self.env_info.get("property_base", None)
model_class = self.env_info.get("model_class", "")
for m in self.known_models:
self.env_info["is_" + m] = False
model_config = (self.models_config.get(m) or {}).get("detect", {}).get(self.env_info.get("app", ""), {})
if model_config is not None:
cls_list = model_config.get("class", [])
if model_class in cls_list:
self.env_info["is_" + m] = True
elif "property" in model_config and prop_base is not None:
prop = model_config["property"]
attr = getattr(prop_base, prop, None)
if isinstance(attr, bool) and attr:
self.env_info["is_" + m] = True
# General options
self.debug_level = DEBUG_LEVEL(options.get("debug_level", self.DEFAULT_DEBUG_LEVEL))
self.gen_onwarning = self.ONWARNING_CHOICES(options.get("on_warning", self.DEFAULT_ONWARNING))
self.variants_definitions = {
v: (m, [vo["find_in_filename"]] if isinstance(vo["find_in_filename"], str) else vo["find_in_filename"])
for m in self.KNOWN_MODELS
for v, vo in (((self.config.get("models") or {}).get(m) or {}).get("variants") or {}).items()
}
self.debug_level = DEBUG_LEVEL(self.options.get("debug_level", self.DEFAULT_DEBUG_LEVEL))
self.gen_onwarning = self.ONWARNING_CHOICES(self.options.get("on_warning", self.DEFAULT_ONWARNING))
self.variants_definitions = {}
for m in self.known_models:
for v, vo in (((self.models_config or {}).get(m) or {}).get("variants") or {}).items():
if v not in self.known_models:
self.variants_definitions[v] = (m, vo["find_in_filename"])
else:
self.logger.warning(
f"Variant name '{v}' in model '{m}' conflicts with a known model name. Discarding variant."
)
if self.debug_level != DEBUG_LEVEL.none:
self.logger.debug(self.format_output(f"Host configuration: {self.host_config}"))
# Wildcards options
self.wil_process_wildcards = options.get("process_wildcards", self.DEFAULT_WC_PROCESS)
self.wil_keep_choices_order = options.get("keep_choices_order", self.DEFAULT_KEEP_CHOICES_ORDER)
self.wil_choice_separator = options.get("choice_separator", self.DEFAULT_CHOICE_SEPARATOR)
self.wil_ifwildcards = self.IFWILDCARDS_CHOICES(options.get("if_wildcards", self.DEFAULT_IF_WILDCARDS))
self.wil_process_wildcards = self.options.get("process_wildcards", self.DEFAULT_WC_PROCESS)
self.wil_keep_choices_order = self.options.get("keep_choices_order", self.DEFAULT_KEEP_CHOICES_ORDER)
self.wil_choice_separator = self.options.get("choice_separator", self.DEFAULT_CHOICE_SEPARATOR)
self.wil_ifwildcards = self.IFWILDCARDS_CHOICES(self.options.get("if_wildcards", self.DEFAULT_IF_WILDCARDS))
# Send to negative options
self.stn_ignore_repeats = options.get("stn_ignore_repeats", self.DEFAULT_STN_IGNORE_REPEATS)
self.stn_separator = options.get("stn_separator", self.DEFAULT_STN_SEPARATOR)
self.stn_ignore_repeats = self.options.get("stn_ignore_repeats", self.DEFAULT_STN_IGNORE_REPEATS)
self.stn_separator = self.options.get("stn_separator", self.DEFAULT_STN_SEPARATOR)
# Cleanup and remove options
self.cup_do_cleanup = options.get("do_cleanup", self.DEFAULT_DO_CLEANUP)
self.cup_cleanup_variables = options.get("cleanup_variables", self.DEFAULT_CLEANUP_VARIABLES)
self.cup_extraspaces = self.cup_do_cleanup and options.get(
self.cup_do_cleanup = self.options.get("do_cleanup", self.DEFAULT_DO_CLEANUP)
self.cup_cleanup_variables = self.options.get("cleanup_variables", self.DEFAULT_CLEANUP_VARIABLES)
self.cup_extraspaces = self.cup_do_cleanup and self.options.get(
"cleanup_extra_spaces", self.DEFAULT_CUP_EXTRA_SPACES
)
self.cup_emptyconstructs = self.cup_do_cleanup and options.get(
self.cup_emptyconstructs = self.cup_do_cleanup and self.options.get(
"cleanup_empty_constructs", self.DEFAULT_CUP_EMPTY_CONSTRUCTS
)
self.cup_extraseparators = self.cup_do_cleanup and options.get(
self.cup_extraseparators = self.cup_do_cleanup and self.options.get(
"cleanup_extra_separators", self.DEFAULT_CUP_EXTRA_SEPARATORS
)
self.cup_extraseparators2 = self.cup_do_cleanup and options.get(
self.cup_extraseparators2 = self.cup_do_cleanup and self.options.get(
"cleanup_extra_separators2", self.DEFAULT_CUP_EXTRA_SEPARATORS2
)
self.cup_extraseparators_include_eol = self.cup_do_cleanup and options.get(
self.cup_extraseparators_include_eol = self.cup_do_cleanup and self.options.get(
"cleanup_extra_separators_include_eol", self.DEFAULT_CUP_EXTRA_SEPARATORS_INCLUDE_EOL
)
self.cup_breaks = self.cup_do_cleanup and options.get("cleanup_breaks", self.DEFAULT_CUP_BREAKS)
self.cup_breaks_eol = self.cup_do_cleanup and options.get("cleanup_breaks_eol", self.DEFAULT_CUP_BREAKS_EOL)
self.cup_ands = self.cup_do_cleanup and options.get("cleanup_ands", self.DEFAULT_CUP_ANDS)
self.cup_ands_eol = self.cup_do_cleanup and options.get("cleanup_ands_eol", self.DEFAULT_CUP_ANDS_EOL)
self.cup_extranetworktags = self.cup_do_cleanup and options.get(
self.cup_breaks = self.cup_do_cleanup and self.options.get("cleanup_breaks", self.DEFAULT_CUP_BREAKS)
self.cup_breaks_eol = self.cup_do_cleanup and self.options.get(
"cleanup_breaks_eol", self.DEFAULT_CUP_BREAKS_EOL
)
self.cup_ands = self.cup_do_cleanup and self.options.get("cleanup_ands", self.DEFAULT_CUP_ANDS)
self.cup_ands_eol = self.cup_do_cleanup and self.options.get("cleanup_ands_eol", self.DEFAULT_CUP_ANDS_EOL)
self.cup_extranetworktags = self.cup_do_cleanup and self.options.get(
"cleanup_extranetwork_tags", self.DEFAULT_CUP_EXTRANETWORK_TAGS
)
self.cup_mergeattention = self.cup_do_cleanup and options.get(
self.cup_mergeattention = self.cup_do_cleanup and self.options.get(
"cleanup_merge_attention", self.DEFAULT_CUP_MERGE_ATTENTION
)
self.rem_removeextranetworktags = self.cup_do_cleanup and options.get(
self.rem_removeextranetworktags = self.cup_do_cleanup and self.options.get(
"remove_extranetwork_tags", self.DEFAULT_CUP_REMOVE_EXTRANETWORK_TAGS
)
@@ -374,6 +393,228 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
self.user_variables = {}
self.echoed_variables = {}
def __merge_configuration(self, user_config):
"""
Merges the user configuration into the default configuration.
Args:
user_config: The user configuration dictionary to merge.
"""
if "hosts" in user_config:
# Options are replaced by host
for host_key, host_value in user_config["hosts"].items():
self.config["hosts"].setdefault(host_key, {}).update(host_value)
if "models" in user_config:
for model_key, model_value in user_config["models"].items():
# We independently update the detection and variants for each model in the user configuration
usr_mdetect = model_value.get("detect", "")
# Detections are replaced by host
cfg_detect = self.config["models"].setdefault(model_key, {}).get("detect")
if cfg_detect is not None:
for host, hdetect in usr_mdetect.items():
cfg_detect[host] = hdetect
# Variants are fully replaced if specified
usr_mvariants = model_value.get("variants", "")
if usr_mvariants is None:
del self.config["models"].setdefault(model_key, {})["variants"]
else:
self.config["models"].setdefault(model_key, {})["variants"] = usr_mvariants
def __re_flags_from_list(self, flags_list: list[str]) -> int:
flag_value = 0
for flag in flags_list:
if hasattr(re, flag):
flag_value |= getattr(re, flag)
else:
return 0
return flag_value
def __validate_find_in_filename_element(
self,
where: str,
model_key: str,
variant_key: str,
find_in_filename: str | dict,
) -> dict | None:
if isinstance(find_in_filename, str):
try:
re.compile(find_in_filename, re.IGNORECASE)
return {"regex": find_in_filename, "flags": re.IGNORECASE}
except re.error:
self.logger.warning(
f"{where.title()}: Invalid regex pattern for variant '{variant_key}' in model '{model_key}'. Discarding variant."
)
elif isinstance(find_in_filename, dict):
regex = find_in_filename.get("regex", "")
flags = find_in_filename.get("flags", [])
if not isinstance(regex, str) or not isinstance(flags, list) or not all(isinstance(f, str) for f in flags):
self.logger.warning(
f"{where.title()}: Invalid format for 'find_in_filename' for variant '{variant_key}' in model '{model_key}'. Discarding variant."
)
else:
fl = self.__re_flags_from_list(flags)
if fl == 0 and len(flags):
self.logger.warning(
f"{where.title()}: Invalid regex flags for variant '{variant_key}' in model '{model_key}'. Discarding variant."
)
try:
re.compile(regex, fl)
return {"regex": regex, "flags": fl}
except re.error:
self.logger.warning(
f"{where.title()}: Invalid regex pattern for variant '{variant_key}' in model '{model_key}'. Discarding variant."
)
return None
def __validate_normalize_configuration(self, cfg: dict[str, Any] | Any, where: str) -> int:
"""
Validates the configuration dictionary and normalizes it.
Args:
cfg (dict): The configuration dictionary to validate.
Returns:
int: 0 if the configuration is valid, 1 if there are warnings, 2 if there are fatal errors.
"""
fatal_errors = False
if not isinstance(cfg, dict):
self.logger.error(f"{where.capitalize()}: is not a dictionary.")
fatal_errors = True
else:
if cfg.get("hosts") and not isinstance(cfg["hosts"], dict):
self.logger.error(f"{where.capitalize()}: 'hosts' is not a valid dictionary.")
fatal_errors = True
if cfg.get("models") and not isinstance(cfg.get("models"), dict):
self.logger.error(f"{where.capitalize()}: 'models' is not a valid dictionary.")
fatal_errors = True
if fatal_errors:
return 2
result = 0
defcfg_hosts: dict[str, Any] = cfg.get("hosts", {})
for host_key, host_value in dict(defcfg_hosts).items():
if host_key not in SUPPORTED_APPS._value2member_map_: # pylint: disable=protected-access
self.logger.warning(f"{where.capitalize()}: Unsupported host '{host_key}'. Discarding host.")
defcfg_hosts.pop(host_key, None)
result = 1
elif host_value is not None and (
not isinstance(host_value, dict)
or not all(k in ["attention", "scheduling", "alternation", "and", "break"] for k in host_value)
):
self.logger.warning(f"{where.capitalize()}: Invalid format for host '{host_key}'. Discarding host.")
defcfg_hosts.pop(host_key, None)
result = 1
defcfg_models: dict[str, Any] = cfg.get("models", {})
for model_key, model_value in dict(defcfg_models).items():
if (
not isinstance(model_value, dict)
or model_value.get("detect") is None
or not isinstance(model_value["detect"], dict)
):
self.logger.warning(f"{where.capitalize()}: Invalid format for model '{model_key}'. Discarding model.")
defcfg_models.pop(model_key, None)
result = 1
else:
defcfg_m_detect: dict[str, Any] = model_value["detect"]
for host_key, host_value in dict(defcfg_m_detect).items():
if host_key not in SUPPORTED_APPS._value2member_map_: # pylint: disable=protected-access
self.logger.warning(
f"{where.capitalize()}: Unsupported host '{host_key}' in 'detect' for model '{model_key}'. Discarding host."
)
defcfg_m_detect.pop(host_key, None)
result = 1
elif host_value is not None:
if not isinstance(host_value, dict):
self.logger.warning(
f"{where.capitalize()}: Invalid format for host '{host_key}' in 'detect' for model '{model_key}'. Discarding host."
)
defcfg_m_detect.pop(host_key, None)
result = 1
elif "class" in host_value:
if not isinstance(host_value["class"], list) or not all(
isinstance(c, str) for c in host_value["class"]
):
self.logger.warning(
f"{where.capitalize()}: Invalid format for 'class' in host '{host_key}' in 'detect' for model '{model_key}'. Discarding host."
)
defcfg_m_detect.pop(host_key, None)
result = 1
elif "property" in host_value:
if not isinstance(host_value["property"], str):
self.logger.warning(
f"{where.capitalize()}: Invalid format for 'property' in host '{host_key}' in 'detect' for model '{model_key}'. Discarding host."
)
defcfg_m_detect.pop(host_key, None)
result = 1
else:
self.logger.warning(
f"{where.capitalize()}: Neither 'class' nor 'property' specified for host '{host_key}' in 'detect' for model '{model_key}'. Discarding host."
)
defcfg_m_detect.pop(host_key, None)
result = 1
if "variants" in model_value:
if not isinstance(model_value["variants"], dict):
self.logger.warning(
f"{where.capitalize()}: Invalid format for 'variants' in model '{model_key}'. Discarding model."
)
defcfg_models.pop(model_key, None)
result = 1
else:
defcfg_m_variants: dict[str, Any] = model_value["variants"]
for variant_key, variant_value in dict(defcfg_m_variants).items():
if not isinstance(variant_key, str) or not variant_key.isidentifier():
self.logger.warning(
f"{where.capitalize()}: Invalid variant name '{variant_key}' in model '{model_key}'. Discarding variant."
)
defcfg_m_variants.pop(variant_key, None)
result = 1
elif not isinstance(variant_value, dict) or not isinstance(
variant_value.get("find_in_filename"), (str, dict, list)
):
self.logger.warning(
f"{where.capitalize()}: Invalid format for variant '{variant_key}' in model '{model_key}'. Discarding variant."
)
defcfg_m_variants.pop(variant_key, None)
result = 1
else:
if isinstance(variant_value["find_in_filename"], list):
normalized_list = []
for elem in variant_value["find_in_filename"]:
validated_elem = self.__validate_find_in_filename_element(
where, model_key, variant_key, elem
)
if validated_elem is not None:
normalized_list.append(validated_elem)
if len(normalized_list) != len(variant_value["find_in_filename"]):
defcfg_m_variants.pop(variant_key, None)
result = 1
else:
variant_value["find_in_filename"] = normalized_list
else:
variant_value["find_in_filename"] = [
self.__validate_find_in_filename_element(
where, model_key, variant_key, variant_value["find_in_filename"]
)
]
return result
def envinfo_hash(self) -> str:
"""
Generates a hash string based on the environment information.
Returns:
str: A hash string representing the environment information.
"""
return hash(tuple(sorted(self.env_info.items())))
def options_hash(self) -> str:
"""
Generates a hash string based on the options.
Returns:
str: A hash string representing the options.
"""
return hash(tuple(sorted(self.options.items())))
def __preprocess_grammar(self, grammar_content: str, options: dict[str, bool]) -> str:
"""
Preprocesses the grammar content to handle conditional compilation directives.
@@ -476,7 +717,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
Initializes the system variables.
"""
self.system_variables = {}
sdchecks = {x: self.env_info.get("is_" + x, False) for x in self.KNOWN_MODELS}
sdchecks = {x: self.env_info.get("is_" + x, False) for x in self.known_models}
sdchecks.update({"": True})
self.system_variables["_model"] = next((k for k, v in sdchecks.items() if v), "")
self.system_variables["_sd"] = self.system_variables["_model"] # deprecated
@@ -486,12 +727,15 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
self.system_variables["_sdname"] = os.path.basename(model_filename) # deprecated
self.system_variables["_modelname"] = os.path.basename(model_filename)
self.system_variables["_modelclass"] = self.env_info.get("model_class", "")
is_models = {
model_name: (model_type_and_substrings[0] == "" or sdchecks.get(model_type_and_substrings[0], False))
and any((re.search(s, model_filename, re.IGNORECASE) is not None) for s in model_type_and_substrings[1])
for model_name, model_type_and_substrings in self.variants_definitions.items()
if model_name not in self.KNOWN_MODELS
}
is_models = {}
for model_name, model_type_and_substrings in self.variants_definitions.items():
if not (model_type_and_substrings[0] == "" or sdchecks.get(model_type_and_substrings[0], False)):
is_models[model_name] = False
else:
is_models[model_name] = any(
(re.search(dre["regex"], model_filename, dre["flags"]) is not None)
for dre in model_type_and_substrings[1]
)
is_models_true = [k for k, v in is_models.items() if v]
if len(is_models_true) > 1:
self.logger.warning(
@@ -1184,7 +1428,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
try:
return int(c)
except ValueError:
# Bare identifier — resolve as variable reference
# Bare identifier - resolve as variable reference
if c.startswith("_"):
val = self.__ppp.system_variables.get(c, None)
if val is None:
@@ -1248,7 +1492,11 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
(
c[1:-1].lower()
if c.startswith('"') or c.startswith("'")
else True if c.lower() == "true" else False if c.lower() == "false" or c == "" else self.__resolve_cond_value(c)
else (
True
if c.lower() == "true"
else False if c.lower() == "false" or c == "" else self.__resolve_cond_value(c)
)
)
for c in cond_value
)
@@ -1264,9 +1512,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
try:
var_value_adjusted = int(var_value)
except (ValueError, TypeError):
self.warn_or_stop(
f"Cannot convert variable value '{var_value}' to integer for comparison"
)
self.warn_or_stop(f"Cannot convert variable value '{var_value}' to integer for comparison")
return False
result = comp_ops[cond_comp](var_value_adjusted, c)
if result:
+1 -55
View File
@@ -218,62 +218,8 @@ class PromptPostProcessorComfyUINode:
"models_path": folder_paths.models_dir,
"model_filename": modelname or "", # path is relative to checkpoints folder
"model_class": modelclass,
"property_base": None,
}
env_info.update({"is_" + k: False for k in PromptPostProcessor.KNOWN_MODELS})
env_info.update(
{
"is_sd1": modelclass in ("SD15", "SD15_instructpix2pix"),
"is_sd2": modelclass in ("SD20", "SD21UnclipL", "SD21UnclipH", "LotusD"),
"is_sdxl": (
modelclass
in ("SDXL", "SDXLRefiner", "SDXL_instructpix2pix", "Segmind_Vega", "KOALA_700M", "KOALA_1B")
),
"is_ssd": modelclass in ("SSD1B",),
"is_sd3": modelclass in ("SD3",),
"is_flux": modelclass in ("Flux", "FluxInpaint", "FluxSchnell"),
"is_auraflow": modelclass in ("AuraFlow",),
"is_pixart": modelclass in ("PixArtAlpha", "PixArtSigma"),
"is_lumina2": modelclass in ("Lumina2", "ZImage"),
"is_ltxv": modelclass in ("LTXV", "LTXAV"),
"is_cosmos": modelclass in ("CosmosT2V", "CosmosI2V"),
"is_cosmospredict2": modelclass in ("CosmosT2IPredict2", "CosmosI2VPredict2"),
"is_genmomochi": modelclass in ("GenmoMochi",),
"is_hunyuan": modelclass in ("HunyuanDiT", "HunyuanDiT1"),
"is_hunyuanvideo": modelclass
in (
"HunyuanVideo",
"HunyuanVideoI2V",
"HunyuanVideoSkyreelsI2V",
"HunyuanImage21",
"HunyuanImage21Refiner",
"HunyuanVideo15",
"HunyuanVideo15_SR_Distilled",
),
"is_hunyuan3d": modelclass in ("Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"),
"is_wan": modelclass
in (
"WAN21_T2V",
"WAN21_I2V",
"WAN21_FunControl2V",
"WAN21_Camera",
"WAN22_Camera",
"WAN21_Vace",
"WAN21_HuMo",
"WAN22_S2V",
"WAN22_Animate",
"WAN22_T2V",
),
"is_hidream": modelclass in ("HiDream",),
"is_chroma": modelclass in ("Chroma", "ChromaRadiance"),
"is_omnigen2": modelclass in ("Omnigen2",),
"is_kandinsky5": modelclass in ("Kandinsky5", "Kandinsky5Image"),
"is_qwenimage": modelclass in ("QwenImage",),
"is_flux2": modelclass in ("Flux2"),
"is_anima": modelclass in ("Anima",),
}
)
# Also supported: SVD_img2vid, SVD3D_u, SVD3_p, Stable_Zero123, SD_X4Upscaler, Stable_Cascade_C, Stable_Cascade_B, StableAudio
wc_wildcards_folders = wc_options["wc_wildcards_folders"] if wc_options else ""
if wc_wildcards_folders == "":
try:
+195 -19
View File
@@ -1,9 +1,12 @@
# Prompt Post-Processor configuration file.
# If you want to change any of these options make first a copy of this file as "ppp_config.yaml" and edit that file.
# This file will be overwritten when updating the extension, but your custom "ppp_config.yaml" should not be affected.
# In ComfyUI the file should be in the user folder.
# This defaults file will be overwritten when updating the extension, but your custom "ppp_config.yaml" should not be affected.
hosts: # Options for supported host applications
# For each host define how to handle some operations:
# Options for supported host applications
hosts:
# For each host, define how to handle some operations:
# attention: how to handle attention operators
# - ok: leave as is
# - parentheses: convert to parentheses-based attention
@@ -34,7 +37,7 @@ hosts: # Options for supported host applications
# - comma: replace with comma
# - remove: remove the operator
# - error: raise an error
# Options in the user file override all these options by host (individual options are not inherited and if missing will use defaults in code).
a1111:
attention: ok
scheduling: ok
@@ -60,25 +63,198 @@ hosts: # Options for supported host applications
and: ok
break: ok
comfyui:
# comfyui only supports parentheses-based attention
# ComfyUI only supports parentheses-based attention
attention: parentheses
# by default comfyui does not support scheduling, alternation, AND or BREAK operators, but you may be able to use them with a custom text encoder node
# by default ComfyUI does not support scheduling, alternation, AND or BREAK operators, but you may be able to use them with a custom text encoder node
scheduling: error
alternation: error
and: comma
break: comma
models: # Options for supported base models and variants
sd1:
sd2:
sdxl:
# Supported base models, variants, and options
# Check supported models for each host in:
# A1111:
# Forge: repositories\huggingface_guess\huggingface_guess\model_list.py
# reForge:
# SD.Next:
# ComfyUI:
models:
# We define supported models and how we detect them in each host application (by class or by a known boolean property, or null for not supported).
# We can also define here variants and some options.
# For each model, detect options in the user file replace these ones by host, and variants are fully replaced (so if you want to add to the default ones you need to copy them to your file).
sd1: # Stable Diffusion 1
detect:
a1111: { property: "is_sd1" }
forge: { property: "is_sd1" }
reforge: { property: "is_sd1" }
sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend; cannot differentiate SD1 and SD2, we set both to True
comfyui: { class: ["SD15", "SD15_instructpix2pix"] }
sd2: # Stable Diffusion 2
detect:
a1111: { property: "is_sd2" }
forge: { property: "is_sd2" }
reforge: { property: "is_sd2" }
sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # cannot differentiate SD1 and SD2, we set both to True; LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend
comfyui: { class: ["SD20", "SD21UnclipL", "SD21UnclipH", "LotusD"] }
ssd: # Segmind Stable Diffusion 1B
detect:
a1111: { property: "is_ssd" }
forge: null
reforge: { property: "is_ssd" }
sdnext: null
comfyui: { class: ["SSD1B"]}
sdxl: # Stable Diffusion XL
detect:
a1111: { property: "is_sdxl" }
forge: { property: "is_sdxl" }
reforge: { property: "is_sdxl" }
sdnext: { class: ["StableDiffusionXLPipeline"] }
comfyui: { class: ["SDXL", "SDXLRefiner", "SDXL_instructpix2pix", "Segmind_Vega", "KOALA_700M", "KOALA_1B"] }
variants:
# At this level goes the name of the defined variants
pony:
# find_in_filename: regex patterns as a string or list of strings to identify the variant in the model filename (ignores case)
find_in_filename: "(?:pony|pny|pdxl)"
illustrious:
find_in_filename: "(?:illustrious|illust|ilxl)"
sd3:
flux:
auraflow:
# find_in_filename: regex patterns as a str or regex/flags dict or list of them to identify the variant in the model filename.
# If specified as a string, it is considered a regex with the IGNORECASE flag;
# If specified as a dict, it should have "regex" and optional "flags" keys;
# If specified as a list, it should be a list of dicts or strings
pony: { find_in_filename: "(?:pony|pny|pdxl)" }
illustrious: { find_in_filename: { regex: "(?:illustrious|illust|ilxl)", flags: ["IGNORECASE"] } }
sd3: # Stable Diffusion 3
detect:
a1111: { property: "is_sd3" }
forge: { property: "is_sd3", class: ["SD3"] }
reforge: { property: "is_sd3" }
sdnext: { class: ["StableDiffusion3Pipeline"] }
comfyui: { class: ["SD3"] }
flux: # Flux 1
detect:
a1111: null
forge: { class: ["Flux", "FluxSchnell"] }
reforge: null
sdnext: { class: ["FluxPipeline"] }
comfyui: { class: ["Flux", "FluxInpaint", "FluxSchnell"] }
auraflow: # AuraFlow
detect:
a1111: null
forge: null
reforge: null
sdnext: { class: ["AuraFlowPipeline"] }
comfyui: { class: ["AuraFlow"] }
pixart: # PixArt
detect:
a1111: null
forge: null
reforge: null
sdnext: { class: ["PixArtAlphaPipeline"] }
comfyui: { class: ["PixArtAlpha", "PixArtSigma"] }
lumina2: # Lumina2 & ZImage
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Lumina2", "ZImage"] }
ltxv: # LTXV
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["LTXV", "LTXAV"] }
cosmos: # Cosmos
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["CosmosT2V", "CosmosI2V"] }
cosmospredict2: # CosmosPredict2
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["CosmosT2IPredict2", "CosmosI2VPredict2"] }
genmomochi: # GenmoMochi
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["GenmoMochi"] }
hunyuan: # Hunyuan
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["HunyuanDiT", "HunyuanDiT1"] }
hunyuanvideo: # HunyuanVideo
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["HunyuanVideo", "HunyuanVideoI2V", "HunyuanVideoSkyreelsI2V", "HunyuanImage21", "HunyuanImage21Refiner", "HunyuanVideo15", "HunyuanVideo15_SR_Distilled"] }
hunyuan3d: # Hunyuan3D
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"] }
wan: # Wan21 & Wan22
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["WAN21_T2V", "WAN21_I2V", "WAN21_FunControl2V", "WAN21_Camera", "WAN22_Camera", "WAN21_Vace", "WAN21_HuMo", "WAN22_S2V", "WAN22_Animate", "WAN22_T2V"] }
hidream: # HiDream
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["HiDream"] }
qwenimage: # QwenImage & QwenImageEdit
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["QwenImage"] }
chroma: # Chroma
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Chroma", "ChromaRadiance"] }
omnigen2: # Omnigen 2
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Omnigen2"] }
flux2: # Flux 2
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Flux2"] }
kandinsky5: # Kandinsky 5
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Kandinsky5", "Kandinsky5Image"] }
anima: # Anima
detect:
a1111: null
forge: null
reforge: null
sdnext: null
comfyui: { class: ["Anima"] }
+1 -1
View File
@@ -66,7 +66,7 @@ class PPPExtraNetworkMappings:
"""
DEFAULT_ENMAPPINGS_FOLDER = "extranetworkmappings"
LOCALINPUT_FILENAME = "#INPUT"
LOCALINPUT_FILENAME = R"//INPUT\\"
def __init__(self, logger):
self.__logger: logging.Logger = logger
+2
View File
@@ -7,6 +7,7 @@ class SUPPORTED_APPS(Enum):
forge = "forge"
reforge = "reforge"
sdnext = "sdnext"
tests = "tests" # for testing purposes only, not a real app
SUPPORTED_APPS_NAMES = {
SUPPORTED_APPS.comfyui: "ComfyUI",
@@ -14,4 +15,5 @@ SUPPORTED_APPS_NAMES = {
SUPPORTED_APPS.forge: "Forge",
SUPPORTED_APPS.reforge: "reForge",
SUPPORTED_APPS.a1111: "A1111 (or compatible)",
SUPPORTED_APPS.tests: "Tests",
}
+1 -1
View File
@@ -50,7 +50,7 @@ class PPPWildcards:
"""
DEFAULT_WILDCARDS_FOLDER = "wildcards"
LOCALINPUT_FILENAME = "#INPUT"
LOCALINPUT_FILENAME = R"//INPUT\\"
def __init__(self, logger):
self.__logger: logging.Logger = logger
+5 -41
View File
@@ -213,48 +213,11 @@ class PromptPostProcessorA1111Script(scripts.Script):
"app": app.value,
"models_path": models_path,
"model_filename": getattr(p.sd_model.sd_checkpoint_info, "filename", ""),
"model_class": "",
"model_class": p.sd_model.__class__.__name__,
"property_base": p.sd_model,
}
env_info.update({"is_" + k: False for k in PromptPostProcessor.KNOWN_MODELS})
if app == SUPPORTED_APPS.sdnext:
# cannot differentiate SD1 and SD2, we set both to True
# LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend
env_info["model_class"] = p.sd_model.__class__.__name__
env_info["is_sd1"] = p.sd_model.__class__.__name__ in ("LatentDiffusion", "StableDiffusionPipeline")
env_info["is_sd2"] = p.sd_model.__class__.__name__ in ("LatentDiffusion", "StableDiffusionPipeline")
env_info["is_sdxl"] = p.sd_model.__class__.__name__ == "StableDiffusionXLPipeline"
env_info["is_sd3"] = p.sd_model.__class__.__name__ == "StableDiffusion3Pipeline"
env_info["is_flux"] = p.sd_model.__class__.__name__ == "FluxPipeline"
env_info["is_auraflow"] = p.sd_model.__class__.__name__ == "AuraFlowPipeline"
env_info["is_pixart"] = p.sd_model.__class__.__name__ == "PixArtAlphaPipeline"
# also supports 'Latent Consistency Model': LatentConsistencyModelPipeline', 'UniDiffuser': 'UniDiffuserPipeline', 'Wuerstchen': 'WuerstchenCombinedPipeline', 'Kandinsky 2.1': 'KandinskyPipeline', 'Kandinsky 2.2': 'KandinskyV22Pipeline', 'Kandinsky 3': 'Kandinsky3Pipeline', 'DeepFloyd IF': 'IFPipeline', 'Custom Diffusers Pipeline': 'DiffusionPipeline', 'InstaFlow': 'StableDiffusionPipeline', 'SegMoE': 'StableDiffusionPipeline', 'Kolors': 'KolorsPipeline', 'CogView': 'CogView3PlusPipeline'
elif app == SUPPORTED_APPS.forge:
# from repositories\huggingface_guess\huggingface_guess\model_list.py
if app == SUPPORTED_APPS.forge:
env_info["model_class"] = p.sd_model.model_config.__class__.__name__
env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False)
env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False)
env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False)
# env_info["is_ssd"] = False # ?
env_info["is_sd3"] = getattr(
p.sd_model, "is_sd3", False
) # p.sd_model.model_config.__class__.__name__ == "SD3" # not actually supported?
env_info["is_flux"] = p.sd_model.model_config.__class__.__name__ in ("Flux", "FluxSchnell")
# env_info["is_auraflow"] = False # p.sd_model.model_config.__class__.__name__ == "AuraFlow" # not supported
elif app == SUPPORTED_APPS.reforge:
env_info["model_class"] = p.sd_model.__class__.__name__
env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False)
env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False)
env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False)
env_info["is_ssd"] = getattr(p.sd_model, "is_ssd", False)
env_info["is_sd3"] = getattr(p.sd_model, "is_sd3", False)
else: # assume A1111 compatible (p.sd_model.__class__.__name__=="DiffusionEngine")
env_info["model_class"] = p.sd_model.__class__.__name__
env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False)
env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False)
env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False)
env_info["is_ssd"] = getattr(p.sd_model, "is_ssd", False)
env_info["is_sd3"] = getattr(p.sd_model, "is_sd3", False)
hash_envinfo = hash(tuple(sorted(env_info.items())))
wc_wildcards_folders = getattr(opts, "ppp_wil_wildcardsfolders", "")
if wc_wildcards_folders == "":
wc_wildcards_folders = os.getenv("WILDCARD_DIR", PPPWildcards.DEFAULT_WILDCARDS_FOLDER)
@@ -318,7 +281,6 @@ class PromptPostProcessorA1111Script(scripts.Script):
opts, "ppp_rem_removeextranetworktags", PromptPostProcessor.DEFAULT_CUP_REMOVE_EXTRANETWORK_TAGS
),
}
hash_options = hash(tuple(sorted(options.items())))
self.wildcards_obj.refresh_wildcards(
self.ppp_debug_level, wildcards_folders if options["process_wildcards"] else None
)
@@ -332,6 +294,8 @@ class PromptPostProcessorA1111Script(scripts.Script):
self.wildcards_obj,
self.extranetwork_mappings_obj,
)
hash_options = ppp.options_hash()
hash_envinfo = ppp.envinfo_hash()
prompts_list = []
if input_force_equal_seeds:
+28 -9
View File
@@ -68,8 +68,8 @@ class TestPromptPostProcessorBase(unittest.TestCase):
self.def_env_info = {
"app": "tests",
"ppp_config": None,
"is_sdxl": True,
"model_class": "DiffusionEngine",
"model_class": "SDXL",
"property_base": {"is_sdxl": True},
"models_path": "./webui/models",
"model_filename": "./webui/models/Stable-diffusion/testmodel.safetensors",
}
@@ -196,7 +196,9 @@ class TestPromptPostProcessorBase(unittest.TestCase):
self.assertEqual(result_negative_prompt, eo.negative_prompt, "Incorrect negative prompt")
if variables is not None:
for var_name, var_value in variables.items():
self.assertIn(var_name, output_variables, f"Variable '{var_name}' not found in output variables")
self.assertIn(
var_name, output_variables, f"Variable '{var_name}' not found in output variables"
)
self.assertEqual(
output_variables[var_name],
var_value,
@@ -1141,7 +1143,10 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase):
def test_wc_wildcard_default_filter(self): # wildcard with default filter
self.process(
PromptPair("<ppp:setwcdeffilter 'yaml/wildcard2' 'label1+label3' />the choice is: __yaml/wildcard2__, <ppp:setwcdeffilter 'yaml/wildcard2' />__yaml/wildcard2__", ""),
PromptPair(
"<ppp:setwcdeffilter 'yaml/wildcard2' 'label1+label3' />the choice is: __yaml/wildcard2__, <ppp:setwcdeffilter 'yaml/wildcard2' />__yaml/wildcard2__",
"",
),
PromptPair("the choice is: choice3-choice3, choice3-choice1- choice2 ", ""),
ppp="nocup",
)
@@ -1720,21 +1725,35 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase):
"ppp_config": {
"models": {
"sd1": {
"detect": {"tests": {"class": ["SD15", "SD15_instructpix2pix"]}},
"variants": {
"test3": {"find_in_filename": "testmodel"},
"sdxl": {"find_in_filename": "testmodel"},
}
},
},
"sdxl": {
"detect": {
"tests": {
"class": [
"SDXL",
"SDXLRefiner",
"SDXL_instructpix2pix",
"Segmind_Vega",
"KOALA_700M",
"KOALA_1B",
]
}
},
"variants": {
"test1": {"find_in_filename": "testmodel"},
"test2": {"find_in_filename": "testmodel"},
}
},
},
"invalid": {
"something": {
"detect": {"tests": {"class": ["something"]}},
"variants": {
"test4": {"find_in_filename": "testmodel"},
}
},
},
}
},
@@ -1860,7 +1879,7 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase):
),
PromptPair("YES", ""),
)
# NaN/undefined variable integer comparison tests
def test_cmd_if_undefined_var_int_compare_warn(self): # undefined var integer compare with on_warning=warn