diff --git a/README.md b/README.md index c64d8b5..67c20a3 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,19 @@ Output text ### D2 Refiner Steps Tester - A node for checking steps + +### Prompt convert dialog + + + +- Dialog for converting weights between NovelAI and StableDiffusion formats +- Open from the `Prompt convert` button +- Hidden by default +- Enable `settings > D2 > Show prompt convert button` to display + + + + ## :card_index_dividers: Sample Workflow You can recreate the workflow by dropping the image into ComfyUI. @@ -514,6 +527,7 @@ In the custom_nodes folder, `git clone` **2024.10.28** +- Prompt convert: Added a dialog for converting prompts between NovelAI and StableDiffusion formats - D2 Folder Image Queue: Fixed a bug where the number of generated images was not consistent **2024.10.26** diff --git a/README_ja.md b/README_ja.md index f85ff8f..00fa893 100644 --- a/README_ja.md +++ b/README_ja.md @@ -480,6 +480,21 @@ Output text - steps を確認するためのノード + + +### Prompt convert dialog + + + +- NovelAI と StableDiffusion の weight を相互変換するダイアログ +- `Prompt convert` ボタンから開く +- 初期状態では非表示になっている +- `settings > D2 > Show prompt convert button` を有効にすると表示する + + + + + ## :card_index_dividers: Sample Workflow 画像を ComfyUI にドロップするとワークフローを再現できます。 @@ -522,6 +537,7 @@ custom_nodes フォルダで `git clone` する **2024.10.28** +- Prompt convert: NovelAI と StableDiffusion のプロンプトを相互変換するダイアログを追加 - D2 Folder Image Queue: 画像生成枚数が同じにならない不具合を修正 **2024.10.26** diff --git a/README_zh.md b/README_zh.md index d08b744..2ca4715 100644 --- a/README_zh.md +++ b/README_zh.md @@ -476,6 +476,18 @@ Output text ### D2 Refiner Steps Tester - 用於確認步數的節點 + +### Prompt convert dialog + + + +- 提供 NovelAI 和 StableDiffusion 權重(weight)互相轉換的對話框 +- 從 `Prompt convert` 按鈕開啟 +- 預設為隱藏狀態 +- 啟用 `settings > D2 > Show prompt convert button` 後即可顯示 + + + ## :card_index_dividers: 示例工作流程 將圖像拖放到 ComfyUI 中可以重現工作流程。 @@ -511,6 +523,7 @@ ComfyUI Manager → Custom Node Manager → 搜索 `D2 Nodes` 並安裝 **2024.10.28** +- Prompt convert: 新增 NovelAI 和 StableDiffusion 提示詞(prompt)互相轉換的對話框 - D2 Folder Image Queue: 修復生成圖片數量不一致的錯誤 **2024.10.26** diff --git a/img/prompt_convert_dialog.png b/img/prompt_convert_dialog.png new file mode 100644 index 0000000..5ca78ff Binary files /dev/null and b/img/prompt_convert_dialog.png differ diff --git a/pyproject.toml b/pyproject.toml index 0d13918..3867a81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "d2-nodes-comfyui" description = "This is a collection of custom nodes that make ComfyUI slightly more convenient." -version = "1.8.1" +version = "1.9.1" license = {file = "LICENSE"} [project.urls] diff --git a/web/D2_FloatContainer.js b/web/D2_FloatContainer.js index df8b859..4b9353a 100644 --- a/web/D2_FloatContainer.js +++ b/web/D2_FloatContainer.js @@ -19,7 +19,6 @@ class D2_FloatContainer { // 初期位置 const pos = this._getPosition(default_left, default_top); - console.log("constructor", pos); this.container.style.left = pos[0] + "px"; this.container.style.top = pos[1] + "px"; @@ -94,7 +93,6 @@ class D2_FloatContainer { _getPosition(default_left, default_top) { const cookieName = `${this.cookieNameBase}_pos`; const cookieValue = getCookie(cookieName); - console.log("_getPosition", cookieName, cookieValue); return cookieValue !== undefined ? cookieValue : [default_left, default_top]; } diff --git a/web/D2_PromptConverDialog.js b/web/D2_PromptConverDialog.js new file mode 100644 index 0000000..ec3c8b2 --- /dev/null +++ b/web/D2_PromptConverDialog.js @@ -0,0 +1,246 @@ +import { app } from "../../scripts/app.js"; +import { D2_FloatContainer } from "./D2_FloatContainer.js"; +import { loadCssFile } from "./utils.js"; + + +/** + * プロンプト変換 + */ + +class D2_PromptConvert { + static RATE = 1.05; + + /** + * StableDiffusionのweightをNAI方式に変換 + */ + static convertToNai(srcPrompt) { + const tempPrompt = srcPrompt.replace(/\n/g, ''); + const convertedPrompt = tempPrompt.replace(/\([^)]+:\s*[0-9.]+\s*\)/g, D2_PromptConvert.$_convertToNai); + return convertedPrompt; + } + + static $_convertToNai(prompt) { + const parts = prompt.substring(1, prompt.length - 1).split(':'); + const text = parts.slice(0, -1).join(':').trim(); + const weightStr = parts[parts.length - 1].trim(); + const weight = parseFloat(weightStr); + const n = Math.round(Math.log(weight) / Math.log(D2_PromptConvert.RATE)); + const count = Math.abs(n); + + let openBra, closeBra; + if (weight < 1) { + openBra = '['; + closeBra = ']'; + } else { + openBra = '{'; + closeBra = '}'; + } + + return openBra.repeat(count) + text + closeBra.repeat(count); + } + + /** + * NAIのweightをStableDiffusion方式に変換 + */ + static convertToSd(srcPrompt) { + let tempPrompt = srcPrompt.replace(/\n/g, ''); + let convertedPrompt = tempPrompt.replace(/[\[{]+[^\]}]+[\]}]+/g, D2_PromptConvert.$_convertToSd); + return convertedPrompt; + } + + static $_convertToSd(prompt) { + const braType = prompt.substring(0, 1); + const braCount = (prompt.match(/[\[{]/g) || []).length; + let weight = 0; + + if (braType === '{') { + weight = 1 * Math.pow(D2_PromptConvert.RATE, braCount); + } else { + weight = 1 * Math.pow(1 / D2_PromptConvert.RATE, braCount); + } + + let weightAdjust; + + // 四捨五入して小数点2位にするか、 + // 四捨五入せず小数点3位で切り捨てるか + weightAdjust = Math.round(weight * 10) / 10; + // if (opts.d2_npc_enable_rounding) { + // weightAdjust = Math.round(weight * 10) / 10; + // } else { + // weightAdjust = Math.floor(weight * 100) / 100; + // } + + const text = prompt.replace(/[\[\]{}]+/g, ''); + return `(${text}:${weightAdjust})`; + } +} + +///////////////////////////////////////////// +///////////////////////////////////////////// +///////////////////////////////////////////// + +/** + * プロンプト変換ダイアログ + */ +class D2_PromptConvertDialog { + static CSS_FILEPATH = "./extensions/D2-nodes-ComfyUI/css/D2_PromptConvertDialog.css"; + + container = undefined; + + constructor() { + this._createDialog(); + loadCssFile(D2_PromptConvertDialog.CSS_FILEPATH); + } + + /** + * モーダルの表示 + */ + showModal() { + // this.container.showModal(); + this.container.style.display = "block"; + } + + _createDialog() { + this.container = document.createElement("div"); + this.container.classList.add("comfy-modal", "d2-prompt-convert"); + document.body.appendChild(this.container); + + const content = document.createElement("div"); + content.classList.add("d2-prompt-convert__content"); + this.container.appendChild(content); + + const sdPrompt = D2_PromptConvertDialog.createPromptArea("input StableDiffusion Prompt"); + content.appendChild(sdPrompt); + + const naiPrompt = D2_PromptConvertDialog.createPromptArea("Input NovelAI Prompt"); + content.appendChild(naiPrompt); + + // SD > NAI 変換ボタン + const sdToNaiBtn = D2_PromptConvertDialog.createButton("SD to NAI", () => { + const prompt = sdPrompt.value; + const newPrompt = D2_PromptConvert.convertToNai(prompt); + naiPrompt.value = newPrompt; + }); + content.appendChild(sdToNaiBtn); + + // NAI > SD 変換ボタン + const naiToSdBtn = D2_PromptConvertDialog.createButton("NAI to SD", () => { + const prompt = naiPrompt.value; + const newPrompt = D2_PromptConvert.convertToSd(prompt); + sdPrompt.value = newPrompt; + }); + content.appendChild(naiToSdBtn); + + // SDクリップボード + const copySdBtn = D2_PromptConvertDialog.createButton("Send SD to Clipboard", () => { + sdPrompt.select(); + document.execCommand("copy"); + }); + content.appendChild(copySdBtn); + + // NAIクリップボード + const copyNaiBtn = D2_PromptConvertDialog.createButton("Send NAI to Clipboard", () => { + naiPrompt.select(); + document.execCommand("copy"); + }); + content.appendChild(copyNaiBtn); + + // 閉じるボタン + const closeBtn = document.createElement("button"); + closeBtn.classList.add("d2-prompt-convert__close-btn"); + closeBtn.textContent = "CLOSE"; + closeBtn.addEventListener("click", () => { + // container.close(); + this.container.style.display = "none"; + }); + content.appendChild(closeBtn); + } + + /** + * プロンプトエリア作成 + * @param {string} placeholder + * @returns HTMLElement + */ + static createPromptArea(placeholder) { + const textArea = document.createElement("textarea"); + textArea.classList.add("comfy-multiline-input", "d2-prompt-convert__prompt-area"); + textArea.placeholder = placeholder; + return textArea; + } + + /** + * ボタン作成 + * @param {string} text + * @param {function} onClick + * @returns + */ + static createButton(text, onClick) { + const btn = document.createElement("button"); + btn.classList.add("d2-prompt-convert__btn"); + btn.textContent = text; + btn.addEventListener("click", () => { + onClick(); + }); + return btn; + } +} + +/////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////// + +/** + * ダイアログ表示ボタン + */ +class D2_PromptConvertButton { + floatContainer = undefined; + dialog = undefined; + + constructor() { + // フロートコンテナ + this.floatContainer = new D2_FloatContainer("D2_PromptConvertBUtton", 50, 100); + // ダイアログ + this.dialog = new D2_PromptConvertDialog(); + + this._createButton(); + + // 表示切り替え + const visible = app.ui.settings.getSettingValue("D2.PromptConvertButton.Visible", false); + this.changeVisible(visible); + } + + /** + * 表示切り替え + */ + changeVisible(bool) { + this.floatContainer.changeVisible(bool); + } + + /** + * ボタン作成 + */ + _createButton() { + const button = document.createElement("button"); + button.classList.add("p-button"); + button.textContent = "Prompt convert"; + this.floatContainer.addButton(button); + + button.addEventListener("click", () => { + this.dialog.showModal(); + }); + } +} + +(function () { + const promptConvertButton = new D2_PromptConvertButton(); + + app.ui.settings.addSetting({ + id: "D2.PromptConvertButton.Visible", + name: "Show prompt convert button", + type: "boolean", + defaultValue: false, + onChange(value) { + promptConvertButton.changeVisible(value); + }, + }); +})(); diff --git a/web/css/D2_PromptConvertDialog.css b/web/css/D2_PromptConvertDialog.css new file mode 100644 index 0000000..2379c0a --- /dev/null +++ b/web/css/D2_PromptConvertDialog.css @@ -0,0 +1,25 @@ +.d2-prompt-convert { + z-index: 1000; + width: 1000px; + max-width: 100%; +} + +.d2-prompt-convert__content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .3rem 1rem; +} + +/* .d2-prompt-convert__button-container { + gap: 0.25rem; +} */ + +.d2-prompt-convert__prompt-area{ + height: 15rem; +} + + +.d2-prompt-convert__close-btn{ + grid-column: span 2; + margin-top: 1rem; +} \ No newline at end of file