Compare commits

...

18 Commits
2.3.0 ... 2.5.0

Author SHA1 Message Date
DominikDoom
1cb4fc8f25 Merge branch 'feature-refresh-temp-files' into main
Added a "fake" settings option that refreshes some internal temporary files without needing to restart
2023-05-26 23:45:19 +02:00
DominikDoom
2d7e6181f5 Fix embed refresh & JS selector 2023-05-26 17:18:53 +02:00
DominikDoom
c6049fc2fa Separate refresh and write function to include embeds 2023-05-26 16:55:34 +02:00
DominikDoom
eff53d0ca7 Reset embeds too 2023-05-26 16:47:42 +02:00
DominikDoom
a7da670e97 New "setting" for reloading temp files
This allows refreshing Lora, lycos, hypernets and wildcards without having to restart the UI
2023-05-26 15:40:25 +02:00
DominikDoom
da65feea31 Update README.md
Added openpose-editor bug to known issues since it appears so often
2023-05-24 12:58:15 +02:00
DominikDoom
6997558714 Support for hires fix prompts
Only available on the webui dev branch for now.
Closes #179
2023-05-21 13:54:38 +02:00
DominikDoom
a95f422f10 Merge branch 'feature-rubytext' into main
Live translation feature, pretty WIP so expect some bugs
2023-05-15 19:06:09 +02:00
DominikDoom
0e177d0945 Add option for live preview 2023-05-02 17:59:36 +02:00
DominikDoom
94365630c7 Fix end-of-tag detection for brackets before a weight modifier 2023-05-02 17:46:04 +02:00
DominikDoom
d4941c7b73 Fix rt color 2023-05-02 17:46:04 +02:00
DominikDoom
91fb1cba38 Fix replacer matching partial words
Now checks for end of word / string or tag boundary correctly
2023-05-02 17:46:04 +02:00
DominikDoom
3169420fd3 Fix parentheses parsing 2023-05-02 17:46:04 +02:00
DominikDoom
84b6a0394e Fix more bugs related to replacing
Also some involving parentheses
2023-05-02 17:46:04 +02:00
DominikDoom
38fd2523e6 Fix issue with self-containing tags, add auto select on click 2023-05-02 17:46:04 +02:00
DominikDoom
85db4a61df Adjust styling for bigger translation 2023-05-02 17:46:04 +02:00
DominikDoom
b18823e88f Sliding window search, fix double replacement 2023-05-02 17:46:04 +02:00
DominikDoom
83461e2f54 Ruby Text live translation feature (WIP) 2023-05-02 17:46:04 +02:00
5 changed files with 224 additions and 33 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)