mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-27 03:29:55 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db29a6a84a | ||
|
|
4785142549 | ||
|
|
cddd9da700 | ||
|
|
ae02c749e9 | ||
|
|
fca985ba39 | ||
|
|
fff756cb86 | ||
|
|
7c21452560 | ||
|
|
4de62638b3 | ||
|
|
d99bfb7c48 | ||
|
|
61e74154b6 | ||
|
|
6548775f36 | ||
|
|
fada5a76c3 | ||
|
|
aa80ed5c7c | ||
|
|
8ddc737e80 | ||
|
|
8d6d3ab584 | ||
|
|
5fa179dde1 |
60
README.md
60
README.md
@@ -1,5 +1,7 @@
|
||||
# Booru tag autocompletion for A1111
|
||||
|
||||
[](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/releases)
|
||||
|
||||
This custom script serves as a drop-in extension for the popular [AUTOMATIC1111 web UI](https://github.com/AUTOMATIC1111/stable-diffusion-webui) for Stable Diffusion.
|
||||
|
||||
It displays autocompletion hints for recognized tags from "image booru" boards such as Danbooru, which are primarily used for browsing Anime-style illustrations.
|
||||
@@ -9,15 +11,17 @@ 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).
|
||||
|
||||
(Note: e621 tags aren't added to the releases yet since coloring is broken for them at the moment).
|
||||
|
||||
### 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, neither keyboard selection for tags nor completion for negative or img2img prompt textboxes is supported, and there's no way to turn the feature 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.
|
||||
For example, editing `atago (azur lane)`, it would be replaced with e.g. `taihou (azur lane), lane)`, since the script currently doesn't see the second part of the bracket as the same tag. So in those cases you should delete the old tag beforehand.
|
||||
|
||||
## Screenshots
|
||||
Demo video
|
||||
Demo video (with keyboard navigation):
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/195185810-547d8d0a-bf87-465d-91f1-7fb5c3259c3f.mp4
|
||||
https://user-images.githubusercontent.com/34448969/195344430-2b5f9945-b98b-4943-9fbc-82cf633321b1.mp4
|
||||
|
||||
Dark and Light mode supported, including tag colors:
|
||||
|
||||
@@ -29,22 +33,51 @@ Dark and Light mode supported, including tag colors:
|
||||
Simply put `tagAutocomplete.js` into the `javascript` folder of your web UI installation. It will run automatically the next time the web UI is started.
|
||||
For the script to work, you also need to download the `tags` folder from this repo and paste it and its contents into the web UI root, or create them there manually.
|
||||
|
||||
The tags folder contains two files: `config.json` and `danbooru.csv`. This is the data the script uses for autocompletion.
|
||||
The tags folder contains `config.json` and the tag data the script uses for autocompletion. By default, Danbooru and e621 tags are included.
|
||||
|
||||
### Config
|
||||
The config contains the following settings and defaults:
|
||||
```json
|
||||
{
|
||||
"tagFile": "danbooru.csv",
|
||||
"activeIn": {
|
||||
"txt2img": true,
|
||||
"img2img": true,
|
||||
"negativePrompts": true
|
||||
},
|
||||
"maxResults": 5,
|
||||
"replaceUnderscores": true
|
||||
"replaceUnderscores": true,
|
||||
"escapeParentheses": true,
|
||||
"colors": {
|
||||
"danbooru": {
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["indianred", "firebrick"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["orange", "darkorange"]
|
||||
},
|
||||
"e621": {
|
||||
"-1": ["red", "maroon"],
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
"1": ["gold", "goldenrod"],
|
||||
"3": ["violet", "darkorchid"],
|
||||
"4": ["lightgreen", "darkgreen"],
|
||||
"5": ["tomato", "darksalmon"],
|
||||
"6": ["red", "maroon"],
|
||||
"7": ["whitesmoke", "black"],
|
||||
"8": ["seagreen", "darkseagreen"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
| 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
|
||||
The script expects a CSV file with tags saved in the following way:
|
||||
@@ -65,4 +98,17 @@ The numbering system follows the [tag API docs](https://danbooru.donmai.us/wiki_
|
||||
|4 | Character |
|
||||
|5 | Meta |
|
||||
|
||||
or of e621:
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
|-1 | Invalid |
|
||||
|0 | General |
|
||||
|1 | Artist |
|
||||
|3 | Copyright |
|
||||
|4 | Character |
|
||||
|5 | Species |
|
||||
|6 | Invalid |
|
||||
|7 | Meta |
|
||||
|8 | Lore |
|
||||
|
||||
The tag type is used for coloring entries in the result list.
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
var acConfig = null;
|
||||
|
||||
// Style for new elements. Gets appended to the Gradio root.
|
||||
const autocompleteCSS_dark = `
|
||||
#autocompleteResults {
|
||||
let autocompleteCSS_dark = `
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
margin: 5px 0 0 0;
|
||||
background-color: #0b0f19 !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#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 {
|
||||
let autocompleteCSS_light = `
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
margin: 5px 0 0 0;
|
||||
background-color: #ffffff !important;
|
||||
border: 1.5px solid #e5e7eb !important;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
`;
|
||||
|
||||
var acConfig = null;
|
||||
|
||||
// Parse the CSV file into a 2D array. Doesn't use regex, so it is very lightweight.
|
||||
function parseCSV(str) {
|
||||
var arr = [];
|
||||
@@ -59,7 +59,7 @@ function parseCSV(str) {
|
||||
|
||||
// Iterate over each character, keep track of current row and column (of the returned array)
|
||||
for (var row = 0, col = 0, c = 0; c < str.length; c++) {
|
||||
var cc = str[c], nc = str[c+1]; // Current character, next character
|
||||
var cc = str[c], nc = str[c + 1]; // Current character, next character
|
||||
arr[row] = arr[row] || []; // Create a new row if necessary
|
||||
arr[row][col] = arr[row][col] || ''; // Create a new column (start with empty string) if necessary
|
||||
|
||||
@@ -105,7 +105,7 @@ function loadCSV() {
|
||||
// Debounce function to prevent spamming the autocomplete function
|
||||
var dbTimeOut;
|
||||
const debounce = (func, wait = 300) => {
|
||||
return function(...args) {
|
||||
return function (...args) {
|
||||
if (dbTimeOut) {
|
||||
clearTimeout(dbTimeOut);
|
||||
}
|
||||
@@ -125,18 +125,40 @@ function difference(a, b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return [...b.reduce( (acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
|
||||
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)), [] );
|
||||
return [...b.reduce((acc, v) => acc.set(v, (acc.get(v) || 0) - 1),
|
||||
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.style.setProperty("max-height", acConfig.maxResults * 50 + "px");
|
||||
resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`);
|
||||
resultsList.setAttribute('class', 'autocompleteResultsList');
|
||||
resultsDiv.appendChild(resultsList);
|
||||
|
||||
return resultsDiv;
|
||||
@@ -146,58 +168,95 @@ 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;
|
||||
}
|
||||
|
||||
let hideBlocked = false;
|
||||
// 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;
|
||||
let sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
|
||||
var prompt = promptTextbox.value;
|
||||
let optionalComma = (prompt[cursorPos] === "," || prompt[cursorPos + tagword.length] === ",") ? "" : ", ";
|
||||
function insertTextAtCursor(textArea, result, tagword) {
|
||||
let text = result[0];
|
||||
let tagType = result[1];
|
||||
|
||||
let cursorPos = textArea.selectionStart;
|
||||
var sanitizedText = text
|
||||
|
||||
// Replace differently depending on if it's a tag or wildcard
|
||||
if (tagType === "wildcardFile") {
|
||||
sanitizedText = "__" + text.replace("Wildcards: ", "") + "__";
|
||||
} else if (tagType === "wildcardTag") {
|
||||
sanitizedText = text.replace(/^.*?: /g, "");
|
||||
} else {
|
||||
sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
}
|
||||
|
||||
sanitizedText = acConfig.escapeParentheses ? sanitizedText.replaceAll("(", "\\(").replaceAll(")", "\\)") : sanitizedText;
|
||||
|
||||
var prompt = textArea.value;
|
||||
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
promptTextbox.value = prompt.substring(0, cursorPos - tagword.length) + sanitizedText + optionalComma + prompt.substring(cursorPos)
|
||||
let editStart = Math.max(cursorPos - tagword.length, 0);
|
||||
let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
|
||||
let surrounding = prompt.substring(editStart, editEnd);
|
||||
let insert = surrounding.replace(tagword, sanitizedText);
|
||||
|
||||
// Add back start
|
||||
var newPrompt = prompt.substring(0, editStart) + insert;
|
||||
// Add comma if needed
|
||||
var optionalComma = "";
|
||||
if (tagType !== "wildcardFile") {
|
||||
optionalComma = surrounding.match(`/${tagword},/g`) !== null ? "" : ", ";
|
||||
}
|
||||
prompt = promptTextbox.value;
|
||||
// Set selection after insertion
|
||||
textArea.selectionStart = editStart + newPrompt.length + optionalComma.length;
|
||||
textArea.selectionEnd = textArea.selectionStart;
|
||||
// Add back end
|
||||
newPrompt += optionalComma + prompt.substring(editEnd);
|
||||
textArea.value = newPrompt;
|
||||
|
||||
// Update cursor position to after the inserted text
|
||||
promptTextbox.selectionStart = cursorPos + sanitizedText.length;
|
||||
promptTextbox.selectionEnd = promptTextbox.selectionStart;
|
||||
|
||||
// Hide results after inserting
|
||||
hideResults();
|
||||
// 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.
|
||||
textArea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
// Update previous tags with the edited prompt to prevent re-searching the same term
|
||||
let tags = prompt.match(/[^, ]+/g);
|
||||
let tags = newPrompt.match(/[^, ]+/g);
|
||||
previousTags = tags;
|
||||
|
||||
// Hide results after inserting
|
||||
if (tagType === "wildcardFile") {
|
||||
// If it's a wildcard, we want to keep the results open so the user can select another wildcard
|
||||
hideBlocked = true;
|
||||
autocomplete(textArea, prompt, sanitizedText);
|
||||
setTimeout(() => { hideBlocked = false; }, 100);
|
||||
} else {
|
||||
hideResults(textArea);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -206,18 +265,28 @@ function addResultsToList(results, tagword) {
|
||||
let li = document.createElement("li");
|
||||
li.innerHTML = result[0];
|
||||
|
||||
// Set the color of the tag
|
||||
let tagType = result[1];
|
||||
li.style = `color: ${tagColors[tagFileName][tagType][mode]};`;
|
||||
// Wildcards have no tag type
|
||||
if (!result[1].startsWith("wildcard")) {
|
||||
// Set the color of the tag
|
||||
let tagType = result[1];
|
||||
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, 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 resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
let resultsList = resultDiv.querySelector('ul');
|
||||
let items = resultsList.getElementsByTagName('li');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -225,57 +294,90 @@ function updateSelectionStyle(num) {
|
||||
}
|
||||
|
||||
items[num].classList.add('selected');
|
||||
|
||||
// Set scrolltop to selected item if we are showing more than max results
|
||||
if (items.length > acConfig.maxResults) {
|
||||
let selected = items[num];
|
||||
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
|
||||
}
|
||||
}
|
||||
|
||||
wildcardFiles = [];
|
||||
wildcards = {};
|
||||
allTags = [];
|
||||
previousTags = [];
|
||||
results = [];
|
||||
tagword = "";
|
||||
resultCount = 0;
|
||||
function autocomplete(prompt) {
|
||||
function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// Guard for empty prompt
|
||||
if (prompt.length === 0) {
|
||||
hideResults();
|
||||
hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
|
||||
// Match tags with RegEx to get the last edited one
|
||||
let tags = prompt.match(/[^, ]+/g);
|
||||
let diff = difference(tags, previousTags)
|
||||
previousTags = tags;
|
||||
if (fixedTag === null) {
|
||||
// Match tags with RegEx to get the last edited one
|
||||
let tags = prompt.match(/[^, ]+/g);
|
||||
let diff = difference(tags, previousTags)
|
||||
previousTags = tags;
|
||||
|
||||
// Guard for no difference / only whitespace remaining
|
||||
if (diff === undefined || diff.length === 0) {
|
||||
hideResults();
|
||||
return;
|
||||
// Guard for no difference / only whitespace remaining
|
||||
if (diff === null || diff.length === 0) {
|
||||
if (!hideBlocked) hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
|
||||
tagword = diff[0]
|
||||
|
||||
// Guard for empty tagword
|
||||
if (tagword === null || tagword.length === 0) {
|
||||
hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
tagword = fixedTag;
|
||||
}
|
||||
|
||||
tagword = diff[0]
|
||||
tagword = tagword.toLowerCase();
|
||||
|
||||
// Guard for empty tagword
|
||||
if (tagword === undefined || tagword.length === 0) {
|
||||
hideResults();
|
||||
return;
|
||||
if ([...tagword.matchAll(/\b__([^,_ ]+)__([^, ]*)\b/g)].length > 0 && acConfig.useWildcards) {
|
||||
// Show wildcards from a file with that name
|
||||
wcMatch = [...tagword.matchAll(/\b__([^,_ ]+)__([^, ]*)\b/g)]
|
||||
let wcFile = wcMatch[0][1];
|
||||
let wcWord = wcMatch[0][2];
|
||||
results = wildcards[wcFile].filter(x => (wcWord !== null) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
|
||||
.map(x => [wcFile + ": " + x.trim(), "wildcardTag"]); // Mark as wildcard
|
||||
} else if ((tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__") && acConfig.useWildcards) {
|
||||
// Show available wildcard files
|
||||
let tempResults = [];
|
||||
if (tagword !== "__") {
|
||||
tempResults = wildcardFiles.filter(x => x.toLowerCase().includes(tagword.replace("__", ""))) // Filter by tagword
|
||||
} else {
|
||||
tempResults = wildcardFiles;
|
||||
}
|
||||
results = tempResults.map(x => ["Wildcards: " + x.trim(), "wildcardFile"]); // Mark as wildcard
|
||||
} else {
|
||||
results = allTags.filter(x => x[0].toLowerCase().includes(tagword)).slice(0, acConfig.maxResults);
|
||||
}
|
||||
|
||||
results = allTags.filter(x => x[0].includes(tagword)).slice(0, acConfig.maxResults);
|
||||
resultCount = results.length;
|
||||
|
||||
// Guard for empty results
|
||||
if (resultCount === 0) {
|
||||
hideResults();
|
||||
hideResults(textArea);
|
||||
return;
|
||||
}
|
||||
|
||||
showResults();
|
||||
addResultsToList(results, tagword);
|
||||
selectedTag = null; // Reset since the list changed
|
||||
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":
|
||||
@@ -300,61 +402,120 @@ function navigateInList(event) {
|
||||
break;
|
||||
case "Enter":
|
||||
if (selectedTag !== null) {
|
||||
insertTextAtCursor(results[selectedTag][0], tagword);
|
||||
insertTextAtCursor(textArea, results[selectedTag], tagword);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
hideResults();
|
||||
hideResults(textArea);
|
||||
break;
|
||||
}
|
||||
// Update highlighting
|
||||
if (selectedTag !== null)
|
||||
updateSelectionStyle(selectedTag);
|
||||
updateSelectionStyle(textArea, selectedTag);
|
||||
|
||||
// Prevent default behavior
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onUiUpdate(function(){
|
||||
// One-time CSV setup
|
||||
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;
|
||||
|
||||
// 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));
|
||||
styleAdded = false;
|
||||
onUiUpdate(function () {
|
||||
// One-time config, tags & wildcards loading
|
||||
if (acConfig === null) {
|
||||
try {
|
||||
acConfig = JSON.parse(readFile("file/tags/config.json"));
|
||||
} catch (e) {
|
||||
console.error("Error loading config.json: " + e);
|
||||
return;
|
||||
}
|
||||
gradioApp().appendChild(acStyle);
|
||||
}
|
||||
if (allTags.length === 0) {
|
||||
try {
|
||||
allTags = loadCSV();
|
||||
} catch (e) {
|
||||
console.error("Error loading tags file: " + e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (wildcardFiles.length === 0 && acConfig.useWildcards) {
|
||||
try {
|
||||
wildcardFiles = readFile("file/tags/wildcardNames.txt").split("\n")
|
||||
.filter(x => !x.startsWith("//")) // Remove comments
|
||||
.filter(x => x.toLowerCase().includes(tagword.substring(2))) // Filter by tagword
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
|
||||
wildcardFiles.forEach(fName => {
|
||||
try {
|
||||
wildcards[fName.trim()] = readFile(`file/scripts/wildcards/${fName}.txt`).split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
} catch (e) {
|
||||
console.log(`Could not load wildcards for ${fName}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error loading wildcardNames.txt: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Find all textareas
|
||||
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]];
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
if (styleAdded) return;
|
||||
|
||||
// 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);
|
||||
styleAdded = true;
|
||||
});
|
||||
@@ -1,7 +1,14 @@
|
||||
{
|
||||
"tagFile": "danbooru.csv",
|
||||
"activeIn": {
|
||||
"txt2img": true,
|
||||
"img2img": true,
|
||||
"negativePrompts": true
|
||||
},
|
||||
"maxResults": 5,
|
||||
"replaceUnderscores": true,
|
||||
"escapeParentheses": true,
|
||||
"useWildcards": false,
|
||||
"colors": {
|
||||
"danbooru": {
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
|
||||
7
tags/wildcardNames.txt
Normal file
7
tags/wildcardNames.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
// Put the file names of wildcard files you want to use here. Needed so that the script can access them.
|
||||
// The default ones are the following, you can uncomment them if you have them
|
||||
//adjective
|
||||
//artist
|
||||
//genre
|
||||
//site
|
||||
//style
|
||||
Reference in New Issue
Block a user