Compare commits

...

10 Commits

Author SHA1 Message Date
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
8 changed files with 199 additions and 59 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>

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

@@ -41,7 +41,8 @@ class WildcardParser extends BaseTagParser {
}
}
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

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);
}
@@ -699,7 +730,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 +742,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";
}
}
}
@@ -1230,8 +1300,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

@@ -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,
@@ -359,11 +360,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 +448,61 @@ 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:
async def get_json_info(base_path: Path, filename: str = None):
if base_path is None or (not base_path.exists()):
return json.dumps({})
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})
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 json.dumps({})
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 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))
@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
@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)
script_callbacks.on_app_started(api_tac)