From a6c726f8d6931a40e047e06b371fbce9b8198763 Mon Sep 17 00:00:00 2001 From: vladmandic Date: Mon, 13 Apr 2026 11:54:59 +0200 Subject: [PATCH] update tag-autocomplete Signed-off-by: vladmandic --- html/locale_en.json | 1 + javascript/autocomplete.js | 41 +++++++++++++++++++++++++++++-------- modules/api/api.py | 2 +- modules/api/autocomplete.py | 21 +++++++++---------- modules/ui_definitions.py | 3 +++ scripts/autocomplete.py | 36 ++++++++++++++++++++++---------- wiki | 2 +- 7 files changed, 74 insertions(+), 32 deletions(-) diff --git a/html/locale_en.json b/html/locale_en.json index 5658fb4b6..3bf4bf950 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -74,6 +74,7 @@ {"id":"","label":"Answer","localized":"","hint":"","ui":"caption"}, {"id":"","label":"Adjust start","localized":"","hint":"Starting step when sigma adjust occurs","ui":"txt2img"}, {"id":"","label":"Adjust end","localized":"","hint":"Ending step when sigma adjust occurs","ui":"txt2img"}, + {"id":"","label":"Autocomplete","localized":"","hint":"Enable or disable Tag Autocomplete. Choose which dictionaries are used for prompt autocompletion in Extras","ui":"control"}, {"id":"","label":"AutoGuidance dropout","localized":"","hint":"","ui":"txt2img"}, {"id":"","label":"AutoGuidance layers","localized":"","hint":"","ui":"txt2img"}, {"id":"","label":"AutoGuidance config","localized":"","hint":"","ui":"txt2img"}, diff --git a/javascript/autocomplete.js b/javascript/autocomplete.js index 6466d129b..9d4eab9c0 100644 --- a/javascript/autocomplete.js +++ b/javascript/autocomplete.js @@ -42,6 +42,8 @@ const CATEGORY_NAMES = { 13: 'color', }; +let active = false; + // -- Utilities (ported from Enso) -- /** Binary search for the first tag where tag.name >= prefix. */ @@ -141,7 +143,8 @@ const engine = { async loadEnabled() { const enabled = window.opts?.autocomplete_enabled || []; - if (!enabled.length) { + active = window.opts?.autocomplete_active || false; + if (!active) { this.indices.clear(); return; } @@ -161,9 +164,9 @@ const engine = { if (cat.name) this.categoryNames[id] = cat.name; }); } - log('autocomplete', `loaded ${name}: ${data.tags?.length || 0} tags`); + log('autoComplete', { loaded: name, tags: data.tags?.length || 0 }); } catch (e) { - log('autocomplete', `failed to load ${name}: ${e}`); + log('autoComplete', { failed: name, error: e }); } })); }, @@ -391,6 +394,7 @@ const dropdown = { let debounceTimer = null; function onInput(textarea) { + if (!active) return; const minChars = window.opts?.autocomplete_min_chars ?? 3; const info = getCurrentWord(textarea); if (!info || info.word.length < minChars) { @@ -457,6 +461,23 @@ const PROMPT_IDS = [ 'video_prompt', 'video_neg_prompt', ]; +// -- Active button -- + +function patchActiveButton() { + const buttons = [...gradioApp().querySelectorAll('.autocomplete-active')]; + active = window.opts?.autocomplete_active || false; + buttons.forEach((btn) => { + btn.classList.toggle('autocomplete-active', active); + btn.classList.toggle('autocomplete-inactive', !active); + btn.parentElement.onclick = () => { + active = !active; + window.opts.autocomplete_active = !active; + btn.classList.toggle('autocomplete-active', active); + btn.classList.toggle('autocomplete-inactive', !active); + }; + }); +} + // -- Config bridge -- /** Monkey-patch script config bridge textboxes to push autocomplete config changes to window.opts immediately. */ @@ -475,7 +496,6 @@ function patchConfigBridge() { try { const cfg = JSON.parse(newValue); for (const [key, val] of Object.entries(cfg)) window.opts[key] = val; - log('autocomplete', 'config updated via bridge'); executeCallbacks(optionsChangedCallbacks); } catch { /* ignore parse errors */ } } @@ -489,7 +509,8 @@ function patchConfigBridge() { async function initAutocomplete() { const enabled = window.opts?.autocomplete_enabled || []; - log('autocomplete', enabled.length ? `init: ${enabled.join(', ')}` : 'init: no dictionaries enabled yet'); + active = window.opts?.autocomplete_active || false; + log('autoComplete', { active, enabled }); // Inject styles (CSS files in javascript/ are not auto-loaded) const style = document.createElement('style'); style.textContent = [ @@ -522,18 +543,22 @@ async function initAutocomplete() { attached++; } }); - log('autocomplete', `attached to ${attached} textareas, ${engine.indices.size} dictionaries`); + log('autoComplete', { attached, dicts: engine.indices.size }); // Reload when settings change onOptionsChanged(async () => { + const newActive = window.opts?.autocomplete_active || false; const newEnabled = window.opts?.autocomplete_enabled || []; const currentKeys = [...engine.indices.keys()].sort().join(','); const newKeys = [...newEnabled].sort().join(','); - if (currentKeys !== newKeys) { - log('autocomplete', `reloading: ${newEnabled.join(', ')}`); + if ((currentKeys !== newKeys) || (active !== newActive)) { + log('autoComplete', { reload: newEnabled }); await engine.loadEnabled(); + active = newActive; + patchActiveButton(); } }); // Watch for config updates from the script UI bridge patchConfigBridge(); + patchActiveButton(); onAfterUiUpdate(() => patchConfigBridge()); } diff --git a/modules/api/api.py b/modules/api/api.py index 889ba6104..5d39ce4ab 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -121,7 +121,7 @@ class Api: # autocomplete api from modules.api import autocomplete as autocomplete_api autocomplete_api.init(getattr(shared.opts, 'autocomplete_dir', '') or os.path.join(paths.models_path, 'autocomplete')) - autocomplete_api.register_api(self.app) + autocomplete_api.register_api(self) # gallery api from modules.api import gallery diff --git a/modules/api/autocomplete.py b/modules/api/autocomplete.py index cb90baee6..33f4e6749 100644 --- a/modules/api/autocomplete.py +++ b/modules/api/autocomplete.py @@ -14,9 +14,9 @@ from fastapi.exceptions import HTTPException from modules.api.models import ItemAutocomplete, ItemAutocompleteContent, ItemAutocompleteRemote from modules.logger import log + autocomplete_dir: str = "" cache: dict[str, dict] = {} - HF_REPO = "CalamitousFelicitousness/prompt-vocab" HF_BASE = f"https://huggingface.co/datasets/{HF_REPO}/resolve/main" MANIFEST_CACHE_SEC = 300 # re-fetch manifest every 5 minutes @@ -40,7 +40,7 @@ def get_cached(name: str) -> dict: try: manifest = fetch_manifest_sync() if any(e.get('name') == name for e in manifest): - log.info(f"Auto-downloading autocomplete: {name}") + log.info(f'Autocomplete: name="{name}" auto-download') download_sync(name) else: raise HTTPException(status_code=404, detail=f"Not found: {name}") @@ -139,7 +139,7 @@ def fetch_manifest_sync() -> list[dict]: manifest_cache['fetched_at'] = now return entries except Exception as e: - log.warning(f"Failed to fetch autocomplete manifest: {e}") + log.warning(f"Autocomplete: Failed to fetch manifest: {e}") return manifest_cache.get('data', []) @@ -202,7 +202,6 @@ def download_sync(name: str) -> str: raise HTTPException(status_code=400, detail="Invalid name") os.makedirs(autocomplete_dir, exist_ok=True) url = f"{HF_BASE}/{name}.json" - log.info(f"Downloading autocomplete: {url}") try: resp = requests.get(url, timeout=120, stream=True) resp.raise_for_status() @@ -217,7 +216,7 @@ def download_sync(name: str) -> str: size += len(chunk) os.replace(tmp, target) cache.pop(name, None) - log.info(f"Downloaded autocomplete: {name} ({size / 1024 / 1024:.1f} MB)") + log.info(f'Autocomplete: name="{name}" url={url} ({size / 1024 / 1024:.2f}MB) downloaded') return target @@ -247,9 +246,9 @@ async def delete(name: str): return {"status": "deleted", "name": name} -def register_api(app): - app.add_api_route("/sdapi/v1/autocomplete", list_all, methods=["GET"], response_model=list[ItemAutocomplete], tags=["Enumerators"]) - app.add_api_route("/sdapi/v1/autocomplete/remote", list_remote, methods=["GET"], response_model=list[ItemAutocompleteRemote], tags=["Enumerators"]) - app.add_api_route("/sdapi/v1/autocomplete/{name}", get_content, methods=["GET"], response_model=ItemAutocompleteContent, tags=["Enumerators"]) - app.add_api_route("/sdapi/v1/autocomplete/{name}/download", download, methods=["POST"], response_model=ItemAutocomplete, tags=["Enumerators"]) - app.add_api_route("/sdapi/v1/autocomplete/{name}", delete, methods=["DELETE"], tags=["Enumerators"]) +def register_api(api): + api.add_api_route("/sdapi/v1/autocomplete", list_all, methods=["GET"], response_model=list[ItemAutocomplete], tags=["Enumerators"]) + api.add_api_route("/sdapi/v1/autocomplete/remote", list_remote, methods=["GET"], response_model=list[ItemAutocompleteRemote], tags=["Enumerators"]) + api.add_api_route("/sdapi/v1/autocomplete/{name}", get_content, methods=["GET"], response_model=ItemAutocompleteContent, tags=["Enumerators"]) + api.add_api_route("/sdapi/v1/autocomplete/{name}/download", download, methods=["POST"], response_model=ItemAutocomplete, tags=["Enumerators"]) + api.add_api_route("/sdapi/v1/autocomplete/{name}", delete, methods=["DELETE"], tags=["Enumerators"]) diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index f229889cf..80dab5cc7 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -656,11 +656,14 @@ def create_settings(cmd_opts): "disabled_extensions": OptionInfo([], "Disable these extensions", gr.Textbox, {"visible": False}), "sd_checkpoint_hash": OptionInfo("", "SHA256 hash of the current checkpoint", gr.Textbox, {"visible": False}), "tooltips": OptionInfo("UI Tooltips", "UI tooltips", gr.Radio, {"choices": ["None", "Browser default", "UI tooltips"], "visible": False}), + # Autocomplete settings (controlled via Tag Autocomplete script UI) + "autocomplete_active": OptionInfo(False, "Enable Autocomplete", gr.Checkbox, {"visible": False}), "autocomplete_enabled": OptionInfo([], "Enabled tag autocomplete files", gr.Dropdown, {"multiselect": True, "choices": [], "visible": False}), "autocomplete_min_chars": OptionInfo(3, "Min autocomplete chars", gr.Slider, {"minimum": 2, "maximum": 6, "step": 1, "visible": False}), "autocomplete_replace_underscores": OptionInfo(True, "Replace underscores in autocomplete", gr.Checkbox, {"visible": False}), "autocomplete_append_comma": OptionInfo(True, "Append comma after autocomplete", gr.Checkbox, {"visible": False}), + # Caption settings (controlled via Caption Tab UI) "caption_default_type": OptionInfo("VLM", "Default caption type", gr.Radio, {"choices": ["VLM", "OpenCLiP", "Tagger"], "visible": False}), "tagger_show_scores": OptionInfo(False, "Tagger: show confidence scores in results", gr.Checkbox, {"visible": False}), diff --git a/scripts/autocomplete.py b/scripts/autocomplete.py index f1e50e865..fea760a08 100644 --- a/scripts/autocomplete.py +++ b/scripts/autocomplete.py @@ -22,6 +22,7 @@ def get_all_names(): def get_config_json(): """Serialize autocomplete opts for the JS config bridge.""" return json.dumps({ + "autocomplete_active": bool(shared.opts.data.get('autocomplete_active', False)), "autocomplete_enabled": list(shared.opts.data.get('autocomplete_enabled', [])), "autocomplete_min_chars": shared.opts.data.get('autocomplete_min_chars', 3), "autocomplete_replace_underscores": shared.opts.data.get('autocomplete_replace_underscores', True), @@ -29,6 +30,12 @@ def get_config_json(): }) +def on_active_change(value): + shared.opts.data['autocomplete_active'] = bool(value) + shared.opts.save(silent=True) + return get_config_json(), "" + + def on_enabled_change(selected): shared.opts.data['autocomplete_enabled'] = list(selected) shared.opts.save(silent=True) @@ -60,13 +67,13 @@ def format_status(local, remote_entries, fetch_ok): for e in remote_entries: name = e.get('name', '') remote_names.add(name) - dl_status = 'local' if name in local else 'available' + dl_status = '' if name in local else symbols.save desc = e.get('description', '') - size = e.get('size_mb', 0) + # size = e.get('size_mb', 0) tags = e.get('tag_count', 0) - lines.append(f"{name} - {desc} ({tags:,} tags, {size:.1f} MB) [{dl_status}]") + lines.append(f"{name} | {desc} | {tags:,} tags {dl_status}") for name in sorted(local - remote_names): - lines.append(f"{name} [local]") + lines.append(f"{name}") if not fetch_ok: lines.insert(0, "Remote fetch failed; showing local files only") elif not lines: @@ -130,6 +137,12 @@ class AutocompleteScript(scripts_manager.Script): initial_enabled = list(shared.opts.data.get('autocomplete_enabled', [])) with gr.Accordion('Tag Autocomplete', open=False, elem_id='autocomplete_settings'): + with gr.Row(): + active_cb = gr.Checkbox( + label="Enable Autocomplete", + value=bool(shared.opts.data.get('autocomplete_active', False)), + elem_id=self.elem_id("active"), + ) with gr.Row(): enabled_dd = gr.Dropdown( label="Active dictionaries", @@ -142,12 +155,6 @@ class AutocompleteScript(scripts_manager.Script): refresh_btn = ToolButton(value=symbols.refresh, elem_id=self.elem_id("refresh")) update_btn = ToolButton(value=symbols.save, elem_id=self.elem_id("update")) with gr.Row(): - min_chars = gr.Slider( - label="Min characters", - minimum=2, maximum=6, step=1, - value=shared.opts.data.get('autocomplete_min_chars', 3), - elem_id=self.elem_id("min_chars"), - ) replace_underscores = gr.Checkbox( label="Replace underscores", value=shared.opts.data.get('autocomplete_replace_underscores', True), @@ -158,6 +165,12 @@ class AutocompleteScript(scripts_manager.Script): value=shared.opts.data.get('autocomplete_append_comma', True), elem_id=self.elem_id("append_comma"), ) + min_chars = gr.Slider( + label="Min characters", + minimum=2, maximum=6, step=1, + value=shared.opts.data.get('autocomplete_min_chars', 3), + elem_id=self.elem_id("min_chars"), + ) with gr.Row(): status = gr.HTML(value="", elem_id=self.elem_id("status")) config_json = gr.Textbox( @@ -166,6 +179,7 @@ class AutocompleteScript(scripts_manager.Script): elem_id=self.elem_id("config_json"), ) + active_cb.change(fn=on_active_change, inputs=[active_cb], outputs=[config_json, status]) enabled_dd.change(fn=on_enabled_change, inputs=[enabled_dd], outputs=[config_json, status]) min_chars.change(fn=on_min_chars_change, inputs=[min_chars], outputs=[config_json]) replace_underscores.change(fn=on_replace_underscores_change, inputs=[replace_underscores], outputs=[config_json]) @@ -176,4 +190,4 @@ class AutocompleteScript(scripts_manager.Script): for comp in [enabled_dd, min_chars, replace_underscores, append_comma, config_json, status]: comp.do_not_save_to_config = True - return [enabled_dd, min_chars, replace_underscores, append_comma, config_json] + return [active_cb, enabled_dd, min_chars, replace_underscores, append_comma, config_json] diff --git a/wiki b/wiki index cbbbfc73a..68225bb7f 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit cbbbfc73af2366650cdf8cc71fabbf3a508b607b +Subproject commit 68225bb7f11eba38d73f2b13758d4306fa05234b