diff --git a/README.md b/README.md index 9ab2e89..07abcea 100644 --- a/README.md +++ b/README.md @@ -193,15 +193,16 @@ This command sets the value of a variable that can be checked later. The format is: ```text -value -value -value -value +value ``` -The `evaluate` parameter makes it so the value of the variable is evaluated at this moment, instead of when it is used. +These are the available optional modifiers: -With the `add` parameter the value is added to the current value of the variable. It does not force an immediate evaluation of the old nor the added value. +* `evaluate`: the value of the variable is evaluated at this moment, instead of when it is used. +* `add`: the value is added to the current value of the variable. It does not force an immediate evaluation of the old nor the added value. +* `ifundefined`: the value will only be set if the variable is undefined. + +The `add` and `ifundefined` modifiers are mutually exclusive and cannot be used together. The Dynamic Prompts format also works: @@ -210,11 +211,13 @@ ${var=value} ${var=!value} # immediate evaluation ``` -If also supports the addition as an extension of the Dynamic Prompts format: +If also supports the addition and undefined check as an extension of the Dynamic Prompts format: ```text -${var+=value} -${var+=!value} +${var+=value} # equivalent to "add" +${var+=!value} # equivalent to "evaluate add" +${var?=value} # equivalent to "ifundefined" +${var?=!value} # equivalent to "evaluate ifundefined" ``` ### Echo command diff --git a/grammar.lark b/grammar.lark index 730ac41..21db359 100644 --- a/grammar.lark +++ b/grammar.lark @@ -67,13 +67,15 @@ SIMPLEVALUE: STRING | NUMBER | BOOLEAN listvalue: "(" _WHITESPACE? SIMPLEVALUE ( _WHITESPACE? "," _WHITESPACE? SIMPLEVALUE )* _WHITESPACE? ")" // command: set -commandset: "" content "" +commandset: "" content "" +commandsetmodifiers: (_WHITESPACE /evaluate|ifundefined|add/ )+ // command: echo commandecho: "" [ content "" ] // variable set -variableset.2: "${" _WHITESPACE? IDENTIFIER _WHITESPACE? [ /\+/ ] "=" [ /!/ ] varvalue "}" +variableset.2: "${" _WHITESPACE? IDENTIFIER [ variablesetmodifiers ] _WHITESPACE? "=" [ /!/ ] varvalue "}" +variablesetmodifiers: /[+?!]+/ // variable use variableuse.2: "${" _WHITESPACE? IDENTIFIER _WHITESPACE? [ ":" varvalue ] "}" diff --git a/ppp.py b/ppp.py index 6ca523b..3cdbd68 100644 --- a/ppp.py +++ b/ppp.py @@ -108,9 +108,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in f"Unsupported model type '{model_type}' in definition for variant '{model_name}'." ) elif model_name in self.SUPPORTED_MODELS: - self.logger.warning( - f"Invalid model name in definition for variant '{model_name}'." - ) + self.logger.warning(f"Invalid model name in definition for variant '{model_name}'.") else: self.variants_definitions[model_name.strip()] = ( model_type or "", @@ -477,7 +475,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.interrupt() # Check for special character sequences that should not be in the result compound_prompt = prompt + "\n" + negative_prompt - found_sequences = re.findall(r"::|\$\$|\$\{|[{|}]", compound_prompt) + found_sequences = re.findall(r"::|\$\$|\$\{|[{}]", compound_prompt) if len(found_sequences) > 0: self.logger.warning( f"""Found probably invalid character sequences on the result ({', '.join(map(lambda x: '"' + x + '"', set(found_sequences)))}). Something might be wrong!""" @@ -521,6 +519,8 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.logger.info(self.format_output(f"Result negative_prompt: {negative_prompt}")) self.logger.info(f"Process prompt pair time: {t2 - t1:.3f} seconds") + # if self.debug_level != DEBUG_LEVEL.none: + # self.logger.debug(f"Wildcards memory usage: {self.wildcard_obj.__sizeof__()}") # Check for constructs not processed due to parsing problems fullcontent: str = prompt + negative_prompt if fullcontent.find("= 0: @@ -761,7 +761,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in if var_value is None: var_value = "" self.__ppp.logger.warning(f"Unknown system variable {cond_var}") - else: # user variable + else: # user variable var_value = self.__get_user_variable_value(cond_var) if var_value is None: var_value = "" @@ -1057,8 +1057,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self, command: str, variable: str, - immediateevaluation: str | None, - adding: str | None, + modifiers: lark.Tree | None, content: lark.Tree | None, ): """ @@ -1073,7 +1072,8 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in info = variable value_description = self.__get_original_node_content(content, None) value = content - if adding is not None: + modifiers_str: list[str] = [m.value for m in modifiers.children] if modifiers is not None else [] + if any(item in modifiers_str for item in ["+", "add"]): info += f" += '{value_description}'" raw_oldvalue = self.__ppp.user_variables.get(variable, None) if raw_oldvalue is None: @@ -1091,19 +1091,28 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in [raw_oldvalue, value], # Meta should be {"content": raw_oldvalue.meta.content + value.meta.content}, ) + elif any(item in modifiers_str for item in ["?", "ifundefined"]): + info += f" ?= '{value_description}'" + raw_oldvalue = self.__ppp.user_variables.get(variable, None) + if raw_oldvalue is None: + newvalue = value + else: + info += " (not set)" + newvalue = None else: newvalue = value - if immediateevaluation is not None: - newvalue = self.__visit(newvalue, False, True) - info += " =! " - else: - info += " = " - self.__set_user_variable_value(variable, newvalue) - currentvalue = self.__get_user_variable_value(variable, False) - if currentvalue is None: - info += "not evaluated yet" - else: - info += f"'{currentvalue}'" + if newvalue is not None: + if any(item in modifiers_str for item in ["!", "evaluate"]): + newvalue = self.__visit(newvalue, False, True) + info += " =! " + else: + info += " = " + self.__set_user_variable_value(variable, newvalue) + currentvalue = self.__get_user_variable_value(variable, False) + if currentvalue is None: + info += "not evaluated yet" + else: + info += f"'{currentvalue}'" t2 = time.time() self.__debug_end(command, start_result, t2 - t1, info) @@ -1111,13 +1120,18 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in """ Process a DP set variable command in the tree and add it to the dictionary of variables. """ - self.__varset("variableset", tree.children[0], tree.children[2], tree.children[1], tree.children[3]) + modifiers = tree.children[1] or lark.Tree(lark.Token("RULE", "variablesetmodifiers"), []) + immediate = tree.children[2] + if immediate is not None: + modifiers.children = modifiers.children.copy() + modifiers.children.append(immediate) + self.__varset("variableset", tree.children[0], modifiers, tree.children[3]) def commandset(self, tree: lark.Tree): """ Process a set command in the tree and add it to the dictionary of variables. """ - self.__varset("commandset", tree.children[0], tree.children[1], tree.children[2], tree.children[3]) + self.__varset("commandset", tree.children[0], tree.children[1], tree.children[2]) def __varecho(self, command: str, variable: str, default: lark.Tree | None): """ @@ -1533,7 +1547,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in t2 = time.time() self.__debug_end("wildcard", start_result, t2 - t1, wc) return - filter_specifier:list[int|str] = None + filter_specifier: list[int | str] = None filter_object = tree.children[2] if filter_object is not None: if ( diff --git a/ppp_cache.py b/ppp_cache.py index a77a87b..44ec45f 100644 --- a/ppp_cache.py +++ b/ppp_cache.py @@ -1,15 +1,20 @@ from collections import OrderedDict +from logging import Logger from typing import Tuple +from ppp_logging import DEBUG_LEVEL + class PPPLRUCache: ProcessInput = Tuple[int, int, str, str] # (seed, wildcards_hash, positive_prompt, negative_prompt) ProcessResult = Tuple[str, str] # (positive_prompt, negative_prompt) - def __init__(self, capacity: int): + def __init__(self, capacity: int, logger: Logger = None, debug_level: DEBUG_LEVEL = DEBUG_LEVEL.none): self.cache = OrderedDict() self.capacity = capacity + self._logger = logger + self._debug_level = debug_level def get(self, key: ProcessInput) -> ProcessResult: if key not in self.cache: @@ -22,3 +27,5 @@ class PPPLRUCache: self.cache.move_to_end(key) if len(self.cache) > self.capacity: self.cache.popitem(last=False) + # if self._logger is not None and self._debug_level != DEBUG_LEVEL.none: + # self._logger.debug(f"Cache size: {self.cache.__sizeof__()}") diff --git a/ppp_comfyui.py b/ppp_comfyui.py index 86767fe..69497b9 100644 --- a/ppp_comfyui.py +++ b/ppp_comfyui.py @@ -355,7 +355,9 @@ class PromptPostProcessorComfyUINode: remove_extranetwork_tags, ): if wc_process_wildcards: - return float("NaN") # since we can't detect changes in wildcards we assume they are always changed when enabled + return float( + "NaN" + ) # since we can't detect changes in wildcards we assume they are always changed when enabled new_run = { # everything except debug_level "model": model, "modelname": modelname, @@ -432,11 +434,11 @@ class PromptPostProcessorComfyUINode: ), "is_ssd": modelclass in ("SSD1B",), "is_sd3": modelclass in ("SD3",), - "is_flux": modelclass in ("Flux",), + "is_flux": modelclass in ("Flux", "FluxInpaint", "FluxSchnell"), "is_auraflow": modelclass in ("AuraFlow",), } - # SVD_img2vid, SVD3D_u, SVD3_p, Stable_Zero123, SD_X4Upscaler, - # Stable_Cascade_C, Stable_Cascade_B, StableAudio + # Also supported: SVD_img2vid, SVD3D_u, SVD3_p, Stable_Zero123, SD_X4Upscaler, + # Stable_Cascade_C, Stable_Cascade_B, StableAudio, HunyuanDiT, HunyuanDiT1, GenmoMochi, LTXV if wc_wildcards_folders == "": wc_wildcards_folders = ",".join(folder_paths.get_folder_paths("wildcards") or []) @@ -447,6 +449,8 @@ class PromptPostProcessorComfyUINode: for f in wc_wildcards_folders.split(",") if f.strip() != "" ] + if variants_definitions != "" and not "=" in variants_definitions: # mainly to warn about the old format + raise ValueError("Invalid variants_definitions format") options = { "debug_level": debug_level, "variants_definitions": variants_definitions, diff --git a/ppp_wildcards.py b/ppp_wildcards.py index 57112d9..65f289d 100644 --- a/ppp_wildcards.py +++ b/ppp_wildcards.py @@ -51,6 +51,15 @@ class PPPWildcard: t = (self.key, deep_freeze(self.unprocessed_choices)) return hash(t) + def __sizeof__(self): + return ( + self.key.__sizeof__() + + self.file.__sizeof__() + + self.unprocessed_choices.__sizeof__() + + self.choices.__sizeof__() + + self.options.__sizeof__() + ) + class PPPWildcards: """ @@ -72,6 +81,13 @@ class PPPWildcards: def __hash__(self) -> int: return hash(deep_freeze(self.wildcards)) + def __sizeof__(self): + return ( + self.wildcards.__sizeof__() + + self.__wildcards_folders.__sizeof__() + + self.__wildcard_files.__sizeof__() + ) + def refresh_wildcards(self, debug_level: DEBUG_LEVEL, wildcards_folders: Optional[list[str]]): """ Initialize the wildcards. diff --git a/scripts/ppp_script.py b/scripts/ppp_script.py index 3525ac1..03d0f78 100644 --- a/scripts/ppp_script.py +++ b/scripts/ppp_script.py @@ -64,13 +64,13 @@ class PromptPostProcessorA1111Script(scripts.Script): self.name = PromptPostProcessor.NAME self.ppp_logger = lf.log self.ppp_debug_level = DEBUG_LEVEL(getattr(opts, "ppp_gen_debug_level", DEBUG_LEVEL.none.value)) - self.lru_cache = PPPLRUCache(1000) + self.lru_cache = PPPLRUCache(1000, logger=self.ppp_logger, debug_level=self.ppp_debug_level) grammar_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../grammar.lark") with open(grammar_filename, "r", encoding="utf-8") as file: self.grammar_content = file.read() self.wildcards_obj = PPPWildcards(lf.log) i = self.get_instance_count() - if i == 1: # some UIs create multiple instances + if i == 1: # some UIs create multiple instances self.ppp_logger.info(f"{PromptPostProcessor.NAME} {PromptPostProcessor.VERSION} initialized") def title(self): @@ -175,6 +175,16 @@ class PromptPostProcessorA1111Script(scripts.Script): if self.ppp_debug_level != DEBUG_LEVEL.none: self.ppp_logger.info("Not processing the prompt for i2i") return + + p.extra_generation_params.update( + { + "PPP force equal seeds": input_force_equal_seeds, + "PPP unlink seed": input_unlink_seed, + "PPP prompt seed": input_seed, + "PPP incremental seed": input_incremental_seed, + } + ) + app_names = { "sdnext": "SD.Next", "forge": "Forge", @@ -228,15 +238,24 @@ class PromptPostProcessorA1111Script(scripts.Script): 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" + # also supports 'Latent Consistency Model': LatentConsistencyModelPipeline', 'PixArt-Alpha': 'PixArtAlphaPipeline', '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', 'AuraFlow': 'AuraFlowPipeline', 'CogView': 'CogView3PlusPipeline' elif app == "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_sd1"] = getattr( + p.sd_model, "is_sd1", False + ) # p.sd_model.model_config.__class__.__name__ == "StableDiffusion" + env_info["is_sd2"] = getattr( + p.sd_model, "is_sd2", False + ) # p.sd_model.model_config.__class__.__name__ == "StableDiffusion2" + env_info["is_sdxl"] = getattr( + p.sd_model, "is_sdxl", False + ) # p.sd_model.model_config.__class__.__name__ == "StableDiffusionXL" env_info["is_ssd"] = False # ? - env_info["is_sd3"] = getattr(p.sd_model, "is_sd3", False) + env_info["is_sd3"] = getattr( + p.sd_model, "is_sd3", False + ) # p.sd_model.model_config.__class__.__name__ == "StableDiffusion3" # not actually supported? env_info["is_flux"] = p.sd_model.model_config.__class__.__name__ == "Flux" - env_info["is_auraflow"] = False # p.sd_model.model_config.__class__.__name__ == "AuraFlow" + env_info["is_auraflow"] = False # p.sd_model.model_config.__class__.__name__ == "AuraFlow" # not supported elif app == "reforge": env_info["model_class"] = p.sd_model.__class__.__name__ env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False) @@ -328,25 +347,45 @@ class PromptPostProcessorA1111Script(scripts.Script): else: calculated_seeds = seeds + # initialize extra generation parameters + extra_params = {} + # adds regular prompts - rpr = getattr(p, "all_prompts", None) - rnr = getattr(p, "all_negative_prompts", None) + rpr: list[str] = getattr(p, "all_prompts", None) + rnr: list[str] = getattr(p, "all_negative_prompts", None) if rpr is not None and rnr is not None: + extra_params.update( + { + "PPP original prompts": rpr.copy(), + "PPP original negative prompts": rnr.copy(), + } + ) prompts_list += [ ("regular", seed, prompt, negative_prompt) for seed, prompt, negative_prompt in zip(calculated_seeds, rpr, rnr) if (seed, prompt, negative_prompt) not in prompts_list ] # make it compatible with A1111 hires fix - rph = getattr(p, "all_hr_prompts", None) - rnh = getattr(p, "all_hr_negative_prompts", None) + rph: list[str] = getattr(p, "all_hr_prompts", None) + rnh: list[str] = getattr(p, "all_hr_negative_prompts", None) if rph is not None and rnh is not None: + extra_params.update( + { + "PPP original HR prompts": rph.copy(), + "PPP original HR negative prompts": rnh.copy(), + } + ) prompts_list += [ ("hiresfix", seed, prompt, negative_prompt) for seed, prompt, negative_prompt in zip(calculated_seeds, rph, rnh) if (seed, prompt, negative_prompt) not in prompts_list ] + # fill extra generation parameters only if not already present + for k, v in extra_params.items(): + if p.extra_generation_params.get(k) is None: + p.extra_generation_params[k] = v + # processes prompts for i, (prompttype, seed, prompt, negative_prompt) in enumerate(prompts_list): if self.ppp_debug_level != DEBUG_LEVEL.none: diff --git a/tests/tests.py b/tests/tests.py index 34f3e6b..0bbc409 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -442,6 +442,42 @@ class TestPromptPostProcessor(unittest.TestCase): ppp=self.__nocupppp, ) + def test_cmd_set_ifundefined_if(self): # set, ifundefined and if commands + self.__process( + PromptPair( + "valuethis test is OKnot OK", + "", + ), + PromptPair("this test is OK", ""), + ) + + def test_cmd_set_ifundefined_if_2(self): # set, ifundefined and if commands + self.__process( + PromptPair( + "valuevalue2this test is OKnot OK", + "", + ), + PromptPair("this test is OK", ""), + ) + + def test_cmd_set_ifundefined_DP_if(self): # set, ifundefined (DP format) and if commands + self.__process( + PromptPair( + "${v?=value}this test is OKnot OK", + "", + ), + PromptPair("this test is OK", ""), + ) + + def test_cmd_set_ifundefined_DP_if_2(self): # set, ifundefined (DP format) and if commands + self.__process( + PromptPair( + "${v=!value}${v?=!value2}this test is OKnot OK", + "", + ), + PromptPair("this test is OK", ""), + ) + # Choices tests def test_ch_choices(self): # simple choices with weights