mirror of
https://github.com/DominikDoom/a1111-sd-webui-tagcomplete.git
synced 2026-01-27 03:29:55 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cb4fc8f25 | ||
|
|
2d7e6181f5 | ||
|
|
c6049fc2fa | ||
|
|
eff53d0ca7 | ||
|
|
a7da670e97 | ||
|
|
da65feea31 | ||
|
|
6997558714 | ||
|
|
a95f422f10 | ||
|
|
0e177d0945 | ||
|
|
94365630c7 | ||
|
|
d4941c7b73 | ||
|
|
91fb1cba38 | ||
|
|
3169420fd3 | ||
|
|
84b6a0394e | ||
|
|
38fd2523e6 | ||
|
|
85db4a61df | ||
|
|
b18823e88f | ||
|
|
83461e2f54 |
@@ -15,6 +15,7 @@ You can install it using the inbuilt available extensions list, clone the files
|
||||
|
||||
## Common Problems & Known Issues:
|
||||
- Depending on your browser settings, sometimes an old version of the script can get cached. Try `CTRL+F5` to force-reload the site without cache if e.g. a new feature doesn't appear for you after an update.
|
||||
- If the prompt popup has broken styling for you or doesn't appear at all (like [this](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete/assets/34448969/7bbfdd54-fc23-4bfc-85af-24704b139b3a)), make sure to update your openpose-editor extension if you have it installed. It is known to cause issues with other extensions in older versions.
|
||||
|
||||
## Screenshots & Demo videos
|
||||
<details>
|
||||
|
||||
@@ -8,7 +8,8 @@ const core = [
|
||||
"#txt2img_prompt > label > textarea",
|
||||
"#img2img_prompt > label > textarea",
|
||||
"#txt2img_neg_prompt > label > textarea",
|
||||
"#img2img_neg_prompt > label > textarea"
|
||||
"#img2img_neg_prompt > label > textarea",
|
||||
".prompt > label > textarea"
|
||||
];
|
||||
|
||||
// Third party text area selectors
|
||||
|
||||
@@ -89,6 +89,14 @@ function difference(a, b) {
|
||||
)].reduce((acc, [v, count]) => acc.concat(Array(Math.abs(count)).fill(v)), []);
|
||||
}
|
||||
|
||||
// Sliding window function to get possible combination groups of an array
|
||||
function toNgrams(inputArray, size) {
|
||||
return Array.from(
|
||||
{ length: inputArray.length - (size - 1) }, //get the appropriate length
|
||||
(_, index) => inputArray.slice(index, index + size) //create the windows
|
||||
);
|
||||
}
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
"--meta-text-color": ["#6b6f7b", "#a2a9b4"],
|
||||
"--embedding-v1-color": ["lightsteelblue", "#2b5797"],
|
||||
"--embedding-v2-color": ["skyblue", "#2d89ef"],
|
||||
"--live-translation-rt": ["whitesmoke", "#222"],
|
||||
"--live-translation-color-1": ["lightskyblue", "#2d89ef"],
|
||||
"--live-translation-color-2": ["palegoldenrod", "#eb5700"],
|
||||
"--live-translation-color-3": ["darkseagreen", "darkgreen"],
|
||||
}
|
||||
const browserVars = {
|
||||
"--results-overflow-y": {
|
||||
@@ -74,6 +78,39 @@ const autocompleteCSS = `
|
||||
.acListItem.acEmbeddingV2 {
|
||||
color: var(--embedding-v2-color);
|
||||
}
|
||||
.acRuby {
|
||||
padding: var(--input-padding);
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
user-select: none;
|
||||
}
|
||||
.acRuby > ruby {
|
||||
display: inline-flex;
|
||||
flex-direction: column-reverse;
|
||||
margin-top: 0.5rem;
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
.acRuby > ruby::hover {
|
||||
text-decoration: underline;
|
||||
text-shadow: 0 0 10px var(--live-translation-color-1);
|
||||
}
|
||||
.acRuby > :nth-child(3n+1) {
|
||||
color: var(--live-translation-color-1);
|
||||
}
|
||||
.acRuby > :nth-child(3n+2) {
|
||||
color: var(--live-translation-color-2);
|
||||
}
|
||||
.acRuby > :nth-child(3n+3) {
|
||||
color: var(--live-translation-color-3);
|
||||
}
|
||||
.acRuby > ruby > rt {
|
||||
line-height: 1rem;
|
||||
padding: 0px 5px 0px 0px;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
color: var(--live-translation-rt);
|
||||
}
|
||||
`;
|
||||
|
||||
async function loadTags(c) {
|
||||
@@ -161,6 +198,7 @@ async function syncOptions() {
|
||||
translationFile: opts["tac_translation.translationFile"],
|
||||
oldFormat: opts["tac_translation.oldFormat"],
|
||||
searchByTranslation: opts["tac_translation.searchByTranslation"],
|
||||
liveTranslation: opts["tac_translation.liveTranslation"],
|
||||
},
|
||||
// Extra file settings
|
||||
extra: {
|
||||
@@ -200,6 +238,13 @@ async function syncOptions() {
|
||||
});
|
||||
}
|
||||
|
||||
// Remove ruby div if live preview was disabled
|
||||
if (newCFG.translation.liveTranslation === false) {
|
||||
[...gradioApp().querySelectorAll('.acRuby')].forEach(r => {
|
||||
r.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
TAC_CFG = newCFG;
|
||||
|
||||
@@ -287,6 +332,7 @@ 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 NORMAL_TAG_REGEX = /[^\s,|<>)\]]+|</g;
|
||||
const RUBY_TAG_REGEX = /[\w\d<][\w\d' \-?!/$%]{2,}>?/g;
|
||||
const TAG_REGEX = new RegExp(`${POINTY_REGEX.source}|${COMPLETED_WILDCARD_REGEX.source}|${NORMAL_TAG_REGEX.source}`, "g");
|
||||
|
||||
// On click, insert the tag into the prompt textbox with respect to the cursor position
|
||||
@@ -555,6 +601,111 @@ function updateSelectionStyle(textArea, newIndex, oldIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateRuby(textArea, prompt) {
|
||||
if (!TAC_CFG.translation.liveTranslation) return;
|
||||
if (!TAC_CFG.translation.translationFile || TAC_CFG.translation.translationFile === "None") return;
|
||||
|
||||
let ruby = gradioApp().querySelector('.acRuby' + getTextAreaIdentifier(textArea));
|
||||
if (!ruby) {
|
||||
let textAreaId = getTextAreaIdentifier(textArea);
|
||||
let typeClass = textAreaId.replaceAll(".", " ");
|
||||
ruby = document.createElement("div");
|
||||
ruby.setAttribute("class", `acRuby${typeClass} notranslate`);
|
||||
textArea.parentNode.appendChild(ruby);
|
||||
}
|
||||
|
||||
ruby.innerText = prompt;
|
||||
|
||||
let bracketEscapedPrompt = prompt.replaceAll("\\(", "$").replaceAll("\\)", "%");
|
||||
|
||||
let rubyTags = bracketEscapedPrompt.match(RUBY_TAG_REGEX);
|
||||
if (!rubyTags) return;
|
||||
|
||||
rubyTags.sort((a, b) => b.length - a.length);
|
||||
rubyTags = new Set(rubyTags);
|
||||
|
||||
const prepareTag = (tag) => {
|
||||
tag = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
|
||||
|
||||
let unsanitizedTag = tag
|
||||
.replaceAll(" ", "_")
|
||||
.replaceAll("\\(", "(")
|
||||
.replaceAll("\\)", ")");
|
||||
|
||||
const translation = translations?.get(tag) || translations?.get(unsanitizedTag);
|
||||
|
||||
let escapedTag = escapeRegExp(tag);
|
||||
return { tag, escapedTag, translation };
|
||||
}
|
||||
|
||||
const replaceOccurences = (text, tuple) => {
|
||||
let { tag, escapedTag, translation } = tuple;
|
||||
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
|
||||
return text.replaceAll(searchRegex, `<ruby>${escapeHTML(tag)}<rt>${translation}</rt></ruby>`);
|
||||
}
|
||||
|
||||
let html = escapeHTML(prompt);
|
||||
|
||||
// First try to find direct matches
|
||||
[...rubyTags].forEach(tag => {
|
||||
let tuple = prepareTag(tag);
|
||||
|
||||
if (tuple.translation) {
|
||||
html = replaceOccurences(html, tuple);
|
||||
} else {
|
||||
let subTags = tuple.tag.split(" ").filter(x => x.trim().length > 0);
|
||||
// Return if there is only one word
|
||||
if (subTags.length === 1) return;
|
||||
|
||||
let subHtml = tag.replaceAll("$", "\\(").replaceAll("%", "\\)");
|
||||
|
||||
let translateNgram = (windows) => {
|
||||
windows.forEach(window => {
|
||||
let combinedTag = window.join(" ");
|
||||
let subTuple = prepareTag(combinedTag);
|
||||
|
||||
if (subTuple.tag.length <= 2) return;
|
||||
|
||||
if (subTuple.translation) {
|
||||
subHtml = replaceOccurences(subHtml, subTuple);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Perform n-gram sliding window search
|
||||
translateNgram(toNgrams(subTags, 3));
|
||||
translateNgram(toNgrams(subTags, 2));
|
||||
translateNgram(toNgrams(subTags, 1));
|
||||
|
||||
let escapedTag = escapeRegExp(tuple.tag);
|
||||
|
||||
let searchRegex = new RegExp(`(?<!<ruby>)(?:\\b)${escapedTag}(?:\\b|$|(?=[,|: \\t\\n\\r]))(?!<rt>)`, "g");
|
||||
html = html.replaceAll(searchRegex, subHtml);
|
||||
}
|
||||
});
|
||||
|
||||
ruby.innerHTML = html;
|
||||
|
||||
// Add listeners for auto selection
|
||||
const childNodes = [...ruby.childNodes];
|
||||
[...ruby.children].forEach(child => {
|
||||
const textBefore = childNodes.slice(0, childNodes.indexOf(child)).map(x => x.childNodes[0]?.textContent || x.textContent).join("")
|
||||
child.onclick = () => rubyTagClicked(child, textBefore, prompt, textArea);
|
||||
});
|
||||
}
|
||||
|
||||
function rubyTagClicked(node, textBefore, prompt, textArea) {
|
||||
let selectionText = node.childNodes[0].textContent;
|
||||
|
||||
// Find start and end position of the tag in the prompt
|
||||
let startPos = prompt.indexOf(textBefore) + textBefore.length;
|
||||
let endPos = startPos + selectionText.length;
|
||||
|
||||
// Select in text area
|
||||
textArea.focus();
|
||||
textArea.setSelectionRange(startPos, endPos);
|
||||
}
|
||||
|
||||
async function autocomplete(textArea, prompt, fixedTag = null) {
|
||||
// Return if the function is deactivated in the UI
|
||||
if (!isEnabled()) return;
|
||||
@@ -807,6 +958,21 @@ function navigateInList(textArea, event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
async function refreshTacTempFiles() {
|
||||
setTimeout(async () => {
|
||||
wildcardFiles = [];
|
||||
wildcardExtFiles = [];
|
||||
yamlWildcards = [];
|
||||
embeddings = [];
|
||||
hypernetworks = [];
|
||||
loras = [];
|
||||
lycos = [];
|
||||
await processQueue(QUEUE_FILE_LOAD, null);
|
||||
|
||||
console.log("TAC: Refreshed temp files");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function addAutocompleteToArea(area) {
|
||||
// Return if autocomplete is disabled for the current area type in config
|
||||
let textAreaId = getTextAreaIdentifier(area);
|
||||
@@ -826,7 +992,10 @@ function addAutocompleteToArea(area) {
|
||||
hideResults(area);
|
||||
|
||||
// Add autocomplete event listener
|
||||
area.addEventListener('input', debounce(() => autocomplete(area, area.value), TAC_CFG.delayTime));
|
||||
area.addEventListener('input', () => {
|
||||
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
|
||||
updateRuby(area, area.value);
|
||||
});
|
||||
// Add focusout event listener
|
||||
area.addEventListener('focusout', debounce(() => hideResults(area), 400));
|
||||
// Add up and down arrow event listener
|
||||
@@ -879,6 +1048,8 @@ async function setup() {
|
||||
}, 500);
|
||||
})
|
||||
});
|
||||
// Listener for internal temp files refresh button
|
||||
gradioApp().querySelector("#refresh_tac_refreshTempFiles")?.addEventListener("click", refreshTacTempFiles);
|
||||
|
||||
// Add mutation observer for the model hash text to also allow hash-based blacklist again
|
||||
let modelHashText = gradioApp().querySelector("#sd_checkpoint_hash");
|
||||
@@ -918,10 +1089,10 @@ async function setup() {
|
||||
let css = autocompleteCSS;
|
||||
// Replace vars with actual values (can't use actual css vars because of the way we inject the css)
|
||||
Object.keys(styleColors).forEach((key) => {
|
||||
css = css.replace(`var(${key})`, styleColors[key][mode]);
|
||||
css = css.replaceAll(`var(${key})`, styleColors[key][mode]);
|
||||
})
|
||||
Object.keys(browserVars).forEach((key) => {
|
||||
css = css.replace(`var(${key})`, browserVars[key][browser]);
|
||||
css = css.replaceAll(`var(${key})`, browserVars[key][browser]);
|
||||
})
|
||||
|
||||
if (acStyle.styleSheet) {
|
||||
|
||||
@@ -241,41 +241,49 @@ write_to_temp_file('lyco.txt', [])
|
||||
if not TEMP_PATH.joinpath("emb.txt").exists():
|
||||
write_to_temp_file('emb.txt', [])
|
||||
|
||||
# Write wildcards to wc.txt if found
|
||||
if WILDCARD_PATH.exists():
|
||||
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:
|
||||
wildcards_ext = get_ext_wildcards()
|
||||
if wildcards_ext:
|
||||
write_to_temp_file('wce.txt', wildcards_ext)
|
||||
# Write yaml extension wildcards to wcet.txt if found
|
||||
wildcards_yaml_ext = get_ext_wildcard_tags()
|
||||
if wildcards_yaml_ext:
|
||||
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
|
||||
|
||||
# Write embeddings to emb.txt if found
|
||||
if EMB_PATH.exists():
|
||||
# Get embeddings after the model loaded callback
|
||||
script_callbacks.on_model_loaded(get_embeddings)
|
||||
|
||||
if HYP_PATH.exists():
|
||||
hypernets = get_hypernetworks()
|
||||
if hypernets:
|
||||
write_to_temp_file('hyp.txt', hypernets)
|
||||
def refresh_temp_files():
|
||||
write_temp_files()
|
||||
get_embeddings(shared.sd_model)
|
||||
|
||||
if LORA_PATH is not None and LORA_PATH.exists():
|
||||
lora = get_lora()
|
||||
if lora:
|
||||
write_to_temp_file('lora.txt', lora)
|
||||
def write_temp_files():
|
||||
# Write wildcards to wc.txt if found
|
||||
if WILDCARD_PATH.exists():
|
||||
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
|
||||
if wildcards:
|
||||
write_to_temp_file('wc.txt', wildcards)
|
||||
|
||||
if LYCO_PATH is not None and LYCO_PATH.exists():
|
||||
lyco = get_lyco()
|
||||
if lyco:
|
||||
write_to_temp_file('lyco.txt', lyco)
|
||||
# Write extension wildcards to wce.txt if found
|
||||
if WILDCARD_EXT_PATHS is not None:
|
||||
wildcards_ext = get_ext_wildcards()
|
||||
if wildcards_ext:
|
||||
write_to_temp_file('wce.txt', wildcards_ext)
|
||||
# Write yaml extension wildcards to wcet.txt if found
|
||||
wildcards_yaml_ext = get_ext_wildcard_tags()
|
||||
if wildcards_yaml_ext:
|
||||
write_to_temp_file('wcet.txt', wildcards_yaml_ext)
|
||||
|
||||
if HYP_PATH.exists():
|
||||
hypernets = get_hypernetworks()
|
||||
if hypernets:
|
||||
write_to_temp_file('hyp.txt', hypernets)
|
||||
|
||||
if LORA_PATH is not None and LORA_PATH.exists():
|
||||
lora = get_lora()
|
||||
if lora:
|
||||
write_to_temp_file('lora.txt', lora)
|
||||
|
||||
if LYCO_PATH is not None and LYCO_PATH.exists():
|
||||
lyco = get_lyco()
|
||||
if lyco:
|
||||
write_to_temp_file('lyco.txt', lyco)
|
||||
|
||||
|
||||
write_temp_files()
|
||||
|
||||
# Register autocomplete options
|
||||
def on_ui_settings():
|
||||
@@ -313,6 +321,7 @@ def on_ui_settings():
|
||||
shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("None", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_translation.oldFormat", shared.OptionInfo(False, "Translation file uses old 3-column translation format instead of the new 2-column one", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_translation.liveTranslation", shared.OptionInfo(False, "Show live tag translation below prompt (WIP, expect some bugs)", section=TAC_SECTION))
|
||||
# Extra file settings
|
||||
shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("extra-quality-tags.csv", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_extra.addMode", shared.OptionInfo("Insert before", "Mode to add the extra tags to the main tag list", gr.Dropdown, lambda: {"choices": ["Insert before","Insert after"]}, section=TAC_SECTION))
|
||||
@@ -365,5 +374,6 @@ def on_ui_settings():
|
||||
shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Textbox, section=TAC_SECTION))
|
||||
shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Textbox, section=TAC_SECTION))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user