From 03e7e9b445fdf26385135151f6b2b022d6ba18fe Mon Sep 17 00:00:00 2001 From: Antonio Cordero Balcazar Date: Thu, 14 May 2026 22:03:43 +0200 Subject: [PATCH] * ComfyUI: Support for setting the model information from the prompt. --- docs/CONFIG.md | 2 ++ docs/COOKBOOK.md | 6 ++++ docs/SYNTAX.md | 79 ++++++++++++++++++++++++++++------------- ppp.py | 71 ++++++++++++++++++++---------------- ppp_classes.py | 3 +- ppp_tree.py | 40 +++++++++++++++++---- tests/tests_varcomms.py | 27 ++++++++++++++ 7 files changed, 167 insertions(+), 61 deletions(-) diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ccf2df0..fb91de4 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -45,6 +45,8 @@ Inputs: 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. +The model/modelname are also optional, but if you don't set them you will not be capable of changing content based on that. You can however set them from the prompt. + Outputs: * **pos_prompt**: the resulting positive prompt diff --git a/docs/COOKBOOK.md b/docs/COOKBOOK.md index 4978660..05d50ff 100644 --- a/docs/COOKBOOK.md +++ b/docs/COOKBOOK.md @@ -477,6 +477,12 @@ You can also use `evaluate ifundefined` (`?=!`) to resolve the wildcard immediat ${quality?=!__qualities__} ``` +## Choosing the model from the prompt + +In ComfyUI, you can leave the model and modelname inputs disconnected and set the `_modelinfo` system variable (or `_modelfullname` and `_modelclass`) at the start of the prompt (maybe through a wildcard or a choice construct). Then extract the `_modelfullname` output variable using the `Select Variable` node and use its value to actually load that model. + +You can also set and extract user variables for other ksampler inputs, like sampler, scheduler, steps, cfg and latent size. The numeric ones will need to be transformed from string to int/float using some other node. + ## Debugging tips When something isn't generating as expected, the debug setting is your first tool. Enable it in the extension settings; it will log all system variables at generation time, which tells you exactly what values are available for your conditions. diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 83c4992..d8fde5f 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -195,6 +195,37 @@ The format is: The wildcard identifier supports globbing. The filter does not allow the `^` or `#` flags. +## Variables + +The prompt has access to some system variables that contain model information, options, and other things. There is also the possibility of defining user variables. + +All these variables can be used to output content or behave differently based on their values. + +### System variables + +Names starting with an underscore are reserved for system variables: + +| System variable | Value | +| --------------- | ----- | +| `_model` | the loaded model identifier (`sd1`, `sd2`, `sdxl`, `sd3`, `flux`, `auraflow`). `_sd` also works but is deprecated. | +| `_modelname` | the loaded model filename (without path). `_sdname` also works but is deprecated. | +| `_modelfullname` | the loaded model filename (with path). `_sdfullname` also works but is deprecated. In ComfyUI this variable can also be **set** to override the filename used for model detection (see below). | +| `_modelclass` | the class used for the model. Note that this is dependent on the webui. In A1111 all SD versions use the same class. Can be used for new models that are not supported yet with the `_is_*` variables. The debug setting will show all system variables when generating in case you need to see which one to use for a certain model. In ComfyUI this variable can also be **set** to override the class used for model detection (see below). | +| `_modelinfo` | Contains both class and model full name separated with `@`. In ComfyUI can be set to change both `_modelfullname` and `_modelclass` at once (see below). | +| `_is_sd` | true if the loaded model version is any version of SD | +| `_is_sd1` | true if the loaded model version is SD 1.x | +| `_is_sd2` | true if the loaded model version is SD 2.x | +| `_is_sdxl` | true if the loaded model version is SDXL (includes Pony models) | +| `_is_sd3` | true if the loaded model version is SD 3.x | +| `_is_flux` | true if the loaded model is Flux | +| `_is_auraflow` | true if the loaded model is AuraFlow | +| `_is_ssd` | true if the loaded model version is SSD (Segmind Stable Diffusion 1B). Note that for an SSD model `_is_sdxl` will also be true. | +| `_is_sdxl_no_ssd` | true if the loaded model version is SDXL and not an SSD model. | +| `_is_sdxl_no_pony` | true if the loaded model version is SDXL and not a Pony model (the `pony` variant must be defined in settings). Kept to maintain compatibility with previous versions. | +| `_is_vvvv` | true if the loaded model matches the *vvvv* model variant definition (based on its filename). Note that the corresponding variable for the model kind will also be true. | +| `_is_pure_kkkk` | true if the loaded model is of kind *kkkk* (f.e. sdxl) and not a variant. | +| `_is_variant_kkkk` | true if the loaded model version is any variant of model kind *kkkk* and not the pure version. Note that the corresponding variable for the model kind will also be true. | + ## Set command This command sets the value of a variable that can be checked later. @@ -227,6 +258,28 @@ If also supports the addition and undefined check as an extension of the *Dynami Set variables are included in the output variables with their last value. +System variables cannot be set, except in some cases (see next point). + +### Overriding model information from the prompt (ComfyUI only) + +In ComfyUI, `_modelfullname`, `_modelclass`, and `_modelinfo` can be **set** from the prompt using the `set` command or `${}` syntax. Setting any of them triggers a re-evaluation of all `_is_*` system variables, so conditions that come later in the prompt will reflect the new values. + +This is useful in workflows where the model information is not passed through the standard ComfyUI model input but is known at prompt time. And the `_modelfullname` variable can be extracted later by using the Select Variable node to actually load that model. + +`_modelinfo` is a shorthand that sets both `_modelfullname` and `_modelclass` in a single call. Its value must use the format `@`: + +```text +${_modelinfo=Flux@fluxmodel.safetensors} +``` + +Or individually: + +```text +${_modelclass=Flux}${_modelfullname=fluxmodel.safetensors} +``` + +Attempting to set these variables in any other host will trigger a warning or error depending on the **What to do on invalid content warnings?** setting. + ## Echo command This command prints the value of a variable, or the specified default if it doesn't exist. If the variable does not exist and has no default, depending on the **What to do on invalid content warnings?** setting it will assume an empty value, or result in an error. @@ -265,9 +318,9 @@ They can be initialized in several ways: | `${var[]+=*var2[]}` | add elements from another array | | `${var[]+=*__wildcard__}` | add elements from a wildcard | -The star operator `*` is always with inmediate evaluation. +The star operator `*` has always an inmediate evaluation. -And can be accesed/echoed with: +And they can be accesed/echoed with: * Empty brackets mean the whole array (used when initializing or when echoing the whole array) * An integer inside the brackets means an indexed value. A variable identifier (not an indexed array) can be used to get the integer. @@ -326,28 +379,6 @@ This list shows what they do depending on the kind of operand (R = regular varia When a comparison tries to compare undefined variables or the values have different types (f.e. an integer and a string), the behavior depends on the `on_warning` setting: in `warn` mode the comparison evaluates to false, and in `stop` mode an error is raised. In non strict mode a numeric string literal (with no leading zeros) will be considered an integer. -The variable can be one set with the `set` command (user variables) or you can use system variables like these (names starting with an underscore are reserved for system variables): - -| System variable | Value | -| --------------- | ----- | -| `_model` | the loaded model identifier (`sd1`, `sd2`, `sdxl`, `sd3`, `flux`, `auraflow`). `_sd` also works but is deprecated. | -| `_modelname` | the loaded model filename (without path). `_sdname` also works but is deprecated. | -| `_modelfullname` | the loaded model filename (with path). `_sdfullname` also works but is deprecated. | -| `_modelclass` | the class used for the model. Note that this is dependent on the webui. In A1111 all SD versions use the same class. Can be used for new models that are not supported yet with the `_is_*` variables. The debug setting will show all system variables when generating in case you need to see which one to use for a certain model. | -| `_is_sd` | true if the loaded model version is any version of SD | -| `_is_sd1` | true if the loaded model version is SD 1.x | -| `_is_sd2` | true if the loaded model version is SD 2.x | -| `_is_sdxl` | true if the loaded model version is SDXL (includes Pony models) | -| `_is_sd3` | true if the loaded model version is SD 3.x | -| `_is_flux` | true if the loaded model is Flux | -| `_is_auraflow` | true if the loaded model is AuraFlow | -| `_is_ssd` | true if the loaded model version is SSD (Segmind Stable Diffusion 1B). Note that for an SSD model `_is_sdxl` will also be true. | -| `_is_sdxl_no_ssd` | true if the loaded model version is SDXL and not an SSD model. | -| `_is_sdxl_no_pony` | true if the loaded model version is SDXL and not a Pony model (the `pony` variant must be defined in settings). Kept to maintain compatibility with previous versions. | -| `_is_vvvv` | true if the loaded model matches the *vvvv* model variant definition (based on its filename). Note that the corresponding variable for the model kind will also be true. | -| `_is_pure_kkkk` | true if the loaded model is of kind *kkkk* (f.e. sdxl) and not a variant. | -| `_is_variant_kkkk` | true if the loaded model version is any variant of model kind *kkkk* and not the pure version. Note that the corresponding variable for the model kind will also be true. | - ### Example (multiline to be easier to read) diff --git a/ppp.py b/ppp.py index 4c60838..638d3a0 100644 --- a/ppp.py +++ b/ppp.py @@ -136,6 +136,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.state = PPPState( logger=self.logger, + env_info=env_info, host_config=host_config, options=options, variables=VariableRepository(), @@ -284,8 +285,6 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in def __load_config_and_detect(self, env_info: dict[str, Any]) -> HostConfig: """Loads config files, performs model detection, and returns the resolved host config.""" - self.env_info = env_info - default_config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ppp_config.yaml.defaults") try: with open(default_config_file, "r", encoding="utf-8") as f: @@ -302,13 +301,13 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in raise PPPInterrupt(errmsg) self.log(logging.WARNING, errmsg) - user_config_file = self.env_info.get("ppp_config", "") + user_config_file = env_info.get("ppp_config", "") if isinstance(user_config_file, dict): user_cfg, _ = self.__parse_configuration(user_config_file, "forced configuration") else: user_raw: dict[str, Any] = {} if user_config_file == "": - if self.env_info.get("app", "") == SUPPORTED_APPS.comfyui.value: + if env_info.get("app", "") == SUPPORTED_APPS.comfyui.value: try: import folder_paths # type: ignore @@ -332,7 +331,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.known_models: list[str] = list(self.models_config.keys()) # Patch for tests (copy comfyui) - if self.env_info.get("app", "") == "tests": + if env_info.get("app", "") == "tests": if self.config.hosts is None: self.config.hosts = {} self.config.hosts.setdefault("tests", HostConfig()) @@ -343,30 +342,14 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in model.detect = {} model.detect.setdefault("tests", model.detect.get("comfyui", None)) - host_config: HostConfig | None = (self.config.hosts or {}).get(self.env_info.get("app", "")) + host_config: HostConfig | None = (self.config.hosts or {}).get(env_info.get("app", "")) if host_config is None: raise PPPInterrupt( - f"No host configuration found for app '{escape_single_quotes(self.env_info.get('app', ''))}'. Please check your configuration." + f"No host configuration found for app '{escape_single_quotes(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", "") - app = self.env_info.get("app", "") - for m in self.known_models: - self.env_info["is_" + m] = False - model_obj = self.models_config.get(m) - model_detect = (model_obj.detect if model_obj else None) or {} - model_detect_for_app: ModelDetectConfig | None = model_detect.get(app) - if model_detect_for_app is not None: - cls_list = model_detect_for_app.class_ or [] - if model_class in cls_list: - self.env_info["is_" + m] = True - elif model_detect_for_app.property is not None and prop_base is not None: - prop = model_detect_for_app.property - attr = getattr(prop_base, prop, None) - if isinstance(attr, bool) and attr: - self.env_info["is_" + m] = True + self.__run_model_detection(env_info) self.variants_definitions: dict[str, tuple[str, list[FindInFilenamePattern]]] = {} for m in self.known_models: model_obj = self.models_config.get(m) @@ -382,6 +365,32 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in return host_config + def __run_model_detection(self, env_info: dict[str, Any]) -> None: + """Updates the is_* model detection flags in env_info based on model_class.""" + prop_base = env_info.get("property_base", None) + model_class = env_info.get("model_class", "") + app = env_info.get("app", "") + for m in self.known_models: + env_info["is_" + m] = False + model_obj = self.models_config.get(m) + model_detect = (model_obj.detect if model_obj else None) or {} + model_detect_for_app: ModelDetectConfig | None = model_detect.get(app) + if model_detect_for_app is not None: + cls_list = model_detect_for_app.class_ or [] + if model_class in cls_list: + env_info["is_" + m] = True + elif model_detect_for_app.property is not None and prop_base is not None: + prop = model_detect_for_app.property + attr = getattr(prop_base, prop, None) + if isinstance(attr, bool) and attr: + env_info["is_" + m] = True + + def __on_model_info_update(self) -> None: + """Called when _modelfullname or _modelclass are set via a prompt command.""" + self.__run_model_detection(self.state.env_info) + self.__init_sysvars() + self.log(logging.INFO, f"Updated system variables: {self.state.variables.get_all_system()}") + def update( self, env_info: dict[str, Any], @@ -394,6 +403,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in host_config = self.__load_config_and_detect(env_info) self.state = PPPState( logger=self.logger, + env_info=env_info, host_config=host_config, options=options, variables=VariableRepository(), @@ -563,7 +573,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in Returns: str: A hash string representing the environment information. """ - return hash(tuple(sorted(self.env_info.items()))) + return hash(tuple(sorted(self.state.env_info.items()))) def options_hash(self) -> str: """ @@ -595,17 +605,18 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in vs.set_system(var_name, str(opt_value).split(".", 1)[-1]) # Model related variables - sdchecks = {x: self.env_info.get("is_" + x, False) for x in self.known_models} + sdchecks = {x: self.state.env_info.get("is_" + x, False) for x in self.known_models} sdchecks.update({"": True}) model_name_val = next((k for k, v in sdchecks.items() if v), "") vs.set_system("_model", model_name_val) vs.set_system("_sd", model_name_val) # deprecated - model_filename = self.env_info.get("model_filename", "") + model_filename = self.state.env_info.get("model_filename", "") vs.set_system("_sdfullname", model_filename) # deprecated vs.set_system("_modelfullname", model_filename) + vs.set_system("_modelinfo", f"{self.state.env_info.get('model_class', '')}@{model_filename}") vs.set_system("_sdname", os.path.basename(model_filename)) # deprecated vs.set_system("_modelname", os.path.basename(model_filename)) - vs.set_system("_modelclass", self.env_info.get("model_class", "")) + vs.set_system("_modelclass", self.state.env_info.get("model_class", "")) 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)): @@ -629,7 +640,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in vs.set_system("_is_variant_" + x, sdchecks[x] and any(is_models.values())) # special cases vs.set_system("_is_sd", sdchecks["sd1"] or sdchecks["sd2"] or sdchecks["sdxl"] or sdchecks["sd3"]) - is_ssd = self.env_info.get("is_ssd", False) + is_ssd = self.state.env_info.get("is_ssd", False) vs.set_system("_is_ssd", is_ssd) vs.set_system("_is_sdxl_no_ssd", sdchecks["sdxl"] and not is_ssd) # backcompatibility (but the modern one to use would be _is_pure_sdxl) @@ -941,7 +952,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.state.variables.clear_echoed() # Parse both prompts - processor = TreeProcessor(self.state, rng) + processor = TreeProcessor(self.state, rng, on_model_info_update=self.__on_model_info_update) # We use the ASCII Group Separator character between prompt and negative prompt since it's unlikely to appear in prompts unified_prompt = prompt + "\x1d" + negative_prompt prompt_parser, parser_description = self.__get_best_parser(unified_prompt) diff --git a/ppp_classes.py b/ppp_classes.py index 7d60219..af7cf7f 100644 --- a/ppp_classes.py +++ b/ppp_classes.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from logging import Logger import re from enum import Enum -from typing import Literal, Optional +from typing import Any, Literal, Optional from lark import Lark from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -257,6 +257,7 @@ class PPPState: """State object passed to various PPP components during prompt processing.""" logger: Logger + env_info: dict[str, Any] = field(default_factory=dict) host_config: HostConfig = field(default_factory=HostConfig) options: PPPStateOptions = field(default_factory=PPPStateOptions) variables: VariableRepository = field(default_factory=VariableRepository) diff --git a/ppp_tree.py b/ppp_tree.py index e15792f..d05bbbe 100644 --- a/ppp_tree.py +++ b/ppp_tree.py @@ -6,11 +6,11 @@ import math import re import textwrap import time -from typing import Any, Optional +from typing import Any, Callable, Optional import lark import numpy as np -from ppp_classes import IFWILDCARDS_CHOICES, PPPState +from ppp_classes import IFWILDCARDS_CHOICES, SUPPORTED_APPS, PPPState from ppp_enmappings import PPPENMappingVariant from ppp_logging import DEBUG_LEVEL, log from ppp_utils import escape_single_quotes @@ -37,9 +37,10 @@ class TreeProcessor(lark.visitors.Interpreter): AccumulatedShell = namedtuple("AccumulatedShell", ["type", "data"]) NegTag = namedtuple("NegTag", ["start", "end", "content", "parameters", "shell"]) - def __init__(self, state: PPPState, rng: np.random.Generator): + def __init__(self, state: PPPState, rng: np.random.Generator, on_model_info_update: Optional[Callable[[], None]] = None): super().__init__() self.state = state + self.__on_model_info_update = on_model_info_update self.__debug_level = state.options.debug_level self.__rng = rng self.__shell: list[TreeProcessor.AccumulatedShell] = [] # type: ignore @@ -1232,10 +1233,37 @@ class TreeProcessor(lark.visitors.Interpreter): """ t1 = time.monotonic_ns() start_result = self.__result + settable_sysvars = {"_modelfullname": "model_filename", "_modelclass": "model_class"} if self.state.variables.name_is_system(variable_name): - self.warn_or_stop( - f"Invalid variable name '{escape_single_quotes(variable_name)}' detected! System variables cannot be set." - ) + if (variable_name not in settable_sysvars and variable_name != "_modelinfo"): + self.warn_or_stop( + f"Invalid variable name '{escape_single_quotes(variable_name)}' detected! System variables cannot be set." + ) + return + app = self.state.env_info.get("app", "") + if app not in (SUPPORTED_APPS.comfyui.value, SUPPORTED_APPS.tests.value): + self.warn_or_stop( + f"Setting '{escape_single_quotes(variable_name)}' is only supported in ComfyUI." + ) + return + evaluated = self.__visit(content, restore_state=False, discard_content=True) + if variable_name == "_modelinfo": + # Format: @ + at_pos = evaluated.find("@") + if at_pos < 0: + self.warn_or_stop( + f"Invalid value for '_modelinfo': expected '@', got '{escape_single_quotes(evaluated)}'." + ) + return + self.state.env_info["model_class"] = evaluated[:at_pos] + self.state.env_info["model_filename"] = evaluated[at_pos + 1 :] + else: + self.state.env_info[settable_sysvars[variable_name]] = evaluated + if self.__on_model_info_update is not None: + self.__on_model_info_update() + info = variable_name + " = " + f"'{escape_single_quotes(evaluated)}'" + t2 = time.monotonic_ns() + self.__debug_end(command, start_result, t2 - t1, info) return info = variable_name is_array = variable_name[-2:] == "[]" diff --git a/tests/tests_varcomms.py b/tests/tests_varcomms.py index 5614ed4..bcac8b7 100644 --- a/tests/tests_varcomms.py +++ b/tests/tests_varcomms.py @@ -942,6 +942,33 @@ class TestVarCommands(TestPromptPostProcessorBase): OutputTuple("this test is OK", ""), ) + def test_set_sysvar_modelclass(self): # setting _modelclass updates model detection + self.process( + InputTuple( + "${_modelclass=Flux}${_model}", + "", + ), + OutputTuple("flux", ""), + ) + + def test_set_sysvar_modelfullname(self): # setting _modelfullname changes variant detection + self.process( + InputTuple( + "${_modelfullname=ponymodel.safetensors}OKnot OK", + "", + ), + OutputTuple("OK", ""), + ) + + def test_set_sysvar_modelinfo(self): # setting _modelinfo sets both class and filename at once + self.process( + InputTuple( + "${_modelinfo=Flux@folder/fluxmodel.safetensors}${_modelname},${_modelclass}", + "", + ), + OutputTuple("fluxmodel.safetensors,Flux", ""), + ) + def test_cmd_echo_sysvar(self): self.process( InputTuple(