From def6ebb798d67dc38d2090a37147276aed795a35 Mon Sep 17 00:00:00 2001 From: Dominik Reh Date: Mon, 21 Nov 2022 19:11:20 +0100 Subject: [PATCH] Initial changes for settings migration --- javascript/tagAutocomplete.js | 221 +++++++++++++++++------------ scripts/tag_autocomplete_helper.py | 37 ++++- 2 files changed, 164 insertions(+), 94 deletions(-) diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index 04f99f9..9e5f9aa 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -1,6 +1,56 @@ -var acConfig = null; -var acActive = true; -var acAppendComma = false; +var CFG = null; + +function syncOptions() { + let newCFG = { + // Main tag file + tagFile: opts["tac_tagFile"], + // Active in settings + activeIn: { + global: opts["tac_active"], + txt2img: opts["tac_activeIn.txt2img"], + img2img: opts["tac_activeIn.img2img"], + negativePrompts: opts["tac_activeIn.negativePrompts"] + }, + // Results related settings + maxResults: opts["tac_maxResults"], + showAllResults: opts["tac_showAllResults"], + resultStepLength: opts["tac_resultStepLength"], + delayTime: opts["tac_delayTime"], + useWildcards: opts["tac_useWildcards"], + useEmbeddings: opts["tac_useEmbeddings"], + useLeftRightArrowKeys: opts["tac_useLeftRightArrowKeys"], + // Insertion related settings + replaceUnderscores: opts["tac_replaceUnderscores"], + escapeParentheses: opts["tac_escapeParentheses"], + appendComma: opts["tac_appendComma"], + // Alias settings + alias: { + searchByAlias: opts["tac_alias.searchByAlias"], + onlyShowAlias: opts["tac_alias.onlyShowAlias"] + }, + // Translation settings + translation: { + translationFile: opts["tac_translation.translationFile"], + oldFormat: opts["tac_translation.oldFormat"], + searchByTranslation: opts["tac_translation.searchByTranslation"], + }, + // Extra file settings + extra: { + extraFile: opts["tac_extra.extraFile"], + onlyAliasExtraFile: opts["tac_extra.onlyAliasExtraFile"] + } + } + + if (CFG && CFG.colors) { + newCFG["colors"] = CFG.colors; + } + if (newCFG.alias.onlyShowAlias) { + newCFG.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary + } + + // Apply changes + CFG = newCFG; +} const styleColors = { "--results-bg": ["#0b0f19", "#ffffff"], @@ -18,7 +68,13 @@ const browserVars = { } } // Style for new elements. Gets appended to the Gradio root. -let autocompleteCSS = ` +const autocompleteCSS = ` + #quicksettings [id^=setting_tac] { + background-color: transparent; + min-width: fit-content; + align-self: center; + margin: 0 5px; + } .autocompleteResults { position: absolute; z-index: 999; @@ -180,7 +236,7 @@ function createResultsDiv(textArea) { let textAreaId = getTextAreaIdentifier(textArea); let typeClass = textAreaId.replaceAll(".", " "); - resultsDiv.style.setProperty("max-height", acConfig.maxResults * 50 + "px"); + resultsDiv.style.setProperty("max-height", CFG.maxResults * 50 + "px"); resultsDiv.setAttribute('class', `autocompleteResults ${typeClass}`); resultsList.setAttribute('class', 'autocompleteResultsList'); resultsDiv.appendChild(resultsList); @@ -258,10 +314,10 @@ function insertTextAtCursor(textArea, result, tagword) { } else if (tagType === "embedding") { sanitizedText = `<${text.replace(/^.*?: /g, "")}>`; } else { - sanitizedText = acConfig.replaceUnderscores ? text.replaceAll("_", " ") : text; + sanitizedText = CFG.replaceUnderscores ? text.replaceAll("_", " ") : text; } - if (acConfig.escapeParentheses) { + if (CFG.escapeParentheses) { sanitizedText = sanitizedText .replaceAll("(", "\\(") .replaceAll(")", "\\)") @@ -279,7 +335,7 @@ function insertTextAtCursor(textArea, result, tagword) { let afterInsertCursorPos = editStart + match.index + sanitizedText.length; var optionalComma = ""; - if (acAppendComma && tagType !== "wildcardFile") { + if (CFG.appendComma && tagType !== "wildcardFile") { optionalComma = surrounding.match(new RegExp(`${escapeRegExp(tagword)}[,:]`, "i")) !== null ? "" : ", "; } @@ -331,10 +387,10 @@ function addResultsToList(textArea, results, tagword, resetList) { } // Find right colors from config - let tagFileName = acConfig.tagFile.split(".")[0]; - let tagColors = acConfig.colors; + let tagFileName = CFG.tagFile.split(".")[0]; + let tagColors = CFG.colors; let mode = gradioApp().querySelector('.dark') ? 0 : 1; - let nextLength = Math.min(results.length, resultCount + acConfig.resultStepLength); + let nextLength = Math.min(results.length, resultCount + CFG.resultStepLength); for (let i = resultCount; i < nextLength; i++) { let result = results[i]; @@ -371,7 +427,7 @@ function addResultsToList(textArea, results, tagword, resetList) { if (translations.has(bestAlias) && translations.get(bestAlias) !== bestAlias && bestAlias !== result[0]) displayText += `[${translations.get(bestAlias)}]`; - if (!acConfig.alias.onlyShowAlias && result[0] !== bestAlias) + if (!CFG.alias.onlyShowAlias && result[0] !== bestAlias) displayText += " ➝ " + result[0]; } else { // No alias displayText = escapeHTML(result[0]); @@ -444,7 +500,7 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) { } // Set scrolltop to selected item if we are showing more than max results - if (items.length > acConfig.maxResults) { + if (items.length > CFG.maxResults) { let selected = items[newIndex]; resultDiv.scrollTop = selected.offsetTop - resultDiv.offsetTop; } @@ -460,7 +516,7 @@ var tagword = ""; var resultCount = 0; async function autocomplete(textArea, prompt, fixedTag = null) { // Return if the function is deactivated in the UI - if (!acActive) return; + if (!CFG.activeIn.global) return; // Guard for empty prompt if (prompt.length === 0) { @@ -502,7 +558,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { tagword = tagword.toLowerCase().replace(/[\n\r]/g, ""); - if (acConfig.useWildcards && [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)].length > 0) { + if (CFG.useWildcards && [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)].length > 0) { // Show wildcards from a file with that name wcMatch = [...tagword.matchAll(/\b__([^, ]+)__([^, ]*)\b/g)] let wcFile = wcMatch[0][1]; @@ -521,7 +577,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { results = wildcards.filter(x => (wcWord !== null && wcWord.length > 0) ? x.toLowerCase().includes(wcWord) : x) // Filter by tagword .map(x => [wcFile + ": " + x.trim(), "wildcardTag"]); // Mark as wildcard - } else if (acConfig.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__")) { + } else if (CFG.useWildcards && (tagword.startsWith("__") && !tagword.endsWith("__") || tagword === "__")) { // Show available wildcard files let tempResults = []; if (tagword !== "__") { @@ -531,7 +587,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { tempResults = wildcardFiles.concat(wildcardExtFiles); } results = tempResults.map(x => ["Wildcards: " + x[1].trim(), "wildcardFile"]); // Mark as wildcard - } else if (acConfig.useEmbeddings && tagword.match(/<[^,> ]*>?/g)) { + } else if (CFG.useEmbeddings && tagword.match(/<[^,> ]*>?/g)) { // Show embeddings let tempResults = []; if (tagword !== "<") { @@ -548,7 +604,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { } else { searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); } - genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, acConfig.maxResults); + genericResults = allTags.filter(x => x[0].toLowerCase().search(searchRegex) > -1).slice(0, CFG.maxResults); results = genericResults.concat(tempResults.map(x => ["Embeddings: " + x.trim(), "embedding"])); // Mark as embedding } else { // Create escaped search regex with support for * as a start placeholder @@ -560,7 +616,7 @@ async function autocomplete(textArea, prompt, fixedTag = null) { searchRegex = new RegExp(`(^|[^a-zA-Z])${escapeRegExp(tagword)}`, 'i'); } // If onlyShowAlias is enabled, we don't need to include normal results - if (acConfig.alias.onlyShowAlias) { + if (CFG.alias.onlyShowAlias) { results = allTags.filter(x => x[3] && x[3].toLowerCase().search(searchRegex) >- 1); } else { // Else both normal tags and aliases/translations are included depending on the config @@ -570,11 +626,11 @@ async function autocomplete(textArea, prompt, fixedTag = null) { || x[3] && x[3].split(",").some(y => translations.has(y) && translations.get(y).toLowerCase().search(searchRegex) >- 1); let fil; - if (acConfig.alias.searchByAlias && acConfig.translation.searchByTranslation) + if (CFG.alias.searchByAlias && CFG.translation.searchByTranslation) fil = (x) => baseFilter(x) || aliasFilter(x) || translationFilter(x); - else if (acConfig.alias.searchByAlias && !acConfig.translation.searchByTranslation) + else if (CFG.alias.searchByAlias && !CFG.translation.searchByTranslation) fil = (x) => baseFilter(x) || aliasFilter(x); - else if (acConfig.translation.searchByTranslation && !acConfig.alias.searchByAlias) + else if (CFG.translation.searchByTranslation && !CFG.alias.searchByAlias) fil = (x) => baseFilter(x) || translationFilter(x); else fil = (x) => baseFilter(x); @@ -582,8 +638,8 @@ async function autocomplete(textArea, prompt, fixedTag = null) { results = allTags.filter(fil); } // Slice if the user has set a max result count - if (!acConfig.showAllResults) { - results = results.slice(0, acConfig.maxResults); + if (!CFG.showAllResults) { + results = results.slice(0, CFG.maxResults); } } @@ -600,10 +656,10 @@ async function autocomplete(textArea, prompt, fixedTag = null) { var oldSelectedTag = null; function navigateInList(textArea, event) { // Return if the function is deactivated in the UI - if (!acActive) return; + if (!CFG.activeIn.global) return; validKeys = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", "Enter", "Tab", "Escape"]; - if (acConfig.useLeftRightArrowKeys) + if (CFG.useLeftRightArrowKeys) validKeys.push("ArrowLeft", "ArrowRight"); if (!validKeys.includes(event.key)) return; @@ -682,35 +738,36 @@ function navigateInList(textArea, event) { event.stopPropagation(); } +onUiUpdate(() => { + if (Object.keys(opts).length === 0) return; + if (CFG) return; + + syncOptions(); + + // Rest of setup + setup(); +}); + // One-time setup -document.addEventListener("DOMContentLoaded", async () => { +async function setup() { // Get our tag base path from the temp file let tagBasePath = await readFile(`tmp/tagAutocompletePath.txt?${new Date().getTime()}`); - // Load config - if (acConfig === null) { - try { - acConfig = await readFile(`${tagBasePath}/config.json?${new Date().getTime()}`, true); - if (acConfig.alias.onlyShowAlias) { - acConfig.alias.searchByAlias = true; // if only show translation, enable search by translation is necessary - } - } catch (e) { - console.error("Error loading config.json: " + e); - return; - } - } + // Load colors + CFG["colors"] = (await readFile(`${tagBasePath}/config.json?${new Date().getTime()}`, true)).colors; + // Load main tags and aliases if (allTags.length === 0) { try { - allTags = await loadCSV(`${tagBasePath}/${acConfig.tagFile}?${new Date().getTime()}`); + allTags = await loadCSV(`${tagBasePath}/${CFG.tagFile}?${new Date().getTime()}`); } catch (e) { console.error("Error loading tags file: " + e); return; } - if (acConfig.extra.extraFile) { + if (CFG.extra.extraFile) { try { - extras = await loadCSV(`${tagBasePath}/${acConfig.extra.extraFile}?${new Date().getTime()}`); - if (acConfig.extra.onlyAliasExtraFile) { + extras = await loadCSV(`${tagBasePath}/${CFG.extra.extraFile}?${new Date().getTime()}`); + if (CFG.extra.onlyAliasExtraFile) { // This works purely on index, so it's not very robust. But a lot faster. for (let i = 0, n = extras.length; i < n; i++) { if (extras[i][0]) { @@ -746,11 +803,11 @@ document.addEventListener("DOMContentLoaded", async () => { } } // Load translations - if (acConfig.translation.translationFile) { + if (CFG.translation.translationFile) { try { - let tArray = await loadCSV(`${tagBasePath}/${acConfig.translation.translationFile}?${new Date().getTime()}`); + let tArray = await loadCSV(`${tagBasePath}/${CFG.translation.translationFile}?${new Date().getTime()}`); tArray.forEach(t => { - if (acConfig.translation.oldFormat) + if (CFG.translation.oldFormat) translations.set(t[0], t[2]); else translations.set(t[0], t[1]); @@ -761,7 +818,7 @@ document.addEventListener("DOMContentLoaded", async () => { } } // Load wildcards - if (acConfig.useWildcards && wildcardFiles.length === 0) { + if (wildcardFiles.length === 0) { try { let wcFileArr = (await readFile(`${tagBasePath}/temp/wc.txt?${new Date().getTime()}`)).split("\n"); let wcBasePath = wcFileArr[0].trim(); // First line should be the base path @@ -797,7 +854,7 @@ document.addEventListener("DOMContentLoaded", async () => { } } // Load embeddings - if (acConfig.useEmbeddings && embeddings.length === 0) { + if (embeddings.length === 0) { try { embeddings = (await readFile(`${tagBasePath}/temp/emb.txt?${new Date().getTime()}`)).split("\n") .filter(x => x.trim().length > 0) // Remove empty lines @@ -814,25 +871,41 @@ document.addEventListener("DOMContentLoaded", async () => { let img2imgTextArea_n = gradioApp().querySelector('#img2img_neg_prompt > label > textarea'); let textAreas = [txt2imgTextArea, img2imgTextArea, txt2imgTextArea_n, img2imgTextArea_n]; + // Add event listener to apply settings button so we can mirror the changes to our internal config + let applySettingsButton = gradioApp().querySelector("#tab_settings > div > .gr-button-primary"); + applySettingsButton.addEventListener("click", () => { + // Wait 500ms to make sure the settings have been applied to the webui opts object + setTimeout(async () => { + syncOptions(); + }, 500); + }); + // Add change listener to our quicksettings to change our internal config without the apply button for them let quicksettings = gradioApp().querySelector('#quicksettings'); + quicksettings.querySelectorAll("[id^=setting_tac] > label > input, [id^=setting_tac] > label > textarea").forEach(e => { + e.addEventListener("change", () => { + setTimeout(async () => { + syncOptions(); + }, 500); + }); + }); // Not found, we're on a page without prompt textareas if (textAreas.every(v => v === null || v === undefined)) return; // Already added or unnecessary to add if (gradioApp().querySelector('.autocompleteResults.p')) { - if (gradioApp().querySelector('.autocompleteResults.n') || !acConfig.activeIn.negativePrompts) { + if (gradioApp().querySelector('.autocompleteResults.n') || !CFG.activeIn.negativePrompts) { return; } - } else if (!acConfig.activeIn.txt2img && !acConfig.activeIn.img2img) { + } else if (!CFG.activeIn.txt2img && !CFG.activeIn.img2img) { return; } textAreas.forEach(area => { // Return if autocomplete is disabled for the current area type in config let textAreaId = getTextAreaIdentifier(area); - if ((!acConfig.activeIn.img2img && textAreaId.includes("img2img")) - || (!acConfig.activeIn.txt2img && textAreaId.includes("txt2img")) - || (!acConfig.activeIn.negativePrompts && textAreaId.includes("n"))) { + if ((!CFG.activeIn.img2img && textAreaId.includes("img2img")) + || (!CFG.activeIn.txt2img && textAreaId.includes("txt2img")) + || (!CFG.activeIn.negativePrompts && textAreaId.includes("n"))) { return; } @@ -845,7 +918,7 @@ document.addEventListener("DOMContentLoaded", async () => { hideResults(area); // Add autocomplete event listener - area.addEventListener('input', debounce(() => autocomplete(area, area.value), acConfig.delayTime)); + area.addEventListener('input', debounce(() => autocomplete(area, area.value), CFG.delayTime)); // Add focusout event listener area.addEventListener('focusout', debounce(() => hideResults(area), 400)); // Add up and down arrow event listener @@ -862,44 +935,6 @@ document.addEventListener("DOMContentLoaded", async () => { } }); - acAppendComma = acConfig.appendComma; - // Add our custom options elements - if (!acConfig.hideUIOptions && gradioApp().querySelector("#tagAutocompleteOptions") === null) { - let optionsDiv = document.createElement("div"); - optionsDiv.id = "tagAutocompleteOptions"; - optionsDiv.classList.add("flex", "flex-col", "p-1", "px-1", "relative", "text-sm"); - - let optionsInner = document.createElement("div"); - optionsInner.classList.add("flex", "flex-row", "p-1", "gap-4", "text-gray-700"); - - // Add label - let title = document.createElement("p"); - title.textContent = "Autocomplete options"; - optionsDiv.appendChild(title); - - // Add toggle switch - let cbActive = createCheckbox("Enable Autocomplete"); - cbActive.querySelector("input").checked = acActive; - cbActive.querySelector("input").addEventListener("change", (e) => { - acActive = e.target.checked; - }); - // Add comma switch - let cbComma = createCheckbox("Append commas"); - cbComma.querySelector("input").checked = acAppendComma; - cbComma.querySelector("input").addEventListener("change", (e) => { - acAppendComma = e.target.checked; - }); - - // Add options to optionsDiv - optionsInner.appendChild(cbActive); - optionsInner.appendChild(cbComma); - optionsDiv.appendChild(optionsInner); - - // Add options div to DOM - //quicksettings.parentNode.insertBefore(optionsDiv, quicksettings.nextSibling); - quicksettings.appendChild(optionsDiv); - } - // Add style to dom let acStyle = document.createElement('style'); //let css = gradioApp().querySelector('.dark') ? autocompleteCSS_dark : autocompleteCSS_light; @@ -922,4 +957,4 @@ document.addEventListener("DOMContentLoaded", async () => { acStyle.appendChild(document.createTextNode(css)); } gradioApp().appendChild(acStyle); -}); +} diff --git a/scripts/tag_autocomplete_helper.py b/scripts/tag_autocomplete_helper.py index 5a5e043..26e8d66 100644 --- a/scripts/tag_autocomplete_helper.py +++ b/scripts/tag_autocomplete_helper.py @@ -2,7 +2,7 @@ # to a temporary file to expose it to the javascript side from pathlib import Path -from modules import scripts, shared +from modules import scripts, script_callbacks, shared # Webui root path FILE_DIR = Path().absolute() @@ -103,3 +103,38 @@ if EMB_PATH.exists(): embeddings = get_embeddings() if embeddings: write_to_temp_file('emb.txt', embeddings) + +# Register autocomplete options +def on_ui_settings(): + TAC_SECTION = ("tac", "Tag Autocomplete") + # Main tag file + shared.opts.add_option("tac_tagFile", shared.OptionInfo("danbooru.csv", "Tag file name (Requires restart)", section=TAC_SECTION)) + # Active in settings + shared.opts.add_option("tac_active", shared.OptionInfo(True, "Enable Tag Autocompletion", section=TAC_SECTION)) + shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION)) + # Results related settings + shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION)) + shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION)) + shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION)) + shared.opts.add_option("tac_delayTime", shared.OptionInfo(100, "Time in ms to wait before triggering completion again (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_useWildcards", shared.OptionInfo(True, "Search for wildcards", section=TAC_SECTION)) + shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION)) + shared.opts.add_option("tac_useLeftRightArrowKeys", shared.OptionInfo(False, "Use left/rigt arrows to jump to the start/end of the list", section=TAC_SECTION)) + # Insertion related settings + shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION)) + shared.opts.add_option("tac_escapeParentheses", shared.OptionInfo(True, "Escape parentheses on insertion", section=TAC_SECTION)) + shared.opts.add_option("tac_appendComma", shared.OptionInfo(True, "Append comma on tag autocompletion", section=TAC_SECTION)) + # Alias settings + shared.opts.add_option("tac_alias.searchByAlias", shared.OptionInfo(True, "Search by alias", section=TAC_SECTION)) + shared.opts.add_option("tac_alias.onlyShowAlias", shared.OptionInfo(False, "Only show alias", section=TAC_SECTION)) + # Translation settings + shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("", "Translation file name (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_translation.oldFormat", shared.OptionInfo(False, "File uses the old 3-column translation format instead of the new 2-column one (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION)) + # Extra file settings + shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("", "Extra file name (Requires restart)", section=TAC_SECTION)) + shared.opts.add_option("tac_extra.onlyAliasExtraFile", shared.OptionInfo(False, "Extra file in alias only format (Requires restart)", section=TAC_SECTION)) + +script_callbacks.on_ui_settings(on_ui_settings) \ No newline at end of file