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