Compare commits

..

34 Commits
1.0.0 ... 1.4.2

Author SHA1 Message Date
Dominik Reh
fbfc988fe5 Fix for broken RegEx with tags containing parentheses
Have I mentioned that all my homies hate RegEx?
2022-10-13 23:37:28 +02:00
Dominik Reh
a93a209e7e Update README.md 2022-10-13 22:37:23 +02:00
Dominik Reh
f5c00d8de4 Update README.md 2022-10-13 22:06:31 +02:00
Dominik Reh
0b7bb146a5 Fixed text insertion & curser position
This time for real (hopefully)
All my homies hate RegEx
2022-10-13 21:31:37 +02:00
Dominik Reh
f098b14248 Update README.md 2022-10-13 17:36:56 +02:00
Dominik Reh
9710eef4cc Added wildcard info to README 2022-10-13 17:26:17 +02:00
Dominik Reh
db29a6a84a Support completion for the wildcard script
Also some reworks for text insertion, should now work no matter where in the word your cursor is
2022-10-13 17:10:35 +02:00
Dominik Reh
4785142549 Added link wrap to badge 2022-10-13 10:58:21 +02:00
Dominik Reh
cddd9da700 Added release badge to README 2022-10-13 10:53:06 +02:00
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
Dominik Reh
4de62638b3 Merge pull request #3 from HalfMAI/fix_input_event 2022-10-13 08:06:22 +02:00
Halfmai
d99bfb7c48 formatting 2022-10-13 11:52:06 +08:00
Halfmai
61e74154b6 fix missing dispatch event to gradio 2022-10-13 11:44:17 +08:00
Dominik Reh
6548775f36 Merge branch 'main' of https://github.com/DominikDoom/a1111-sd-webui-tagcomplete into main 2022-10-12 18:31:38 +02:00
Dominik Reh
fada5a76c3 Config option to escape parentheses 2022-10-12 18:31:32 +02:00
Dominik Reh
aa80ed5c7c Update README.md 2022-10-12 14:52:12 +02:00
Dominik Reh
8ddc737e80 Update README.md 2022-10-12 14:41:55 +02:00
Dominik Reh
8d6d3ab584 Update README.md 2022-10-12 14:40:54 +02:00
Dominik Reh
5fa179dde1 Fix cursor jumping to wrong position after insert 2022-10-12 14:21:36 +02:00
Dominik Reh
a782f951a6 Code style fixes 2022-10-12 13:54:04 +02:00
Dominik Reh
fd0d05101a Prompt tag insertion bugfix 2022-10-12 13:53:21 +02:00
Dominik Reh
6fa1d1d041 Load custom colors from config 2022-10-12 13:52:20 +02:00
Dominik Reh
00a12b4e41 Removed duplicate tag entries in e621 2022-10-12 13:50:26 +02:00
Dominik Reh
d88ab906d7 Update README.md 2022-10-12 00:15:03 +02:00
Dominik Reh
4243ebe645 e621 tags
Note: This will break coloring at the moment since e621 uses more tag types than Danbooru. I'll try to implement custom color support soon.
2022-10-12 00:05:12 +02:00
Dominik Reh
f224eda78c Added left/right arrow capture
Also fixes unexpected insertion behavior when moving after the window opened
2022-10-11 23:32:29 +02:00
Dominik Reh
f432e84279 Added keyboard navigation support 2022-10-11 23:26:02 +02:00
Dominik Reh
c7258a5839 Hotfix for duplicates not recognized as changes 2022-10-11 22:43:04 +02:00
Dominik Reh
a94be12e86 Fix default values 2022-10-11 22:17:19 +02:00
Dominik Reh
4047e2da3d Update README.md 2022-10-11 22:15:10 +02:00
Dominik Reh
c4ad9f250d Update README.md 2022-10-11 22:14:53 +02:00
5 changed files with 66587 additions and 125 deletions

View File

