From 2e271aea5c5b03cd7691dd085e0f2dca44e80d6a Mon Sep 17 00:00:00 2001 From: DominikDoom Date: Wed, 26 Jul 2023 14:38:51 +0200 Subject: [PATCH] Support for new webui 1.5.0 lora features Prefers trigger words over the model-keyword ones Uses custom per-lora multiplier if set --- README.md | 35 ++++++++++++++++--------- javascript/_utils.js | 20 ++++++++++++++ javascript/ext_loras.js | 14 ++++++++-- javascript/ext_lycos.js | 14 ++++++++-- javascript/tagAutocomplete.js | 42 ++++++++++++++++++++---------- scripts/model_keyword_support.py | 2 +- scripts/tag_autocomplete_helper.py | 41 +++++++++++++++++++++++++---- 7 files changed, 132 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 5421788..d51b509 100644 --- a/README.md +++ b/README.md @@ -124,23 +124,34 @@ Completion for these types is triggered by typing `<`. By default it will show t - ` -Walkthrough to add custom keywords +#### 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) - -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 + -
+ Image example + + ![image](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/4302c44e-c632-473d-a14a-76f164f966cb) +
+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. + -
+ Image example + + ![image](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/4302c44e-c632-473d-a14a-76f164f966cb) +
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. diff --git a/javascript/_utils.js b/javascript/_utils.js index b3728d1..4fea691 100644 --- a/javascript/_utils.js +++ b/javascript/_utils.js @@ -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) => { diff --git a/javascript/ext_loras.js b/javascript/ext_loras.js index 56e5473..b5b9458 100644 --- a/javascript/ext_loras.js +++ b/javascript/ext_loras.js @@ -38,9 +38,19 @@ async function load() { } } -function sanitize(tagType, text) { +async function sanitize(tagType, text) { if (tagType === ResultType.lora) { - return ``; + 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 ``; } return null; } diff --git a/javascript/ext_lycos.js b/javascript/ext_lycos.js index f24fa01..b053ecb 100644 --- a/javascript/ext_lycos.js +++ b/javascript/ext_lycos.js @@ -38,9 +38,19 @@ async function load() { } } -function sanitize(tagType, text) { +async function sanitize(tagType, text) { if (tagType === ResultType.lyco) { - return ``; + 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 ``; } return null; } diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index cfad76a..c4ec5b2 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -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); } diff --git a/scripts/model_keyword_support.py b/scripts/model_keyword_support.py index f51104b..4ce6416 100644 --- a/scripts/model_keyword_support.py +++ b/scripts/model_keyword_support.py @@ -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 diff --git a/scripts/tag_autocomplete_helper.py b/scripts/tag_autocomplete_helper.py index 5635aad..7d91371 100644 --- a/scripts/tag_autocomplete_helper.py +++ b/scripts/tag_autocomplete_helper.py @@ -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 model-keyword 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 model-keyword 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) \ No newline at end of file