Compare commits

..
12 Commits
Author SHA1 Message Date
Antonio Cordero Balcazar 77a74a8088 * Renamed the extension to "Prompt Post-Processor".
* Improved logging.
* Fix compatibility with A1111 hiresfix.
2023-12-02 11:12:00 +01:00
Antonio Cordero Balcazar f59b3e51a5 Fix processing with A1111 hires fix. (#4)
# Pull Request

## Description

Fixes processing A1111 hr prompts.

Fixes #3 

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Test Configuration**:

- A1111 v1.6
- SD.Next

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
- [x] I have checked my code and corrected any misspellings
2023-10-14 14:29:26 +02:00
Antonio Cordero Balcazar 496117e004 * Fix processing with A1111 hires fix.
* Remove version from title.
2023-10-14 13:51:09 +02:00
Antonio Cordero Balcazar 5a87292a18 * Fix processing of weights/steps with spaces around. 2023-08-24 18:22:40 +02:00
Antonio Cordero Balcazar a0d22862ca Fix weight calculation. 2023-08-20 23:49:09 +02:00
Antonio Cordero Balcazar 72d3866d65 Fix [Issue] 'Options' object has no attribute 'prompt_attention' #2 2023-08-18 10:40:50 +02:00
Antonio Cordero Balcazar ac4a8e79f5 * Forgot to commit file. 2023-08-15 18:40:16 +02:00
Antonio Cordero Balcazar 6224adde0f * Logging and documentation improvements 2023-08-15 18:01:39 +02:00
Antonio Cordero Balcazar c8a7b6c815 * New parser, which now supports the attention, alternation and scheduling modifiers and replicates them in the negative prompt when necessary. Implements issue #1.
* Removed configuration of tag format since it cannot be done with the new parser.
2023-08-13 14:07:41 +02:00
Antonio Cordero Balcazar 7f36587bfd update gitignore and readme 2023-05-17 20:33:10 +02:00
Antonio Cordero Balcazar 46d5100e7e * added pull request template and updated issues templates
* updated readme
2023-05-17 19:14:14 +02:00
Antonio Cordero Balcazar d31736187b Updated readme. 2023-05-16 10:29:03 +02:00
11 changed files with 661 additions and 302 deletions
+33 -12
View File
@@ -6,20 +6,41 @@ assignees: acorderob
---
**Describe the bug**
A clear and concise description of what the issue is.
## Prerequisites
**To Reproduce**
Steps to reproduce the behavior
Please answer the following questions for yourself before submitting an issue. **YOU MAY DELETE THE PREREQUISITES SECTION.**
**Expected behavior**
A clear and concise description of what you expected to happen.
- [ ] I am running the latest version
- [ ] I checked the documentation and found no answer
- [ ] I checked to make sure that this issue has not already been filed
- [ ] I'm reporting the issue to the correct repository (for multi-repository projects)
**Screenshots**
If applicable, add screenshots to help explain your problem.
## Current Behavior
**Commit where the problem happens**
Installed commit or version tag
What is the current behavior?
**Additional information**
Add any other context about the problem here.
## Expected Behavior
Please describe the behavior you are expecting
## Failure Information (for bugs)
Please help provide information about the failure if this is a bug. If it is not a bug, please remove the rest of this template.
## Steps to Reproduce
Please provide detailed steps for reproducing the issue.
1. step 1
2. step 2
3. you get it...
## Context
Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
- WebUI used and version:
## Failure Logs
Please include any relevant log snippets or files here.
+12 -7
View File
@@ -6,14 +6,19 @@ assignees: acorderob
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
## Prerequisites
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
Please answer the following questions for yourself before submitting an issue. **YOU MAY DELETE THE PREREQUISITES SECTION.**
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
- [ ] I am running the latest version
- [ ] I checked the documentation and found no answer
- [ ] I checked to make sure that this issue has not already been filed
- [ ] I'm reporting the issue to the correct repository (for multi-repository projects)
## Description
A clear and concise description of the feature that you want.
## Additional context
**Additional context**
Add any other context or screenshots about the feature request here.
+39
View File
@@ -0,0 +1,39 @@
# Pull Request
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
**Test Configuration**:
- WebUI used and version:
## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
- [ ] I have checked my code and corrected any misspellings
+4
View File
@@ -1 +1,5 @@
**/__pycache__
.vscode/**/*
!.vscode/settings.json
!.vscode/launch.json
+1 -2
View File
@@ -1,4 +1,5 @@
{
"python.analysis.extraPaths": ["../.."],
"python.testing.unittestArgs": [
"-v",
"-s",
@@ -9,8 +10,6 @@
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.analysis.typeCheckingMode": "basic",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"black-formatter.args": [
"--line-length=120"
]
+69 -12
View File
@@ -1,22 +1,51 @@
# 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 wildcard extension.
Note: The extension must be loaded after the installed wildcards extension. Extensions
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 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.
Note: it only recognizes regular A1111 prompt formats. So:
* Attention: \[prompt\] (prompt) (prompt:weight)
* Alternation: \[prompt1|prompt2|...\]
* Scheduling: \[prompt1:prompt2:step\]
In SD.Next that means only the A1111 or Full parsers.
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-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
@@ -37,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
@@ -54,11 +83,39 @@ that part to the negative prompt.
## Configuration
The extension settings allow you to change the format of the tag in case there
is some incompatibility with another extension.
Separator used when adding to the negative prompt: You can specify the separator used when adding to the negative prompt (by default it's ", ").
You can also specify the separator added to the negative prompt which by
default is ", ".
Ignore tags with repeated content: by default it ignores repeated content to avoid repetitions in the negative prompt.
By default it ignores repeated content and also tries to clean up the prompt
after removing the tags, but these can also be changed in the settings.
Join attention modifiers (weights) when possible: by default it joins attention modifiers when possible (joins into one, multipliying their values).
Try to clean-up the prompt after processing: by default cleans up the positive prompt after processing, removing extra spaces and separators.
## 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.
### Attention modifiers (weights)
They will be translated to the negative prompt. For example:
* `(red<!square!>:1.5)` will end up as `(square:1.5)` in the negative prompt
* `(red[<!square!>]:1.5)` will end up as `(square:1.35)` in the negative prompt (weight=1.5*0.9)
* However `(red<![square]!>:1.5)` will end up as `([square]:1.5)` in the negative prompt. The content of the negative tag is copied as is, and not joined with the surrounding modifier.
### Prompt editing constructs (alternation and scheduling)
Negative tags inside such constructs will copy the construct to the negative prompt, but separating its elements. For example:
* Alternation: `[red<!square!>|blue<!circle!>]` will end up as `[square|], [|circle]` in the negative prompt, instead of `[square|circle]`
* Scheduling: `[red<!square!>:blue<!circle!>:0.5]` will end up as `[square::0.5], [:circle:0.5]` instead of `[square:circle:0.5]`
This should still work as intended, and the only negative point i see is the unnecessary separators.
## License
MIT
## Contact
If you have any questions or concerns, please leave an issue, or start a thread in the discussions.
+353
View File
@@ -0,0 +1,353 @@
from collections import namedtuple
import re
import math
import lark
class PromptPostProcessor: # pylint: disable=too-few-public-methods
NAME = "Prompt Post-Processor"
VERSION = "2.1.5"
DEFAULT_SEPARATOR = ", "
def __init__(
self,
log,
separator=None,
ignore_repeats=None,
join_attention=None,
cleanup=None,
opts=None,
):
"""
Format for the tag:
<!content!>
<!!x!content!>
with x being:
s - content is added at the start of the negative prompt. This is the default if no parameter exists.
e - content is added at the end of the negative prompt.
pN - content is added where the insertion point N is in the negative prompt or at the start if it does not exist. N can be 0 to 9.
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, "ppp_stn_ignorerepeats", True)
)
self.__join_attention = (
join_attention
if join_attention is not None
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, "ppp_cleanup", True) if opts is not None else True
)
self.__separator = (
separator
if separator is not None
else getattr(opts, "ppp_separator", self.DEFAULT_SEPARATOR)
if opts is not None
else self.DEFAULT_SEPARATOR
)
self.__insertion_point_tags = [f"<!!i{x}!!>" for x in range(10)]
# Process with lark (debug with https://www.lark-parser.org/ide/)
self.__schedule_parser = lark.Lark(
r"""
start: (prompt | /[\][():|<>!]/+)*
?prompt: (emphasized | deemphasized | scheduled | alternate | modeltag | negtag | plain)*
?nonegprompt: (emphasized | deemphasized | scheduled | alternate | modeltag | plain)*
emphasized: "(" prompt [":" numpar] ")"
deemphasized: "[" prompt "]"
scheduled: "[" [prompt ":"] prompt ":" numpar "]"
alternate: "[" alternateoption ("|" alternateoption)+ "]"
alternateoption: prompt
negtag: "<!" [negtagparameters] nonegprompt "!>"
negtagparameters: "!" /s|e|[ip]\d/ "!"
modeltag: "<" /(?!!)[^>]+/ ">"
numpar: WHITESPACE* NUMBER WHITESPACE*
WHITESPACE: /\s+/
?plain: /([^\\[\]():|<>!]|\\.)+/s
%import common.SIGNED_NUMBER -> NUMBER
""",
propagate_positions=True,
)
class ReadTree(lark.visitors.Interpreter):
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
self.AccumulatedShell = namedtuple("AccumulatedShell", ["type", "info1", "info2"])
AccumulatedShell = self.AccumulatedShell
self.__shell: list[AccumulatedShell] = []
self.NegTag = namedtuple("NegTag", ["start", "end", "content", "parameters", "shell"])
NegTag = self.NegTag
self.__negtags: list[NegTag] = []
self.__already_processed = []
self.add_at = add_at
self.remove = []
def __get_numpar_value(self, numpar):
return float(next(x for x in numpar.children if x.type == "NUMBER").value)
def scheduled(self, tree):
if len(tree.children) > 2: # before & after
before = tree.children[0]
else:
before = None
after = tree.children[-2]
numpar = tree.children[-1]
pos = self.__get_numpar_value(numpar)
if pos >= 1:
pos = int(pos)
# self.__shell.append(self.AccumulatedShell("sc", tree.meta.start_pos, pos))
if before is not None and hasattr(before, "data"):
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"):
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()
# self.__shell.pop()
def alternate(self, tree):
# self.__shell.append(self.AccumulatedShell("al", tree.meta.start_pos, len(tree.children)))
for i, opt in enumerate(tree.children):
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)
self.__shell.pop()
# self.__shell.pop()
def emphasized(self, tree):
numpar = tree.children[-1]
weight = self.__get_numpar_value(numpar) if numpar is not None else 1.1
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
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 negtag(self, tree):
negtagparameters = tree.children[0]
parameters = negtagparameters.children[0].value if negtagparameters is not None else ""
rest = []
for x in tree.children[1::]:
rest.append(
self.__prompt[x.meta.start_pos : x.meta.end_pos]
if hasattr(x, "meta") and not x.meta.empty
else x.value
)
content = "".join(rest)
self.__negtags.append(
self.NegTag(tree.meta.start_pos, tree.meta.end_pos, content, parameters, self.__shell.copy())
)
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)
# process the found negtags
for nt in self.__negtags:
if self.__join_attention:
# join consecutive attention elements
for i in range(len(nt.shell) - 1, 0, -1):
if nt.shell[i].type == "at" and nt.shell[i - 1].type == "at":
nt.shell[i - 1] = self.AccumulatedShell(
"at",
math.floor(100 * nt.shell[i - 1].info1 * nt.shell[i].info1)
/ 100, # we limit to two decimals
None,
)
nt.shell.pop(i)
start = ""
end = ""
for s in nt.shell:
match s.type:
case "at":
if s.info1 == 0.9:
start += "["
end = "]" + end
elif s.info1 == 1.1:
start += "("
end = ")" + end
else:
start += "("
end = f":{s.info1})" + end
# case "sc":
case "scb":
start += "["
end = f"::{s.info1}]" + end
case "sca":
start += "["
end = f":{s.info1}]" + end
# case "al":
case "alo":
start += "[" + ("|" * int(s.info1 - 1))
end = ("|" * int(s.info2 - s.info1)) + "]" + end
content = start + nt.content + end
position = nt.parameters or "s"
if len(content) > 0:
if content not in self.__already_processed:
if self.__ignore_repeats:
self.__already_processed.append(content)
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"):
n = int(position[1])
self.add_at["insertion_point"][n].append(content)
else: # position == "s" or invalid
self.add_at["start"].append(content)
else:
self.__logger.warning(
f"Ignoring repeated content: {content.encode('unicode_escape').decode('utf-8')}"
)
# remove from prompt
self.remove.append([nt.start, nt.end])
def process_prompt(self, original_prompt, original_negative_prompt):
"""
Extract from the prompt the tagged parts and add them to the negative prompt
"""
try:
prompt = original_prompt
negative_prompt = original_negative_prompt
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"])
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)
return original_prompt, original_negative_prompt
def __find_tags(self, prompt):
add_at = {"start": [], "insertion_point": [[] for x in range(10)], "end": []}
tree = self.__schedule_parser.parse(prompt)
# if self.__debug:
# self.__logger.info(f"Initial tree:\n{tree.pretty()}")
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
prompt = re.sub(r"[ ]{2,}", " ", prompt) # collapse spaces
# clean up extra separators
prompt = (
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
if self.__debug:
self.__logger.info(f"New negative additions: {add_at}")
return prompt, add_at
def __add_to_insertion_points(self, negative_prompt, add_at_insertion_point):
for n in range(10):
ipp = negative_prompt.find(self.__insertion_point_tags[n])
if ipp >= 0:
ipl = len(self.__insertion_point_tags[n])
if negative_prompt[ipp - len(self.__separator) : ipp] == self.__separator:
ipp -= len(self.__separator) # adjust for existing start separator
ipl += len(self.__separator)
add_at_insertion_point[n].insert(0, negative_prompt[:ipp])
if negative_prompt[ipp + ipl : ipp + ipl + len(self.__separator)] == self.__separator:
ipl += len(self.__separator) # adjust for existing end separator
endPart = negative_prompt[ipp + ipl :]
if len(endPart) > 0:
add_at_insertion_point[n].append(endPart)
negative_prompt = self.__separator.join(add_at_insertion_point[n])
else:
ipp = 0
if negative_prompt.startswith(self.__separator):
ipp = len(self.__separator)
add_at_insertion_point[n].append(negative_prompt[ipp:])
negative_prompt = self.__separator.join(add_at_insertion_point[n])
return negative_prompt
def __add_to_start(self, negative_prompt, add_at_start):
if len(negative_prompt) > 0:
ipp = 0
if negative_prompt.startswith(self.__separator):
ipp = len(self.__separator) # adjust for existing end separator
add_at_start.append(negative_prompt[ipp:])
negative_prompt = self.__separator.join(add_at_start)
return negative_prompt
def __add_to_end(self, negative_prompt, add_at_end):
if len(negative_prompt) > 0:
ipl = len(negative_prompt)
if negative_prompt.endswith(self.__separator):
ipl -= len(self.__separator) # adjust for existing start separator
add_at_end.insert(0, negative_prompt[:ipl])
negative_prompt = self.__separator.join(add_at_end)
return negative_prompt
+37
View File
@@ -0,0 +1,37 @@
import sys
import copy
import logging
class PromptPostProcessorLogFactory:
class ColoredFormatter(logging.Formatter):
COLORS = {
"DEBUG": "\033[0;36m", # CYAN
"INFO": "\033[0;32m", # GREEN
"WARNING": "\033[0;33m", # YELLOW
"ERROR": "\033[0;31m", # RED
"CRITICAL": "\033[0;37;41m", # WHITE ON RED
"RESET": "\033[0m", # RESET COLOR
}
def format(self, record):
colored_record = copy.copy(record)
levelname = colored_record.levelname
seq = self.COLORS.get(levelname, self.COLORS["RESET"])
colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
return super().format(colored_record)
def __init__(self):
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"))
ppplog.addHandler(handler)
ppplog.setLevel(logging.INFO)
self.log = PromptPostProcessorLogCustomAdapter(ppplog)
class PromptPostProcessorLogCustomAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
return f"[PromptPostProcessor] {msg}", kwargs
+41 -43
View File
@@ -12,72 +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 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 = PromptPostProcessorLogFactory()
self.__logppp = lf.log
script_callbacks.on_ui_settings(self.__on_ui_settings)
self.callbacks_added = True
def title(self):
return f"{SendToNegative.NAME} v{SendToNegative.VERSION}"
return PromptPostProcessor.NAME
def show(self, is_img2img):
return scripts.AlwaysVisible
def process(self, p: StableDiffusionProcessing, *args, **kwargs):
stn = SendToNegative(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 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] = 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_tagstart",
key="ppp_separator",
info=shared.OptionInfo(
SendToNegative.DEFAULT_TAG_START,
label="Tag start",
section=section,
),
)
shared.opts.add_option(
key="stn_tagend",
info=shared.OptionInfo(
SendToNegative.DEFAULT_TAG_END,
label="Tag end",
section=section,
),
)
shared.opts.add_option(
key="stn_tagparamstart",
info=shared.OptionInfo(
SendToNegative.DEFAULT_TAG_PARAM_START,
label="Tag parameter start",
section=section,
),
)
shared.opts.add_option(
key="stn_tagparamend",
info=shared.OptionInfo(
SendToNegative.DEFAULT_TAG_PARAM_END,
label="Tag parameter end",
section=section,
),
)
shared.opts.add_option(
key="stn_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",
@@ -85,10 +67,26 @@ class SendToNegativeScript(scripts.Script):
),
)
shared.opts.add_option(
key="stn_cleanup",
key="ppp_stn_joinattention",
info=shared.OptionInfo(
True,
label="Try to clean-up the prompt after processing (removes extra spaces or the configured separator)",
label="Join attention modifiers (weights) when possible",
section=section,
),
)
shared.opts.add_option(
key="ppp_cleanup",
info=shared.OptionInfo(
True,
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,
),
)
-199
View File
@@ -1,199 +0,0 @@
import logging
import re
class SendToNegative: # pylint: disable=too-few-public-methods
NAME = "Send to Negative"
VERSION = "1.1"
DEFAULT_TAG_START = "<!"
DEFAULT_TAG_END = "!>"
DEFAULT_TAG_PARAM_START = "!"
DEFAULT_TAG_PARAM_END = "!"
DEFAULT_SEPARATOR = ", "
def __init__(
self,
tag_start=None,
tag_end=None,
tag_param_start=None,
tag_param_end=None,
separator=None,
ignore_repeats=None,
cleanup=None,
opts=None,
):
"""
Default format for the tag:
<!content!>
<!!x!content!>
with x being:
s - content is added at the start of the negative prompt. This is the default if no parameter exists.
e - content is added at the end of the negative prompt.
pN - content is added where the insertion point N is in the negative prompt or at the start if it does not exist. N can be 0 to 9.
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.__logger = logging.getLogger(__name__)
str_start = (
tag_start
if tag_start is not None
else getattr(opts, "stn_tagstart", self.DEFAULT_TAG_START)
if opts is not None
else self.DEFAULT_TAG_START
)
str_end = (
tag_end
if tag_end is not None
else getattr(opts, "stn_tagend", self.DEFAULT_TAG_END)
if opts is not None
else self.DEFAULT_TAG_END
)
str_param_start = (
tag_param_start
if tag_param_start is not None
else getattr(opts, "stn_tagparamstart", self.DEFAULT_TAG_PARAM_START)
if opts is not None
else self.DEFAULT_TAG_PARAM_START
)
str_param_end = (
tag_param_end
if tag_param_end is not None
else getattr(opts, "stn_tagparamend", self.DEFAULT_TAG_PARAM_END)
if opts is not None
else self.DEFAULT_TAG_PARAM_END
)
escape_sequence = r"(?<!\\)"
self.__ignore_repeats = (
ignore_repeats if ignore_repeats is not None else getattr(opts, "stn_ignorerepeats", True)
)
self.__cleanup = (
cleanup if cleanup is not None else getattr(opts, "stn_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)
if opts is not None
else self.DEFAULT_SEPARATOR
)
self.__insertion_point_tags = [
(str_start + str_param_start + "i" + str(x) + str_param_end + str_end) for x in range(10)
]
self.__regex = re.compile(
"("
+ escape_sequence
+ re.escape(str_start)
+ "(?:"
+ re.escape(str_param_start)
+ "([se]|(?:[pi][0-9]))"
+ re.escape(str_param_end)
+ ")?(.*?)"
+ escape_sequence
+ re.escape(str_end)
+ ")",
re.S,
)
def process_prompt(self, original_prompt, original_negative_prompt):
"""
Extract from the prompt the tagged parts and add them to the negative prompt
"""
try:
prompt = original_prompt
negative_prompt = original_negative_prompt
self.__logger.debug(f"Input prompt: {prompt}")
self.__logger.debug(f"Input negative_prompt: {negative_prompt}")
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}")
self.__logger.debug(f"Output negative_prompt: {negative_prompt}")
return prompt, negative_prompt
except Exception as e: # pylint: disable=broad-exception-caught
self.__logger.exception(e)
return original_prompt, original_negative_prompt
def __find_tags(self, prompt):
already_processed = []
add_at = {"start": [], "insertion_point": [[] for x in range(10)], "end": []}
# process tags in prompt
matches = self.__regex.findall(prompt)
for match in matches:
position = match[1] or "s"
content = match[2]
if len(content) > 0:
if content not in already_processed:
if self.__ignore_repeats:
already_processed.append(content)
self.__logger.debug(f"Processing content at position {position}: {content}")
if position == "e":
add_at["end"].append(content)
elif position.startswith("p"):
n = int(position[1])
add_at["insertion_point"][n].append(content)
else: # position == "s" or invalid
add_at["start"].append(content)
else:
self.__logger.warning(f"Ignoring repeated content: {content}")
# clean-up
prompt = prompt.replace(match[0], "")
if self.__cleanup:
prompt = (
prompt.replace(" ", " ")
.replace(self.__separator + self.__separator, self.__separator)
.replace(" " + self.__separator, self.__separator)
.removeprefix(self.__separator)
.removesuffix(self.__separator)
.strip()
)
return prompt, add_at
def __add_to_insertion_points(self, negative_prompt, add_at_insertion_point):
for n in range(10):
ipp = negative_prompt.find(self.__insertion_point_tags[n])
if ipp >= 0:
ipl = len(self.__insertion_point_tags[n])
if negative_prompt[ipp - len(self.__separator) : ipp] == self.__separator:
ipp -= len(self.__separator) # adjust for existing start separator
ipl += len(self.__separator)
add_at_insertion_point[n].insert(0, negative_prompt[:ipp])
if negative_prompt[ipp + ipl : ipp + ipl + len(self.__separator)] == self.__separator:
ipl += len(self.__separator) # adjust for existing end separator
endPart = negative_prompt[ipp + ipl :]
if len(endPart) > 0:
add_at_insertion_point[n].append(endPart)
negative_prompt = self.__separator.join(add_at_insertion_point[n])
else:
ipp = 0
if negative_prompt.startswith(self.__separator):
ipp = len(self.__separator)
add_at_insertion_point[n].append(negative_prompt[ipp:])
negative_prompt = self.__separator.join(add_at_insertion_point[n])
return negative_prompt
def __add_to_start(self, negative_prompt, add_at_start):
if len(negative_prompt) > 0:
ipp = 0
if negative_prompt.startswith(self.__separator):
ipp = len(self.__separator) # adjust for existing end separator
add_at_start.append(negative_prompt[ipp:])
negative_prompt = self.__separator.join(add_at_start)
return negative_prompt
def __add_to_end(self, negative_prompt, add_at_end):
if len(negative_prompt) > 0:
ipl = len(negative_prompt)
if negative_prompt.endswith(self.__separator):
ipl -= len(self.__separator) # adjust for existing start separator
add_at_end.insert(0, negative_prompt[:ipl])
negative_prompt = self.__separator.join(add_at_end)
return negative_prompt
+72 -27
View File
@@ -1,23 +1,20 @@
import logging
import unittest
import sys
import os
sys.path.insert(1, os.path.join(sys.path[0], ".."))
from sendtonegative import SendToNegative # pylint: disable=import-error
from ppp import PromptPostProcessor # pylint: disable=import-error
from ppp_logging import PromptPostProcessorLogFactory
class TestSendToNegative(unittest.TestCase):
class TestPromptPostProcessor(unittest.TestCase):
def setUp(self):
self.defstn = SendToNegative(
tag_start="<!",
tag_end="!>",
tag_param_start="!",
tag_param_end="!",
separator=", ",
ignore_repeats=True,
cleanup=True,
)
lf = PromptPostProcessorLogFactory()
self.__log = lf.log
self.__log.setLevel(logging.DEBUG)
self.defppp = PromptPostProcessor(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=True)
def process(
self,
@@ -25,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(
@@ -110,27 +107,75 @@ class TestSendToNegative(unittest.TestCase):
def test_complex(self):
self.process(
"<!red!> <!!s!pink!>, flowers <!!e!purple!>, <!!e!blue!>, <!!p0!yellow!> <!!p1!green!>",
"<!red!> (<!!s!pink!>), flowers <!!e!purple!>, <!!e!blue!>, <!!p0!yellow!> <!!p1!green!>",
"normal quality, <!!i0!!>, bad quality<!!i1!!>, worse quality",
"flowers",
"red, pink, normal quality, yellow, bad quality, green, worse quality, purple, blue",
"red, (pink), normal quality, yellow, bad quality, green, worse quality, purple, blue",
)
def test_complex_no_cleanup(self):
self.process(
"<!red!> <!!s!pink!>, flowers <!!e!purple!>, <!!e!blue!>, <!!p0!yellow!> <!!p1!green!>",
"<!red!> (<!!s!pink!>), flowers <!!e!purple!>, <!!e!blue!>, <!!p0!yellow!> <!!p1!green!>",
"normal quality, <!!i0!!>, bad quality<!!i1!!>, worse quality",
" , flowers , , ",
"red, pink, normal quality, yellow, bad quality, green, worse quality, purple, blue",
SendToNegative(
tag_start="<!",
tag_end="!>",
tag_param_start="!",
tag_param_end="!",
separator=", ",
ignore_repeats=True,
cleanup=False,
),
" (), flowers , , ",
"red, (pink), normal quality, yellow, bad quality, green, worse quality, purple, blue",
PromptPostProcessor(self.__log, separator=", ", ignore_repeats=True, join_attention=True, cleanup=False),
)
def test_inside_attention1(self):
self.process(
"[<!neg1!>] this is a ((test<!!e!neg2!>) (test:2.0): 1.5 )",
"normal quality",
"this is a ((test) (test:2.0): 1.5 )",
"[neg1], normal quality, (neg2:1.65)",
)
def test_inside_attention2(self):
self.process(
"(red<![square]!>:1.5)",
"",
"(red:1.5)",
"([square]:1.5)",
)
def test_inside_alternation1(self):
self.process(
"this is a (([complex|simple<!neg1!>|regular] test)(test:2.0):1.5)",
"normal quality",
"this is a (([complex|simple|regular] test)(test:2.0):1.5)",
"([|neg1|]:1.65), normal quality",
)
def test_inside_alternation2(self):
self.process(
"this is a (([complex<!neg1!>|simple<!neg2!>|regular<!neg3!>] test)(test:2.0):1.5)",
"normal quality",
"this is a (([complex|simple|regular] test)(test:2.0):1.5)",
"([neg1||]:1.65), ([|neg2|]:1.65), ([||neg3]:1.65), normal quality",
)
def test_inside_alternation3(self):
self.process(
"this is a (([complex<!neg1!>[one|two<!neg12!>||three|four(<!neg14!>)]|simple<!neg2!>|regular<!neg3!>] test)(test:2.0):1.5)",
"normal quality",
"this is a (([complex[one|two||three|four]|simple|regular] test)(test:2.0):1.5)",
"([neg1||]:1.65), ([[|neg12|||]||]:1.65), ([[||||(neg14)]||]:1.65), ([|neg2|]:1.65), ([||neg3]:1.65), normal quality",
)
def test_inside_scheduling(self):
self.process(
"this is [abc<!neg1!>:def<!!e!neg2!>: 5 ]",
"normal quality",
"this is [abc:def: 5 ]",
"[neg1::5], normal quality, [neg2:5]",
)
def test_complex_features(self):
self.process(
"[<!neg5!>] this is: a (([complex|simple<!neg6!>|regular] test<!neg1!>)(test:2.0):1.5) \nBREAK with [abc<!neg4!>:def<!!p0!neg2(neg3:1.6)!>:5] <lora:xxx:1>",
"normal quality, <!!i0!!>",
"this is: a (([complex|simple|regular] test)(test:2.0):1.5) \nBREAK with [abc:def:5] <lora:xxx:1>",
"[neg5], ([|neg6|]:1.65), (neg1:1.65), [neg4::5], normal quality, [neg2(neg3:1.6):5]",
)