Compare commits

...

26 Commits

Author SHA1 Message Date
DominikDoom
e23bb6d4ea Add support for --wildcards-dir cmd argument
Refactor PR #229 a bit to share code with this
2023-09-02 17:59:27 +02:00
DominikDoom
d4cca00575 Merge pull request #229 from azmodii/main 2023-09-02 17:27:00 +02:00
DominikDoom
86ea94a565 Merge pull request #228 from re-unknown/main 2023-09-02 17:08:13 +02:00
Joel Clark
53f46c91a2 feat: Allow support for custom wildcard directory in sd-dynamic-prompts 2023-09-02 21:30:01 +10:00
ReUnknown
e5f93188c3 Support for updated style editor 2023-09-02 16:51:41 +09:00
DominikDoom
3e57842ac6 Remove unnecessary autocomplete call in wildcards
which would result in duplicate file requests
2023-08-29 10:23:13 +02:00
DominikDoom
32c4589df3 Rework wildcards to use own API endpoint
Maybe fixes #226
2023-08-29 09:39:32 +02:00
DominikDoom
5bbd97588c Remove duplicate slash from wildcard files
(should be cosmetic only)
2023-08-28 19:15:34 +02:00
DominikDoom
b2a663f7a7 Merge pull request #223 from Symbiomatrix/embload 2023-08-20 19:08:40 +02:00
Symbiomatrix
6f93d19a2b Edit error message. 2023-08-20 20:02:57 +03:00
Symbiomatrix
79bab04fd2 Typo. 2023-08-20 18:59:12 +03:00
Symbiomatrix
5b69d1e622 Embedding forced reload. 2023-08-20 18:51:37 +03:00
DominikDoom
651cf5fb46 Add metaKey and Shift to non-captured modifiers
Fixes #222
2023-08-19 11:59:41 +02:00
DominikDoom
5deb72cddf Add clearer README description for legacy translations
As suggested in #221
2023-08-16 10:50:05 +02:00
DominikDoom
97ebe78205 !After Detailer (adetailer) support 2023-08-15 14:44:38 +02:00
DominikDoom
b937e853c9 Fix booru wiki links with translations 2023-08-08 19:23:13 +02:00
DominikDoom
f63bbf947f Fix API endpoint to work with symlinks / external folders
Fixes #217
2023-08-07 22:15:48 +02:00
DominikDoom
16bc6d8868 Update README.md 2023-08-07 19:48:27 +02:00
DominikDoom
ebe276ee44 Fix for lora filenames containing dots
Since file extensions are already cut off before the client-side request, it's not needed here anymore
2023-08-07 19:22:50 +02:00
DominikDoom
995a5ecdba Live preview images for extra networks
Same as the thumbnails in the extra networks tab, just in a small preview window during completion
2023-08-07 18:50:55 +02:00
DominikDoom
90d144a5f4 Fix for new trimming rule cutting off first letter
if Loras weren't in a subfolder
2023-08-07 17:51:21 +02:00
DominikDoom
14a4440c33 Fix extra network sorting
Caused by loras including their (hidden) folder prefixes instead of just by name
2023-08-07 17:38:40 +02:00
DominikDoom
cdf092f3ac Fix lora keyword lookup for deep subfolders 2023-08-07 15:17:49 +02:00
DominikDoom
e1598378dc Merge pull request #215 from bluelovers/pr/model-keyword-001 2023-08-07 09:24:38 +02:00
bluelovers
599ad7f95f fix: known_lora_hashes.txt
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/issues/214

https://github.com/canisminor1990/sd-webui-lobe-theme/issues/324
2023-08-07 09:54:10 +08:00
DominikDoom
0b2bb138ee Add option to keep wildcard file content order
instead of sorting alphabetically
Fixes #211
2023-08-05 13:42:24 +02:00
10 changed files with 287 additions and 76 deletions

View File

