* Renamed the extension to "Prompt Post-Processor".

* Improved logging.
* Fix compatibility with A1111 hiresfix.
This commit is contained in:
Antonio Cordero Balcazar
2023-12-02 11:12:00 +01:00
parent f59b3e51a5
commit 77a74a8088
5 changed files with 123 additions and 86 deletions
+16 -10
View File
@@ -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 <https://github.com/acorderob/sd-webui-sendtonegative> in the URL for extension's git repository text field
2. Paste <https://github.com/acorderob/sd-webui-prompt-postprocessor> 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)
+60 -39
View File
@@ -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
+9 -12
View File
@@ -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
+30 -17
View File
@@ -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,
),
)
+8 -8
View File
@@ -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, <!!i0!!>, bad quality<!!i1!!>, 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):