Skip to content

Commit

Permalink
* Simpler versioning
Browse files Browse the repository at this point in the history
* Cache now takes wildcard definitions into consideration.
* ComfyUI: node is not cached if wildcard processing is active.
* Add a space with prefix and suffix if needed.
* Checks requirements before installing them.
  • Loading branch information
acorderob committed Oct 27, 2024
1 parent c87c249 commit 3810320
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 82 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
The Prompt PostProcessor (PPP), formerly known as "sd-webui-sendtonegative", is an extension designed to process the prompt, possibly after other extensions have modified it. This extension is compatible with:

* [AUTOMATIC1111 Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)
* [SD.Next](https://github.com/vladmandic/automatic).
* [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge)
* [reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge)
* [SD.Next](https://github.com/vladmandic/automatic).
* ...and probably other forks
* [ComfyUI](https://github.com/comfyanonymous/ComfyUI)

Expand Down Expand Up @@ -49,6 +49,8 @@ On A1111 compatible webuis:
3. Click the Install button
4. Restart the webui

On SD.Next I recommend you disable the native wildcard processing.

On ComfyUI:

1. Go to Manager > Custom Nodes Manager
Expand Down Expand Up @@ -130,7 +132,7 @@ The parameters, the filter, and the setting of a variable are optional. The para

The wildcard identifier can contain globbing formatting, to read multiple wildcards and merge their choices. Note that if there are no parameters specified, the globbing will use the ones from the first wildcard that matches and have parameters (sorted by keys), so if you don't want that you might want to specify them. Also note that, unlike with Dynamic Prompts, the wildcard name has to be specified with its full path (unless you use globbing).

The filter can be used to filter specific choices from the wildcard. The filtering works before applying the choice conditions (if any). The surrounding quotes can be single or double. The filter is a comma separated list of an integer (positional choice index) or choice label. You can also compound them with "+". That is, the comma separated items act as an OR and the "+" inside them as an AND. Using labels can simplify the definitions of complex wildcards where you want to have direct access to specific choices on occasion (you don't need to create wildcards for each individual choice). There are some additional formats when using filters. You can specify "^wildcard" as a filter to use the filter of a previous wildcard in the chain. You can start the filter (regular or inherited) with "#" and it will not be applied to the current wildcard choices, but the filter will remain in memory to use by other descendant wildcards. You use "#" and "^" when you want to pass a filter to inner wildcards (see the test files).
The filter can be used to filter specific choices from the wildcard. The filtering works before applying the choice conditions (if any). The surrounding quotes can be single or double. The filter is a comma separated list of an integer (positional choice index; zero-based) or choice label. You can also compound them with "+". That is, the comma separated items act as an OR and the "+" inside them as an AND. Using labels can simplify the definitions of complex wildcards where you want to have direct access to specific choices on occasion (you don't need to create wildcards for each individual choice). There are some additional formats when using filters. You can specify "^wildcard" as a filter to use the filter of a previous wildcard in the chain. You can start the filter (regular or inherited) with "#" and it will not be applied to the current wildcard choices, but the filter will remain in memory to use by other descendant wildcards. You use "#" and "^" when you want to pass a filter to inner wildcards (see the test files).

The variable value only applies during the evaluation of the selected choices and is discarded afterward (the variable keeps its original value if there was one).

Expand Down
10 changes: 0 additions & 10 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@
from .ppp_comfyui import PromptPostProcessorComfyUINode

NODE_CLASS_MAPPINGS = {"ACBPromptPostProcessor": PromptPostProcessorComfyUINode}

NODE_DISPLAY_NAME_MAPPINGS = {"ACBPromptPostProcessor": "ACB Prompt Post Processor"}

MANIFEST = {
"name": "ACB Prompt Post Processor",
"version": PromptPostProcessorComfyUINode.VERSION,
"author": "ACB",
"project": "https://github.com/acorderob/sd-webui-prompt-postprocessor",
"description": "Node for processing prompts",
"license": "MIT",
}

__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
12 changes: 10 additions & 2 deletions install.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import os
import launch

requirements_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt")
launch.run_pip(f'install -r "{requirements_filename}"', "requirements for Prompt Post-Processor")

try:
from modules.launch_utils import requirements_met, run_pip # A1111

if not requirements_met(requirements_filename):
run_pip(f'install -r "{requirements_filename}"', "requirements for Prompt Post-Processor")
except ImportError:
import launch

launch.run_pip(f'install -r "{requirements_filename}"', "requirements for Prompt Post-Processor")
105 changes: 92 additions & 13 deletions ppp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
"""

@staticmethod
def get_version_from_pyproject() -> tuple:
def get_version_from_pyproject() -> str:
"""
Reads the version from the pyproject.toml file.
Returns:
tuple: A tuple containing the version numbers.
str: The version string.
"""
version_str = "0.0.0"
try:
Expand All @@ -40,7 +40,7 @@ def get_version_from_pyproject() -> tuple:
break
except Exception as e: # pylint: disable=broad-exception-caught
logging.getLogger().exception(e)
return tuple(map(int, version_str.split(".")))
return version_str

NAME = "Prompt Post-Processor"
VERSION = get_version_from_pyproject()
Expand Down Expand Up @@ -175,6 +175,9 @@ def isComfyUI(self) -> bool:
return self.env_info.get("app", "") == "comfyui"

def __init_sysvars(self):
"""
Initializes the system variables.
"""
self.system_variables = {}
sdchecks = {
"sd1": self.env_info.get("is_sd1", False),
Expand Down Expand Up @@ -384,6 +387,16 @@ def __cleanup(self, text: str) -> str:
return text

def __processprompts(self, prompt, negative_prompt):
"""
Process the prompt and negative prompt.
Args:
prompt (str): The prompt.
negative_prompt (str): The negative prompt.
Returns:
tuple: A tuple containing the processed prompt and negative prompt.
"""
self.user_variables = {}

# Process prompt
Expand Down Expand Up @@ -444,7 +457,7 @@ def process_prompt(
seed: int = 0,
):
"""
Process the prompt and negative prompt by moving content to the negative prompt, and cleaning up.
Initializes the random number generator and processes the prompt and negative prompt.
Args:
original_prompt (str): The original prompt.
Expand Down Expand Up @@ -486,6 +499,18 @@ def process_prompt(
return original_prompt, original_negative_prompt

def parse_prompt(self, prompt_description: str, prompt: str, parser: lark.Lark, raise_parsing_error: bool = False):
"""
Parses a prompt using the specified parser.
Args:
prompt_description (str): The description of the prompt.
prompt (str): The prompt to be parsed.
parser (lark.Lark): The parser to be used.
raise_parsing_error (bool): Whether to raise a parsing error.
Returns:
Tree: The parsed prompt.
"""
t1 = time.time()
try:
if self.debug_level == DEBUG_LEVEL.full:
Expand Down Expand Up @@ -605,6 +630,16 @@ def __visit(
return added_result

def __get_original_node_content(self, node: lark.Tree | lark.Token, default=None) -> str:
"""
Get the original content of a node.
Args:
node (Tree|Token): The node to get the content from.
default: The default value to return if the content is not found.
Returns:
str: The original content of the node.
"""
return (
node.meta.content
if hasattr(node, "meta") and node.meta is not None and not node.meta.empty
Expand Down Expand Up @@ -637,13 +672,35 @@ def __get_user_variable_value(self, name: str, evaluate=True, visit=False) -> st
return v

def __set_user_variable_value(self, name: str, value: str):
"""
Set the value of a user variable.
Args:
name (str): The name of the user variable.
value (str): The value to be set.
"""
self.__ppp.user_variables[name] = value

def __remove_user_variable(self, name: str):
"""
Remove a user variable.
Args:
name (str): The name of the user variable.
"""
if name in self.__ppp.user_variables:
del self.__ppp.user_variables[name]

def __debug_end(self, construct: str, start_result: str, duration: float, info=None):
"""
Log the end of a construct processing.
Args:
construct (str): The name of the construct.
start_result (str): The initial result.
duration (float): The duration of the processing.
info: Additional information to log.
"""
if self.__ppp.debug_level == DEBUG_LEVEL.full:
info = f"({info}) " if info is not None and info != "" else ""
output = self.result[len(start_result) :]
Expand Down Expand Up @@ -1208,6 +1265,8 @@ def __get_choices(
if options.get("prefix", None) is not None
else ""
)
if prefix != "" and re.match(r"\w", prefix[-1]):
prefix += " "
for i, c in enumerate(selected_choices):
t1 = time.time()
choice_content_obj = c.get("content", c.get("text", None))
Expand All @@ -1227,12 +1286,23 @@ def __get_choices(
if options.get("suffix", None) is not None
else ""
)
if suffix != "" and re.match(r"\w", suffix[0]):
suffix = " " + suffix
# remove comments
results = [re.sub(r"\s*#[^\n]*(?:\n|$)", "", r, flags=re.DOTALL) for r in selected_choices_text]
return prefix + separator.join(results) + suffix
return ""

def __convert_choices_options(self, options: Optional[lark.Tree]) -> dict:
"""
Convert the choices options to a dictionary.
Args:
options (Tree): The choices options tree.
Returns:
dict: The converted choices options.
"""
if options is None:
return None
the_options = {}
Expand Down Expand Up @@ -1263,6 +1333,15 @@ def __convert_choices_options(self, options: Optional[lark.Tree]) -> dict:
return the_options

def __convert_choice(self, choice: lark.Tree) -> dict:
"""
Convert the choice to a dictionary.
Args:
choice (Tree): The choice tree.
Returns:
dict: The converted choice.
"""
the_choice = {}
c_label_obj = choice.children[0]
the_choice["labels"] = (
Expand All @@ -1276,6 +1355,12 @@ def __convert_choice(self, choice: lark.Tree) -> dict:
return the_choice

def __check_wildcard_initialization(self, wildcard: PPPWildcard):
"""
Initializes a wildcard if it hasn't been yet.
Args:
wildcard (PPPWildcard): The wildcard to check.
"""
choice_values = wildcard.choices
options = wildcard.options
if choice_values is None:
Expand All @@ -1284,10 +1369,7 @@ def __check_wildcard_initialization(self, wildcard: PPPWildcard):
n = 0
# we check the first choice to see if it is actually options
if isinstance(wildcard.unprocessed_choices[0], dict):
if all(
k in ["sampler", "repeating", "count", "from", "to", "prefix", "suffix", "separator"]
for k in wildcard.unprocessed_choices[0].keys()
):
if self.__ppp.wildcard_obj.is_dict_choices_options(wildcard.unprocessed_choices[0]):
options = wildcard.unprocessed_choices[0]
prefix = options.get("prefix", None)
if prefix is not None and isinstance(prefix, str):
Expand Down Expand Up @@ -1331,7 +1413,7 @@ def __check_wildcard_initialization(self, wildcard: PPPWildcard):
# we process the choices
for cv in wildcard.unprocessed_choices[n:]:
if isinstance(cv, dict):
if all(k in ["labels", "weight", "if", "content", "text"] for k in cv.keys()):
if self.__ppp.wildcard_obj.is_dict_choice_options(cv):
theif = cv.get("if", None)
if theif is not None and isinstance(theif, str):
try:
Expand Down Expand Up @@ -1379,12 +1461,9 @@ def __check_wildcard_initialization(self, wildcard: PPPWildcard):
wildcard.choices = choice_values
t2 = time.time()
if self.__ppp.debug_level == DEBUG_LEVEL.full:
self.__ppp.logger.debug(
f"Processed choices for wildcard '{wildcard.key}' ({t2-t1:.3f} seconds)"
)
self.__ppp.logger.debug(f"Processed choices for wildcard '{wildcard.key}' ({t2-t1:.3f} seconds)")
return (options, choice_values)


def wildcard(self, tree: lark.Tree):
"""
Process a wildcard construct in the tree.
Expand Down
4 changes: 2 additions & 2 deletions ppp_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

class PPPLRUCache:

ProcessInput = Tuple[int, str, str]
ProcessResult = Tuple[str, str]
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):
self.cache = OrderedDict()
Expand Down
5 changes: 3 additions & 2 deletions ppp_comfyui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

class PromptPostProcessorComfyUINode:

VERSION = PromptPostProcessor.VERSION

logger = None

def __init__(self):
Expand All @@ -27,6 +25,7 @@ def __init__(self):
with open(grammar_filename, "r", encoding="utf-8") as file:
self.grammar_content = file.read()
self.wildcards_obj = PPPWildcards(lf.log)
self.logger.info(f"{PromptPostProcessor.NAME} {PromptPostProcessor.VERSION} initialized")

class SmartType(str):
def __ne__(self, other):
Expand Down Expand Up @@ -354,6 +353,8 @@ def IS_CHANGED(
cleanup_merge_attention,
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
new_run = { # everything except debug_level
"model": model,
"modelname": modelname,
Expand Down
Loading

0 comments on commit 3810320

Please sign in to comment.