Compare commits

..

4 Commits
1.2.2 ... 1.3.0

Author SHA1 Message Date
Dominik Reh
ae02c749e9 Negative prompts now exclusive to their category
They now only autocomplete if their parent category is active in the config as well
2022-10-13 10:35:08 +02:00
Dominik Reh
fca985ba39 Update README.md 2022-10-13 10:19:47 +02:00
Dominik Reh
fff756cb86 Added functionality for img2img & negative prompts 2022-10-13 10:15:21 +02:00
Dominik Reh
7c21452560 Default to danbooru tag colors 2022-10-13 08:30:28 +02:00
3 changed files with 156 additions and 89 deletions

View File

@@ -10,7 +10,7 @@ I created this script as a convenience tool since it reduces the need of switchi
You can either download the files manually as described below, or use a pre-packaged version from [Releases](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases).
### Disclaimer:
This script is definitely not optimized, and it's not very intelligent. The tags are simply recommended based on their natural order in the CSV, which is their respective image count for the default Danbooru tag list. Also, at least for now, completion for negative or img2img prompt textboxes isn't supported, and there's no way to turn the script off from the ui, but I plan to get around to those features eventually.
This script is definitely not optimized, and it's not very intelligent. The tags are simply recommended based on their natural order in the CSV, which is their respective image count for the default Danbooru tag list. Also, at least for now there's no way to turn the script off from the ui, but I plan to get around to that eventually.
### Known Issues:
If `replaceUnderscores` is active, the script will currently only partly replace edited tags containing multiple words in brackets.
@@ -38,8 +38,14 @@ The config contains the following settings and defaults:
```json
{
"tagFile": "danbooru.csv",
"activeIn": {
"txt2img": true,
"img2img": true,
"negativePrompts": true
},
"maxResults": 5,
"replaceUnderscores": true,
"escapeParentheses": true,
"colors": {
"danbooru": {
"0": ["lightblue", "dodgerblue"],
@@ -65,8 +71,10 @@ The config contains the following settings and defaults:
| Setting | Description |
|---------|-------------|
| tagFile | Specifies the tag file to use. You can provide a custom tag database of your liking, but since the script was developed with Danbooru tags in mind, it might not work properly with other configurations.|
| activeIn | Allows to selectively (de)activate the script for txt2img, img2img, and the negative prompts for both. |
| maxResults | How many results to show max. For the default tag set, the results are ordered by occurence count. |
| replaceUnderscores | If true, undescores are replaced with spaces on clicking a tag. Might work better for some models. |
| escapeParentheses | If true, escapes tags containing () so they don't contribute to the web UI's prompt weighting functionality. |
| colors | Contains customizable colors for the tag types, you can add new ones here for custom tag files (same name as filename, without the .csv). The first value is for dark, the second for light mode. Color names and hex codes should both work.|
### CSV tag data

View File

@@ -1,6 +1,6 @@
// Style for new elements. Gets appended to the Gradio root.
const autocompleteCSS_dark = `
#autocompleteResults {
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
@@ -9,23 +9,23 @@ const autocompleteCSS_dark = `
border-radius: 12px !important;
overflow: hidden;
}
#autocompleteResultsList > li:nth-child(odd) {
.autocompleteResultsList > li:nth-child(odd) {
background-color: #111827;
}
#autocompleteResultsList > li {
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
#autocompleteResultsList > li:hover {
.autocompleteResultsList > li:hover {
background-color: #1f2937;
}
#autocompleteResultsList > li.selected {
.autocompleteResultsList > li.selected {
background-color: #374151;
}
`;
const autocompleteCSS_light = `
#autocompleteResults {
.autocompleteResults {
position: absolute;
z-index: 999;
margin: 5px 0 0 0;
@@ -34,18 +34,18 @@ const autocompleteCSS_light = `
border-radius: 12px !important;
overflow: hidden;
}
#autocompleteResultsList > li:nth-child(odd) {
.autocompleteResultsList > li:nth-child(odd) {
background-color: #f9fafb;
}
#autocompleteResultsList > li {
.autocompleteResultsList > li {
list-style-type: none;
padding: 10px;
cursor: pointer;
}
#autocompleteResultsList > li:hover {
.autocompleteResultsList > li:hover {
background-color: #f5f6f8;
}
#autocompleteResultsList > li.selected {
.autocompleteResultsList > li.selected {
background-color: #e5e7eb;
}
`;
@@ -129,14 +129,35 @@ function difference(a, b) {
a.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) + 1), new Map() )
)].reduce( (acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), [] );
}
// Get the identifier for the text area to differentiate between positive and negative
function getTextAreaIdentifier(textArea) {
let txt2img_n = gradioApp().querySelector('#negative_prompt > label > textarea');
let img2img = gradioApp().querySelector('#tab_img2img');
let img2img_p = img2img.querySelector('#img2img_prompt > label > textarea');
let img2img_n = img2img.querySelector('#negative_prompt > label > textarea');
let modifier = "";
if (textArea === img2img_p || textArea === img2img_n) {
modifier += ".img2img";
}
if (textArea === txt2img_n || textArea === img2img_n) {
modifier += ".n";
} else {
modifier += ".p";
}
return modifier;
}
// Create the result list div and necessary styling
function createResultsDiv() {
function createResultsDiv(textArea) {
let resultsDiv = document.createElement("div");
let resultsList = document.createElement('ul');
resultsDiv.setAttribute('id', 'autocompleteResults');
resultsList.setAttribute('id', 'autocompleteResultsList');
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
resultsList.setAttribute('class', 'autocompleteResultsList');
resultsDiv.appendChild(resultsList);
return resultsDiv;
@@ -146,63 +167,67 @@ function createResultsDiv() {
var selectedTag = null;
// Show or hide the results div
var isVisible = false;
function showResults() {
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
resultsDiv.style.display = "block";
isVisible = true;
function isVisible(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
return resultsDiv.style.display === "block";
}
function hideResults() {
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
}
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "none";
isVisible = false;
selectedTag = null;
}
// On click, insert the tag into the prompt textbox with respect to the cursor position
function insertTextAtCursor(text, tagword) {
let promptTextbox = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let cursorPos = promptTextbox.selectionStart;
function insertTextAtCursor(textArea, text, tagword) {
let cursorPos = textArea.selectionStart;
let sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
sanitizedText = acConfig.escapeParentheses ? sanitizedText.replaceAll("(", "\\(").replaceAll(")", "\\)") : sanitizedText;
var prompt = promptTextbox.value;
var prompt = textArea.value;
let optionalComma = (prompt[cursorPos] === "," || prompt[cursorPos + tagword.length] === ",") ? "" : ", ";
// Edit prompt text
let direction = prompt.substring(cursorPos, cursorPos + tagword.length) === tagword ? 1 : -1;
if (direction === 1) {
promptTextbox.value = prompt.substring(0, cursorPos) + sanitizedText + optionalComma + prompt.substring(cursorPos + tagword.length)
let toRight = prompt.substring(cursorPos, cursorPos + tagword.length) === tagword;
if (toRight) {
textArea.value = prompt.substring(0, cursorPos) + sanitizedText + optionalComma + prompt.substring(cursorPos + tagword.length)
// Update cursor position to after the inserted text
promptTextbox.selectionStart = cursorPos + sanitizedText.length + optionalComma.length;
} else {
promptTextbox.value = prompt.substring(0, cursorPos - tagword.length) + sanitizedText + optionalComma + prompt.substring(cursorPos)
promptTextbox.selectionStart = cursorPos - tagword.length + sanitizedText.length + optionalComma.length;
textArea.selectionStart = cursorPos + sanitizedText.length + optionalComma.length;
} else {
textArea.value = prompt.substring(0, cursorPos - tagword.length) + sanitizedText + optionalComma + prompt.substring(cursorPos)
textArea.selectionStart = cursorPos - tagword.length + sanitizedText.length + optionalComma.length;
}
prompt = promptTextbox.value;
promptTextbox.selectionEnd = promptTextbox.selectionStart;
prompt = textArea.value;
textArea.selectionEnd = textArea.selectionStart;
// Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure its
// internal Svelte data binding remains in sync.
promptTextbox.dispatchEvent(new Event("input", { bubbles: true }));
textArea.dispatchEvent(new Event("input", { bubbles: true }));
// Hide results after inserting
hideResults();
hideResults(textArea);
// Update previous tags with the edited prompt to prevent re-searching the same term
let tags = prompt.match(/[^, ]+/g);
previousTags = tags;
}
function addResultsToList(results, tagword) {
let resultsList = gradioApp().querySelector('#autocompleteResultsList');
function addResultsToList(textArea, results, tagword) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsList = gradioApp().querySelector('.autocompleteResults' + textAreaId + ' > ul');
resultsList.innerHTML = "";
// Find right colors from config
let tagFileName = acConfig.tagFile.split(".")[0];
let tagColors = acConfig.colors;
//let colorIndex = Object.keys(tagColors).findIndex(key => key === tagFileName);
//let colorValues = Object.values(tagColors)[colorIndex];
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
@@ -213,16 +238,21 @@ function addResultsToList(results, tagword) {
// Set the color of the tag
let tagType = result[1];
li.style = `color: ${tagColors[tagFileName][tagType][mode]};`;
let colorGroup = tagColors[tagFileName];
// Default to danbooru scheme if no matching one is found
if (colorGroup === undefined) colorGroup = tagColors["danbooru"];
li.style = `color: ${colorGroup[tagType][mode]};`;
// Add listener
li.addEventListener("click", function() { insertTextAtCursor(result[0], tagword); });
li.addEventListener("click", function() { insertTextAtCursor(textArea, result[0], tagword); });
// Add element to list
resultsList.appendChild(li);
}
}
function updateSelectionStyle(num) {
let resultsList = gradioApp().querySelector('#autocompleteResultsList');
function updateSelectionStyle(textArea, num) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsList = gradioApp().querySelector('.autocompleteResults' + textAreaId + ' > ul');
let items = resultsList.getElementsByTagName('li');
for (let i = 0; i < items.length; i++) {
@@ -237,10 +267,10 @@ previousTags = [];
results = [];
tagword = "";
resultCount = 0;
function autocomplete(prompt) {
function autocomplete(textArea, prompt) {
// Guard for empty prompt
if (prompt.length === 0) {
hideResults();
hideResults(textArea);
return;
}
@@ -251,7 +281,7 @@ function autocomplete(prompt) {
// Guard for no difference / only whitespace remaining
if (diff === undefined || diff.length === 0) {
hideResults();
hideResults(textArea);
return;
}
@@ -259,7 +289,7 @@ function autocomplete(prompt) {
// Guard for empty tagword
if (tagword === undefined || tagword.length === 0) {
hideResults();
hideResults(textArea);
return;
}
@@ -268,19 +298,19 @@ function autocomplete(prompt) {
// Guard for empty results
if (resultCount === 0) {
hideResults();
hideResults(textArea);
return;
}
showResults();
addResultsToList(results, tagword);
showResults(textArea);
addResultsToList(textArea, results, tagword);
}
function navigateInList(event) {
function navigateInList(textArea, event) {
validKeys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter", "Escape"];
if (!validKeys.includes(event.key)) return;
if (!isVisible) return
if (!isVisible(textArea)) return
switch (event.key) {
case "ArrowUp":
@@ -305,16 +335,16 @@ function navigateInList(event) {
break;
case "Enter":
if (selectedTag !== null) {
insertTextAtCursor(results[selectedTag][0], tagword);
insertTextAtCursor(textArea, results[selectedTag][0], tagword);
}
break;
case "Escape":
hideResults();
hideResults(textArea);
break;
}
// Update highlighting
if (selectedTag !== null)
updateSelectionStyle(selectedTag);
updateSelectionStyle(textArea, selectedTag);
// Prevent default behavior
event.preventDefault();
@@ -326,38 +356,62 @@ onUiUpdate(function(){
if (acConfig === null) acConfig = JSON.parse(readFile("file/tags/config.json"));
if (allTags.length === 0) allTags = loadCSV();
let promptTextbox = gradioApp().querySelector('#txt2img_prompt > label > textarea');
if (promptTextbox === null) return;
if (gradioApp().querySelector('#autocompleteResults') != null) return;
let txt2imgTextArea = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let img2imgTextArea = gradioApp().querySelector('#img2img_prompt > label > textarea');
let negativeTextAreas = Array.from(gradioApp().querySelectorAll('#negative_prompt > label > textarea'));
let textAreas = [txt2imgTextArea, img2imgTextArea, negativeTextAreas[0], negativeTextAreas[1]];
// Only add listeners once
if (!promptTextbox.classList.contains('autocomplete')) {
// Add our new element
var resultsDiv = gradioApp().querySelector('#autocompleteResults') ?? createResultsDiv();
promptTextbox.parentNode.insertBefore(resultsDiv, promptTextbox.nextSibling);
// Hide by default so it doesn't show up on page load
hideResults();
// Add autocomplete event listener
promptTextbox.addEventListener('input', debounce(() => autocomplete(promptTextbox.value), 100));
// Add focusout event listener
promptTextbox.addEventListener('focusout', debounce(() => hideResults(), 400));
// Add up and down arrow event listener
promptTextbox.addEventListener('keydown', (e) => navigateInList(e));
// Add class so we know we've already added the listeners
promptTextbox.classList.add('autocomplete');
// Add style to dom
let acStyle = document.createElement('style');
let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
if (acStyle.styleSheet) {
acStyle.styleSheet.cssText = css;
} else {
acStyle.appendChild(document.createTextNode(css));
}
gradioApp().appendChild(acStyle);
// Not found, we're on a page without prompt textareas
if (textAreas.every(v => v === null || v === undefined)) return;
// Already added?
if (gradioApp().querySelector('.autocompleteResults.p') !== null
&& (gradioApp().querySelector('.autocompleteResults.n') === null
&& !acConfig.activeIn.negativePrompts)) {
return;
}
textAreas.forEach(area => {
// Skip directly if not found on the page
if (area === null || area === undefined) return;
// Return if autocomplete is disabled for the current area type in config
let textAreaId = getTextAreaIdentifier(area);
if (textAreaId.includes("p") || (textAreaId.includes("n") && acConfig.activeIn.negativePrompts)) {
if (textAreaId.includes("img2img")) {
if (!acConfig.activeIn.img2img) return;
} else {
if (!acConfig.activeIn.txt2img) return;
}
}
// Only add listeners once
if (!area.classList.contains('autocomplete')) {
// Add our new element
var resultsDiv = createResultsDiv(area);
area.parentNode.insertBefore(resultsDiv, area.nextSibling);
// Hide by default so it doesn't show up on page load
hideResults(area);
// Add autocomplete event listener
area.addEventListener('input', debounce(() => autocomplete(area, area.value), 100));
// Add focusout event listener
area.addEventListener('focusout', debounce(() => hideResults(area), 400));
// Add up and down arrow event listener
area.addEventListener('keydown', (e) => navigateInList(area, e));
// Add class so we know we've already added the listeners
area.classList.add('autocomplete');
// Add style to dom
let acStyle = document.createElement('style');
let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light;
if (acStyle.styleSheet) {
acStyle.styleSheet.cssText = css;
} else {
acStyle.appendChild(document.createTextNode(css));
}
gradioApp().appendChild(acStyle);
}
});
});

View File

@@ -1,5 +1,10 @@
{
"tagFile": "danbooru.csv",
"activeIn": {
"txt2img": true,
"img2img": true,
"negativePrompts": true
},
"maxResults": 5,
"replaceUnderscores": true,
"escapeParentheses": true,