main
Haoming 2025-06-04 14:21:01 +08:00
parent e77467fc42
commit 4135db4752
9 changed files with 318 additions and 405 deletions

View File

@ -1,13 +1,14 @@
# 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.
> Also supports [SD.Next](https://github.com/vladmandic/automatic) and [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) out of the box!
> Compatible with [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge)
<p align="center"><img src="sample.jpg" width=512></p>
<p align="center">
<img src="sample.png" width=768><br>
Sometimes, when you type too fast or copy prompts from all over the places, you end up with duplicated <b>spaces</b> and <b>commas</b>. This simple Extension helps removing them whenever you click <b>Generate</b>.
</p>
Sometimes, when you type too fast or copy prompts from all over the places, you end up with duplicated **spaces** or **commas**. This simple Extension helps removing them whenever you click **Generate**.
<br>
## Features
- [x] Works in both `txt2img` and `img2img`
@ -16,57 +17,48 @@ Sometimes, when you type too fast or copy prompts from all over the places, you
- [x] Fix misplaced **brackets** and **commas**
- [x] Enable `Remove Duplicates` to remove identical tags found in the prompts
- **Note:** Only works for tag-based prompt, not sentence-based prompt
- **eg.** `1girl, solo, smile, 1girl` will become `1girl, solo, smile`
- **eg.** `a girl smiling, a girl standing` will not be changed
- **e.g.** `1girl, solo, smile, 1girl` will become `1girl, solo, smile`
- **e.g.** `a girl smiling, a girl standing` will not be changed
- [x] Enable `Remove Underscores` to replace `_` with `space`
- [x] Respect line breaks
- `Remove Duplicates` only checks within the same line
- [x] Append a comma before a line break
- [x] Append a comma every line break
- [x] Toggle between auto formatting and manual formatting
- In `Auto` mode: The process is ran whenever you click on **Generate**
- In `Manual` mode: The process is only ran when you click the **Format** button
- [x] **New:** Trigger the formatting on the pasted text
- [x] Pressing `Alt` + `Shift` + `F` can also manually trigger formatting
- [x] Format the text pasted from clipboard
<br>
- [x] Toggle whether the above features are enabled / disabled by default in the `Prompt Format` section under the <ins>System</ins> category of the **Settings** tab
- [x] Pressing `Alt` + `Shift` + `F` can also trigger formatting
- [x] Assign "[alias](#tag-alias)" that counts as duplicates for the specified tags
- [x] Exclude specific tags from `Remove Underscores`
- [x] Click `Reload` to cache new cards
- By default, the `ExtraNetwork` cards are cached once at the start, to be excluded from `Remove Underscores`. If you added more cards while the Webui is already running, click this button to re-cache again.
- [x] Assign "[alias](#tag-alias)" that counts as duplicates for the specified tags
- [x] Click `Reload` to refresh the 2 settings above
### Tag Alias
- In the `Prompt Format` settings, there is a new field for **Tag Alias**
- You can assign other tags that count as the same as the main tag, and thus get removed during `Remove Duplicates`
- The syntax is in the format of `main tag: alias1, alias2, alias3`
- **example:**
```
1girl: girl, woman, lady
```
- If you type `girl`, it will get converted into `1girl`, and if you already have `1girl`, then the future ones will get removed.
- The pattern for alias uses **Regular Expression**, so certain symbols *(**eg.** `(`, `)`)* will need to be escaped *(**ie.** `\(`, `\)`)*
- Comma is not supported, as it is used to separate multiple patterns
- Check out [RegExr](https://regexr.com/) for cheatsheet
- **example:**
```regex
adult: \d*\s*(y\.?o\.?|[Yy]ear[s]? [Oo]ld)
```
- It will convert `15 yo`, `20 y.o.`, `25 years old`, `30 Year Old` all into `adult`
<hr>
### Note
1. Since the formatting in `Auto` mode is triggered at the same time as the generation, the immediate image might not have its prompts updated.
2. Some Extensions *(**eg.** [tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete))* listen to the text editing event, meaning the formatting will cause them to be triggered. You can disable updating the actual prompts in the `Prompt Format` settings to prevent this. Though you may need to manually type something else for the prompt to get actually updated.
<hr>
> [!Note]
> Some Extensions *(**eg.** [tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete))* listen to the text editing event, meaning the formatting will cause them to be triggered. You can disable updating the actual prompts in the settings to prevent this.
<details>
<summary><b>Booru</b></summary>
- [x] Automatically clean up unwanted texts
- [x] Clean up unwanted texts when pasting tags from Booru sites
- Currently supports [gelbooru](https://gelbooru.com/) and [danbooru](https://safebooru.donmai.us/)
<p align="center"><img src="booru.png" width=512></p>
</details>
<br>
### Tag Alias
- You can assign other tags that count as the same as the main tag, which then get removed during `Remove Duplicates`
- The syntax is in the format of `main tag: alias1, alias2, alias3`
- **example:**
```
1girl: girl, woman, lady
```
- If you type `girl`, it will get converted into `1girl`, which will get removed if the prompt already contains `1girl`
- The pattern for alias uses **Regular Expression**, so certain symbols *(**e.g.** `(`, `)`)* will need to be escaped *(**i.e.** `\(`, `\)`)*
- Comma is not supported, as it is used to separate multiple patterns

View File

@ -1,72 +0,0 @@
# SD Webui Prompt Format
[[English](README.md)|中文]
這是一個[Automatic1111 Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui)的插件,用來幫忙校正咒語。
> 亦支援 [SD.Next](https://github.com/vladmandic/automatic) 與 [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) !
<p align="center"><img src="sample.jpg" width=512></p>
有時候如果打字太快或是從各處東拼西湊咒語,常會造成多個重複的空格或逗點。這個擴充可以幫忙移除它們。
## 功能
- [x] 在`txt2img`和`img2img`都有用
- [x] 對`正面`和`負面`以及`Hires. fix`之咒語都有用
- [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] 在換行前加入逗點
- [x] 按下`Auto Format`以在手動與自動間切換
- `自動`: 每次按下 **生成 (Generate)** 時處裡
- `手動`: 手動按下 **Format** 時才處裡
- [x] **新功能:** 對貼上的咒語進行處裡
- [x] 在 **Settings** 頁面 <ins>System</ins> 下的 `Prompt Format` 區可以 開啟/關閉 上述功能
- [x] 按下 `Alt` + `Shift` + `F` 亦可觸發格式化
- [x] 為指定單字新增 "[同義詞](#同義詞)"
- [x] 將指定字詞除外 `Remove Underscores` 的影響
- [x] 點擊 `Reload` 以緩存卡片
- 在 Webui 剛開啟時, `ExtraNetwork` 中的卡片會被緩存一次以防被 `Remove Underscores` 影響。如果你在 Webui 已運行時加入更多的卡片,點擊此按鈕來重新緩存。
### 同義詞
- 在 `Prompt Format` 的設定裡,有個新的 **Tag Alias** 欄位
- 你可以在此把其它字詞設為主單字的同義詞,使其在 `Remove Duplicates` 中被當作重複字而刪去
- 格式為 `main tag: alias1, alias2, alias3`
- **範例:**
```
1girl: girl, woman, lady
```
- 如果輸入 `girl`, 便會轉換成 `1girl`; 而如果 `1girl` 已經存在,多餘的便會被刪除。
- 同義詞判斷使用 **Regular Expression**,故特定文字 *(**如.** `(`, `)`)* 便需要被跳脫 *(**即.** `\(`, `\)`)*
- 逗號用來分開多個同義詞,故無法用於同義詞
- 可參考 [RegExr](https://regexr.com/) 以便學習
- **範例:**
```regex
adult: \d*\s*(y\.?o\.?|[Yy]ear[s]? [Oo]ld)
```
- 此便會將 `15 yo`, `20 y.o.`, `25 years old`, `30 Year Old` 都轉為 `adult`
<hr>
### 注意
1. 由於 `自動`校正 和 生成 是同時觸發,當下所生產的第一張圖片之咒語可能不會是已更新的。
2. 有些擴充 *(如. [tagcomplete](https://github.com/DominikDoom/a1111-sd-webui-tagcomplete))* 追蹤文字的編輯事件,意即文字校正會導致它們啟動。你可以到設定關閉咒語的自動更新。
<hr>
<details>
<summary><b>Booru</b></summary>
- [x] 自動清除多餘字詞
- 目前支援 [gelbooru](https://gelbooru.com/) 及 [danbooru](https://safebooru.donmai.us/)
<p align="center"><img src="booru.png" width=512></p>
</details>

BIN
booru.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,94 +1,51 @@
class pfConfigs {
constructor() {
this.refresh = this.#shouldRefresh();
this.autoRun = this.#defaultAuto();
this.dedupe = this.#defaultDedupe();
this.removeUnderscore = this.#defaultRemoveUnderscore();
this.comma = this.#appendComma();
this.paste = this.#onpaste();
this.booru = this.#procBooru();
/** @type {HTMLTextAreaElement[]} */ this.promptFields = this.#getPromptFields();
this.promptFields = this.#getPromptFields();
/** @type {boolean} */ this.refresh = !this.#getConfig("setting_pf_disableupdateinput");
/** @type {boolean} */ this.autoRun = this.#getConfig("setting_pf_startinauto");
/** @type {boolean} */ this.dedupe = this.#getConfig("setting_pf_startwithdedupe");
/** @type {boolean} */ this.rmUnderscore = this.#getConfig("setting_pf_startwithrmudscr");
/** @type {boolean} */ this.comma = this.#getConfig("setting_pf_appendcomma");
/** @type {boolean} */ this.paste = this.#getConfig("setting_pf_onpaste");
/** @type {boolean} */ this.booru = this.#getConfig("setting_pf_booru");
}
/** @returns {boolean} */
#shouldRefresh() {
const config = document.getElementById('setting_pf_disableupdateinput').querySelector('input[type=checkbox]');
return !config.checked;
}
/** @returns {boolean} */
#defaultAuto() {
const config = document.getElementById('setting_pf_startinauto').querySelector('input[type=checkbox]');
#getConfig(id) {
const config = document
.getElementById(id)
.querySelector("input[type=checkbox]");
return config.checked;
}
/** @returns {boolean} */
#defaultDedupe() {
const config = document.getElementById('setting_pf_startwithdedupe').querySelector('input[type=checkbox]');
return config.checked;
}
/** @returns {boolean} */
#defaultRemoveUnderscore() {
const config = document.getElementById('setting_pf_startwithrmudscr').querySelector('input[type=checkbox]');
return config.checked;
}
/** @returns {boolean} */
#appendComma() {
const config = document.getElementById('setting_pf_appendcomma').querySelector('input[type=checkbox]');
return config.checked;
}
/** @returns {boolean} */
#onpaste() {
const config = document.getElementById('setting_pf_onpaste').querySelector('input[type=checkbox]');
return config.checked;
}
/** @returns {boolean} */
#procBooru() {
const config = document.getElementById('setting_pf_booru').querySelector('input[type=checkbox]');
return config.checked;
}
/**
* Cache All Prompt Fields
* @returns {HTMLTextAreaElement[]}
*/
#getPromptFields() {
const textareas = [];
/** Expandable List of IDs in 1 place */
const IDs = [
'txt2img_prompt',
'txt2img_neg_prompt',
'img2img_prompt',
'img2img_neg_prompt',
'hires_prompt',
'hires_neg_prompt'
"txt2img_prompt",
"txt2img_neg_prompt",
"img2img_prompt",
"img2img_neg_prompt",
"hires_prompt",
"hires_neg_prompt",
];
for (const id of IDs) {
const textArea = document.getElementById(id)?.querySelector('textarea');
if (textArea != null)
textareas.push(textArea);
const textArea = document.getElementById(id)?.querySelector("textarea");
if (textArea != null) textareas.push(textArea);
}
const ADetailer = [
"script_txt2img_adetailer_ad_main_accordion",
"script_img2img_adetailer_ad_main_accordion"
"script_img2img_adetailer_ad_main_accordion",
];
for (const id of ADetailer) {
const fields = document.getElementById(id)?.querySelectorAll('textarea');
if (fields == null)
continue;
const fields = document.getElementById(id)?.querySelectorAll("textarea");
if (fields == null) continue;
for (const textArea of fields) {
if (textArea.placeholder.length > 0)
textareas.push(textArea);
if (textArea.placeholder.length > 0) textareas.push(textArea);
}
}
@ -97,40 +54,35 @@ class pfConfigs {
/** @returns {string[]} */
static cacheCards() {
const extras = document.getElementById('txt2img_extra_tabs');
if (!extras)
return [];
const cards = document
.getElementById("pf_embeddings")
.querySelector("textarea")
.value.split("\n");
const cards = [];
for (const card of extras.querySelectorAll('span.name')) {
if (card.textContent.includes('_'))
cards.push(card.textContent);
}
const config = document.getElementById('setting_pf_exclusion').querySelector('input').value;
if (config.trim()) {
for (const tag of config.split(","))
cards.push(tag.trim());
}
const config = document
.getElementById("setting_pf_exclusion")
.querySelector("textarea").value;
for (const tag of config.split(",").map((t) => t.trim()))
if (tag) cards.push(tag);
return cards;
}
/** @returns {Map<RegExp, string>} */
/** @returns {{RegExp: string}} */
static getTagAlias() {
const alias = new Map();
const config = document.getElementById('setting_pf_alias').querySelector('textarea').value;
if (!config.trim())
return alias;
const config = document
.getElementById("setting_pf_alias")
.querySelector("textarea").value;
if (!config.includes(":")) return alias;
for (const line of config.split("\n")) {
const [tag, words] = line.split(":");
const mainTag = tag.trim();
for (const word of words.split(",").map(part => part.trim())) {
if (word === mainTag)
continue;
for (const word of words.split(",").map((part) => part.trim())) {
if (word === mainTag) continue;
const pattern = this.#parseRegExp(word);
alias.set(pattern, mainTag);
@ -142,9 +94,6 @@ class pfConfigs {
/** @param {string} input @returns {RegExp} */
static #parseRegExp(input) {
const startAnchor = input.startsWith('^');
const endAnchor = input.endsWith('$');
return new RegExp(`${startAnchor ? '' : '^'}${input}${endAnchor ? '' : '$'}`);
return new RegExp(`^${input.trimStart("^").trimEnd("$")}$`);
}
}

View File

@ -1,9 +1,8 @@
class pfUI {
/** @param {string} text @param {string} tip @returns {HTMLButtonElement} */
static #button(text, tip) {
const button = document.createElement('button');
button.classList.add(['lg', 'secondary', 'gradio-button']);
const button = document.createElement("button");
button.classList.add(["lg", "secondary", "gradio-button"]);
button.textContent = text;
if (tip) button.title = tip;
return button;
@ -11,9 +10,12 @@ class pfUI {
/** @param {boolean} value @param {string} text @returns {HTMLLabelElement} */
static #checkbox(value, text) {
const label = document.getElementById('tab_settings').querySelector('input[type=checkbox]').parentNode.cloneNode(true);
const label = document
.getElementById("pf_checkbox")
.querySelector("label")
.cloneNode(true);
label.classList.add("pf-checkbox");
label.removeAttribute('id');
label.removeAttribute("id");
const checkbox = label.children[0];
checkbox.checked = value;
@ -23,24 +25,19 @@ class pfUI {
return label;
}
/**
* @param {boolean} autoRun
* @param {boolean} dedupe
* @param {boolean} removeUnderscore
* @returns {HTMLDivElement}
*/
static setupUIs(autoRun, dedupe, removeUnderscore) {
const formatter = document.createElement('div');
formatter.id = 'le-formatter';
/** @param {boolean} autoRun @param {boolean} dedupe @param {boolean} rmUnderscore @returns {HTMLDivElement} */
static setupUIs(autoRun, dedupe, rmUnderscore) {
const formatter = document.createElement("div");
formatter.id = "le-formatter";
const manualBtn = this.#button('Format', null);
manualBtn.style.display = autoRun ? 'none' : 'flex';
const refreshBtn = this.#button('Reload', 'Reload Cached Cards & Alias');
refreshBtn.style.display = removeUnderscore ? 'flex' : 'none';
const manualBtn = this.#button("Format", "Manually Format the Prompts");
manualBtn.style.display = autoRun ? "none" : "flex";
const refreshBtn = this.#button("Reload", "Reload Exclusion & Alias");
refreshBtn.style.display = rmUnderscore ? "flex" : "none";
const autoCB = this.#checkbox(autoRun, 'Auto Format');
const dedupeCB = this.#checkbox(dedupe, 'Remove Duplicates');
const underscoreCB = this.#checkbox(removeUnderscore, 'Remove Underscores');
const autoCB = this.#checkbox(autoRun, "Auto Format");
const dedupeCB = this.#checkbox(dedupe, "Remove Duplicates");
const underscoreCB = this.#checkbox(rmUnderscore, "Remove Underscores");
formatter.appendChild(autoCB);
formatter.appendChild(manualBtn);
@ -56,5 +53,4 @@ class pfUI {
return formatter;
}
}

View File

@ -1,76 +1,52 @@
class LeFormatter {
static #_alias = null;
static #_cachedCards = null;
static #aliasData = null;
static #cardsData = null;
static forceReload() {
this.#_alias = null;
this.#_cachedCards = null;
this.#aliasData = null;
this.#cardsData = null;
}
/** @returns {{RegExp: string}} */
static get #alias() {
return (this.#aliasData ??= pfConfigs.getTagAlias());
}
/** @returns {string[]} */
static get #cachedCards() {
if (this.#_cachedCards == null)
this.#_cachedCards = pfConfigs.cacheCards();
return this.#_cachedCards;
}
/** @returns {Map<RegExp, string>} */
static get #alias() {
if (this.#_alias == null)
this.#_alias = pfConfigs.getTagAlias();
return this.#_alias;
static get #cards() {
return (this.#cardsData ??= pfConfigs.cacheCards());
}
/**
* @param {HTMLTextAreaElement} textArea
* @param {boolean} dedupe
* @param {boolean} removeUnderscore
* @param {boolean} rmUnderscore
* @param {boolean} autoRefresh
* @param {boolean} appendComma
*/
static formatPipeline(textArea, dedupe, removeUnderscore, autoRefresh, appendComma) {
const lines = textArea.value.split('\n');
static formatPipeline(textArea, dedupe, rmUnderscore, autoRefresh, appendComma) {
const lines = textArea.value.split("\n");
for (let i = 0; i < lines.length; i++)
lines[i] = this.formatString(lines[i], dedupe, removeUnderscore);
lines[i] = this.formatString(lines[i], dedupe, rmUnderscore);
if (!appendComma)
textArea.value = lines.join('\n');
if (!appendComma) textArea.value = lines.join("\n");
else {
const val = lines.join(',\n');
textArea.value = val.replace(/\n,\n/g, '\n\n');
const val = lines.join(",\n");
textArea.value = val
.replace(/\n,\n/g, "\n\n")
.replace(/\s*,\s*$/g, "")
.replace(/\s*,\s*$/g, "");
}
if (autoRefresh)
updateInput(textArea);
}
/** @param {string[]} tags @returns {string[]} */
static #joinExtraNet(tags) {
let isNet = false;
let i = 0;
while (i < tags.length) {
if (!isNet) {
if (tags[i].startsWith('<'))
isNet = true;
i++;
}
else {
isNet = !tags[i].endsWith('>');
tags[i - 1] = `${tags[i - 1]}, ${tags.splice(i, 1)[0]}`;
}
}
return tags;
if (autoRefresh) updateInput(textArea);
}
/** @param {string} input @returns {string} */
static #toExpression(input) {
return input
.replace(/[,\n]\s*> <\s*[,\n]/g, ", $SHY$,")
.replace(/[,\n]\s*:3\s*[,\n]/g, ", $CAT$,");
.replace(/[,\n^]\s*> <\s*[,\n$]/g, ", $SHY$,")
.replace(/[,\n^]\s*:3\s*[,\n$]/g, ", $CAT$,");
}
/** @param {string} input @returns {string} */
@ -80,68 +56,99 @@ class LeFormatter {
.replace("$CAT$", ":3");
}
/** @param {string} input @param {boolean} dedupe @param {boolean} removeUnderscore @returns {string} */
static formatString(input, dedupe, removeUnderscore) {
/** @type {Map<string, string>} */
static #networkDB = new Map();
/** @param {string} input @returns {string} */
static #toNetwork(input) {
this.#networkDB.clear();
const output = input.replace(/\s*<.+?>\s*/g, (match) => {
const UID = `@NET${this.#networkDB.size}WORK@`;
this.#networkDB.set(UID, match.trim());
return UID;
});
return output;
}
/** @param {string} input @returns {string} */
static #fromNetwork(input) {
const len = this.#networkDB.size;
for (let i = 0; i < len; i++) {
const UID = `@NET${i}WORK@`;
input = input.replace(UID, this.#networkDB.get(UID));
}
return input;
}
/** @param {string} input @param {boolean} dedupe @param {boolean} rmUnderscore @returns {string} */
static formatString(input, dedupe, rmUnderscore) {
// Substitute LoRAs
input = this.#toNetwork(input);
// Remove Underscore
input = removeUnderscore ? this.#removeUnderscore(input) : input;
input = rmUnderscore ? this.#rmUnderscore(input) : input;
// Special Tags
input = this.#toExpression(input);
// Restore LoRAs
input = this.#fromNetwork(input);
// Fix Commas inside Brackets
input = input
.replace(/,+\s*\)/g, '),')
.replace(/,+\s*\]/g, '],')
.replace(/,+\s*\>/g, '>,')
.replace(/,+\s*\}/g, '},')
.replace(/\(\s*,+/g, ',(')
.replace(/\[\s*,+/g, ',[')
.replace(/\<\s*,+/g, ',<')
.replace(/\{\s*,+/g, ',{');
.replace(/,+\s*\)/g, "),")
.replace(/,+\s*\]/g, "],")
.replace(/,+\s*\>/g, ">,")
.replace(/,+\s*\}/g, "},")
.replace(/\(\s*,+/g, ",(")
.replace(/\[\s*,+/g, ",[")
.replace(/\<\s*,+/g, ",<")
.replace(/\{\s*,+/g, ",{");
// Fix Bracket & Space
input = input
.replace(/\s+\)/g, ')')
.replace(/\s+\]/g, ']')
.replace(/\s+\>/g, '>')
.replace(/\s+\}/g, '}')
.replace(/\(\s+/g, '(')
.replace(/\[\s+/g, '[')
.replace(/\<\s+/g, '<')
.replace(/\{\s+/g, '{');
.replace(/\s+\)/g, ")")
.replace(/\s+\]/g, "]")
.replace(/\s+\>/g, ">")
.replace(/\s+\}/g, "}")
.replace(/\(\s+/g, "(")
.replace(/\[\s+/g, "[")
.replace(/\<\s+/g, "<")
.replace(/\{\s+/g, "{");
// Remove Space around Syntax
input = input
.replace(/\s*\|\s*/g, '|')
.replace(/\s*\:\s*/g, ':');
input = input.replace(/\s*\|\s*/g, "|").replace(/\s*\:\s*/g, ":");
// Sentence -> Tags
let tags = input.split(',').map(word => word.trim());
// ["<lora:name:weight:lbw=1", "2", "3>"] -> ["<lora:name:weight:lbw=1,2,3>"]
tags = this.#joinExtraNet(tags);
let tags = input.split(",").map((word) => word.trim());
// Remove Duplicate
tags = dedupe ? this.#dedupe(tags) : tags;
// Remove extra Spaces
input = tags.join(', ').replace(/\s+/g, ' ');
input = tags.join(", ").replace(/\s+/g, " ");
// Remove Empty Brackets
while (/\(\s*\)|\[\s*\]/.test(input))
input = input.replace(/\(\s*\)|\[\s*\]/g, '');
input = input.replace(/\(\s*\)|\[\s*\]/g, "");
// Space after Comma in Escaped Brackets (for franchise)
input = input.replace(/\\\(([^\\\)]+?):([^\\\)]+?)\\\)/g, '\\($1: $2\\)');
input = input.replace(/\\\(([^\\\)]+?):([^\\\)]+?)\\\)/g, "\\($1: $2\\)");
// Prune empty Chunks
input = input.split(',').map(word => word.trim()).filter(word => word).join(', ')
input = input.split(",").map((word) => word.trim()).filter((word) => word).join(", ");
// LoRA Block Weights
input = input.replace(/\<[^\>]+\>/g, (match) => {
return match.replace(/\,\s+/g, ',');
input = input.replace(/<.+?>/g, (match) => {
return match.replace(/\,\s+/g, ",");
});
// Remove empty before Colon
input = input.replace(/\,\s*\:(\d)/g, ':$1');
input = input.replace(/,\s*:(\d)/g, ":$1");
input = this.#fromExpression(input);
@ -155,7 +162,7 @@ class LeFormatter {
const results = [];
for (const tag of input) {
const cleanedTag = tag.replace(/\[|\]|\(|\)/g, '').replace(/\s+/g, ' ').trim();
const cleanedTag = tag.replace(/\[|\]|\(|\)/g, "").replace(/\s+/g, " ").trim();
if (KEYWORD.test(cleanedTag)) {
results.push(tag);
@ -170,66 +177,62 @@ class LeFormatter {
}
}
if ((substitute == null) && (!uniqueSet.has(cleanedTag))) {
if (substitute == null && !uniqueSet.has(cleanedTag)) {
uniqueSet.add(cleanedTag);
results.push(tag);
continue;
}
if ((substitute != null) && (!uniqueSet.has(substitute))) {
if (substitute != null && !uniqueSet.has(substitute)) {
uniqueSet.add(substitute);
results.push(tag.replace(cleanedTag, substitute));
continue;
}
results.push(tag.replace(cleanedTag, ''));
results.push(tag.replace(cleanedTag, ""));
}
return results;
}
/** @param {string} input @returns {string} */
static #removeUnderscore(input) {
if (!input.trim())
return "";
static #rmUnderscore(input) {
if (!input.trim()) return "";
const syntax = /\,\|\:\<\>\(\)\[\]\{\}/;
const pattern = new RegExp(`([${syntax.source}]+|[^${syntax.source}]+)`, 'g');
const parts = input.match(pattern);
for (let i = 0; i < this.#cards.length; i++)
input = input.replaceAll(this.#cards[i], `@TEXTUAL${i}INVERSION@`);
const processed = parts.map((part) => {
if (new RegExp(`[${syntax.source}]+`).test(part))
return part;
if (/^\s+$/.test(part))
return part;
input = input.replaceAll("_", " ");
if (!this.#cachedCards.includes(part.trim()))
part = part.replaceAll('_', ' ');
for (let i = 0; i < this.#cards.length; i++)
input = input.replaceAll(`@TEXTUAL${i}INVERSION@`, this.#cards[i]);
return part;
});
return processed.join('');
return input;
}
}
(function () {
onUiLoaded(() => {
const config = new pfConfigs();
const formatter = pfUI.setupUIs(config.autoRun, config.dedupe, config.removeUnderscore);
const formatter = pfUI.setupUIs(config.autoRun, config.dedupe, config.rmUnderscore);
document.addEventListener('keydown', (e) => {
if (e.altKey && e.shiftKey && e.code === 'KeyF') {
document.addEventListener("keydown", (e) => {
if (e.altKey && e.shiftKey && e.code === "KeyF") {
e.preventDefault();
for (const field of config.promptFields)
LeFormatter.formatPipeline(field, config.dedupe, config.removeUnderscore, true, config.comma);
LeFormatter.formatPipeline(
field,
config.dedupe,
config.rmUnderscore,
config.refresh,
config.comma,
);
}
});
formatter.auto.addEventListener("change", () => {
config.autoRun = formatter.auto.checked;
formatter.manual.style.display = config.autoRun ? 'none' : 'flex';
formatter.manual.style.display = config.autoRun ? "none" : "flex";
});
formatter.dedupe.addEventListener("change", () => {
@ -237,52 +240,61 @@ class LeFormatter {
});
formatter.underscore.addEventListener("change", () => {
config.removeUnderscore = formatter.underscore.checked;
formatter.refresh.style.display = config.removeUnderscore ? 'flex' : 'none';
config.rmUnderscore = formatter.underscore.checked;
formatter.refresh.style.display = config.rmUnderscore ? "flex" : "none";
});
formatter.manual.addEventListener("click", () => {
for (const field of config.promptFields)
LeFormatter.formatPipeline(field, config.dedupe, config.removeUnderscore, config.refresh, config.comma);
LeFormatter.formatPipeline(
field,
config.dedupe,
config.rmUnderscore,
config.refresh,
config.comma,
);
});
formatter.refresh.addEventListener("click", () => {
LeFormatter.forceReload();
});
const tools = document.getElementById('quicksettings');
const tools = document.getElementById("quicksettings");
tools.after(formatter);
/** Expandable List of IDs in 1 place */
const IDs = [
'txt2img_generate',
'txt2img_enqueue',
'img2img_generate',
'img2img_enqueue'
"txt2img_generate",
"txt2img_enqueue",
"img2img_generate",
"img2img_enqueue",
];
for (const id of IDs) {
const button = document.getElementById(id);
button?.addEventListener('click', () => {
if (config.autoRun) {
for (const field of config.promptFields)
LeFormatter.formatPipeline(field, config.dedupe, config.removeUnderscore, config.refresh, config.comma);
}
button?.addEventListener("click", () => {
if (!config.autoRun) return;
for (const field of config.promptFields)
LeFormatter.formatPipeline(
field,
config.dedupe,
config.rmUnderscore,
config.refresh,
config.comma,
);
});
}
if (!config.paste)
return;
if (!config.paste) return;
/** https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.10.1/modules/infotext_utils.py#L16 */
const paramPatterns = /\s*(\w[\w \-/]+):\s*("(?:\\.|[^\\"])+"|[^,]*)(?:,|$)/g;
const paramPatterns =
/\s*(\w[\w \-/]+):\s*("(?:\\.|[^\\"])+"|[^,]*)(?:,|$)/g;
for (const field of config.promptFields) {
field.addEventListener('paste', (event) => {
/** @type {string} */
let paste = (event.clipboardData || window.clipboardData).getData('text');
if ([...paste.matchAll(paramPatterns)].length > 3)
return; // Infotext
field.addEventListener("paste", (event) => {
/** @type {string} */ let paste = (event.clipboardData || window.clipboardData).getData("text");
if ([...paste.matchAll(paramPatterns)].length > 3) return; // Infotext
event.preventDefault();
@ -292,25 +304,26 @@ class LeFormatter {
const multiline = !paste.includes(",");
if (config.booru) {
paste = paste.replace(/\s*[\d.]+[kM]\s*|(?:^|,|\s+)\d{2,}(?:\s+|,|\?|$)|[\?\+\-]\s+/g, ", ");
paste = paste.replace(/\s*[\d.]+[kM]\s*|(?:^|,|\s+)\d+(?:\s+|,|\?|$)|[\?\+\-]\s+/g, ", ");
for (const excl of ["Artist", "Characters", "Character", "Copyright", "Tags", "Tag", "General"])
paste = paste.replace(excl, "");
const name_franchise = /\w+?[\_\s]\(.*?\)/g;
paste = paste.replace(name_franchise, (match) => {
return match.replace(/[()]/g, '\\$&');
return match.replace(/[()]/g, "\\$&");
});
}
if (multiline) {
const lines = [];
for (const line of paste.split("\n"))
lines.push(LeFormatter.formatString(line, config.dedupe, config.removeUnderscore));
paste = lines.join("\n");
lines.push(LeFormatter.formatString(line, config.dedupe, config.rmUnderscore));
paste = lines.filter((l) => l).join("\n");
if (!paste.includes(",")) paste = paste.replaceAll("\n", ", ");
} else
paste = LeFormatter.formatString(paste, config.dedupe, config.removeUnderscore);
paste = LeFormatter.formatString(paste, config.dedupe, config.rmUnderscore);
paste = `${commaStart ? ", " : ""}${paste}${commaEnd ? ", " : ""}`
paste = `${commaStart ? ", " : ""}${paste}${commaEnd ? ", " : ""}`;
const currentText = field.value;
const cursorPosition = field.selectionStart;
@ -320,26 +333,8 @@ class LeFormatter {
field.selectionStart = field.selectionEnd = cursorPosition + paste.length;
updateInput(field);
return false;
});
}
let refreshTimer;
function onRefresh() { setTimeout(() => LeFormatter.forceReload(), 100); }
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type !== "childList")
continue;
if (refreshTimer) clearTimeout(refreshTimer);
refreshTimer = setTimeout(onRefresh, 250);
break;
}
});
const lora = document.getElementById('txt2img_lora_cards_html');
const ti = document.getElementById('txt2img_textual_inversion_cards_html');
observer.observe(lora, { childList: true, subtree: true });
observer.observe(ti, { childList: true, subtree: true });
});
})();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

BIN
sample.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,10 +1,54 @@
from gradio import Checkbox, Column, Textbox
from modules import scripts
from modules.script_callbacks import on_ui_settings
from modules.shared import OptionInfo, cmd_opts, opts
def get_embeddings() -> set[str]:
from pathlib import Path
EXTENSIONS = (".pt", ".pth", ".ckpt", ".safetensors", ".sft")
items: set[str] = set()
embeddings = Path(cmd_opts.embeddings_dir)
for ext in EXTENSIONS:
files = embeddings.glob(f"**/*{ext}")
for file in files:
items.add(file.stem)
return items
class PFServer(scripts.Script):
def title(self):
return "Prompt Format"
def show(self, is_img2img):
return scripts.AlwaysVisible if is_img2img else None
def ui(self, is_img2img):
if not is_img2img:
return None
with Column(visible=False):
emb = get_embeddings()
link = Textbox(
value="\n".join(emb),
elem_id="pf_embeddings",
interactive=False,
)
dummy = Checkbox(
label="Enable",
elem_id="pf_checkbox",
interactive=True,
)
link.do_not_save_to_config = True
dummy.do_not_save_to_config = True
def on_settings():
from modules.shared import OptionInfo, opts
from gradio import Textbox
args = {"section": ("pf", "Prompt Format"), "category_id": "system"}
opts.add_option(
@ -20,17 +64,23 @@ def on_settings():
opts.add_option(
"pf_startinauto",
OptionInfo(True, "Launch in Auto Mode", **args),
OptionInfo(
True, "Launch the WebUI with Auto Format enabled", **args
).needs_reload_ui(),
)
opts.add_option(
"pf_startwithdedupe",
OptionInfo(True, "Launch with Remove Duplicates", **args),
OptionInfo(
True, "Launch the WebUI with Remove Duplicates enabled", **args
).needs_reload_ui(),
)
opts.add_option(
"pf_startwithrmudscr",
OptionInfo(True, "Launch with Remove Underscores", **args),
OptionInfo(
True, "Launch the WebUI with Remove Underscores enabled", **args
).needs_reload_ui(),
)
opts.add_option(
@ -42,22 +92,31 @@ def on_settings():
opts.add_option(
"pf_onpaste",
OptionInfo(False, "Format the pasted text", **args).needs_reload_ui(),
OptionInfo(
False, "Format the texts pasted from clipboard", **args
).needs_reload_ui(),
)
opts.add_option(
"pf_booru",
OptionInfo(False, 'Process the "Booru Structure"', **args)
.info("only take effect when the above option is enabled")
.needs_reload_ui(),
)
opts.add_option(
"pf_exclusion",
OptionInfo(
default="",
label="Exclude Tags from Remove Underscores",
label="Tags excluded from Remove Underscores",
component=Textbox,
component_args={
"placeholder": "score_9, score_8_up, score_7_up",
"max_lines": 1,
"max_lines": 4,
"lines": 1,
},
**args,
),
).info("requires <b>Reload</b> button"),
)
opts.add_option(
@ -67,23 +126,17 @@ def on_settings():
label="Tag Alias for Remove Duplicates",
component=Textbox,
component_args={
"placeholder": "1girl: girl, woman, lady\nadult: \\d*\\s*(y\\.?o\\.?|[Yy]ear[s]? [Oo]ld)",
"max_lines": 16,
"lines": 4,
"placeholder": "1girl: girl, woman, lady\nadult: \\d+\\s*(y\\.?o\\.?|[Yy]ear[s]? [Oo]ld)",
"max_lines": 8,
"lines": 2,
},
**args,
)
.info("treat tags on the right as duplicates of the main tag on the left")
.link("RegExr", "https://regexr.com/"),
)
opts.add_option(
"pf_booru",
OptionInfo(
False,
'Process the "Booru Structure"',
**args,
).info("requires format on paste) (<b>Experimental</b>"),
.info("requires <b>Reload</b> button")
.link(
"Regexper",
"https://regexper.com/#%5Cd%2B%5Cs*%28y%5C.%3Fo%5C.%3F%7C%5BYy%5Dear%5Bs%5D%3F%20%5BOo%5Dld%29",
),
)