Skip to content

Commit

Permalink
* Support anonymous wildcards.
Browse files Browse the repository at this point in the history
  • Loading branch information
acorderob committed Oct 19, 2024
1 parent e9092b8 commit c87c249
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 29 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
96 changes: 67 additions & 29 deletions ppp_wildcards.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def __get_wildcards_in_file(self, base, full_path: str):
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)):
Expand All @@ -148,34 +148,70 @@ def __get_choices(self, obj: object) -> list[dict]:
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:
Expand All @@ -188,35 +224,37 @@ def __get_wildcards_in_structured_file(self, full_path, base):
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)
with open(full_path, "r", encoding="utf-8") as file:
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):
"""
Expand Down
7 changes: 7 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,13 @@ def test_wc_wildcardPS_yaml(self): # yaml wildcard with object formatted choice
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
Expand Down
11 changes: 11 additions & 0 deletions tests/wildcards/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c87c249

Please sign in to comment.