@@ -1,5 +1,7 @@
# Booru tag autocompletion for A1111
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/DominikDoom/a1111-sd-webui-tagcomplete)](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,15 +9,28 @@ 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.
[[Setup instructions]](#installation)
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, 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.
### NEW - Wildcard 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.
#### Important:
Since not everyone has the script, it is **disabled by default**. Edit the config to enable it and uncomment / add the filenames you use in `wildcardNames.txt`.
As per the instructions of the wildcard script, the files are expected in `/scripts/wildcards/`, it will likely fail if you have another folder structure.
### 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.
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.
## 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
Wildcard script support:
https://user-images.githubusercontent.com/34448969/195632461-49d226ae-d393-453d-8f04-1e44b073234c.mp4
Dark and Light mode supported, including tag colors:
@@ -24,25 +39,60 @@ 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.
Simply put `tagAutocomplete.js` into the **`javascript`** folder of your web UI installation (**NOT** the `scripts` folder where most other scripts are installed). 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 folder structure should look similar to this at the end:
![image](https://user-images.githubusercontent.com/34448969/195697260-526a1ab8-4a63-4b8b-a9bf-ae0f3eef780f.png)
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,
"useWildcards": false,
"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. |
| useWildcards | Used to toggle the recently added wildcard completion functionality. Also needs `wildcardNames.txt` to contain proper file names for your wildcard files. |
| 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:
@@ -63,4 +113,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.

View File

@@ -1,51 +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 {
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 {
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 = [];
@@ -53,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
@@ -99,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);
}
@@ -110,146 +116,411 @@ const debounce = (func, wait = 300) => {
}
}
// Difference function to fix duplicates not being seen as changes in normal filter
function difference(a, b) {
if (a.length == 0) {
return b;
}
if (b.length == 0) {
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)), []);
}
// 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;
}
// 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 showResults() {
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
function isVisible(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
return resultsDiv.style.display === "block";
}
function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
}
function hideResults() {
let resultsDiv = gradioApp().querySelector('#autocompleteResults');
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "none";
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(text, tagword) {
let promptTextbox = gradioApp().querySelector('#txt2img_prompt > label > textarea');
let cursorPos = promptTextbox.selectionStart;
let sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text;
let optionalComma = (promptTextbox.value[cursorPos] == ",") ? "" : ", ";
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
var prompt = promptTextbox.value;
promptTextbox.value = prompt.substring(0, cursorPos - tagword.length) + sanitizedText + optionalComma + prompt.substring(cursorPos);
prompt = promptTextbox.value;
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;
// Update cursor position to after the inserted text
promptTextbox.selectionStart = cursorPos + sanitizedText.length;
promptTextbox.selectionEnd = promptTextbox.selectionStart;
var optionalComma = "";
if (tagType !== "wildcardFile") {
optionalComma = surrounding.match(new RegExp(escapeRegExp(`${tagword},`))) !== null ? "" : ", ";
}
// Hide results after inserting
hideResults();
// 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 }));
// 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);
}
}
const colors_dark = ["lightblue", "indianred", "unused", "violet", "lightgreen", "orange"];
const colors_light = ["dodgerblue", "firebrick", "unused", "darkorchid", "darkgreen", "darkorange" ]
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 = "";
let colors = gradioApp().querySelector('.dark') ? colors_dark : colors_light;
// Find right colors from config
let tagFileName = acConfig.tagFile.split(".")[0];
let tagColors = acConfig.colors;
let mode = gradioApp().querySelector('.dark') ? 0 : 1;
for (let i = 0; i < results.length; i++) {
let result = results[i];
let li = document.createElement("li");
li.innerHTML = result[0];
li.style = `color: ${colors[result[1]]};`;
li.addEventListener("click", function() { insertTextAtCursor(result[0], tagword); });
// 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(textArea, result, tagword); });
// Add element to list
resultsList.appendChild(li);
}
}
allTags = [];
previousTags = [];
function updateSelectionStyle(textArea, num) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsList = resultDiv.querySelector('ul');
let items = resultsList.getElementsByTagName('li');
function autocomplete(prompt) {
// Guard for empty prompt
if (prompt.length == 0) {
hideResults();
return;
for (let i = 0; i < items.length; i++) {
items[i].classList.remove('selected');
}
// Match tags with RegEx to get the last edited one
let tags = prompt.match(/[^, ]+/g);
let difference = tags.filter(x => !previousTags.includes(x));
previousTags = tags;
items[num].classList.add('selected');
// Guard for no difference / only whitespace remaining
if (difference == undefined || difference.length == 0) {
hideResults();
return;
// 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;
}
let tagword = difference[0]
// Guard for empty tagword
if (tagword == undefined || tagword.length == 0) {
hideResults();
return;
}
let results = allTags.filter(x => x[0].includes(tagword)).slice(0, acConfig.maxResults);
// Guard for empty results
if (results.length == 0) {
hideResults();
return;
}
showResults();
addResultsToList(results, tagword);
}
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 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);
wildcardFiles = [];
wildcards = {};
allTags = [];
results = [];
tagword = "";
resultCount = 0;
function autocomplete(textArea, prompt, fixedTag = null) {
// Guard for empty prompt
if (prompt.length === 0) {
hideResults(textArea);
return;
}
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 === 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 = tagword.toLowerCase();
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);
}
resultCount = results.length;
// Guard for empty results
if (resultCount === 0) {
hideResults(textArea);
return;
}
selectedTag = null; // Reset since the list changed
showResults(textArea);
addResultsToList(textArea, results, tagword);
}
function navigateInList(textArea, event) {
validKeys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter", "Escape"];
if (!validKeys.includes(event.key)) return;
if (!isVisible(textArea)) return
switch (event.key) {
case "ArrowUp":
if (selectedTag === null) {
selectedTag = resultCount - 1;
} else {
selectedTag = (selectedTag - 1 + resultCount) % resultCount;
}
break;
case "ArrowDown":
if (selectedTag === null) {
selectedTag = 0;
} else {
selectedTag = (selectedTag + 1) % resultCount;
}
break;
case "ArrowLeft":
selectedTag = 0;
break;
case "ArrowRight":
selectedTag = resultCount - 1;
break;
case "Enter":
if (selectedTag !== null) {
insertTextAtCursor(textArea, results[selectedTag], tagword);
}
break;
case "Escape":
hideResults(textArea);
break;
}
// Update highlighting
if (selectedTag !== null)
updateSelectionStyle(textArea, selectedTag);
// Prevent default behavior
event.preventDefault();
event.stopPropagation();
}
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;
}
}
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;
});

View File

@@ -1,5 +1,32 @@
{
"tagFile": "danbooru.csv",
"maxResults": 10,
"replaceUnderscores": false
"activeIn": {
"txt2img": true,
"img2img": true,
"negativePrompts": true
},
"maxResults": 5,
"replaceUnderscores": true,
"escapeParentheses": true,
"useWildcards": false,
"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"]
}
}
}

66094
tags/e621.csv Normal file

File diff suppressed because it is too large Load Diff

7
tags/wildcardNames.txt Normal file
View 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