@@ -74,6 +74,10 @@ Wildcard script support:
https://user-images.githubusercontent.com/34448969/200128031-22dd7c33-71d1-464f-ae36-5f6c8fd49df0.mp4
Extra Network preview support:
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/3c0cad84-fb5f-436d-b05a-28db35860d13
Dark and Light mode supported, including tag colors:
![results_dark](https://user-images.githubusercontent.com/34448969/200128214-3b6f21b4-9dda-4acf-820e-5df0285c30d6.png)
@@ -123,6 +127,13 @@ Completion for these types is triggered by typing `<`. By default it will show t
- Or `<lora:` and `<lyco:` respectively for the long form
- `<h:` or `<hypernet:` will only show Hypernetworks
### Live previews
Tag Autocomplete will now also show the preview images used for the cards in the Extra Networks menu in a small window next to the regular popup.
This enables quick comparisons and additional info for unclear filenames without having to stop typing to look it up in the webui menu.
It works for all supported extra network types that use preview images (Loras/Lycos, Embeddings & Hypernetworks). The preview window will stay hidden for normal tags or if no preview was found.
![extra_live_preview](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/6a5d81e6-b3a0-407b-8bac-c9790f86016c)
### Lora / Lyco trigger word completion
This feature will try to add known trigger words on autocompleting a Lora/Lyco.
@@ -307,6 +318,14 @@ If this option is turned on, it will show a `?` link next to the tag. Clicking t
![wikiLink](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/733e1ba8-89e1-4c2b-8c4e-2d23352bd3d7)
</details>
<!-- Wiki links -->
<details>
<summary>Extra network live previews</summary>
This option enables a small preview window alongside the normal completion popup that will show the card preview also usd in the extra networks tab for that file.
![extraNetworkPreviews](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/72b5473f-563e-4238-a513-38b60ac87e96)
</details>
<!-- Insertion -->
<details>
<summary>Completion settings</summary>
@@ -455,7 +474,9 @@ You can also add this to your quicksettings bar to have the refresh button avail
# Translations
An additional file can be added in the translation section, which will be used to translate both tags and aliases and also enables searching by translation.
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`, but for backwards compatibility with older files that used a three column format, you can turn on `Translation file uses old 3-column translation format instead of the new 2-column one` to support them. In that case, the second column will be unused and skipped during parsing.
This file needs to be a CSV in the format `<English tag/alias>,<Translation>`. Some older files use a three column format, which requires a compatibility setting to be activated.
You can find it under `Settings > Tag autocomplete > Translation filename > Translation file uses old 3-column translation format instead of the new 2-column one`.
With it on, the second column will be unused and skipped during parsing.
Example with Chinese translation:

View File

@@ -9,7 +9,11 @@ const core = [
"#img2img_prompt > label > textarea",
"#txt2img_neg_prompt > label > textarea",
"#img2img_neg_prompt > label > textarea",
".prompt > label > textarea"
".prompt > label > textarea",
"#txt2img_edit_style_prompt > label > textarea",
"#txt2img_edit_style_neg_prompt > label > textarea",
"#img2img_edit_style_prompt > label > textarea",
"#img2img_edit_style_neg_prompt > label > textarea"
];
// Third party text area selectors
@@ -57,6 +61,24 @@ const thirdParty = {
"[id^=MD-i2i][id$=prompt] textarea",
"[id^=MD-i2i][id$=prompt] input[type='text']"
]
},
"adetailer-t2i": {
"base": "#txt2img_script_container",
"hasIds": true,
"onDemand": true,
"selectors": [
"[id^=script_txt2img_adetailer_ad_prompt] textarea",
"[id^=script_txt2img_adetailer_ad_negative_prompt] textarea"
]
},
"adetailer-i2i": {
"base": "#img2img_script_container",
"hasIds": true,
"onDemand": true,
"selectors": [
"[id^=script_img2img_adetailer_ad_prompt] textarea",
"[id^=script_img2img_adetailer_ad_negative_prompt] textarea"
]
}
}
@@ -90,7 +112,7 @@ function addOnDemandObservers(setupFunction) {
let base = gradioApp().querySelector(entry.base);
if (!base) continue;
let accordions = [...base?.querySelectorAll(".gradio-accordion")];
if (!accordions) continue;
@@ -115,12 +137,12 @@ function addOnDemandObservers(setupFunction) {
[...gradioApp().querySelectorAll(entry.selectors.join(", "))].forEach(x => setupFunction(x));
} else { // Otherwise, we have to find the text areas by their adjacent labels
let base = gradioApp().querySelector(entry.base);
// Safety check
if (!base) continue;
let allTextAreas = [...base.querySelectorAll("textarea, input[type='text']")];
// Filter the text areas where the adjacent label matches one of the selectors
let matchingTextAreas = allTextAreas.filter(ta => [...ta.parentElement.childNodes].some(x => entry.selectors.includes(x.innerText)));
matchingTextAreas.forEach(x => setupFunction(x));

View File

@@ -81,6 +81,23 @@ async function fetchAPI(url, json = true, cache = false) {
return await response.text();
}
// Extra network preview thumbnails
async function getExtraNetworkPreviewURL(filename, type) {
const previewJSON = await fetchAPI(`tacapi/v1/thumb-preview/${filename}?type=${type}`, true, true);
if (previewJSON?.url) {
const properURL = `sd_extra_networks/thumb?filename=${previewJSON.url}`;
if ((await fetch(properURL)).status == 200) {
return properURL;
} else {
// create blob url
const blob = await (await fetch(`tacapi/v1/thumb-preview-blob/${filename}?type=${type}`)).blob();
return URL.createObjectURL(blob);
}
} else {
return null;
}
}
// Debounce function to prevent spamming the autocomplete function
var dbTimeOut;
const debounce = (func, wait = 300) => {

View File

@@ -16,7 +16,12 @@ class LoraParser extends BaseTagParser {
// Add final results
let finalResults = [];
tempResults.forEach(t => {
let result = new AutocompleteResult(t[0].trim(), ResultType.lora)
const text = t[0].trim();
let lastDot = text.lastIndexOf(".") > -1 ? text.lastIndexOf(".") : text.length;
let lastSlash = text.lastIndexOf("/") > -1 ? text.lastIndexOf("/") : -1;
let name = text.substring(lastSlash + 1, lastDot);
let result = new AutocompleteResult(name, ResultType.lora)
result.meta = "Lora";
result.hash = t[1];
finalResults.push(result);
@@ -46,11 +51,7 @@ async function sanitize(tagType, text) {
multiplier = info["preferred weight"];
}
const lastDot = text.lastIndexOf(".");
const lastSlash = text.lastIndexOf("/");
const name = text.substring(lastSlash + 1, lastDot);
return `<lora:${name}:${multiplier}>`;
return `<lora:${text}:${multiplier}>`;
}
return null;
}

View File

@@ -16,7 +16,12 @@ class LycoParser extends BaseTagParser {
// Add final results
let finalResults = [];
tempResults.forEach(t => {
let result = new AutocompleteResult(t[0].trim(), ResultType.lyco)
const text = t[0].trim();
let lastDot = text.lastIndexOf(".") > -1 ? text.lastIndexOf(".") : text.length;
let lastSlash = text.lastIndexOf("/") > -1 ? text.lastIndexOf("/") : -1;
let name = text.substring(lastSlash + 1, lastDot);
let result = new AutocompleteResult(name, ResultType.lyco)
result.meta = "Lyco";
result.hash = t[1];
finalResults.push(result);
@@ -46,11 +51,7 @@ async function sanitize(tagType, text) {
multiplier = info["preferred weight"];
}
const lastDot = text.lastIndexOf(".");
const lastSlash = text.lastIndexOf("/");
const name = text.substring(lastSlash + 1, lastDot);
return `<lyco:${name}:${multiplier}>`;
return `<lyco:${text}:${multiplier}>`;
}
return null;
}

View File

@@ -22,10 +22,13 @@ class WildcardParser extends BaseTagParser {
let wildcards = [];
for (let i = 0; i < wcPairs.length; i++) {
const wcPair = wcPairs[i];
if (!wcPair[0] || !wcPair[1]) continue;
const basePath = wcPairs[i][0];
const fileName = wcPairs[i][1];
if (!basePath || !fileName) return;
if (wcPair[0].endsWith(".yaml")) {
// YAML wildcards are already loaded as json, so we can get the values directly.
// basePath is the name of the file in this case, and fileName the key
if (basePath.endsWith(".yaml")) {
const getDescendantProp = (obj, desc) => {
const arr = desc.split("/");
while (arr.length) {
@@ -33,15 +36,17 @@ class WildcardParser extends BaseTagParser {
}
return obj;
}
wildcards = wildcards.concat(getDescendantProp(yamlWildcards[wcPair[0]], wcPair[1]));
wildcards = wildcards.concat(getDescendantProp(yamlWildcards[basePath], fileName));
} else {
const fileContent = (await readFile(`${wcPair[0]}/${wcPair[1]}.txt`)).split("\n")
.filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments
const fileContent = (await fetchAPI(`tacapi/v1/wildcard-contents?basepath=${basePath}&filename=${fileName}.txt`, false))
.split("\n")
.filter(x => x.trim().length > 0 && !x.startsWith('#')); // Remove empty lines and comments
wildcards = wildcards.concat(fileContent);
}
}
wildcards.sort((a, b) => a.localeCompare(b));
if (TAC_CFG.sortWildcardResults)
wildcards.sort((a, b) => a.localeCompare(b));
let finalResults = [];
let tempResults = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword
@@ -154,7 +159,6 @@ function keepOpenIfWildcard(tagType, sanitizedText, newPrompt, textArea) {
// If it's a wildcard, we want to keep the results open so the user can select another wildcard
if (tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) {
hideBlocked = true;
autocomplete(textArea, newPrompt, sanitizedText);
setTimeout(() => { hideBlocked = false; }, 450);
return true;
}

View File

@@ -25,18 +25,36 @@ const autocompleteCSS = `
background-color: transparent;
min-width: fit-content;
}
.autocompleteResults {
.autocompleteParent {
display: flex;
position: absolute;
z-index: 999;
max-width: calc(100% - 1.5rem);
margin: 5px 0 0 0;
}
.autocompleteResults {
background-color: var(--results-bg) !important;
border: var(--results-border-width) solid var(--results-border-color) !important;
border-radius: 12px !important;
height: fit-content;
flex-basis: fit-content;
flex-shrink: 0;
overflow-y: var(--results-overflow-y);
overflow-x: hidden;
word-break: break-word;
}
.sideInfo {
display: none;
position: relative;
margin-left: 10px;
height: 18rem;
max-width: 16rem;
}
.sideInfo > img {
object-fit: cover;
height: 100%;
width: 100%;
}
.autocompleteResultsList > li:nth-child(odd) {
background-color: var(--results-bg-odd);
}
@@ -56,6 +74,7 @@ const autocompleteCSS = `
}
.acListItem {
white-space: break-spaces;
min-width: 100px;
}
.acMetaText {
position: relative;
@@ -190,11 +209,13 @@ async function syncOptions() {
resultStepLength: opts["tac_resultStepLength"],
delayTime: opts["tac_delayTime"],
useWildcards: opts["tac_useWildcards"],
sortWildcardResults: opts["tac_sortWildcardResults"],
useEmbeddings: opts["tac_useEmbeddings"],
useHypernetworks: opts["tac_useHypernetworks"],
useLoras: opts["tac_useLoras"],
useLycos: opts["tac_useLycos"],
showWikiLinks: opts["tac_showWikiLinks"],
showExtraNetworkPreviews: opts["tac_showExtraNetworkPreviews"],
// Insertion related settings
replaceUnderscores: opts["tac_replaceUnderscores"],
escapeParentheses: opts["tac_escapeParentheses"],
@@ -269,47 +290,62 @@ async function syncOptions() {
// Create the result list div and necessary styling
function createResultsDiv(textArea) {
let parentDiv = document.createElement("div");
let resultsDiv = document.createElement("div");
let resultsList = document.createElement("ul");
let sideDiv = document.createElement("div");
let sideDivImg = document.createElement("img");
let textAreaId = getTextAreaIdentifier(textArea);
let typeClass = textAreaId.replaceAll(".", " ");
parentDiv.setAttribute("class", `autocompleteParent${typeClass}`);
resultsDiv.style.maxHeight = `${TAC_CFG.maxResults * 50}px`;
resultsDiv.setAttribute("class", `autocompleteResults ${typeClass} notranslate`);
resultsDiv.setAttribute("class", `autocompleteResults${typeClass} notranslate`);
resultsDiv.setAttribute("translate", "no");
resultsList.setAttribute("class", "autocompleteResultsList");
resultsDiv.appendChild(resultsList);
return resultsDiv;
sideDiv.setAttribute("class", `autocompleteResults${typeClass} sideInfo`);
sideDiv.appendChild(sideDivImg);
parentDiv.appendChild(resultsDiv);
parentDiv.appendChild(sideDiv);
return parentDiv;
}
// Show or hide the results div
function isVisible(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
return resultsDiv.style.display === "block";
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
return parentDiv.style.display === "flex";
}
function showResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
resultsDiv.style.display = "block";
let parentDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
parentDiv.style.display = "flex";
if (TAC_CFG.slidingPopup) {
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth);
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth);
resultsDiv.style.left = `${offset}px`;
parentDiv.style.left = `${offset}px`;
} else {
if (resultsDiv.style.left)
resultsDiv.style.removeProperty("left");
if (parentDiv.style.left)
parentDiv.style.removeProperty("left");
}
// Reset here too to make absolutely sure the browser registers it
resultsDiv.scrollTop = 0;
parentDiv.scrollTop = 0;
// Ensure preview is hidden
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
previewDiv.style.display = "none";
}
function hideResults(textArea) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsDiv = gradioApp().querySelector('.autocompleteParent' + textAreaId);
if (!resultsDiv) return;
@@ -583,11 +619,6 @@ function addResultsToList(textArea, results, tagword, resetList) {
if (!TAC_CFG.alias.onlyShowAlias && result.text !== bestAlias)
displayText += " ➝ " + result.text;
} else if (result.type === ResultType.lora || result.type === ResultType.lyco) {
let lastDot = result.text.lastIndexOf(".");
let lastSlash = result.text.lastIndexOf("/");
let name = result.text.substring(lastSlash + 1, lastDot);
displayText = escapeHTML(name);
} else { // No alias
displayText = escapeHTML(result.text);
}
@@ -620,6 +651,11 @@ function addResultsToList(textArea, results, tagword, resetList) {
// Only use alias result if it is one
if (displayText.includes("➝"))
linkPart = displayText.split(" ➝ ")[1];
// Remove any trailing translations
if (linkPart.includes("[")) {
linkPart = linkPart.split("[")[0]
}
// Set link based on selected file
let tagFileNameLower = tagFileName.toLowerCase();
@@ -699,7 +735,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
}
}
function updateSelectionStyle(textArea, newIndex, oldIndex) {
async function updateSelectionStyle(textArea, newIndex, oldIndex) {
let textAreaId = getTextAreaIdentifier(textArea);
let resultDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId);
let resultsList = resultDiv.querySelector('ul');
@@ -711,13 +747,52 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) {
// make it safer
if (newIndex !== null) {
items[newIndex].classList.add('selected');
let selected = items[newIndex];
selected.classList.add('selected');
// Set scrolltop to selected item
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
}
// Set scrolltop to selected item if we are showing more than max results
if (items.length > TAC_CFG.maxResults) {
// Show preview if enabled and the selected type supports it
if (newIndex !== null) {
let selected = items[newIndex];
resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop;
let previewTypes = ["v1 Embedding", "v2 Embedding", "Hypernetwork", "Lora", "Lyco"];
let selectedType = selected.querySelector(".acMetaText").innerText;
let selectedFilename = selected.querySelector(".acListItem").innerText;
let previewDiv = gradioApp().querySelector(`.autocompleteParent${textAreaId} .sideInfo`);
if (TAC_CFG.showExtraNetworkPreviews && previewTypes.includes(selectedType)) {
let shorthandType = "";
switch (selectedType) {
case "v1 Embedding":
case "v2 Embedding":
shorthandType = "embed";
break;
case "Hypernetwork":
shorthandType = "hyper";
break;
case "Lora":
shorthandType = "lora";
break;
case "Lyco":
shorthandType = "lyco";
break;
}
let img = previewDiv.querySelector("img");
let url = await getExtraNetworkPreviewURL(selectedFilename, shorthandType);
if (url) {
img.src = url;
previewDiv.style.display = "block";
} else {
previewDiv.style.display = "none";
}
} else {
previewDiv.style.display = "none";
}
}
}
@@ -1039,7 +1114,7 @@ function navigateInList(textArea, event) {
if (!validKeys.includes(event.key)) return;
if (!isVisible(textArea)) return
// Return if ctrl key is pressed to not interfere with weight editing shortcut
if (event.ctrlKey || event.altKey) return;
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;
oldSelectedTag = selectedTag;
@@ -1230,8 +1305,8 @@ async function setup() {
// Not found, we're on a page without prompt textareas
if (textAreas.every(v => v === null || v === undefined)) return;
// Already added or unnecessary to add
if (gradioApp().querySelector('.autocompleteResults.p')) {
if (gradioApp().querySelector('.autocompleteResults.n') || !TAC_CFG.activeIn.negativePrompts) {
if (gradioApp().querySelector('.autocompleteParent.p')) {
if (gradioApp().querySelector('.autocompleteParent.n') || !TAC_CFG.activeIn.negativePrompts) {
return;
}
} else if (!TAC_CFG.activeIn.txt2img && !TAC_CFG.activeIn.img2img) {

View File

@@ -28,9 +28,10 @@ def load_hash_cache():
def update_hash_cache():
global file_needs_update
if file_needs_update:
with open(known_hashes_file, "w", encoding="utf-8") as file:
with open(known_hashes_file, "w", encoding="utf-8", newline='') as file:
writer = csv.writer(file)
for name, (hash, mtime) in hash_dict.items():
file.write(f'"{name}",{hash},{mtime}\n')
writer.writerow([name, hash, mtime])
# Copy of the fast inaccurate hash function from the extension

View File

@@ -1,4 +1,5 @@
from pathlib import Path
from modules import scripts, shared
try:
@@ -37,6 +38,21 @@ except AttributeError:
def find_ext_wildcard_paths():
"""Returns the path to the extension wildcards folder"""
found = list(EXT_PATH.glob("*/wildcards/"))
# Try to find the wildcard path from the shared opts
try:
from modules.shared import opts
except ImportError: # likely not in an a1111 context
opts = None
# Append custom wildcard paths
custom_paths = [
getattr(shared.cmd_opts, "wildcards_dir", None), # Cmd arg from the wildcard extension
getattr(opts, "wildcard_dir", None), # Custom path from sd-dynamic-prompts
]
for path in [Path(p) for p in custom_paths if p is not None]:
if path.exists():
found.append(path)
return found

View File

@@ -3,12 +3,13 @@
import glob
import json
import urllib.parse
from pathlib import Path
import gradio as gr
import yaml
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from modules import script_callbacks, sd_hijack, shared
from scripts.model_keyword_support import (get_lora_simple_hash,
@@ -16,6 +17,12 @@ from scripts.model_keyword_support import (get_lora_simple_hash,
write_model_keyword_path)
from scripts.shared_paths import *
# Attempt to get embedding load function, using the same call as api.
try:
load_textual_inversion_embeddings = sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings
except Exception as e: # Not supported.
load_textual_inversion_embeddings = lambda *args, **kwargs: None
print("Tag Autocomplete: Cannot reload embeddings instantly:", e)
def get_wildcards():
"""Returns a list of all wildcards. Works on nested folders."""
@@ -279,6 +286,7 @@ if EMB_PATH.exists():
def refresh_temp_files():
global WILDCARD_EXT_PATHS
WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
load_textual_inversion_embeddings(force_reload = True) # Instant embedding reload.
write_temp_files()
get_embeddings(shared.sd_model)
@@ -359,11 +367,13 @@ def on_ui_settings():
"tac_resultStepLength": shared.OptionInfo(100, "How many results to load at once"),
"tac_delayTime": shared.OptionInfo(100, "Time in ms to wait before triggering completion again").needs_restart(),
"tac_useWildcards": shared.OptionInfo(True, "Search for wildcards"),
"tac_sortWildcardResults": shared.OptionInfo(True, "Sort wildcard file contents alphabetically").info("If your wildcard files have a specific custom order, disable this to keep it"),
"tac_useEmbeddings": shared.OptionInfo(True, "Search for embeddings"),
"tac_useHypernetworks": shared.OptionInfo(True, "Search for hypernetworks"),
"tac_useLoras": shared.OptionInfo(True, "Search for Loras"),
"tac_useLycos": shared.OptionInfo(True, "Search for LyCORIS/LoHa"),
"tac_showWikiLinks": shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page").info("Warning: This is an external site and very likely contains NSFW examples!"),
"tac_showExtraNetworkPreviews": shared.OptionInfo(True, "Show preview thumbnails for extra networks if available"),
# Insertion related settings
"tac_replaceUnderscores": shared.OptionInfo(True, "Replace underscores with spaces on insertion"),
"tac_escapeParentheses": shared.OptionInfo(True, "Escape parentheses on insertion"),
@@ -445,34 +455,77 @@ def on_ui_settings():
script_callbacks.on_ui_settings(on_ui_settings)
def api_tac(_: gr.Blocks, app: FastAPI):
async def get_json_info(path: Path):
if not path:
return json.dumps({})
async def get_json_info(base_path: Path, filename: str = None):
if base_path is None or (not base_path.exists()):
return JSONResponse({}, status_code=404)
try:
if path is not None and path.exists() and path.parent.joinpath(path.stem + ".json").exists():
return FileResponse(path.parent.joinpath(path.stem + ".json").as_posix())
json_candidates = glob.glob(base_path.as_posix() + f"/**/{filename}.json", recursive=True)
if json_candidates is not None and len(json_candidates) > 0:
return FileResponse(json_candidates[0])
except Exception as e:
return json.dumps({"error": e})
@app.get("/tacapi/v1/lora-info/{folder}/{lora_name}")
async def get_lora_info_subfolder(folder, lora_name):
if LORA_PATH is None:
return json.dumps({})
return await get_json_info(LORA_PATH.joinpath(folder).joinpath(lora_name))
@app.get("/tacapi/v1/lyco-info/{folder}/{lyco_name}")
async def get_lyco_info_subfolder(folder, lyco_name):
if LYCO_PATH is None:
return json.dumps({})
return await get_json_info(LYCO_PATH.joinpath(folder).joinpath(lyco_name))
return JSONResponse({"error": e}, status_code=500)
async def get_preview_thumbnail(base_path: Path, filename: str = None, blob: bool = False):
if base_path is None or (not base_path.exists()):
return JSONResponse({}, status_code=404)
try:
img_glob = glob.glob(base_path.as_posix() + f"/**/{filename}.*", recursive=True)
img_candidates = [img for img in img_glob if Path(img).suffix in [".png", ".jpg", ".jpeg", ".webp", ".gif"]]
if img_candidates is not None and len(img_candidates) > 0:
if blob:
return FileResponse(img_candidates[0])
else:
return JSONResponse({"url": urllib.parse.quote(img_candidates[0])})
except Exception as e:
return JSONResponse({"error": e}, status_code=500)
@app.get("/tacapi/v1/lora-info/{lora_name}")
async def get_lora_info(lora_name):
return await get_lora_info_subfolder(".", lora_name)
return await get_json_info(LORA_PATH, lora_name)
@app.get("/tacapi/v1/lyco-info/{lyco_name}")
async def get_lyco_info(lyco_name):
return await get_lyco_info_subfolder(".", lyco_name)
return await get_json_info(LYCO_PATH, lyco_name)
def get_path_for_type(type):
if type == "lora":
return LORA_PATH
elif type == "lyco":
return LYCO_PATH
elif type == "hyper":
return HYP_PATH
elif type == "embed":
return EMB_PATH
else:
return None
script_callbacks.on_app_started(api_tac)
@app.get("/tacapi/v1/thumb-preview/{filename}")
async def get_thumb_preview(filename, type):
return await get_preview_thumbnail(get_path_for_type(type), filename, False)
@app.get("/tacapi/v1/thumb-preview-blob/{filename}")
async def get_thumb_preview_blob(filename, type):
return await get_preview_thumbnail(get_path_for_type(type), filename, True)
@app.get("/tacapi/v1/wildcard-contents")
async def get_wildcard_contents(basepath: str, filename: str):
if basepath is None or basepath == "":
return JSONResponse({}, status_code=404)
base = Path(basepath)
if base is None or (not base.exists()):
return JSONResponse({}, status_code=404)
try:
wildcard_path = base.joinpath(filename)
if wildcard_path.exists():
return FileResponse(wildcard_path)
else:
return JSONResponse({}, status_code=404)
except Exception as e:
return JSONResponse({"error": e}, status_code=500)
script_callbacks.on_app_started(api_tac)