From 77a74a80884af78065d3cae99f9ff932be66f2b1 Mon Sep 17 00:00:00 2001 From: Antonio Cordero Balcazar Date: Fri, 17 Nov 2023 17:03:20 +0100 Subject: [PATCH] * Renamed the extension to "Prompt Post-Processor". * Improved logging. * Fix compatibility with A1111 hiresfix. --- README.md | 26 ++++--- sendtonegative.py => ppp.py | 99 +++++++++++++++---------- stnlogging.py => ppp_logging.py | 21 +++--- scripts/{extension.py => ppp_script.py} | 47 +++++++----- tests/tests.py | 16 ++-- 5 files changed, 123 insertions(+), 86 deletions(-) rename sendtonegative.py => ppp.py (76%) rename stnlogging.py => ppp_logging.py (64%) rename scripts/{extension.py => ppp_script.py} (61%) diff --git a/README.md b/README.md index a85412d..e659dc9 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ -# Send to Negative for Stable Diffusion WebUI +# Prompt Postprocessor for Stable Diffusion WebUI -Extension for the [AUTOMATIC1111 Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) or compatible UIs. +(formerly known as "sd-webui-sendtonegative") + +Extension for [AUTOMATIC1111 Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui). Compatible with [SD.Next](https://github.com/vladmandic/automatic). ## Purpose -This extension allows the tagging of parts of the prompt and moves them to the +The purpose of this extension is to process the prompt after other extensions have possibly modified it. + +Currently this extension allows the tagging of parts of the prompt and moves them to the negative prompt. This allows useful tricks when using a wildcard extension since you can add negative content from choices made in the positive prompt. Note: The extension must be loaded after the installed wildcards extension. Extensions -load by their folder in alphanumeric order. +load by their folder name in alphanumeric order. With the ["Dynamic Prompts" extension](https://github.com/adieyal/sd-dynamic-prompts) this happens by default due to default folder names for both extensions. But if -this is not the case, you can just rename the extension folder so the ordering +this is not the case, you can just rename this extension's folder so the ordering works out. With the ["AUTOMATIC1111 Wildcards" extension](https://github.com/AUTOMATIC1111/stable-diffusion-webui-wildcards) -you will have to rename one of the folders, so that it loads before than "Send to Negative". +you will have to rename one of the folders, so that it loads before than this extension. When in doubt, just rename this extension's folder with a "z" in front (for example) so that it is the last one to load, or manually set such folder name when installing it. @@ -29,17 +33,19 @@ Note: it only recognizes regular A1111 prompt formats. So: In SD.Next that means only the A1111 or Full parsers. -It does not build AND/BREAK separations into the negative prompt. +It does not build equivalent AND/BREAK separations into the negative prompt. ## Installation 1. Go to Extensions > Install from URL -2. Paste in the URL for extension's git repository text field +2. Paste in the URL for extension's git repository text field 3. Click the Install button 4. Restart the webui ## Usage +### Sending content to the negative prompt + The format of the tags is like this: ```text @@ -60,7 +66,7 @@ Where position can be: If the insertion point is not found it inserts at the start. -## Example +#### Example You have a wildcard for hair colors (\_\_haircolors\_\_) with one being strawberry blonde, but you don't want strawberries. So in that option you add a @@ -87,7 +93,7 @@ Try to clean-up the prompt after processing: by default cleans up the positive p ## Notes -The content of the negative tags is not processed and is copied as is to the negative prompt. Other modifiers around the tags are processed in the following way. +The content of the negative tags is not processed and is copied as-is to the negative prompt. Other modifiers around the tags are processed in the following way. ### Attention modifiers (weights) diff --git a/sendtonegative.py b/ppp.py similarity index 76% rename from sendtonegative.py rename to ppp.py index 75b9233..1d0e8f0 100644 --- a/sendtonegative.py +++ b/ppp.py @@ -4,9 +4,9 @@ import math import lark -class SendToNegative: # pylint: disable=too-few-public-methods - NAME = "Send to Negative" - VERSION = "2.1.4" +class PromptPostProcessor: # pylint: disable=too-few-public-methods + NAME = "Prompt Post-Processor" + VERSION = "2.1.5" DEFAULT_SEPARATOR = ", " @@ -34,26 +34,28 @@ class SendToNegative: # pylint: disable=too-few-public-methods iN - tags the position of insertion point N. Used only in the negative prompt and does not accept content. N can be 0 to 9. """ + self.__opts = opts self.__logger = log + self.__debug = getattr(self.__opts, "ppp_debug", False) if opts is not None and getattr(opts, "prompt_attention", "") == "Compel parser": self.__logger.warning("Compel parser is not supported!") self.__ignore_repeats = ( - ignore_repeats if ignore_repeats is not None else getattr(opts, "stn_ignorerepeats", True) + ignore_repeats if ignore_repeats is not None else getattr(opts, "ppp_stn_ignorerepeats", True) ) self.__join_attention = ( join_attention if join_attention is not None - else getattr(opts, "stn_joinattention", True) + else getattr(opts, "ppp_stn_joinattention", True) if opts is not None else True ) self.__cleanup = ( - cleanup if cleanup is not None else getattr(opts, "stn_cleanup", True) if opts is not None else True + cleanup if cleanup is not None else getattr(opts, "ppp_cleanup", True) if opts is not None else True ) self.__separator = ( separator if separator is not None - else getattr(opts, "stn_separator", self.DEFAULT_SEPARATOR) + else getattr(opts, "ppp_separator", self.DEFAULT_SEPARATOR) if opts is not None else self.DEFAULT_SEPARATOR ) @@ -81,9 +83,10 @@ class SendToNegative: # pylint: disable=too-few-public-methods ) class ReadTree(lark.visitors.Interpreter): - def __init__(self, logger, ignorerepeats, joinattention, prompt, add_at): + def __init__(self, logger, debug, ignorerepeats, joinattention, prompt, add_at): super().__init__() self.__logger = logger + self.__debug = debug self.__ignore_repeats = ignorerepeats self.__join_attention = joinattention self.__prompt = prompt @@ -112,16 +115,18 @@ class SendToNegative: # pylint: disable=too-few-public-methods pos = int(pos) # self.__shell.append(self.AccumulatedShell("sc", tree.meta.start_pos, pos)) if before is not None and hasattr(before, "data"): - self.__logger.debug( - f"Shell scheduled before at {[before.meta.start_pos,before.meta.end_pos] if hasattr(before,'meta') and not before.meta.empty else '?'} : {pos}" - ) + if self.__debug: + self.__logger.info( + f"Shell scheduled before at {[before.meta.start_pos,before.meta.end_pos] if hasattr(before,'meta') and not before.meta.empty else '?'} with position {pos}" + ) self.__shell.append(self.AccumulatedShell("scb", pos, None)) self.visit(before) self.__shell.pop() if hasattr(after, "data"): - self.__logger.debug( - f"Shell scheduled after at {[after.meta.start_pos,after.meta.end_pos] if hasattr(after,'meta') and not after.meta.empty else '?'} : {pos}" - ) + if self.__debug: + self.__logger.info( + f"Shell scheduled after at {[after.meta.start_pos,after.meta.end_pos] if hasattr(after,'meta') and not after.meta.empty else '?'} with position {pos}" + ) self.__shell.append(self.AccumulatedShell("sca", pos, None)) self.visit(after) self.__shell.pop() @@ -130,9 +135,10 @@ class SendToNegative: # pylint: disable=too-few-public-methods def alternate(self, tree): # self.__shell.append(self.AccumulatedShell("al", tree.meta.start_pos, len(tree.children))) for i, opt in enumerate(tree.children): - self.__logger.debug( - f"Shell alternate at {[opt.meta.start_pos,opt.meta.end_pos] if hasattr(opt,'meta') and not opt.meta.empty else '?'} : {i+1}" - ) + if self.__debug: + self.__logger.info( + f"Shell alternate at {[opt.meta.start_pos,opt.meta.end_pos] if hasattr(opt,'meta') and not opt.meta.empty else '?'} option {i+1}" + ) if hasattr(opt, "data"): self.__shell.append(self.AccumulatedShell("alo", i + 1, len(tree.children))) self.visit(opt) @@ -142,18 +148,20 @@ class SendToNegative: # pylint: disable=too-few-public-methods def emphasized(self, tree): numpar = tree.children[-1] weight = self.__get_numpar_value(numpar) if numpar is not None else 1.1 - self.__logger.debug( - f"Shell attention at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'}: {weight}" - ) + if self.__debug: + self.__logger.info( + f"Shell attention at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'} with weight {weight}" + ) self.__shell.append(self.AccumulatedShell("at", weight, None)) self.visit_children(tree) self.__shell.pop() def deemphasized(self, tree): weight = 0.9 - self.__logger.debug( - f"Shell attention at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'}: {weight}" - ) + if self.__debug: + self.__logger.info( + f"Shell attention at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'} with weight {weight}" + ) self.__shell.append(self.AccumulatedShell("at", weight, None)) self.visit_children(tree) self.__shell.pop() @@ -172,9 +180,10 @@ class SendToNegative: # pylint: disable=too-few-public-methods self.__negtags.append( self.NegTag(tree.meta.start_pos, tree.meta.end_pos, content, parameters, self.__shell.copy()) ) - self.__logger.debug( - f"Negative tag at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'}: {parameters}: {content.encode('unicode_escape').decode('utf-8')}" - ) + if self.__debug: + self.__logger.info( + f"Negative tag at {[tree.meta.start_pos,tree.meta.end_pos] if hasattr(tree,'meta') and not tree.meta.empty else '?'}: {parameters or 'with no parameters :'} {content.encode('unicode_escape').decode('utf-8')}" + ) def start(self, tree): self.visit_children(tree) @@ -222,9 +231,10 @@ class SendToNegative: # pylint: disable=too-few-public-methods if content not in self.__already_processed: if self.__ignore_repeats: self.__already_processed.append(content) - self.__logger.debug( - f"Adding content at position {position}: {content.encode('unicode_escape').decode('utf-8')}" - ) + if self.__debug: + self.__logger.info( + f"Adding content at position {position}: {content.encode('unicode_escape').decode('utf-8')}" + ) if position == "e": self.add_at["end"].append(content) elif position.startswith("p"): @@ -246,16 +256,21 @@ class SendToNegative: # pylint: disable=too-few-public-methods try: prompt = original_prompt negative_prompt = original_negative_prompt - self.__logger.debug(f"Input prompt: {prompt.encode('unicode_escape').decode('utf-8')}") - self.__logger.debug(f"Input negative_prompt: {negative_prompt.encode('unicode_escape').decode('utf-8')}") + self.__debug = getattr(self.__opts, "ppp_debug", False) + if self.__debug: + self.__logger.info(f"Input prompt: {prompt.encode('unicode_escape').decode('utf-8')}") + self.__logger.info(f"Input negative_prompt: {negative_prompt.encode('unicode_escape').decode('utf-8')}") prompt, add_at = self.__find_tags(prompt) negative_prompt = self.__add_to_insertion_points(negative_prompt, add_at["insertion_point"]) if len(add_at["start"]) > 0: negative_prompt = self.__add_to_start(negative_prompt, add_at["start"]) if len(add_at["end"]) > 0: negative_prompt = self.__add_to_end(negative_prompt, add_at["end"]) - self.__logger.debug(f"Output prompt: {prompt.encode('unicode_escape').decode('utf-8')}") - self.__logger.debug(f"Output negative_prompt: {negative_prompt.encode('unicode_escape').decode('utf-8')}") + if self.__debug: + self.__logger.info(f"Output prompt: {prompt.encode('unicode_escape').decode('utf-8')}") + self.__logger.info( + f"Output negative_prompt: {negative_prompt.encode('unicode_escape').decode('utf-8')}" + ) return prompt, negative_prompt except Exception as e: # pylint: disable=broad-exception-caught self.__logger.exception(e) @@ -264,29 +279,35 @@ class SendToNegative: # pylint: disable=too-few-public-methods def __find_tags(self, prompt): add_at = {"start": [], "insertion_point": [[] for x in range(10)], "end": []} tree = self.__schedule_parser.parse(prompt) - self.__logger.debug(f"Initial tree:\n{tree.pretty()}") + # if self.__debug: + # self.__logger.info(f"Initial tree:\n{tree.pretty()}") - readtree = self.ReadTree(self.__logger, self.__ignore_repeats, self.__join_attention, prompt, add_at) + readtree = self.ReadTree( + self.__logger, self.__debug, self.__ignore_repeats, self.__join_attention, prompt, add_at + ) readtree.visit(tree) for r in readtree.remove[::-1]: prompt = prompt[: r[0]] + prompt[r[1] :] if self.__cleanup: + if self.__debug: + self.__logger.info(f"Prompt before cleanup: {prompt.encode('unicode_escape').decode('utf-8')}") prompt = re.sub(r"\((?::[+-]?[\d\.]+)?\)", "", prompt) # clean up empty attention prompt = re.sub(r"\[\]", "", prompt) # clean up empty attention prompt = re.sub(r"\[:?:[+-]?[\d\.]+\]", "", prompt) # clean up empty scheduling prompt = re.sub(r"\[\|+\]", "", prompt) # clean up empty alternation - # clean up whitespace and extra separators + prompt = re.sub(r"[ ]{2,}", " ", prompt) # collapse spaces + # clean up extra separators prompt = ( - prompt.replace(" ", " ") - .replace(self.__separator + self.__separator, self.__separator) + prompt.replace(self.__separator + self.__separator, self.__separator) .replace(" " + self.__separator, self.__separator) .removeprefix(self.__separator) .removesuffix(self.__separator) .strip() ) add_at = readtree.add_at - self.__logger.debug(f"New negative additions: {add_at}") + if self.__debug: + self.__logger.info(f"New negative additions: {add_at}") return prompt, add_at diff --git a/stnlogging.py b/ppp_logging.py similarity index 64% rename from stnlogging.py rename to ppp_logging.py index 1ed8844..0a6f515 100644 --- a/stnlogging.py +++ b/ppp_logging.py @@ -3,7 +3,7 @@ import copy import logging -class SendToNegativeLogFactory: +class PromptPostProcessorLogFactory: class ColoredFormatter(logging.Formatter): COLORS = { "DEBUG": "\033[0;36m", # CYAN @@ -22,19 +22,16 @@ class SendToNegativeLogFactory: return super().format(colored_record) def __init__(self): - logsd = logging.getLogger("sd") - stnlog = logging.getLogger("SendToNegative") - stnlog.setLevel(logging.INFO) - stnlog.handlers = logsd.handlers - if not stnlog.handlers: + ppplog = logging.getLogger("PromptPostProcessor") + ppplog.propagate = False + if not ppplog.handlers: handler = logging.StreamHandler(sys.stdout) handler.setFormatter(self.ColoredFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) - stnlog.addHandler(handler) - self.log = stnlog - else: - self.log = SendToNegativeLogCustomAdapter(stnlog) + ppplog.addHandler(handler) + ppplog.setLevel(logging.INFO) + self.log = PromptPostProcessorLogCustomAdapter(ppplog) -class SendToNegativeLogCustomAdapter(logging.LoggerAdapter): +class PromptPostProcessorLogCustomAdapter(logging.LoggerAdapter): def process(self, msg, kwargs): - return f"[SendToNegative] {msg}", kwargs + return f"[PromptPostProcessor] {msg}", kwargs diff --git a/scripts/extension.py b/scripts/ppp_script.py similarity index 61% rename from scripts/extension.py rename to scripts/ppp_script.py index 54d49be..a4c6f3b 100644 --- a/scripts/extension.py +++ b/scripts/ppp_script.py @@ -12,49 +12,54 @@ sys.path.insert(1, os.path.join(sys.path[0], "..")) from modules import scripts, shared, script_callbacks from modules.processing import StableDiffusionProcessing from modules.shared import opts -from sendtonegative import SendToNegative -from stnlogging import SendToNegativeLogFactory +from ppp import PromptPostProcessor +from ppp_logging import PromptPostProcessorLogFactory -class SendToNegativeScript(scripts.Script): +class PromptPostProcessorScript(scripts.Script): def __init__(self): if not hasattr(self, "callbacks_added"): - lf = SendToNegativeLogFactory() - self.__logstn = lf.log + lf = PromptPostProcessorLogFactory() + self.__logppp = lf.log script_callbacks.on_ui_settings(self.__on_ui_settings) self.callbacks_added = True def title(self): - return SendToNegative.NAME + return PromptPostProcessor.NAME def show(self, is_img2img): return scripts.AlwaysVisible def process(self, p: StableDiffusionProcessing, *args, **kwargs): - stn = SendToNegative(self.__logstn, opts=opts) + ppp = PromptPostProcessor(self.__logppp, opts=opts) for i in range(len(p.all_prompts)): # pylint: disable=consider-using-enumerate - p.all_prompts[i], p.all_negative_prompts[i] = stn.process_prompt( + p.all_prompts[i], p.all_negative_prompts[i] = ppp.process_prompt( p.all_prompts[i], p.all_negative_prompts[i] ) # make it compatible with A1111 hires fix - if hasattr(p, "all_hr_prompts") and hasattr(p, "all_hr_negative_prompts"): + if ( + hasattr(p, "all_hr_prompts") + and p.all_hr_prompts is not None + and hasattr(p, "all_hr_negative_prompts") + and p.all_hr_negative_prompts is not None + ): for i in range(len(p.all_hr_prompts)): # pylint: disable=consider-using-enumerate - p.all_hr_prompts[i], p.all_hr_negative_prompts[i] = stn.process_prompt( + p.all_hr_prompts[i], p.all_hr_negative_prompts[i] = ppp.process_prompt( p.all_hr_prompts[i], p.all_hr_negative_prompts[i] ) def __on_ui_settings(self): - section = ("send-to-negative", SendToNegative.NAME) + section = ("prompt-post-processor", PromptPostProcessor.NAME) shared.opts.add_option( - key="stn_separator", + key="ppp_separator", info=shared.OptionInfo( - SendToNegative.DEFAULT_SEPARATOR, + PromptPostProcessor.DEFAULT_SEPARATOR, label="Separator used when adding to the negative prompt", section=section, ), ) shared.opts.add_option( - key="stn_ignorerepeats", + key="ppp_stn_ignorerepeats", info=shared.OptionInfo( True, label="Ignore tags with repeated content", @@ -62,7 +67,7 @@ class SendToNegativeScript(scripts.Script): ), ) shared.opts.add_option( - key="stn_joinattention", + key="ppp_stn_joinattention", info=shared.OptionInfo( True, label="Join attention modifiers (weights) when possible", @@ -70,10 +75,18 @@ class SendToNegativeScript(scripts.Script): ), ) shared.opts.add_option( - key="stn_cleanup", + key="ppp_cleanup", info=shared.OptionInfo( True, - label="Try to clean-up the prompt after processing (removes extra spaces or the configured separator)", + label="Try to clean-up the prompt after processing (removes extra spaces, empty attention, or the configured separator)", + section=section, + ), + ) + shared.opts.add_option( + key="ppp_debug", + info=shared.OptionInfo( + False, + label="Debug", section=section, ), ) diff --git a/tests/tests.py b/tests/tests.py index 75c423a..52ea092 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -5,16 +5,16 @@ import os sys.path.insert(1, os.path.join(sys.path[0], "..")) -from sendtonegative import SendToNegative # pylint: disable=import-error -from stnlogging import SendToNegativeLogFactory +from ppp import PromptPostProcessor # pylint: disable=import-error +from ppp_logging import PromptPostProcessorLogFactory -class TestSendToNegative(unittest.TestCase): +class TestPromptPostProcessor(unittest.TestCase): def setUp(self): - lf = SendToNegativeLogFactory() + lf = PromptPostProcessorLogFactory() self.__log = lf.log self.__log.setLevel(logging.DEBUG) - self.defstn = SendToNegative(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=True) + self.defppp = PromptPostProcessor(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=True) def process( self, @@ -22,9 +22,9 @@ class TestSendToNegative(unittest.TestCase): negative_prompt, expected_prompt, expected_negative_prompt, - stn=None, + ppp=None, ): - the_obj = self.defstn if stn is None else stn + the_obj = self.defppp if ppp is None else ppp result_prompt, result_negative_prompt = the_obj.process_prompt(prompt, negative_prompt) self.assertEqual(result_prompt, expected_prompt, f"Prompt should be '{expected_prompt}'") self.assertEqual( @@ -119,7 +119,7 @@ class TestSendToNegative(unittest.TestCase): "normal quality, , bad quality, worse quality", " (), flowers , , ", "red, (pink), normal quality, yellow, bad quality, green, worse quality, purple, blue", - SendToNegative(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=False), + PromptPostProcessor(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=False), ) def test_inside_attention1(self):