diff --git a/README.md b/README.md index dc869c7..a8d2eb5 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,10 @@ In a choice, the content after a "#" is ignored. If the first choice follows the format of wildcard parameters, it will be used as default parameters for that wildcard (see examples in the tests folder). The choices of the wildcard follow the same format as in the choices construct, or the object format of **Dynamic Prompts** (only in structured files). If using the object format for a choice you can use a new "if" property for the condition, and the "labels" property (an array of strings) in addition to the standard "weight" and "text"/"content". +```yaml +{ labels: ["some_label"], weight: 2, if: "_is_pony", content: "the text" } # "text" property can be used instead of "content" +``` + Wildcard parameters in a json/yaml file can also be in object format, and support two additional properties, prefix and suffix: ```yaml @@ -175,6 +179,8 @@ It is recommended to use the object format for the wildcard parameters and for c Wildcards can contain just one choice. In json and yaml formats this allows the use of a string value for the keys, rather than an array. +A choice inside a wildcard can also be a list or a dictionary of one element containing a list. These are considered anonymous wildcards. With a list it will be an anonymous wildcard with no choice options, and with a dictionary the key will be the options for the choice containing the anonymous wildcard and the value the choices of the anonymous wildcard. Anonymous wildcards can help formatting complex choice values that are used in only one place and thus creating a regular wildcard is not necessary. See test.yaml for examples. + #### Detection of remaining wildcards This extension should run after any other wildcard extensions, so if you don't use the internal wildcards processing, any remaining wildcards present in the prompt or negative_prompt at this point must be invalid. Usually you might not notice this problem until you check the image metadata, so this option gives you some ways to detect and treat the problem. diff --git a/ppp_wildcards.py b/ppp_wildcards.py index c29ae65..3c018e0 100644 --- a/ppp_wildcards.py +++ b/ppp_wildcards.py @@ -139,7 +139,7 @@ class PPPWildcards: self.__get_wildcards_in_structured_file(full_path, base) self.wildcard_files[full_path] = last_modified - def __get_choices(self, obj: object) -> list[dict]: + def __get_choices(self, obj: object, full_path: str, key_parts: list[str]) -> list[dict]: choices = None if obj is not None: if isinstance(obj, (str, dict)): @@ -148,34 +148,70 @@ class PPPWildcards: choices = [str(obj)] elif isinstance(obj, list) and len(obj) > 0: choices = [] - for c in obj: - if isinstance(c, (str, dict)): - choices.append(c) + for i, c in enumerate(obj): + invalid_choice = False + if isinstance(c, str): + choice = c elif isinstance(c, (int, float, bool)): - choices.append(str(c)) + choice = str(c) + elif isinstance(c, list): + # we create an anonymous wildcard + choice = self.__create_anonymous_wildcard(full_path, key_parts, i, c) + elif isinstance(c, dict): + if all( + k in ["sampler", "repeating", "count", "from", "to", "prefix", "suffix", "separator"] + for k in c.keys() + ) or all(k in ["labels", "weight", "if", "content", "text"] for k in c.keys()): + # we assume it is a choice or wildcard parameters in object format + choice = c + choice_content = choice.get("content", choice.get("text", None)) + if choice_content is not None and isinstance(choice_content, list): + # we create an anonymous wildcard + choice["content"] = self.__create_anonymous_wildcard( + full_path, key_parts, i, choice_content + ) + if "text" in choice: + del choice["text"] + elif len(c) == 1: + # we assume it is an anonymous wildcard with options + firstkey = list(c.keys())[0] + choice = self.__create_anonymous_wildcard(full_path, key_parts, i, c[firstkey], firstkey) + else: + invalid_choice = True + else: + invalid_choice = True + if invalid_choice: + self.logger.warning( + f"Invalid choice {i+1} in wildcard '{'/'.join(key_parts)}' in file '{full_path}'!" + ) + else: + choices.append(choice) return choices - def __get_wildcards_in_structured_file(self, full_path, base): - external_key: str = os.path.relpath(os.path.splitext(full_path)[0], base) - external_key_parts = external_key.split(os.sep) - _, extension = os.path.splitext(full_path) - with open(full_path, "r", encoding="utf-8") as file: - if extension == ".json": - content = json.loads(file.read()) - else: - content = yaml.safe_load(file) + def __create_anonymous_wildcard(self, full_path, key_parts, i, content, options=None): + new_parts = key_parts + [f"#ANON_{i}"] + self.__add_wildcard(content, full_path, new_parts) + value = f"__{'/'.join(new_parts)}__" + if options is not None: + value = f"{options}::{value}" + return value + + def __add_wildcard(self, content: object, full_path: str, external_key_parts: list[str]): + key_parts = external_key_parts.copy() if isinstance(content, dict): - external_key_parts = external_key_parts[:-1] + key_parts.pop() keys = self.__get_keys_in_dict(content) for key in keys: - fullkey = "/".join(external_key_parts + [key]) + tmp_key_parts = key_parts.copy() + tmp_key_parts.extend(key.split("/")) + fullkey = "/".join(tmp_key_parts) if self.wildcards.get(fullkey, None) is not None: self.logger.warning( f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!" ) else: obj = self.__get_nested(content, key) - choices = self.__get_choices(obj) + choices = self.__get_choices(obj, full_path, tmp_key_parts) if choices is None: self.logger.warning(f"Invalid wildcard '{fullkey}' in file '{full_path}'!") else: @@ -188,18 +224,29 @@ class PPPWildcards: if not isinstance(content, list): self.logger.warning(f"Invalid wildcard in file '{full_path}'!") return - fullkey = "/".join(external_key_parts) + fullkey = "/".join(key_parts) if self.wildcards.get(fullkey, None) is not None: self.logger.warning( f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!" ) else: - choices = self.__get_choices(content) + choices = self.__get_choices(content, full_path, key_parts) if choices is None: self.logger.warning(f"Invalid wildcard '{fullkey}' in file '{full_path}'!") else: self.wildcards[fullkey] = PPPWildcard(full_path, fullkey, choices) + def __get_wildcards_in_structured_file(self, full_path, base): + external_key: str = os.path.relpath(os.path.splitext(full_path)[0], base) + external_key_parts = external_key.split(os.sep) + _, extension = os.path.splitext(full_path) + with open(full_path, "r", encoding="utf-8") as file: + if extension == ".json": + content = json.loads(file.read()) + else: + content = yaml.safe_load(file) + self.__add_wildcard(content, full_path, external_key_parts) + def __get_wildcards_in_text_file(self, full_path, base): external_key: str = os.path.relpath(os.path.splitext(full_path)[0], base) external_key_parts = external_key.split(os.sep) @@ -207,16 +254,7 @@ class PPPWildcards: text_content = map(lambda x: x.strip("\n\r"), file.readlines()) text_content = list(filter(lambda x: x.strip() != "" and not x.strip().startswith("#"), text_content)) text_content = [x.split("#")[0].rstrip() if len(x.split("#")) > 1 else x for x in text_content] - fullkey = "/".join(external_key_parts) - if self.wildcards.get(fullkey, None) is not None: - self.logger.warning( - f"Duplicate wildcard '{fullkey}' in file '{full_path}' and '{self.wildcards[fullkey].file}'!" - ) - else: - if len(text_content) == 0: - self.logger.warning(f"Invalid wildcard in file '{full_path}'!") - else: - self.wildcards[fullkey] = PPPWildcard(full_path, fullkey, text_content) + self.__add_wildcard(text_content, full_path, external_key_parts) def __get_wildcards_in_directory(self, base: str, directory: str): """ diff --git a/tests/tests.py b/tests/tests.py index 75d3e3c..b3e91c4 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -822,6 +822,13 @@ class TestPromptPostProcessor(unittest.TestCase): ppp=self.__nocupppp, ) + def test_wc_anonymouswildcard_yaml(self): # yaml anonymous wildcard + self.__process( + PromptPair("the choices are: __yaml/nesteddict__", ""), + PromptPair("the choices are: six", ""), + ppp=self.__nocupppp, + ) + # ComfyUI tests def test_comfyui_attention(self): # attention conversion diff --git a/tests/wildcards/test.yaml b/tests/wildcards/test.yaml index e6de874..d1952dc 100644 --- a/tests/wildcards/test.yaml +++ b/tests/wildcards/test.yaml @@ -52,3 +52,14 @@ yaml: even_more_nested: # this would be __yaml/more_nested/even_more_nested__ - one - two + + nesteddict: + - one + - two + - # anonymous wildcard without options + - three + - four + - 3 if _is_sdxl: # anonymous wildcard with options + - five + - six + - { weight: 1, text: [seven, eight] } # anonymous wildcard used in a choice in object format