mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-27 03:29:55 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53899093c8 | ||
|
|
f9d98740f4 | ||
|
|
534f07225e | ||
|
|
b8b0673e2d | ||
|
|
2f0d18a73f | ||
|
|
e68e7389dd | ||
|
|
b5cecc4e8d | ||
|
|
96828c241c | ||
|
|
07d7eddf66 | ||
|
|
08c10928f8 | ||
|
|
a628d96a41 | ||
|
|
3a47a9b010 | ||
|
|
fbfc988fe5 | ||
|
|
a93a209e7e | ||
|
|
f5c00d8de4 | ||
|
|
0b7bb146a5 | ||
|
|
f098b14248 | ||
|
|
9710eef4cc | ||
|
|
db29a6a84a | ||
|
|
4785142549 | ||
|
|
cddd9da700 |
29
README.md
29
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.
|
||||
@@ -7,10 +9,14 @@ Since some Stable Diffusion models were trained using this information, for exam
|
||||
|
||||
I created this script as a convenience tool since it reduces the need of switching back and forth between the web UI and a booru site to copy-paste tags.
|
||||
|
||||
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).
|
||||
You can either clone / download the files manually as described [below](#installation), 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 there's no way to turn the script off from the ui, but I plan to get around to that eventually.
|
||||
### Wildcard & Embedding support
|
||||
Autocompletion also works with wildcard files used by [this script](https://github.com/jtkelm2/stable-diffusion-webui-1/blob/master/scripts/wildcards.py) of the same name (demo video further down). This enables you to either insert categories to be replaced by the script, or even replace them with the actual wildcard file content in the same step.
|
||||
|
||||
It also scans the embeddings folder and displays completion hints for the names of all .pt and .bin files inside if you start typing `<`. Note that some normal tags also use < in Kaomoji (like ">_<" for example), so the results will contain both.
|
||||
|
||||
Both are now enabled by default and scan the `/embeddings` and `/scripts/wildcards` folders automatically.
|
||||
|
||||
### Known Issues:
|
||||
If `replaceUnderscores` is active, the script will currently only partly replace edited tags containing multiple words in brackets.
|
||||
@@ -21,17 +27,22 @@ Demo video (with keyboard navigation):
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/195344430-2b5f9945-b98b-4943-9fbc-82cf633321b1.mp4
|
||||
|
||||
Wildcard script support:
|
||||
|
||||
https://user-images.githubusercontent.com/34448969/195632461-49d226ae-d393-453d-8f04-1e44b073234c.mp4
|
||||
|
||||
Dark and Light mode supported, including tag colors:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## Installation
|
||||
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.
|
||||
Simply copy the `javascript`, `scripts` and `tags` folder into your web UI installation root. It will run automatically the next time the web UI is started.
|
||||
|
||||
The tags folder contains `config.json` and the tag data the script uses for autocompletion. By default, Danbooru and e621 tags are included.
|
||||
After scanning for embeddings and wildcards, the script will also create a `temp` directory here which lists the found files so they can be accessed in the browser side of the script. You can delete the temp folder without consequences as it will be recreated on the next startup.
|
||||
### Important:
|
||||
The script needs **all three folders** to work properly.
|
||||
|
||||
### Config
|
||||
The config contains the following settings and defaults:
|
||||
@@ -46,6 +57,8 @@ The config contains the following settings and defaults:
|
||||
"maxResults": 5,
|
||||
"replaceUnderscores": true,
|
||||
"escapeParentheses": true,
|
||||
"useWildcards": true,
|
||||
"useEmbeddings": true,
|
||||
"colors": {
|
||||
"danbooru": {
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
@@ -72,9 +85,11 @@ The config contains the following settings and defaults:
|
||||
|---------|-------------|
|
||||
| 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. |
|
||||
| maxResults | How many results to show max. For the default tag set, the results are ordered by occurence count. For embeddings and wildcards it will show all results in a scrollable list. |
|
||||
| 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. |
|
||||
| useWildcards | Used to toggle the wildcard completion functionality. |
|
||||
| useEmbeddings | Used to toggle the embedding completion 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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
var acConfig = null;
|
||||
var acActive = true;
|
||||
|
||||
// Style for new elements. Gets appended to the Gradio root.
|
||||
const autocompleteCSS_dark = `
|
||||
let autocompleteCSS_dark = `
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
@@ -7,7 +10,7 @@ const autocompleteCSS_dark = `
|
||||
background-color: #0b0f19 !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.autocompleteResultsList > li:nth-child(odd) {
|
||||
background-color: #111827;
|
||||
@@ -24,7 +27,7 @@ const autocompleteCSS_dark = `
|
||||
background-color: #374151;
|
||||
}
|
||||
`;
|
||||
const autocompleteCSS_light = `
|
||||
let autocompleteCSS_light = `
|
||||
.autocompleteResults {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
@@ -32,7 +35,7 @@ const autocompleteCSS_light = `
|
||||
background-color: #ffffff !important;
|
||||
border: 1.5px solid #e5e7eb !important;
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.autocompleteResultsList > li:nth-child(odd) {
|
||||
background-color: #f9fafb;
|
||||
@@ -50,8 +53,6 @@ const autocompleteCSS_light = `
|
||||
}
|
||||
`;
|
||||
|
||||
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 +60,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 +106,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,17 +126,17 @@ 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 txt2img_n = gradioApp().querySelector('#txt2img_neg_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 img2img_n = img2img.querySelector('#img2img_neg_prompt > label > textarea');
|
||||
|
||||
let modifier = "";
|
||||
if (textArea === img2img_p || textArea === img2img_n) {
|
||||
modifier += ".img2img";
|
||||
@@ -152,10 +153,11 @@ function getTextAreaIdentifier(textArea) {
|
||||
function createResultsDiv(textArea) {
|
||||
let resultsDiv = document.createElement("div");
|
||||
let resultsList = document.createElement('ul');
|
||||
|
||||
|
||||
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);
|
||||
@@ -163,8 +165,28 @@ function createResultsDiv(textArea) {
|
||||
return resultsDiv;
|
||||
}
|
||||
|
||||
// Create the checkbox to enable/disable autocomplete
|
||||
function createCheckbox() {
|
||||
let label = document.createElement("label");
|
||||
let input = document.createElement("input");
|
||||
let span = document.createElement("span");
|
||||
|
||||
label.setAttribute('id', 'acActiveCheckbox');
|
||||
label.setAttribute('class', '"flex items-center text-gray-700 text-sm rounded-lg cursor-pointer dark:bg-transparent');
|
||||
input.setAttribute('type', 'checkbox');
|
||||
input.setAttribute('class', 'gr-check-radio gr-checkbox')
|
||||
span.setAttribute('class', 'ml-2');
|
||||
|
||||
span.textContent = "Enable Autocomplete";
|
||||
|
||||
label.appendChild(input);
|
||||
label.appendChild(span);
|
||||
return label;
|
||||
}
|
||||
|
||||
// The selected tag index. Needs to be up here so hide can access it.
|
||||
var selectedTag = null;
|
||||
var previousTags = [];
|
||||
|
||||
// Show or hide the results div
|
||||
function isVisible(textArea) {
|
||||
@@ -186,44 +208,90 @@ function hideResults(textArea) {
|
||||
selectedTag = null;
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
let hideBlocked = false;
|
||||
// On click, insert the tag into the prompt textbox with respect to the cursor position
|
||||
function insertTextAtCursor(textArea, text, tagword) {
|
||||
function insertTextAtCursor(textArea, result, tagword) {
|
||||
let text = result[0];
|
||||
let tagType = result[1];
|
||||
|
||||
let cursorPos = textArea.selectionStart;
|
||||
let sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
sanitizedText = acConfig.escapeParentheses ? sanitizedText.replaceAll("(", "\\(").replaceAll(")", "\\)") : sanitizedText;
|
||||
|
||||
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 if (tagType === "embedding") {
|
||||
sanitizedText = `<${text.replace(/^.*?: /g, "")}>`;
|
||||
} else {
|
||||
sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
|
||||
}
|
||||
|
||||
if (acConfig.escapeParentheses) {
|
||||
sanitizedText = sanitizedText
|
||||
.replaceAll("(", "\\(")
|
||||
.replaceAll(")", "\\)")
|
||||
.replaceAll("[", "\\[")
|
||||
.replaceAll("]", "\\]");
|
||||
}
|
||||
|
||||
var prompt = textArea.value;
|
||||
let optionalComma = (prompt[cursorPos] === "," || prompt[cursorPos + tagword.length] === ",") ? "" : ", ";
|
||||
|
||||
|
||||
// Edit prompt text
|
||||
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
|
||||
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;
|
||||
let editStart = Math.max(cursorPos - tagword.length, 0);
|
||||
let editEnd = Math.min(cursorPos + tagword.length, prompt.length);
|
||||
let surrounding = prompt.substring(editStart, editEnd);
|
||||
let match = surrounding.match(new RegExp(escapeRegExp(`${tagword}`)));
|
||||
let afterInsertCursorPos = editStart + match.index + sanitizedText.length;
|
||||
|
||||
var optionalComma = "";
|
||||
if (tagType !== "wildcardFile") {
|
||||
optionalComma = surrounding.match(new RegExp(escapeRegExp(`${tagword},`))) !== null ? "" : ", ";
|
||||
}
|
||||
prompt = textArea.value;
|
||||
textArea.selectionEnd = textArea.selectionStart;
|
||||
|
||||
// Replace partial tag word with new text, add comma if needed
|
||||
let insert = surrounding.replace(tagword, sanitizedText + optionalComma);
|
||||
|
||||
// Add back start
|
||||
var newPrompt = prompt.substring(0, editStart) + insert + prompt.substring(editEnd);
|
||||
textArea.value = newPrompt;
|
||||
textArea.selectionStart = afterInsertCursorPos + optionalComma.length;
|
||||
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.
|
||||
textArea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
// Hide results after inserting
|
||||
hideResults(textArea);
|
||||
|
||||
// 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(textArea, results, tagword) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsList = gradioApp().querySelector('.autocompleteResults' + textAreaId + ' > ul');
|
||||
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
let resultsList = resultDiv.querySelector('ul');
|
||||
|
||||
// Reset list, selection and scrollTop since the list changed
|
||||
resultsList.innerHTML = "";
|
||||
selectedTag = null;
|
||||
resultDiv.scrollTop = 0;
|
||||
|
||||
// Find right colors from config
|
||||
let tagFileName = acConfig.tagFile.split(".")[0];
|
||||
@@ -234,17 +302,21 @@ function addResultsToList(textArea, results, tagword) {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
let result = results[i];
|
||||
let li = document.createElement("li");
|
||||
li.innerHTML = result[0];
|
||||
li.textContent = result[0];
|
||||
|
||||
// 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"];
|
||||
// Wildcards & Embeds have no tag type
|
||||
if (!result[1].startsWith("wildcard") && result[1] !== "embedding") {
|
||||
// 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]};`;
|
||||
}
|
||||
|
||||
li.style = `color: ${colorGroup[tagType][mode]};`;
|
||||
// Add listener
|
||||
li.addEventListener("click", function() { insertTextAtCursor(textArea, result[0], tagword); });
|
||||
li.addEventListener("click", function () { insertTextAtCursor(textArea, result, tagword); });
|
||||
// Add element to list
|
||||
resultsList.appendChild(li);
|
||||
}
|
||||
@@ -252,7 +324,8 @@ function addResultsToList(textArea, results, tagword) {
|
||||
|
||||
function updateSelectionStyle(textArea, num) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let resultsList = gradioApp().querySelector('.autocompleteResults' + textAreaId + ' > ul');
|
||||
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
|
||||
let resultsList = resultDiv.querySelector('ul');
|
||||
let items = resultsList.getElementsByTagName('li');
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
@@ -260,40 +333,86 @@ function updateSelectionStyle(textArea, 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 = {};
|
||||
embeddings = [];
|
||||
allTags = [];
|
||||
previousTags = [];
|
||||
results = [];
|
||||
tagword = "";
|
||||
resultCount = 0;
|
||||
function autocomplete(textArea, prompt) {
|
||||
function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// Return if the function is deactivated in the UI
|
||||
if (!acActive) return;
|
||||
|
||||
// Guard for empty prompt
|
||||
if (prompt.length === 0) {
|
||||
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(textArea);
|
||||
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(textArea);
|
||||
return;
|
||||
if (acConfig.useWildcards && [...tagword.matchAll(/\b__([^,_ ]+)__([^, ]*)\b/g)].length > 0) {
|
||||
// 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 (acConfig.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__")) {
|
||||
// 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 if (acConfig.useEmbeddings && tagword.match(/<[^,> ]*>?/g)) {
|
||||
// Show embeddings
|
||||
let tempResults = [];
|
||||
if (tagword !== "<") {
|
||||
tempResults = embeddings.filter(x => x.toLowerCase().includes(tagword.replace("<", ""))) // Filter by tagword
|
||||
} else {
|
||||
tempResults = embeddings;
|
||||
}
|
||||
// Since some tags are kaomoji, we have to still get the normal results first.
|
||||
genericResults = allTags.filter(x => x[0].toLowerCase().includes(tagword)).slice(0, acConfig.maxResults);
|
||||
results = genericResults.concat(tempResults.map(x => ["Embeddings: " + x.trim(), "embedding"])); // Mark as embedding
|
||||
} 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
|
||||
@@ -307,6 +426,9 @@ function autocomplete(textArea, prompt) {
|
||||
}
|
||||
|
||||
function navigateInList(textArea, event) {
|
||||
// Return if the function is deactivated in the UI
|
||||
if (!acActive) return;
|
||||
|
||||
validKeys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter", "Escape"];
|
||||
|
||||
if (!validKeys.includes(event.key)) return;
|
||||
@@ -335,7 +457,7 @@ function navigateInList(textArea, event) {
|
||||
break;
|
||||
case "Enter":
|
||||
if (selectedTag !== null) {
|
||||
insertTextAtCursor(textArea, results[selectedTag][0], tagword);
|
||||
insertTextAtCursor(textArea, results[selectedTag], tagword);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
@@ -351,15 +473,64 @@ function navigateInList(textArea, event) {
|
||||
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();
|
||||
styleAdded = false;
|
||||
onUiUpdate(function () {
|
||||
// Load config
|
||||
if (acConfig === null) {
|
||||
try {
|
||||
acConfig = JSON.parse(readFile("file/tags/config.json"));
|
||||
} catch (e) {
|
||||
console.error("Error loading config.json: " + e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Load main tags
|
||||
if (allTags.length === 0) {
|
||||
try {
|
||||
allTags = loadCSV();
|
||||
} catch (e) {
|
||||
console.error("Error loading tags file: " + e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Load wildcards
|
||||
if (wildcardFiles.length === 0 && acConfig.useWildcards) {
|
||||
try {
|
||||
wildcardFiles = readFile("file/tags/temp/wc.txt").split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.trim().replace(".txt", "")); // Remove file extension & newlines
|
||||
|
||||
let txt2imgTextArea = gradioApp().querySelector('#txt2img_prompt > label > textarea');
|
||||
wildcardFiles.forEach(fName => {
|
||||
try {
|
||||
wildcards[fName] = 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);
|
||||
}
|
||||
}
|
||||
// Load embeddings
|
||||
if (embeddings.length === 0 && acConfig.useEmbeddings) {
|
||||
try {
|
||||
embeddings = readFile("file/tags/temp/emb.txt").split("\n")
|
||||
.filter(x => x.trim().length > 0) // Remove empty lines
|
||||
.map(x => x.replace(".bin", "").replace(".pt", "")); // Remove file extensions
|
||||
} catch (e) {
|
||||
console.error("Error loading embeddings.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]];
|
||||
let txt2imgTextArea_n = gradioApp().querySelector('#txt2img_neg_prompt > label > textarea');
|
||||
let img2imgTextArea_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea');
|
||||
let textAreas = [txt2imgTextArea, img2imgTextArea, txt2imgTextArea_n, img2imgTextArea_n];
|
||||
|
||||
let quicksettings = gradioApp().querySelector('#quicksettings');
|
||||
|
||||
// Not found, we're on a page without prompt textareas
|
||||
if (textAreas.every(v => v === null || v === undefined)) return;
|
||||
@@ -373,7 +544,7 @@ onUiUpdate(function(){
|
||||
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)) {
|
||||
@@ -391,7 +562,7 @@ onUiUpdate(function(){
|
||||
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
|
||||
@@ -401,17 +572,29 @@ onUiUpdate(function(){
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
if (gradioApp().querySelector("#acActiveCheckbox") === null) {
|
||||
// Add toggle switch
|
||||
let cb = createCheckbox();
|
||||
cb.querySelector("input").checked = acActive;
|
||||
cb.querySelector("input").addEventListener("change", (e) => {
|
||||
acActive = e.target.checked;
|
||||
});
|
||||
quicksettings.parentNode.insertBefore(cb, quicksettings.nextSibling);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
48
scripts/tag_autocomplete_helper.py
Normal file
48
scripts/tag_autocomplete_helper.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# This helper script scans folders for wildcards and embeddings and writes them
|
||||
# to a temporary file to expose it to the javascript side
|
||||
|
||||
import os
|
||||
|
||||
# The path to the folder containing the wildcards and embeddings
|
||||
FILE_DIR = os.path.dirname(os.path.realpath("__file__"))
|
||||
WILDCARD_PATH = os.path.join(FILE_DIR, 'scripts/wildcards')
|
||||
EMB_PATH = os.path.join(FILE_DIR, 'embeddings')
|
||||
# The path to the temporary file
|
||||
TEMP_PATH = os.path.join(FILE_DIR, 'tags/temp')
|
||||
|
||||
|
||||
def get_wildcards():
|
||||
"""Returns a list of all wildcards"""
|
||||
return filter(lambda f: f.endswith(".txt"), os.listdir(WILDCARD_PATH))
|
||||
|
||||
|
||||
def get_embeddings():
|
||||
"""Returns a list of all embeddings"""
|
||||
return filter(lambda f: f.endswith(".bin") or f.endswith(".pt"), os.listdir(EMB_PATH))
|
||||
|
||||
|
||||
def write_to_temp_file(name, data):
|
||||
"""Writes the given data to a temporary file"""
|
||||
with open(os.path.join(TEMP_PATH, name), 'w', encoding="utf-8") as f:
|
||||
f.write(('\n'.join(data)))
|
||||
|
||||
|
||||
# Check if the temp path exists and create it if not
|
||||
if not os.path.exists(TEMP_PATH):
|
||||
os.makedirs(TEMP_PATH)
|
||||
# Set up files to ensure the script doesn't fail to load them
|
||||
# even if no wildcards or embeddings are found
|
||||
write_to_temp_file('wc.txt', [])
|
||||
write_to_temp_file('emb.txt', [])
|
||||
|
||||
# Write wildcards to wc.txt if found
|
||||
if os.path.exists(WILDCARD_PATH):
|
||||
wildcards = get_wildcards()
|
||||
if wildcards:
|
||||
write_to_temp_file('wc.txt', wildcards)
|
||||
|
||||
# Write embeddings to emb.txt if found
|
||||
if os.path.exists(EMB_PATH):
|
||||
embeddings = get_embeddings()
|
||||
if embeddings:
|
||||
write_to_temp_file('emb.txt', embeddings)
|
||||
@@ -8,6 +8,8 @@
|
||||
"maxResults": 5,
|
||||
"replaceUnderscores": true,
|
||||
"escapeParentheses": true,
|
||||
"useWildcards": true,
|
||||
"useEmbeddings": true,
|
||||
"colors": {
|
||||
"danbooru": {
|
||||
"0": ["lightblue", "dodgerblue"],
|
||||
|
||||
Reference in New Issue
Block a user