diff --git a/README.md b/README.md index 1e34370..27bb709 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ These are some features: * Process wildcards. Compatible with the Dynamic Prompts extension. * Sending parts of the prompt to the negative prompt (the original function of the extension). This allows for useful tricks when using wildcards since you can add negative content from choices made in the positive prompt. -* Set and modify local variables. -* Define model variants (pony, illustrious, ...). -* Filter content based on the loaded SD model/variant or a variable. -* Map extranetworks (LoRAs) depending on conditions (like the loaded model variant). This allows you to add "virtual" LoRAs to the prompt that will be translated to the correct one. +* Define model variants (pony, illustrious, ...) and have them set as system variables along with the current model, to detect which one is being used. +* Set and modify user variables. +* Set content based on conditions (like the system/user variables). +* Map extranetworks (LoRAs) depending on conditions (like the loaded model/variant). This allows you to add "virtual" LoRAs to the prompt that will be translated to the correct one. * Clean up the prompt of unnecessary separators or spaces. * Combinatorial mode. diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index d6b23d0..83c4992 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -2,7 +2,7 @@ ## Commands -The extension uses a format for its commands similar to an extranetwork, but it has a "ppp:" prefix followed by the command, and then a space and any parameters (if any). +The extension uses a format for its commands similar to an extranetwork, but it has a `ppp:` prefix followed by the command, and then a space and any parameters (if any). `` @@ -31,33 +31,33 @@ characters: The generic format is: `{parameters$$opt1::choice1|opt2::choice2|opt3::choice3}` -Both the construct parameters (up to the `$$`) and the individual choice options (up to the '::') are optional. +Both the construct parameters (up to the `$$`) and the individual choice options (up to the `::`) are optional. There is also a format where instead of `parameters$$` you just put the sampler, for compatibility with *Dynamic Prompts*. The construct parameters can be written with the following options (all are optional): -* "**~**" (random) or "**@**" (cyclical): sampler (for compatibility with *Dynamic Prompts*). The cyclical sampler cycles through all combinations in order across consecutive `process_prompt` calls, resuming where the previous call left off (as long as the input prompt and negative prompt do not change). -* "**r**": means it allows repetition of the choices. -* "**o**": means it is "optional", and no error will be raised if there are no choices to select from. -* "**n**" or "**n-m**" or "**n-**" or "**-m**": number or range of choices to select. Allows zero as the start of a range. Default is 1. -* "**'description'**": optional description, only valid in wildcard definitions. Used only in the Wildcards Concat node in ComfyUI. -* "**$$sep**": separator when multiple choices are selected. Default is set in settings. -* "**$$**": end of the parameters (not optional if any parameters). +* `~` (random) or `@` (cyclical): sampler (for compatibility with *Dynamic Prompts*). The cyclical sampler cycles through all combinations in order across consecutive `process_prompt` calls, resuming where the previous call left off (as long as the input prompt and negative prompt do not change). +* `r`: means it allows repetition of the choices. +* `o`: means it is optional, and no error will be raised if there are no choices to select from. +* `n` or `n-m` or `n-` or `-m`: number or range of choices to select. Allows zero as the start of a range. Default is 1. +* `'description'`: optional description (quoted), only valid in wildcard definitions. Used only in the Wildcards Concat node in ComfyUI. +* `$$sep`: separator when multiple choices are selected. Default is set in settings. +* `$$`: end of the parameters (not optional if any parameters). -Regarding the "optional" flag, consider this scenario: due to their conditions no choice is available. It will raise an error. If you add the `o` then it will just return an empty string. This is only necessary if all choices have conditions and they could all be false. It is not the same as setting a range starting at 0, because that would be an allowed number of returned choices. If you do this and no choices are available, no error is raised. +Regarding the optional flag, consider this scenario: due to their conditions no choice is available. It will raise an error. If you add the `o` then it will just return an empty string. This is only necessary if all choices have conditions and they could all be false. It is not the same as setting a range starting at 0, because that would be an allowed number of returned choices. If you do this and no choices are available, no error is raised. The choice options are as follows: -* "**%**": indicates that the content of the choice is a command -* "**'identifiers'**": comma separated labels for the choice (optional, quotes can be single or double). Only makes sense inside a wildcard definition. Can be used when specifying the wildcard to select this specific choice. It's case insensitive. -* "**n**": weight of the choice (optional, default 1). -* "**if condition**": filters out the choice if the condition is false (optional; this is an extension to the *Dynamic Prompts* syntax). Same conditions as in the `if` command. -* "**::**": end of choice options (not optional if any options) +* `%`: indicates that the content of the choice is a command +* `'identifiers'`: comma separated labels for the choice (optional, quotes can be single or double). Only makes sense inside a wildcard definition. Can be used when specifying the wildcard to select this specific choice. It's case insensitive. +* `n`: weight of the choice (optional, default 1). +* `if condition`: filters out the choice if the condition is false (optional; this is an extension to the *Dynamic Prompts* syntax). Same conditions as in the `if` command. +* `::`: end of choice options (not optional if any options) Whitespace is allowed between parameters/options. -The only command available is `include wildcard`, which will include the choices of the specified wildcard in place of this choice. This allows composing choices from multiple wildcards. It also works in the choices of a wildcard, but note that in yaml you cannot start an array element with "%" and you will have to put the full choice in quotes, or use the object format. +The only command available is `include wildcard`, which will include the choices of the specified wildcard in place of this choice. This allows composing choices from multiple wildcards. It also works in the choices of a wildcard, but note that in yaml you cannot start an array element with `%` and you will have to put the full choice in quotes, or use the object format. These are examples of formats you can use to insert a choice construct: @@ -67,7 +67,7 @@ These are examples of formats you can use to insert a choice construct: | `{3$$choice1\|5 if _is_sd1::choice2\|choice3}` | select 3 choices, one has a weight and a condition | | `{2-3$$2::choice1\|choice2\|choice3}` | select 2 to 3 choices, one of them has a weight | | `{r2-3$$choice1\|choice2\|choice3}` | select 2 to 3 choices allowing repetition | -| `{2-3$$ / $$choice1\|choice2\|choice3}` | select 2 to 3 choices with separator " / " | +| `{2-3$$ / $$choice1\|choice2\|choice3}` | select 2 to 3 choices with separator ` / ` | | `{o$$if _is_sd1::choice1\|if _is_sd2::choice2}` | select 1 choice, both have conditions, if none matches it is allowed because we indicate that it is optional | | `{choice1\|choice2\|%0.5::include path/wildcard}` | select 1 choice from the two specified and the ones inside the path/wildcard wildcard, which will be weighted with half their weights | @@ -114,13 +114,13 @@ These are examples of formats you can use to insert a wildcard: | `__wildcard__` | select 1 choice | | `__path/wildcard'0'__` | select the first choice | | `__path/wildcard'1-2'__` | select the second or third choice | -| `__path/wildcard'label'__` | select the choices with label "label" | -| `__path/wildcard'0,label1,label2'__` | select the first choice and those with labels "label1" or "label2" | -| `__path/wildcard'0,label1+label2'__` | select the first choice and those with both labels "label1" and "label2" | +| `__path/wildcard'label'__` | select the choices with label `label` | +| `__path/wildcard'0,label1,label2'__` | select the first choice and those with labels `label1` or `label2` | +| `__path/wildcard'0,label1+label2'__` | select the first choice and those with both labels `label1` and `label2` | | `__3$$path/wildcard__` | select 3 choices | | `__2-3$$path/wildcard__` | select 2 to 3 choices | | `__r2-3$$path/wildcard__` | select 2 to 3 choices allowing repetition | -| `__2-3$$ / $$path/wildcard__` | select 2 to 3 choices with separator " / " | +| `__2-3$$ / $$path/wildcard__` | select 2 to 3 choices with separator ` / ` | | `__path/wildcard(var=value)__` | select 1 choice using the specified variable value in the evaluation. | ### Wildcard definitions @@ -220,10 +220,10 @@ If also supports the addition and undefined check as an extension of the *Dynami | Construct | Meaning | | --------- | ------- | -| `${var+=value}` | equivalent to "add" | -| `${var+=!value}` | equivalent to "evaluate add" | -| `${var?=value}` | equivalent to "ifundefined" | -| `${var?=!value}` | equivalent to "evaluate ifundefined" | +| `${var+=value}` | equivalent to `add` | +| `${var+=!value}` | equivalent to `evaluate add` | +| `${var?=value}` | equivalent to `ifundefined` | +| `${var?=!value}` | equivalent to `evaluate ifundefined` | Set variables are included in the output variables with their last value. @@ -330,7 +330,7 @@ The variable can be one set with the `set` command (user variables) or you can u | System variable | Value | | --------------- | ----- | -| `_model` | the loaded model identifier (`"sd1"`, `"sd2"`, `"sdxl"`, `"sd3"`, `"flux"`, `"auraflow"`). `_sd` also works but is deprecated. | +| `_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. | @@ -343,7 +343,7 @@ The variable can be one set with the `set` command (user variables) or you can u | `_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_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. | @@ -433,9 +433,9 @@ Each mapping can have any number of elements in its list of mappings. There are * `extnettype`: the kind of extranetwork, for example `lora`. * `mappingname`: the name you want to give to the mapping, to be referenced in the command. -* `condition`: the condition to check for this mapping to be used (usually it should be one of the `_is_*` variables). If the conditions of multiple mappings evaluate to True, one will be chosen randomly. If the condition is missing it is considered True, to be used in the last mapping to catch as an "else" condition, and will be used if no other mapping applies. +* `condition`: the condition to check for this mapping to be used (usually it should be one of the `_is_*` variables). If the conditions of multiple mappings evaluate to True, one will be chosen randomly. If the condition is missing it is considered True, to be used in the last mapping to catch as an `else` condition, and will be used if no other mapping applies. * `name`: name of the real extranetwork. If it is missing no extranetwork tag will be added. -* `parameters`: parameters for the real extranetwork. If it is missing it is assumed "1" for LoRAs and HyperNets. If both this parameter and the parameter in the ext command are numbers they are multiplied for the result. In other case the parameter of the ext command, if it exists, is used. +* `parameters`: parameters for the real extranetwork. If it is missing it is assumed `1` for LoRAs and HyperNets. If both this parameter and the parameter in the ext command are numbers they are multiplied for the result. In other case the parameter of the ext command, if it exists, is used. * `triggers`: list of trigger strings. If it is missing, only the inline triggers in the ext command will be added. * `weight`: weight for this variant, in case multiple of them apply, to choose one. Default is 1. diff --git a/ppp_comfyui.py b/ppp_comfyui.py index e8fe831..5aad3dd 100644 --- a/ppp_comfyui.py +++ b/ppp_comfyui.py @@ -1,5 +1,6 @@ import logging import os +from typing import Any import folder_paths # type: ignore import nodes # type: ignore @@ -96,6 +97,7 @@ class PromptPostProcessorComfyUINode: "multiline": True, "default": "", "dynamicPrompts": False, + "tooltip": "Positive prompt to process", }, ), "neg_prompt": ( @@ -104,6 +106,7 @@ class PromptPostProcessorComfyUINode: "multiline": True, "default": "", "dynamicPrompts": False, + "tooltip": "Negative prompt to process", }, ), }, @@ -113,6 +116,7 @@ class PromptPostProcessorComfyUINode: { "default": "", "placeholder": "internal model class name", + "tooltip": "Model or model class name. Needed to know model kind.", }, ), "modelname": ( @@ -121,6 +125,7 @@ class PromptPostProcessorComfyUINode: "default": "", "placeholder": "full path of the model", "dynamicPrompts": False, + "tooltip": "Full path of the model. Needed to detect variants.", }, ), "seed": ( @@ -132,15 +137,17 @@ class PromptPostProcessorComfyUINode: }, ), "debug_level": ( - [e.value for e in DEBUG_LEVEL], + "COMBO", { + "options": [e.value for e in DEBUG_LEVEL], "default": PromptPostProcessor.DEFAULT_DEBUG_LEVEL, "tooltip": "Debug level", }, ), "on_warnings": ( - [e.value for e in ONWARNING_CHOICES], + "COMBO", { + "options": [e.value for e in ONWARNING_CHOICES], "default": PromptPostProcessor.DEFAULT_ON_WARNING, "tooltip": "How to handle invalid content warnings", }, @@ -265,6 +272,11 @@ class PromptPostProcessorComfyUINode: "neg_prompt", "variables", ) + OUTPUT_TOOLTIPS = ( + "Processed positive prompt (list of prompts if combinatorial mode is enabled)", + "Processed negative prompt (list of prompts if combinatorial mode is enabled)", + "Output variables (list of dictionaries if combinatorial mode is enabled)", + ) FUNCTION = "process" @@ -465,8 +477,9 @@ class PromptPostProcessorWildcardOptionsComfyUINode: }, ), "if_wildcards": ( - [e.value for e in IFWILDCARDS_CHOICES], + "COMBO", { + "options": [e.value for e in IFWILDCARDS_CHOICES], "default": IFWILDCARDS_CHOICES.stop.value, "tooltip": "How to handle invalid wildcards in the prompt", }, @@ -797,6 +810,7 @@ class PromptPostProcessorSelectVariableComfyUINode: "PPP_DICT", { "forceInput": True, + "tooltip": "Dictionary of variables to select from", }, ), }, @@ -808,12 +822,13 @@ class PromptPostProcessorSelectVariableComfyUINode: "multiline": False, "default": "", "dynamicPrompts": False, + "tooltip": "Name of the variable to select", }, ), }, } - RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("*",) RETURN_NAMES = ("value",) FUNCTION = "select" @@ -822,7 +837,7 @@ class PromptPostProcessorSelectVariableComfyUINode: def select( self, - variables: dict[str, str], + variables: dict[str, Any], name: str, ): value = "" diff --git a/ppp_wildcards.py b/ppp_wildcards.py index 8f5aafd..605e40c 100644 --- a/ppp_wildcards.py +++ b/ppp_wildcards.py @@ -1,7 +1,7 @@ import fnmatch import os from pathlib import Path -from typing import Optional +from typing import Any, Optional import logging import yaml @@ -17,8 +17,8 @@ class PPPWildcard: key (str): The key of the wildcard. file (str): The path to the file where the wildcard is defined. unprocessed_choices (list[str]): The unprocessed choices of the wildcard. - choices (list[dict]): The processed choices of the wildcard. options (dict): The options of the wildcard. + choices (list[dict]): The processed choices of the wildcard. """ def __init__(self, fullpath: str, key: str, choices: list[str]): @@ -115,43 +115,24 @@ class PPPWildcards: keys = sorted(fnmatch.filter(self.wildcards.keys(), key)) return [self.wildcards[k] for k in keys] - def __get_keys_in_dict(self, dictionary: dict, prefix="") -> list[str]: + def __get_wc_in_dict(self, dictionary: dict, prefix="") -> list[tuple[str, Any]]: """ - Get all keys in a dictionary. + Get all wildcards in a dictionary, along their object. Args: dictionary (dict): The dictionary to check. prefix (str): The prefix for the current key. Returns: - list: A list of all keys in the dictionary, including nested keys. + list: A list of all leaf wildcards in the dictionary. """ - keys = [] - for key in dictionary.keys(): - if isinstance(dictionary[key], dict): - keys.extend(self.__get_keys_in_dict(dictionary[key], prefix + key + "/")) + wc = [] + for key, obj in dictionary.items(): + if isinstance(obj, dict): + wc.extend(self.__get_wc_in_dict(obj, prefix + str(key) + "/")) else: - keys.append(prefix + str(key)) - return keys - - def __get_nested(self, dictionary: dict, keys: str) -> object: - """ - Get a nested value from a dictionary. - - Args: - dictionary (dict): The dictionary to check. - keys (str): The keys to get the value from. - - Returns: - object: The value of the nested keys in the dictionary. - """ - keys = keys.split("/") - current_dict = dictionary - for key in keys: - current_dict = current_dict.get(key) - if current_dict is None: - return None - return current_dict + wc.append((prefix + str(key), obj)) + return wc def __remove_wildcards_from_path(self, full_path: str, debug=True): """ @@ -395,8 +376,8 @@ class PPPWildcards: key_parts = external_key_parts.copy() if isinstance(content, dict): key_parts.pop() - keys = self.__get_keys_in_dict(content) - for key in keys: + keys = self.__get_wc_in_dict(content) + for key, obj in keys: tmp_key_parts = key_parts.copy() tmp_key_parts.extend(key.split("/")) fullkey = "/".join(tmp_key_parts) @@ -408,7 +389,6 @@ class PPPWildcards: f"Duplicate wildcard '{escape_single_quotes(fullkey)}' in file '{escape_single_quotes(full_path)}' and '{escape_single_quotes(self.wildcards[fullkey].file)}'!", ) else: - obj = self.__get_nested(content, key) choices = self.__get_choices(obj, full_path, tmp_key_parts) if choices is None: log( diff --git a/pyproject.toml b/pyproject.toml index 38a028b..83ee573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,18 @@ [project] name = "sd-webui-prompt-postprocessor" -description = "Stable Diffusion WebUI & ComfyUI extension to post-process the prompt, including sending content from the prompt to the negative prompt and wildcards." -version = "3.1.1" +description = "Stable Diffusion WebUI & ComfyUI extension to post-process the prompt. Features include: wildcards, sending content from the prompt to the negative prompt, variables, model detection, extranetwork mapping, cleanup." +version = "3.1.0" license = { file = "LICENSE.txt" } dependencies = ["lark", "numpy", "pyyaml", "pydantic"] +requires-python = ">=3.10" [project.urls] -Repository = "https://github.com/acorderob/sd-webui-prompt-postprocessor" -# Used by Comfy Registry https://comfyregistry.org -Documentation = "https://github.com/acorderob/sd-webui-prompt-postprocessor/main/README.md" -"Bug Tracker" = "https://github.com/acorderob/sd-webui-prompt-postprocessor/issues" +repository = "https://github.com/acorderob/sd-webui-prompt-postprocessor" +# Used by Comfy Registry https://registry.comfy.org/ +documentation = "https://github.com/acorderob/sd-webui-prompt-postprocessor/main/README.md" +issues = "https://github.com/acorderob/sd-webui-prompt-postprocessor/issues" [tool.comfy] -PublisherId = "acorderob" -#DisplayName = "sd-webui-prompt-postprocessor" -DisplayName = "ACB Prompt PostProcessor" -Icon = "https://raw.githubusercontent.com/acorderob/sd-webui-prompt-postprocessor/main/images/prompt-postprocessor-icon.png" +publisher_id = "acorderob" +display_name = "ACB Prompt PostProcessor" +icon = "https://raw.githubusercontent.com/acorderob/sd-webui-prompt-postprocessor/main/images/prompt-postprocessor-icon.png"