From 4b0942d2305b9ba9c874d41be0c0aea6ca62529b Mon Sep 17 00:00:00 2001 From: Haoming Date: Fri, 2 Jun 2023 21:41:58 +0800 Subject: [PATCH] New feature & optimization Added manual mode Added Chinese localization --- README.md | 14 ++- README_ZH.md | 30 +++++ javascript/prompt_format.js | 230 +++++++++++++++++++++--------------- 3 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 README_ZH.md diff --git a/README.md b/README.md index 88b321e..99cca0f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# SD Webui Prompt Format +# SD Webui Prompt Format +[English|[中文](README_ZH.md)] + This is an Extension for the [Automatic1111 Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui), which helps formatting prompts.

@@ -19,8 +21,10 @@ Sometimes, when you type too fast or copy prompts from all over the places, you - *Some newer anime checkpoints claim to eliminate the need of using underscores* - [x] Respect line breaks - **Note:** `Remove Duplicates` only checks within the same line -- [x] **[New]** Pressing `Ctrl + \` to quickly escape the **brackets** of the hovered tag - - Normally, **brackets** *(parentheses)* are used to increase the weight of a prompt. Therefore, for tags like `mejiro mcqueen (umamusume)`, you will need to escape it like `mejiro mcqueen \(umamusume\)`. - +- [x] Pressing `Ctrl + \` to quickly escape the **parentheses** of the hovered tag *(the words where the caret is)* + - Normally, **parentheses** are used to increase the weight of a prompt. Therefore, for tags like `mejiro mcqueen (umamusume)`, you will need to escape it like `mejiro mcqueen \(umamusume\)`. +- [X] **[New]** Toggle between auto formatting and manual formatting + - In `Auto`: The process is ran whenever you press **Generate** + - In `Manual`: The process is only ran when you press the **Format** button -Note: This is purely visual. The actual prompt is unchanged until you manually edit the text again. \ No newline at end of file +Note: The formatting is purely visual. The actual prompt is unchanged until you manually edit the texts again. \ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..a13a153 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,30 @@ +# SD Webui Prompt Format +[[English](README.md)|中文] + +這是一個[Automatic1111 Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui)的插件,可以幫忙校正咒語。 + +

+ +> 示範圖 + +有時候打字太快,或是從各地東拼西湊咒語,常造成多個重複的空格或逗點。這項插件可以幫忙移除它們。 + +## 功能實作 +- [x] 在`txt2img`和`img2img`都有用 +- [x] 移除多餘**空格**和**逗點** +- [x] 修改錯誤的**括弧** +- [x] 開啟`Remove Duplicates`會把咒語中重複的單詞消除 + - **注意:** 只對單詞類咒語有效 + - **例.** `1girl, solo, smile, 1girl` 會變成 `1girl, solo, smile` + - **例.** `a girl smiling, a girl standing` 則不變 +- [x] 開啟`Remove Underscores`會將 `_` 換成**空格** + - *一些較新的動漫模型聲稱不用再加底線* +- [x] 保留咒語的換行 + - **注意:** 上述的`Remove Duplicates`只在同一行中有效 +- [x] 按下`Ctrl + \`來跳脫目前游標所在的單字 + - 平時,**括弧**是用來強調單字。所以若使用像是`mejiro mcqueen (umamusume)`的咒語,便必須跳脫成`mejiro mcqueen \(umamusume\)` +- [X] **[New]** 按下`Auto Format`以在手動與自動間切換 + - `自動`: 每次按下 **生成 (Generate)** 時處裡 + - `手動`: 手動按下`Format`時才處裡 + +注意: 上述美化只是視覺效果。唯有再次手動編輯後,咒語才會更新。 \ No newline at end of file diff --git a/javascript/prompt_format.js b/javascript/prompt_format.js index 919ca8b..e8ae460 100644 --- a/javascript/prompt_format.js +++ b/javascript/prompt_format.js @@ -1,18 +1,30 @@ class LeFormatter { + static manualButton(text, id, { onClick }) { + const button = gradioApp().getElementById(id).cloneNode() + + button.id = 'manual-format' + button.classList.remove('gr-button-lg', 'gr-button-primary', 'lg', 'primary') + button.classList.add('secondary') + button.textContent = text + button.addEventListener('click', onClick) + + return button + } + static injectButton(id, { onClick }) { const button = gradioApp().getElementById(id) button.addEventListener('click', onClick) } - static checkbox(text, { onChange }) { + static checkbox(text, def, { onChange }) { const label = document.createElement('label') label.style.display = 'flex' label.style.alignItems = 'center' label.style.margin = '2px 8px' const checkbox = gradioApp().querySelector('input[type=checkbox]').cloneNode() - checkbox.checked = false + checkbox.checked = def checkbox.addEventListener('change', (event) => { onChange(event.target.checked) }) @@ -27,154 +39,178 @@ class LeFormatter { return label } -} + static formatString(input, dedupe, removeUnderscore) { + // Fix Bracket & Comma + input = input.replace(/,\)/g, '),').replace(/,\]/g, '],').replace(/\(,/g, ',(').replace(/\[,/g, ',[') -function injectBracketEscape(id) { - const textarea = gradioApp().getElementById(id).querySelector('textarea') + // Remove Commas + let tags = input.split(',').map(word => (removeUnderscore ? word.replace(/_/g, ' ').trim() : word.trim())).filter(word => word !== '') - textarea.addEventListener('keydown', (event) => { - if (event.ctrlKey && event.key === '\\') { - event.preventDefault() + // Remove Duplicate + input = dedupe ? [...new Set(tags)].join(', ') : tags.join(', ') - let cursorPosition = textarea.selectionStart; + // Remove Spaces + input = input.replace(/\s+/g, ' ') - if (textarea.selectionStart !== textarea.selectionEnd) - cursorPosition++ + // Fix Bracket & Space + input = input.replace(/ \)/g, ')').replace(/ \]/g, ']').replace(/\( /g, '(').replace(/\[ /g, '[') - let result = pf_GrabBrackets(textarea.value, cursorPosition) + // Fix Empty Bracket + input = input.replace(/\(\s+\)/g, '').replace(/\[\s+\]/g, '') - if (result) { - const original = textarea.value + while (input.includes('()')) + input = input.replace(/\(\s*\)/g, '') + while (input.includes('[]')) + input = input.replace(/\[\s*\]/g, '') - if (result[0] !== 0 && textarea.value[result[0] - 1] === '\\' && textarea.value[result[1] - 1] === '\\') { - textarea.value = original.slice(0, result[0] - 1) + original.slice(result[0] - 1, result[1]).replace(/\\/g, '') + original.slice(result[1]) - textarea.selectionStart = result[0] - 1 - textarea.selectionEnd = result[1] - 1 - } - else { - textarea.value = original.slice(0, result[0]) + '\\' + original.slice(result[0], result[1]) + '\\' + original.slice(result[1]) - textarea.selectionStart = result[0] - textarea.selectionEnd = result[1] + 3 - } + return input.trim() + } + + static grabBrackets(str, index) { + let openBracket = -1 + let closeBracket = -1 - // updateInput(textarea) + for (let i = index; i >= 0; i--) { + if (str[i] === '(') { + openBracket = i + break; + } + if (str[i] === ')' && i !== index) { + break; } } - }) -} -function pf_GrabBrackets(str, index) { - let openBracket = -1; - let closeBracket = -1; - - for (let i = index; i >= 0; i--) { - if (str[i] === '(') { - openBracket = i - break; - } - if (str[i] === ')' && i !== index) { - break; + for (let i = index; i < str.length; i++) { + if (str[i] === ')') { + closeBracket = i + break; + } + if (str[i] === '(' && i !== index) { + break; + } } - } - for (let i = index; i < str.length; i++) { - if (str[i] === ')') { - closeBracket = i - break; - } - if (str[i] === '(' && i !== index) { - break; - } + if (openBracket !== -1 && closeBracket !== -1 && openBracket !== closeBracket) + return [openBracket, closeBracket] + else + return null } - if (openBracket !== -1 && closeBracket !== -1 && openBracket !== closeBracket) - return [openBracket, closeBracket]; - else - return null -} + static injectBracketEscape(id) { + const textarea = gradioApp().getElementById(id).querySelector('textarea') -function fixBracketComma(input) { - return input.replace(/,\)/g, '),').replace(/,\]/g, '],').replace(/\(,/g, ',(').replace(/\[,/g, ',['); -} + textarea.addEventListener('keydown', (event) => { + if (event.ctrlKey && event.key === '\\') { + event.preventDefault() -function fixBracketSpace(input) { - return input.replace(/ \)/g, ')').replace(/ \]/g, ']').replace(/\( /g, '(').replace(/\[ /g, '['); -} + let cursorPosition = textarea.selectionStart -function fixBracketEmpty(input) { - let temp = input.replace(/\(\s+\)/g, '').replace(/\[\s+\]/g, '') + if (textarea.selectionStart !== textarea.selectionEnd) + cursorPosition++ - while (temp.includes('()')) - temp = temp.replace(/\(\s*\)/g, '') - while (temp.includes('[]')) - temp = temp.replace(/\[\s*\]/g, '') - return temp -} + let result = LeFormatter.grabBrackets(textarea.value, cursorPosition) -function formatString(input, dedupe, deunderline) { - const tags = fixBracketComma(input).split(',').map(word => (deunderline ? word.replace(/_/g, ' ').trim() : word.trim())).filter(word => word !== ''); - const sentence = dedupe ? [...new Set(tags)].join(', ') : tags.join(', '); - return fixBracketEmpty(fixBracketSpace(sentence.replace(/\s+/g, ' ')).trim()); -} + if (result) { + const original = textarea.value -onUiLoaded(async () => { + if (result[0] !== 0 && textarea.value[result[0] - 1] === '\\' && textarea.value[result[1] - 1] === '\\') { + textarea.value = original.slice(0, result[0] - 1) + original.slice(result[0] - 1, result[1]).replace(/\\/g, '') + original.slice(result[1]) + textarea.selectionStart = result[0] - 1 + textarea.selectionEnd = result[1] - 1 + } + else { + textarea.value = original.slice(0, result[0]) + '\\' + original.slice(result[0], result[1]) + '\\' + original.slice(result[1]) + textarea.selectionStart = result[0] + textarea.selectionEnd = result[1] + 3 + } + + updateInput(textarea) + } + } + }) + } - // SETTINGS - const iterations = 1 - // SETTINGS +} +onUiLoaded(async () => { + const Modes = ['txt', 'img'] + let autoRun = true let dedupe = false - let deunderline = false + let removeUnderscore = false + + const manualBtn = LeFormatter.manualButton('Format', 'txt2img_generate', { + onClick: () => { + const ids = ['txt2img_prompt', 'txt2img_neg_prompt', 'img2img_prompt', 'img2img_neg_prompt'] + + ids.forEach((id) => { + const textArea = gradioApp().getElementById(id).querySelector('textarea') + + let lines = textArea.value.split('\n') + + for (let i = 0; i < lines.length; i++) + lines[i] = LeFormatter.formatString(lines[i], dedupe, removeUnderscore) + + textArea.value = lines.join('\n') + updateInput(textArea) + }) + + } + }) + + const autoCB = LeFormatter.checkbox('Auto Format', autoRun, { + onChange: (checked) => { + autoRun = checked + manualBtn.style.display = autoRun ? 'none' : 'block' + } + }) - const dedupeCB = LeFormatter.checkbox('Remove Duplicates', { + const dedupeCB = LeFormatter.checkbox('Remove Duplicates', dedupe, { onChange: (checked) => { dedupe = checked } }) - const underlineCB = LeFormatter.checkbox('Remove Underscores', { - onChange: (checked) => { deunderline = checked } + const underlineCB = LeFormatter.checkbox('Remove Underscores', removeUnderscore, { + onChange: (checked) => { removeUnderscore = checked } }) + manualBtn.style.display = 'none' + const formatter = document.createElement('div') formatter.id = 'le-formatter' - formatter.style.display = 'flex'; - formatter.style.flex.direction = 'row'; + formatter.style.display = 'flex' + formatter.style.flex.direction = 'row' + formatter.appendChild(autoCB) + formatter.appendChild(manualBtn) formatter.appendChild(dedupeCB) formatter.appendChild(underlineCB) const tools = document.getElementById('quicksettings') tools.after(formatter) - const Modes = ['txt', 'img'] - Modes.forEach((mode) => { LeFormatter.injectButton(mode + '2img_generate', { onClick: () => { + if (!autoRun) + return; + const ids = [mode + '2img_prompt', mode + '2img_neg_prompt'] const textAreas = [gradioApp().getElementById(ids[0]).querySelector('textarea'), gradioApp().getElementById(ids[1]).querySelector('textarea')] let lines = [textAreas[0].value.split('\n'), textAreas[1].value.split('\n')] - for (let i = 0; i < lines[0].length; i++) - for (let it = 0; it < iterations; it++) - lines[0][i] = formatString(lines[0][i], dedupe, deunderline) - - for (let i = 0; i < lines[1].length; i++) - for (let it = 0; it < iterations; it++) - lines[1][i] = formatString(lines[1][i], dedupe, deunderline) + for (let m = 0; m < 2; m++) { + for (let i = 0; i < lines[0].length; i++) + lines[m][i] = LeFormatter.formatString(lines[m][i], dedupe, removeUnderscore) - textAreas[0].value = lines[0].join('\n') - // updateInput(textAreas[0]) - - textAreas[1].value = lines[1].join('\n') - // updateInput(textAreas[1]) + textAreas[m].value = lines[m].join('\n') + } } }) - injectBracketEscape(mode + '2img_prompt') - injectBracketEscape(mode + '2img_neg_prompt') + LeFormatter.injectBracketEscape(mode + '2img_prompt') + LeFormatter.injectBracketEscape(mode + '2img_neg_prompt') }) }) \ No newline at end of file