diff --git a/README.md b/README.md index 1deb335..4d583fd 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,7 @@ Example with Chinese translation: ## List of translations - [🇨🇳 Chinese tags](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/23) by @HalfMAI, using machine translation and manual correction for the most common tags (uses legacy format) - [🇨🇳 Chinese tags](https://github.com/sgmklp/tag-for-autocompletion-with-translation) by @sgmklp, smaller set of manual translations based on https://github.com/zcyzcy88/TagTable +- [🇯🇵 Japanese tags](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/265) by @applemango, both machine and human translations available > ### 🫵 I need your help! > Translations are a community effort. If you have translated a tag file or want to create one, please open a Pull Request or Issue so your link can be added here. diff --git a/README_JA.md b/README_JA.md index a7a0901..c470afc 100644 --- a/README_JA.md +++ b/README_JA.md @@ -410,8 +410,9 @@ https://www.w3.org/TR/uievents-key/#named-key-attribute-value ![english-input](https://user-images.githubusercontent.com/34448969/200126513-bf6b3940-6e22-41b0-a369-f2b4640f87d6.png) ## 翻訳リスト -- [🇨🇳 Chinese tags](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/23) by @HalfMAI, 最も一般的なタグを機械翻訳と手作業で修正(レガシーフォーマットを使用) -- [🇨🇳 Chinese tags](https://github.com/sgmklp/tag-for-autocompletion-with-translation) by @sgmklp, [こちら](https://github.com/zcyzcy88/TagTable)をベースにして、より小さくした手動での翻訳セット。 +- [🇨🇳 中国語訳](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/23) by @HalfMAI, 最も一般的なタグを機械翻訳と手作業で修正(レガシーフォーマットを使用) +- [🇨🇳 中国語訳](https://github.com/sgmklp/tag-for-autocompletion-with-translation) by @sgmklp, [こちら](https://github.com/zcyzcy88/TagTable)をベースにして、より小さくした手動での翻訳セット。 +- [🇯🇵 日本語訳](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/discussions/265) by @applemango, 機械翻訳と人力翻訳の両方が利用可能。 > ### 🫵 あなたの助けが必要です! > 翻訳はコミュニティの努力により支えられています。もしあなたがタグファイルを翻訳したことがある場合、または作成したい場合は、あなたの成果をここに追加できるように、Pull RequestまたはIssueを開いてください。 diff --git a/javascript/__globals.js b/javascript/__globals.js index cf75e81..32cf94c 100644 --- a/javascript/__globals.js +++ b/javascript/__globals.js @@ -19,6 +19,7 @@ var loras = []; var lycos = []; var modelKeywordDict = new Map(); var chants = []; +var styleNames = []; // Selected model info for black/whitelisting var currentModelHash = ""; diff --git a/javascript/_result.js b/javascript/_result.js index 45c8f0a..8b81a1b 100644 --- a/javascript/_result.js +++ b/javascript/_result.js @@ -12,7 +12,8 @@ const ResultType = Object.freeze({ "hypernetwork": 8, "lora": 9, "lyco": 10, - "chant": 11 + "chant": 11, + "styleName": 12 }); // Class to hold result data and annotations to make it clearer to use diff --git a/javascript/_textAreas.js b/javascript/_textAreas.js index 3e8af58..8ce694c 100644 --- a/javascript/_textAreas.js +++ b/javascript/_textAreas.js @@ -79,6 +79,13 @@ const thirdParty = { "[id^=script_img2img_adetailer_ad_prompt] textarea", "[id^=script_img2img_adetailer_ad_negative_prompt] textarea" ] + }, + "deepdanbooru-object-recognition": { + "base": "#tab_deepdanboru_object_recg_tab", + "hasIds": false, + "selectors": [ + "Found tags", + ] } } @@ -187,4 +194,4 @@ function getTextAreaIdentifier(textArea) { break; } return modifier; -} \ No newline at end of file +} diff --git a/javascript/_utils.js b/javascript/_utils.js index c967d2b..f42cd80 100644 --- a/javascript/_utils.js +++ b/javascript/_utils.js @@ -123,6 +123,29 @@ async function getExtraNetworkPreviewURL(filename, type) { } } +lastStyleRefresh = 0; +// Refresh style file if needed +async function refreshStyleNamesIfChanged() { + // Only refresh once per second + currentTimestamp = new Date().getTime(); + if (currentTimestamp - lastStyleRefresh < 1000) return; + lastStyleRefresh = currentTimestamp; + + const response = await fetch(`tacapi/v1/refresh-styles-if-changed?${new Date().getTime()}`) + if (response.status === 304) { + // Not modified + } else if (response.status === 200) { + // Reload + QUEUE_FILE_LOAD.forEach(async fn => { + if (fn.toString().includes("styleNames")) + await fn.call(null, true); + }) + } else { + // Error + console.error(`Error refreshing styles.txt: ` + response.status, response.statusText); + } +} + // Debounce function to prevent spamming the autocomplete function var dbTimeOut; const debounce = (func, wait = 300) => { diff --git a/javascript/ext_chants.js b/javascript/ext_chants.js index 28ba2c4..30de964 100644 --- a/javascript/ext_chants.js +++ b/javascript/ext_chants.js @@ -41,7 +41,7 @@ async function load() { function sanitize(tagType, text) { if (tagType === ResultType.chant) { - return text.replace(/^.*?: /g, ""); + return text; } return null; } diff --git a/javascript/ext_embeddings.js b/javascript/ext_embeddings.js index 820aae4..1a7ae86 100644 --- a/javascript/ext_embeddings.js +++ b/javascript/ext_embeddings.js @@ -53,7 +53,7 @@ async function load() { function sanitize(tagType, text) { if (tagType === ResultType.embedding) { - return text.replace(/^.*?: /g, ""); + return text; } return null; } diff --git a/javascript/ext_lycos.js b/javascript/ext_lycos.js index ad6271e..94d5fe2 100644 --- a/javascript/ext_lycos.js +++ b/javascript/ext_lycos.js @@ -5,8 +5,8 @@ class LycoParser extends BaseTagParser { parse() { // Show lyco let tempResults = []; - if (tagword !== "<" && tagword !== " x.toLowerCase().includes(searchTerm) || x.toLowerCase().replaceAll(" ", "_").includes(searchTerm); tempResults = lycos.filter(x => filterCondition(x[0])); // Filter by tagword } else { @@ -52,7 +52,8 @@ async function sanitize(tagType, text) { multiplier = info["preferred weight"]; } - return ``; + let prefix = TAC_CFG.useLoraPrefixForLycos ? "lora" : "lyco"; + return `<${prefix}:${text}:${multiplier}>`; } return null; } diff --git a/javascript/ext_styles.js b/javascript/ext_styles.js new file mode 100644 index 0000000..7f26a9c --- /dev/null +++ b/javascript/ext_styles.js @@ -0,0 +1,67 @@ +const STYLE_REGEX = /(\$(\d*)\(?)[^$|\]\s]*\)?/; +const STYLE_TRIGGER = () => TAC_CFG.useStyleVars && tagword.match(STYLE_REGEX); + +var lastStyleVarIndex = ""; + +class StyleParser extends BaseTagParser { + async parse() { + // Refresh if needed + await refreshStyleNamesIfChanged(); + + // Show styles + let tempResults = []; + let matchGroups = tagword.match(STYLE_REGEX); + + // Save index to insert again later or clear last one + lastStyleVarIndex = matchGroups[2] ? matchGroups[2] : ""; + + if (tagword !== matchGroups[1]) { + let searchTerm = tagword.replace(matchGroups[1], ""); + + let filterCondition = x => x[0].toLowerCase().includes(searchTerm) || x[0].toLowerCase().replaceAll(" ", "_").includes(searchTerm); + tempResults = styleNames.filter(x => filterCondition(x)); // Filter by tagword + } else { + tempResults = styleNames; + } + + // Add final results + let finalResults = []; + tempResults.forEach(t => { + let result = new AutocompleteResult(t[0].trim(), ResultType.styleName) + result.meta = "Style"; + finalResults.push(result); + }); + + return finalResults; + } +} + +async function load(force = false) { + if (styleNames.length === 0 || force) { + try { + styleNames = (await loadCSV(`${tagBasePath}/temp/styles.txt`)) + .filter(x => x[0]?.trim().length > 0) // Remove empty lines + .filter(x => x[0] !== "None") // Remove "None" style + .map(x => [x[0].trim()]); // Trim name + } catch (e) { + console.error("Error loading styles.txt: " + e); + } + } +} + +function sanitize(tagType, text) { + if (tagType === ResultType.styleName) { + if (text.includes(" ")) { + return `$${lastStyleVarIndex}(${text})`; + } else { + return`$${lastStyleVarIndex}${text}` + } + } + return null; +} + +PARSERS.push(new StyleParser(STYLE_TRIGGER)); + +// Add our utility functions to their respective queues +QUEUE_FILE_LOAD.push(load); +QUEUE_SANITIZE.push(sanitize); \ No newline at end of file diff --git a/javascript/ext_wildcards.js b/javascript/ext_wildcards.js index 5762e8d..98c365a 100644 --- a/javascript/ext_wildcards.js +++ b/javascript/ext_wildcards.js @@ -153,7 +153,7 @@ function sanitize(tagType, text) { if (tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) { return `__${text}__`; } else if (tagType === ResultType.wildcardTag) { - return text.replace(/^.*?: /g, ""); + return text; } return null; } diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index afee57b..5737cb5 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -221,6 +221,7 @@ async function syncOptions() { useHypernetworks: opts["tac_useHypernetworks"], useLoras: opts["tac_useLoras"], useLycos: opts["tac_useLycos"], + useLoraPrefixForLycos: opts["tac_useLoraPrefixForLycos"], showWikiLinks: opts["tac_showWikiLinks"], showExtraNetworkPreviews: opts["tac_showExtraNetworkPreviews"], modelSortOrder: opts["tac_modelSortOrder"], @@ -229,6 +230,7 @@ async function syncOptions() { frequencyMinCount: opts["tac_frequencyMinCount"], frequencyMaxAge: opts["tac_frequencyMaxAge"], frequencyIncludeAlias: opts["tac_frequencyIncludeAlias"], + useStyleVars: opts["tac_useStyleVars"], // Insertion related settings replaceUnderscores: opts["tac_replaceUnderscores"], escapeParentheses: opts["tac_escapeParentheses"], @@ -407,9 +409,10 @@ function isEnabled() { const WEIGHT_REGEX = /[([]([^()[\]:|]+)(?::(?:\d+(?:\.\d+)?|\.\d+))?[)\]]/g; const POINTY_REGEX = /<[^\s,<](?:[^\t\n\r,<>]*>|[^\t\n\r,> ]*)/g; const COMPLETED_WILDCARD_REGEX = /__[^\s,_][^\t\n\r,_]*[^\s,_]__[^\s,_]*/g; +const STYLE_VAR_REGEX = /\$\(?[^$|\]\s]*\)?/g; const NORMAL_TAG_REGEX = /[^\s,|<>\]:]+_\([^\s,|<>\]:]*\)?|[^\s,|<>():\]]+|?/g; -const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g"); +const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${STYLE_VAR_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g"); // On click, insert the tag into the prompt textbox with respect to the cursor position async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithoutChoice = false) { @@ -606,6 +609,9 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout textArea.selectionStart = afterInsertCursorPos + optionalSeparator.length + keywordsLength; textArea.selectionEnd = textArea.selectionStart + // Set self trigger flag to show wildcard contents after the filename was inserted + if ([ResultType.wildcardFile, ResultType.yamlWildcard, ResultType.umiWildcard].includes(result.type)) + tacSelfTrigger = true; // Since we've modified a Gradio Textbox component manually, we need to simulate an `input` DOM event to ensure it's propagated back to python. // Uses a built-in method from the webui's ui.js which also already accounts for event target if (tagType === ResultType.wildcardTag || tagType === ResultType.wildcardFile || tagType === ResultType.yamlWildcard) @@ -1050,7 +1056,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { .map(match => match[1]); let tags = prompt.match(TAG_REGEX) if (weightedTags !== null && tags !== null) { - tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<["))) + tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$("))) .concat(weightedTags); } diff --git a/scripts/model_keyword_support.py b/scripts/model_keyword_support.py index 43c8f38..eaf7d99 100644 --- a/scripts/model_keyword_support.py +++ b/scripts/model_keyword_support.py @@ -16,6 +16,8 @@ hash_dict = {} def load_hash_cache(): + if not known_hashes_file.exists(): + known_hashes_file.touch() with open(known_hashes_file, "r", encoding="utf-8") as file: reader = csv.reader( file.readlines(), delimiter=",", quotechar='"', skipinitialspace=True @@ -28,6 +30,8 @@ def load_hash_cache(): def update_hash_cache(): global file_needs_update if file_needs_update: + if not known_hashes_file.exists(): + known_hashes_file.touch() with open(known_hashes_file, "w", encoding="utf-8", newline='') as file: writer = csv.writer(file) for name, (hash, mtime) in hash_dict.items(): diff --git a/scripts/tag_autocomplete_helper.py b/scripts/tag_autocomplete_helper.py index 0a0ae15..ff4b20c 100644 --- a/scripts/tag_autocomplete_helper.py +++ b/scripts/tag_autocomplete_helper.py @@ -11,7 +11,7 @@ from pathlib import Path import gradio as gr import yaml from fastapi import FastAPI -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import Response, FileResponse, JSONResponse from modules import script_callbacks, sd_hijack, shared, hashes from pydantic import BaseModel @@ -85,12 +85,13 @@ def get_wildcards(): def get_ext_wildcards(): """Returns a list of all extension wildcards. Works on nested folders.""" wildcard_files = [] - + excluded_folder_names = [s.strip() for s in getattr(shared.opts, "tac_wildcardExclusionList", "").split(",")] for path in WILDCARD_EXT_PATHS: wildcard_files.append(path.as_posix()) resolved = [(w, w.relative_to(path).as_posix()) for w in path.rglob("*.txt") if w.name != "put wildcards here.txt" + and not any(excluded in w.parts for excluded in excluded_folder_names) and w.is_file()] wildcard_files.extend(sort_models(resolved, name_has_subpath=True)) wildcard_files.append("-----") @@ -293,6 +294,8 @@ def get_lora(): valid_loras = _get_lora() loras_with_hash = [] for l in valid_loras: + if not l.exists() or not l.is_file(): + continue name = l.relative_to(LORA_PATH).as_posix() if model_keyword_installed: hash = get_lora_simple_hash(l) @@ -309,6 +312,8 @@ def get_lyco(): valid_lycos = _get_lyco() lycos_with_hash = [] for ly in valid_lycos: + if not ly.exists() or not ly.is_file(): + continue name = ly.relative_to(LYCO_PATH).as_posix() if model_keyword_installed: hash = get_lora_simple_hash(ly) @@ -318,6 +323,13 @@ def get_lyco(): # Sort return sort_models(lycos_with_hash) +def get_style_names(): + try: + style_names: list[str] = shared.prompt_styles.styles.keys() + style_names = sorted(style_names, key=len, reverse=True) + return style_names + except Exception: + return None def write_tag_base_path(): """Writes the tag base path to a fixed location temporary file""" @@ -372,6 +384,7 @@ write_to_temp_file('umi_tags.txt', []) write_to_temp_file('hyp.txt', []) write_to_temp_file('lora.txt', []) write_to_temp_file('lyco.txt', []) +write_to_temp_file('styles.txt', []) # Only reload embeddings if the file doesn't exist, since they are already re-written on model load if not TEMP_PATH.joinpath("emb.txt").exists(): write_to_temp_file('emb.txt', []) @@ -396,19 +409,26 @@ def refresh_embeddings(force: bool, *args, **kwargs): def refresh_temp_files(*args, **kwargs): global WILDCARD_EXT_PATHS - WILDCARD_EXT_PATHS = find_ext_wildcard_paths() - write_temp_files() + skip_wildcard_refresh = getattr(shared.opts, "tac_skipWildcardRefresh", False) + if skip_wildcard_refresh: + WILDCARD_EXT_PATHS = find_ext_wildcard_paths() + write_temp_files(skip_wildcard_refresh) refresh_embeddings(force=True) -def write_temp_files(): +def write_style_names(*args, **kwargs): + styles = get_style_names() + if styles: + write_to_temp_file('styles.txt', styles) + +def write_temp_files(skip_wildcard_refresh = False): # Write wildcards to wc.txt if found - if WILDCARD_PATH.exists(): + if WILDCARD_PATH.exists() and not skip_wildcard_refresh: wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards() if wildcards: write_to_temp_file('wc.txt', wildcards) # Write extension wildcards to wce.txt if found - if WILDCARD_EXT_PATHS is not None: + if WILDCARD_EXT_PATHS is not None and not skip_wildcard_refresh: wildcards_ext = get_ext_wildcards() if wildcards_ext: write_to_temp_file('wce.txt', wildcards_ext) @@ -440,6 +460,8 @@ def write_temp_files(): if model_keyword_installed: update_hash_cache() + if shared.prompt_styles is not None: + write_style_names() write_temp_files() @@ -485,14 +507,18 @@ def on_ui_settings(): "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_wildcardExclusionList": shared.OptionInfo("", "Wildcard folder exclusion list").info("Add folder names that shouldn't be searched for wildcards, separated by comma.").needs_restart(), + "tac_skipWildcardRefresh": shared.OptionInfo(False, "Don't re-scan for wildcard files when pressing the extra networks refresh button").info("Useful to prevent hanging if you use a very large wildcard collection."), "tac_useEmbeddings": shared.OptionInfo(True, "Search for embeddings"), "tac_includeEmbeddingsInNormalResults": shared.OptionInfo(False, "Include embeddings in normal tag results").info("The 'JumpTo...' keybinds (End & Home key by default) will select the first non-embedding result of their direction on the first press for quick navigation in longer lists."), "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_useLoraPrefixForLycos": shared.OptionInfo(True, "Use the 'style-vars to actually apply the styles before generating."), # Frequency sorting settings "tac_frequencySort": shared.OptionInfo(True, "Locally record tag usage and sort frequent tags higher").info("Will also work for extra networks, keeping the specified base order"), "tac_frequencyFunction": shared.OptionInfo("Logarithmic (weak)", "Function to use for frequency sorting", gr.Dropdown, lambda: {"choices": list(frequency_sort_functions.keys())}).info("; ".join([f'{key}: {val}' for key, val in frequency_sort_functions.items()])), @@ -580,10 +606,18 @@ def on_ui_settings(): script_callbacks.on_ui_settings(on_ui_settings) +def get_style_mtime(): + style_file = getattr(shared, "styles_filename", "styles.csv") + style_file = Path(FILE_DIR).joinpath(style_file) + if Path.exists(style_file): + return style_file.stat().st_mtime + +last_style_mtime = get_style_mtime() + def api_tac(_: gr.Blocks, app: FastAPI): 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) + return Response(status_code=404) try: json_candidates = glob.glob(base_path.as_posix() + f"/**/{filename}.json", recursive=True) @@ -594,7 +628,7 @@ def api_tac(_: gr.Blocks, app: FastAPI): 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) + return Response(status_code=404) try: img_glob = glob.glob(base_path.as_posix() + f"/**/{filename}.*", recursive=True) @@ -658,21 +692,35 @@ def api_tac(_: gr.Blocks, app: FastAPI): @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) + return Response(status_code=404) base = Path(basepath) if base is None or (not base.exists()): - return JSONResponse({}, status_code=404) + return Response(status_code=404) try: wildcard_path = base.joinpath(filename) if wildcard_path.exists() and wildcard_path.is_file(): return FileResponse(wildcard_path) else: - return JSONResponse({}, status_code=404) + return Response(status_code=404) except Exception as e: return JSONResponse({"error": e}, status_code=500) + @app.get("/tacapi/v1/refresh-styles-if-changed") + async def refresh_styles_if_changed(): + global last_style_mtime + + mtime = get_style_mtime() + if mtime > last_style_mtime: + last_style_mtime = mtime + # Update temp file + if shared.prompt_styles is not None: + write_style_names() + + return Response(status_code=200) # Success + else: + return Response(status_code=304) # Not modified def db_request(func, get = False): if db is not None: try: