Support for new webui 1.5.0 lora features

Prefers trigger words over the model-keyword ones
Uses custom per-lora multiplier if set
This commit is contained in:
DominikDoom
2023-07-26 14:38:51 +02:00
parent b28497764f
commit 2e271aea5c
7 changed files with 132 additions and 36 deletions

View File

@@ -124,23 +124,34 @@ Completion for these types is triggered by typing `<`. By default it will show t
- `<h:` or `<hypernet:` will only show Hypernetworks
### Lora / Lyco trigger word completion
This is an advanced feature that will try to add known trigger words on autocompleting a Lora/Lyco.
This feature will try to add known trigger words on autocompleting a Lora/Lyco.
It uses the list provided by the [model-keyword](https://github.com/mix1009/model-keyword/) extension, which thus needs to be installed to use this feature. The list is also regularly updated through it.
It primarily uses the list provided by the [model-keyword](https://github.com/mix1009/model-keyword/) extension, which thus needs to be installed to use this feature. The list is also regularly updated through it.
However, once installed, you can deactivate it if you want, since tag autocomplete only needs the local keyword lists it ships with, not the extension itself.
The used files are `lora-keywords.txt` and `lora-keywords-user.txt` in the model-keyword installation folder.
The used files are `lora-keyword.txt` and `lora-keyword-user.txt` in the model-keyword installation folder.
If the main file isn't found, the feature will simply deactivate itself, everything else should work normally.
To add custom mappings for unknown Loras, you can use the UI provided by model-keyword, it will automatically write it to the `lora-keywords-user.txt` for you (and create it if it doesn't exist).
The only issue is that it has no official support for the Lycoris extension and doesn't scan its folder for files, so to add them through the UI you will have to temporarily move them into the Lora model folder to be able to select them in model-keywords dropdown.
Some are already included in the default list though, so trying it out first is advisable.
<details>
<summary>Walkthrough to add custom keywords</summary>
#### Note:
As of [v1.5.0](https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/a3ddf464a2ed24c999f67ddfef7969f8291567be), the webui provides a native method to add activation keywords for Lora through the Extra networks config UI.
These trigger words will always be preferred over the model-keyword ones and can be used without needing to install the extension. This will however, obviously, be limited to those manually added keywords. For automatic discovery of keywords, you will still need the big list provided by model-keyword.
![image](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/4302c44e-c632-473d-a14a-76f164f966cb)
</details>
After having added your custom keywords, you will need to either restart the UI or use the "Refresh TAC temp files" setting button.
Custom trigger words can be added through two methods:
1. Using the extra networks UI (recommended):
- Only works with webui version v1.5.0 upwards, but much easier to use and works without the model-keyword extension
- This method requires no manual refresh
- <details>
<summary>Image example</summary>
![image](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/4302c44e-c632-473d-a14a-76f164f966cb)
</details>
2. Through the model-keyword UI:
- One issue with this method is that it has no official support for the Lycoris extension and doesn't scan its folder for files, so to add them through the UI you will have to temporarily move them into the Lora model folder to be able to select them in model-keywords dropdown. Some are already included in the default list though, so trying it out first is advisable.
- After having added your custom keywords, you will need to either restart the UI or use the "Refresh TAC temp files" setting button.
- <details>
<summary>Image example</summary>
![image](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/4302c44e-c632-473d-a14a-76f164f966cb)
</details>
Sometimes the inserted keywords can be wrong due to a hash collision, however model-keyword and tag autocomplete take the name of the file into account too if the collision is known.

View File

@@ -61,6 +61,26 @@ async function loadCSV(path) {
return parseCSV(text);
}
// Fetch API
async function fetchAPI(url, json = true, cache = false) {
if (!cache) {
const appendChar = url.includes("?") ? "&" : "?";
url += `${appendChar}${new Date().getTime()}`
}
let response = await fetch(url);
if (response.status != 200) {
console.error(`Error fetching API endpoint "${url}": ` + response.status, response.statusText);
return null;
}
if (json)
return await response.json();
else
return await response.text();
}
// Debounce function to prevent spamming the autocomplete function
var dbTimeOut;
const debounce = (func, wait = 300) => {

View File

@@ -38,9 +38,19 @@ async function load() {
}
}
function sanitize(tagType, text) {
async function sanitize(tagType, text) {
if (tagType === ResultType.lora) {
return `<lora:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
let multiplier = TAC_CFG.extraNetworksDefaultMultiplier;
let info = await fetchAPI(`tacapi/v1/lora-info/${text}`)
if (info && info["preferred weight"]) {
multiplier = info["preferred weight"];
}
const lastDot = text.lastIndexOf(".");
const lastSlash = text.lastIndexOf("/");
const name = text.substring(lastSlash + 1, lastDot);
return `<lora:${name}:${multiplier}>`;
}
return null;
}

View File

@@ -38,9 +38,19 @@ async function load() {
}
}
function sanitize(tagType, text) {
async function sanitize(tagType, text) {
if (tagType === ResultType.lyco) {
return `<lyco:${text}:${TAC_CFG.extraNetworksDefaultMultiplier}>`;
let multiplier = TAC_CFG.extraNetworksDefaultMultiplier;
let info = await fetchAPI(`tacapi/v1/lyco-info/${text}`)
if (info && info["preferred weight"]) {
multiplier = info["preferred weight"];
}
const lastDot = text.lastIndexOf(".");
const lastSlash = text.lastIndexOf("/");
const name = text.substring(lastSlash + 1, lastDot);
return `<lyco:${name}:${multiplier}>`;
}
return null;
}

View File

@@ -445,9 +445,18 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
// Add lora/lyco keywords if enabled and found
let keywordsLength = 0;
if (TAC_CFG.modelKeywordCompletion !== "Never" && modelKeywordPath.length > 0 && (tagType === ResultType.lora || tagType === ResultType.lyco)) {
if (result.hash && result.hash !== "NOFILE" && result.hash.length > 0) {
let keywords = null;
if (TAC_CFG.modelKeywordCompletion !== "Never" && (tagType === ResultType.lora || tagType === ResultType.lyco)) {
let keywords = null;
// Check built-in activation words first
if (tagType === ResultType.lora || tagType === ResultType.lyco) {
let info = await fetchAPI(`tacapi/v1/lora-info/${result.text}`)
if (info && info["activation text"]) {
keywords = info["activation text"];
}
}
if (!keywords && modelKeywordPath.length > 0 && result.hash && result.hash !== "NOFILE" && result.hash.length > 0) {
let nameDict = modelKeywordDict.get(result.hash);
let names = [result.text + ".safetensors", result.text + ".pt", result.text + ".ckpt"];
@@ -463,18 +472,18 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
if (!found)
keywords = nameDict.get("none");
}
}
if (keywords && keywords.length > 0) {
textBeforeKeywordInsertion = newPrompt;
newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords
textAfterKeywordInsertion = newPrompt;
keywordInsertionUndone = false;
setTimeout(() => lastEditWasKeywordInsertion = true, 200)
keywordsLength = keywords.length + 2; // +2 for the comma and space
}
if (keywords && keywords.length > 0) {
textBeforeKeywordInsertion = newPrompt;
newPrompt = `${keywords}, ${newPrompt}`; // Insert keywords
textAfterKeywordInsertion = newPrompt;
keywordInsertionUndone = false;
setTimeout(() => lastEditWasKeywordInsertion = true, 200)
keywordsLength = keywords.length + 2; // +2 for the comma and space
}
}
@@ -572,6 +581,11 @@ 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);
}

View File

@@ -75,6 +75,6 @@ def write_model_keyword_path():
return True
else:
print(
"Tag Autocomplete: Could not locate model-keyword extension, LORA/LYCO trigger word completion will be unavailable."
"Tag Autocomplete: Could not locate model-keyword extension, Lora trigger word completion will be limited to those added through the extra networks menu."
)
return False

View File

@@ -2,10 +2,13 @@
# to a temporary file to expose it to the javascript side
import glob
import json
from pathlib import Path
import gradio as gr
import yaml
from fastapi import FastAPI
from fastapi.responses import FileResponse
from modules import script_callbacks, sd_hijack, shared
from scripts.model_keyword_support import (get_lora_simple_hash,
@@ -142,12 +145,11 @@ def get_lora():
valid_loras = [lf for lf in lora_paths if lf.suffix in {".safetensors", ".ckpt", ".pt"}]
hashes = {}
for l in valid_loras:
name = l.name[:l.name.rfind('.')]
name = l.relative_to(LORA_PATH).as_posix()
if model_keyword_installed:
hashes[name] = get_lora_simple_hash(l)
else:
hashes[name] = ""
# Sort
sorted_loras = dict(sorted(hashes.items()))
# Add hashes and return
@@ -164,8 +166,11 @@ def get_lyco():
valid_lycos = [lyf for lyf in lyco_paths if lyf.suffix in {".safetensors", ".ckpt", ".pt"}]
hashes = {}
for ly in valid_lycos:
name = ly.name[:ly.name.rfind('.')]
hashes[name] = get_lora_simple_hash(ly)
name = ly.relative_to(LYCO_PATH).as_posix()
if model_keyword_installed:
hashes[name] = get_lora_simple_hash(ly)
else:
hashes[name] = ""
# Sort
sorted_lycos = dict(sorted(hashes.items()))
@@ -328,7 +333,7 @@ def on_ui_settings():
"tac_appendComma": shared.OptionInfo(True, "Append comma on tag autocompletion"),
"tac_appendSpace": shared.OptionInfo(True, "Append space on tag autocompletion").info("will append after comma if the above is enabled"),
"tac_alwaysSpaceAtEnd": shared.OptionInfo(True, "Always append space if inserting at the end of the textbox").info("takes precedence over the regular space setting for that position"),
"tac_modelKeywordCompletion": shared.OptionInfo("Never", "Try to add known trigger words for LORA/LyCO models", gr.Dropdown, lambda: {"interactive": model_keyword_installed, "choices": ["Never","Only user list","Always"]}).info("Requires the <a href=\"https://github.com/mix1009/model-keyword\" target=\"_blank\">model-keyword</a> extension to be installed, but will work with it disabled.").needs_restart(),
"tac_modelKeywordCompletion": shared.OptionInfo("Never", "Try to add known trigger words for LORA/LyCO models", gr.Dropdown, lambda: {"choices": ["Never","Only user list","Always"]}).info("Will use & prefer the native activation keywords settable in the extra networks UI. Other functionality requires the <a href=\"https://github.com/mix1009/model-keyword\" target=\"_blank\">model-keyword</a> extension to be installed, but will work with it disabled.").needs_restart(),
"tac_wildcardCompletionMode": shared.OptionInfo("To next folder level", "How to complete nested wildcard paths", gr.Dropdown, lambda: {"choices": ["To next folder level","To first difference","Always fully"]}).info("e.g. \"hair/colours/light/...\""),
# Alias settings
"tac_alias.searchByAlias": shared.OptionInfo(True, "Search by alias"),
@@ -401,3 +406,29 @@ def on_ui_settings():
shared.opts.add_option("tac_refreshTempFiles", shared.OptionInfo("Refresh TAC temp files", "Refresh internal temp files", gr.HTML, {}, refresh=refresh_temp_files, section=TAC_SECTION))
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({})
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())
except Exception as e:
return json.dumps({"error": e})
@app.get("/tacapi/v1/lora-info/{folder}/{lora_name}")
async def get_lora_info(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(folder, lyco_name):
if LYCO_PATH is None:
return json.dumps({})
return await get_json_info(LYCO_PATH.joinpath(folder).joinpath(lyco_name))
script_callbacks.on_app_started(api_tac)