From 8d3aa1fa9c3647ef30df19fc15d206c7035507ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 12:56:47 +0200 Subject: [PATCH 01/13] Settings now update textbox on change. --- web/js/textbox.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/web/js/textbox.js b/web/js/textbox.js index 6bd997f..f2954ab 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -11,6 +11,13 @@ app.registerExtension({ return textbox; }; + nodeType.prototype.updateHighlightType = function () { + const inputEl = this.inputEl.inputEl; + const overlayEl = this.overlayEl; + setTextColors(inputEl, overlayEl); + setTimeout(() => syncText(this.inputEl.inputEl, this.overlayEl), 50); + } + nodeType.prototype.populate = function () { this.inputEl = this.widgets[0]; @@ -321,6 +328,12 @@ const settingsDefinitions = [ type: SettingUtils.createMultilineSetting, defaultValue: '#559c22\n#229c57\n#229c8b\n#226f9c\n#22479c', attrs: { tooltip: 'A list of either rgb or hex colors, one color per line.' }, + onChange: () => { + const nodes = app.graph._nodes.filter(node => node.type === 'Copy/Paste Textbox'); + nodes.forEach(node => { + node.updateHighlightType(); + }); + }, }, { id: 'sn0w.TextboxGradientColors', @@ -328,6 +341,12 @@ const settingsDefinitions = [ type: 'boolean', defaultValue: false, tooltip: 'Makes the textbox highlighting be a gradient between the first and last color based on the strength of the selection.', + onChange: () => { + const nodes = app.graph._nodes.filter(node => node.type === 'Copy/Paste Textbox'); + nodes.forEach(node => { + node.updateHighlightType(); + }); + }, } ]; @@ -342,6 +361,7 @@ const registerSetting = (settingDefinition) => { defaultValue: settingDefinition.defaultValue, tooltip: settingDefinition.tooltip, attrs: settingDefinition.attrs, + onChange: settingDefinition.onChange, }); }, }; From 872f5fc03b22eb6dc3a5644b180289a6ee8c8032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 14:31:12 +0200 Subject: [PATCH 02/13] Update sn0w.js --- web/js/sn0w.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/js/sn0w.js b/web/js/sn0w.js index 354914a..b610da8 100644 --- a/web/js/sn0w.js +++ b/web/js/sn0w.js @@ -333,7 +333,8 @@ export class SettingUtils { } if (!nodeDef) { - throw new Error(`Node definition with name ${nodeName} not found`); + console.error(`Node definition with name ${nodeName} not found`); + return; } // Find the specific node in the graph by name @@ -346,7 +347,8 @@ export class SettingUtils { } if (!node) { - throw new Error(`Node with name ${nodeName} not found in the graph`); + console.warn(`Node with name ${nodeName} not found in the graph`); + return; } for (let nodeNum in graphCanvas.graph._nodes) { From 71567e2382aacd9c770962983c5760d66f0db2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 14:36:50 +0200 Subject: [PATCH 03/13] Fix problem with new menu and workflow management --- web/js/textbox.js | 94 +++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index f2954ab..58da674 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -48,54 +48,58 @@ app.registerExtension({ this.inputEl.inputEl.style.background = 'transparent'; - // Sync text initially and on input - this.inputEl.inputEl.addEventListener('input', () => { - syncText(this.inputEl.inputEl, this.overlayEl); - setOverlayStyle(this.inputEl, this.overlayEl); - }); + addEventListeners(this); + addObversers(this); - const observer = new MutationObserver(() => { - setOverlayPosition(this.inputEl, this.overlayEl); - }); - - observer.observe(this.inputEl.inputEl, { - attributes: true, - attributeFilter: ['style'], - childList: true, - subtree: true, - characterData: true, - }); - - const parentObserver = new MutationObserver(() => { - if (!document.contains(this.inputEl.inputEl)) { - this.overlayEl.remove(); - } - }); - - parentObserver.observe(this.inputEl.inputEl.parentNode, { - childList: true, - }); - - this.inputEl.inputEl.addEventListener('keydown', (event) => { - if ( - event.ctrlKey && - (event.key === 'ArrowUp' || event.key === 'ArrowDown') - ) { - setTimeout(() => { - syncText(this.inputEl.inputEl, this.overlayEl); - }, 10); - } - }); - - setTimeout(() => { - setOverlayPosition(this.inputEl, this.overlayEl); - setOverlayStyle(this.inputEl, this.overlayEl); - }, 10); + setOverlayPosition(this.inputEl, this.overlayEl); + setOverlayStyle(this.inputEl, this.overlayEl); } else { console.error('Parent node of input element is not available.'); } }; + function addEventListeners(node) { + node.inputEl.inputEl.addEventListener('input', () => { + syncText(node.inputEl.inputEl, node.overlayEl); + setOverlayStyle(node.inputEl, node.overlayEl); + }); + + node.inputEl.inputEl.addEventListener('keydown', (event) => { + if ( + event.ctrlKey && + (event.key === 'ArrowUp' || event.key === 'ArrowDown') + ) { + setTimeout(() => { + syncText(node.inputEl.inputEl, node.overlayEl); + }, 10); + } + }); + } + + function addObversers(node) { + const observer = new MutationObserver(() => { + setOverlayPosition(node.inputEl, node.overlayEl); + }); + + observer.observe(node.inputEl.inputEl, { + attributes: true, + attributeFilter: ['style'], + childList: true, + subtree: true, + characterData: true, + }); + + const parentObserver = new MutationObserver(() => { + if (!document.contains(node.inputEl.inputEl)) { + node.overlayEl.remove(); + } + }); + + parentObserver.observe(node.inputEl.inputEl.parentNode, { + childList: true, + }); + } + async function setTextHighlightType(inputEl) { const highlightGradient = await SettingUtils.getSetting('sn0w.TextboxGradientColors'); if (highlightGradient === null) { @@ -166,7 +170,7 @@ app.registerExtension({ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } - async function syncText(inputEl, overlayEl) { + async function syncText(inputEl, overlayEl, tries = 1) { const text = inputEl.value; overlayEl.textContent = text; @@ -176,7 +180,9 @@ app.registerExtension({ const loraColor = colors ? colors[0] : undefined; if (!colors || !errorColor || !loraColor || shouldHighlightGradient == undefined) { - setTimeout(() => syncText(inputEl, overlayEl), 5); + if (tries < 5) { + setTimeout(() => syncText(inputEl, overlayEl, tries++), tries * 5); + } return; } From 4f48994b1a7746ff8817963ef4e02d10b12c973b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 15:17:14 +0200 Subject: [PATCH 04/13] Rewrite highlighting to use id and make lora highlighting more unique from parentheses. --- web/js/textbox.js | 70 +++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index 58da674..1a364b5 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -170,6 +170,20 @@ app.registerExtension({ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } + function charToType(char) { + switch(char) { + case '<': + return '-lora'; + default: + return ''; + } + } + + const charPairs = { + '(': ')', + '<': '>', + }; + async function syncText(inputEl, overlayEl, tries = 1) { const text = inputEl.value; overlayEl.textContent = text; @@ -187,7 +201,7 @@ app.registerExtension({ } let uniqueIdCounter = 0; - const generateUniqueId = () => `unique-span-${uniqueIdCounter++}`; + const generateUniqueId = (type = "") => `unique-span-${uniqueIdCounter++}${type}`; let nestingLevel = 0; let highlightedText = ''; @@ -213,47 +227,43 @@ app.registerExtension({ } let color = colors[nestingLevel % colors.length]; - let uniqueId = generateUniqueId(); + let uniqueId = generateUniqueId(charToType(char)); if (inputEl.highlightGradient === true) { color = `id-${uniqueId}`; } switch(char) { case '(': - highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}`; - spanStack.push({ id: uniqueId, start: highlightedText.length, originalSpan: ``, nestingLevel }); - nestingLevel++; - lastIndex = i + 1; - break; case '<': - uniqueId = `${uniqueId}-lora`; - color = `id-${uniqueId}`; highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}`; - spanStack.push({ id: uniqueId, start: highlightedText.length - 3, originalSpan: ``, nestingLevel }); + spanStack.push({ id: uniqueId, start: highlightedText.length, originalSpan: ``, nestingLevel, originalColor: color, originalChar: char }); nestingLevel++; lastIndex = i + 1; break; case ')': case '>': if (nestingLevel > 0) { - highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}`; - const { id } = spanStack.pop(); - nestingLevel--; + const { id, originalColor, originalChar } = spanStack[spanStack.length - 1]; + if (charPairs[originalChar] === char) { + spanStack.pop(); + highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}`; + nestingLevel--; - if (inputEl.highlightGradient === true || id.endsWith('lora')) { - // Check for the strength - const strengthText = text.slice(Math.max(0, i - 10), i); - const match = strengthText.match(/(\d+(\.\d+)?)\s*$/); - if (match) { - const strength = parseFloat(match[1]); - const clampedStrength = Math.max(0, Math.min(2, strength)); - const normalizedStrength = clampedStrength / 2; - const newColor = interpolateColor(colors[0], colors[colors.length - 1], easeInOutCubic(normalizedStrength)); - uniqueIdMap.set(id, newColor); - } else { - uniqueIdMap.set(id, interpolateColor(colors[0], colors[colors.length - 1], 0.5)); + if (inputEl.highlightGradient === true || id.endsWith('lora')) { + // Check for the strength + const strengthText = text.slice(Math.max(0, i - 10), i); + const match = strengthText.match(/(\d+(\.\d+)?)\s*$/); + if (match) { + const strength = parseFloat(match[1]); + const clampedStrength = Math.max(0, Math.min(2, strength)); + const normalizedStrength = clampedStrength / 2; + const newColor = interpolateColor(colors[0], colors[colors.length - 1], easeInOutCubic(normalizedStrength)); + uniqueIdMap.set(id, [newColor, originalColor]); + } else { + uniqueIdMap.set(id, [interpolateColor(colors[0], colors[colors.length - 1], 0.5), originalColor]); + } } + lastIndex = i + 1; } - lastIndex = i + 1; } break; } @@ -270,8 +280,7 @@ app.registerExtension({ const errorSpanTag = ``; if (originalSpan) { - const newText = highlightedText.slice(start - (originalSpan.length + 1), highlightedText.length).replace(originalSpan, errorSpanTag); - highlightedText = highlightedText.slice(0, start - originalSpan.length + 1) + newText; + highlightedText = highlightedText.replace(originalSpan, errorSpanTag); highlightedText += ``; } } @@ -279,8 +288,9 @@ app.registerExtension({ } // Apply the updated colors to the highlighted text - uniqueIdMap.forEach((newColor, id) => { - highlightedText = highlightedText.replace(`background-color: id-${id}`, `background-color: ${newColor}`); + uniqueIdMap.forEach((colors, id) => { + const [newColor, originalColor] = [colors[0], colors[1]]; + highlightedText = highlightedText.replace(`id="${id}" style="background-color: ${originalColor};"`, `id="${id}" style="background-color: ${newColor};"`); }); overlayEl.innerHTML = highlightedText; From fb27fb396c5f12a998ac7cda7dff6bfdd18b4d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 19:46:49 +0200 Subject: [PATCH 05/13] Add embedding highlighting --- web/js/textbox.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index 1a364b5..62b261e 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -174,6 +174,8 @@ app.registerExtension({ switch(char) { case '<': return '-lora'; + case 'e': + return '-embedding'; default: return ''; } @@ -201,7 +203,7 @@ app.registerExtension({ } let uniqueIdCounter = 0; - const generateUniqueId = (type = "") => `unique-span-${uniqueIdCounter++}${type}`; + const generateUniqueId = (type = "") => `span-${uniqueIdCounter++}${type}`; let nestingLevel = 0; let highlightedText = ''; @@ -209,8 +211,13 @@ app.registerExtension({ let spanStack = []; const uniqueIdMap = new Map(); + /* + * This loop iterates over each character in the `text` string to apply syntax highlighting and handle special cases. + * - lastIndex: index of the last processed character, used to slice text segments. + * - spanStack: stack to manage opened spans, storing their ids, start positions, original colors, and characters. + */ for (let i = 0; i < text.length; i++) { - const char = text[i]; + const char = text[i].toLowerCase(); // Handle escape characters if (char === '\\' && i + 1 < text.length && (text[i + 1] === '(' || text[i + 1] === ')')) { @@ -239,6 +246,26 @@ app.registerExtension({ nestingLevel++; lastIndex = i + 1; break; + case 'e': + const embeddingColor = colors[0]; + const embeddingPrefix = "embedding:"; + const startIndex = i; + + // Check if the text starts with "embedding:" at position i + if (text.toLowerCase().startsWith(embeddingPrefix, i)) { + // Find the end of the embedding (next space or comma) + let endIndex = i + embeddingPrefix.length; + while (endIndex < text.length && text[endIndex] !== ' ' && text[endIndex] !== ',') { + endIndex++; + } + + // Get the full embedding text + const embeddingText = text.slice(startIndex, endIndex); + const wrappedEmbedding = `${escapeHtml(embeddingText)}`; + highlightedText += text.slice(lastIndex, i) + wrappedEmbedding; + lastIndex = i + embeddingText.length; + } + break; case ')': case '>': if (nestingLevel > 0) { From 17f1d8654a87426ce76b5328656b61b8cbdc57cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 20:23:37 +0200 Subject: [PATCH 06/13] Add lora validation for highlighting --- sn0w.py | 8 +++++++ web/js/textbox.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/sn0w.py b/sn0w.py index 40d6741..54082e8 100644 --- a/sn0w.py +++ b/sn0w.py @@ -16,6 +16,8 @@ import torch from server import PromptServer from aiohttp import web +import folder_paths + class ConfigReader: """ @@ -331,3 +333,9 @@ class MessageHolder: except ValueError: cls.logger.log(f"failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}", "ERROR") return [1] if asList else 1 + + +@PromptServer.instance.routes.get("/sn0w/loras") +async def get_loras(request): + loras = folder_paths.get_filename_list("loras") + return web.json_response(list(map(lambda a: os.path.splitext(a)[0], loras))) diff --git a/web/js/textbox.js b/web/js/textbox.js index 62b261e..786c7a2 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -1,5 +1,6 @@ import { SettingUtils } from './sn0w.js'; import { app } from '../../../scripts/app.js'; +import { api } from '../../../scripts/api.js'; app.registerExtension({ name: 'sn0w.Textbox', @@ -181,6 +182,40 @@ app.registerExtension({ } } + function extractLoraName(text, currentIndex) { + const loraPrefix = "lora:"; + const startIndex = text.lastIndexOf(loraPrefix, currentIndex) + loraPrefix.length; + + // Find the end of the LoRA name (next space, comma, or ".safetensors") + let endIndex = text.indexOf(' ', startIndex); + const commaIndex = text.indexOf(',', startIndex); + const safetensorsIndex = text.indexOf('.safetensors', startIndex); + + if (endIndex === -1 || (commaIndex !== -1 && commaIndex < endIndex)) { + endIndex = commaIndex; + } + if (endIndex === -1 || (safetensorsIndex !== -1 && safetensorsIndex < endIndex)) { + endIndex = safetensorsIndex; + } + + // If there's no space, comma, or ".safetensors" after the LoRA name, consider the end of the text + const loraName = endIndex === -1 ? text.slice(startIndex) : text.slice(startIndex, endIndex); + return loraName; + } + + function validateLoraName(inputEl, loraName) { + if (!inputEl.validLoras) { + console.error('Valid LoRA names not defined or not an array.'); + return false; + } + + if (inputEl.validLoras.includes(loraName)) { + return true; + } else { + return false; + } + } + const charPairs = { '(': ')', '<': '>', @@ -275,6 +310,16 @@ app.registerExtension({ highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}`; nestingLevel--; + // Extract and validate the LoRA name + if (id.endsWith('lora')) { + const loraName = extractLoraName(text, i); + if (validateLoraName(inputEl, loraName) === false) { + uniqueIdMap.set(id, [errorColor, originalColor]); + lastIndex = i + 1; + continue; + } + } + if (inputEl.highlightGradient === true || id.endsWith('lora')) { // Check for the strength const strengthText = text.slice(Math.max(0, i - 10), i); @@ -355,9 +400,22 @@ app.registerExtension({ overlayEl.style.wordWrap = 'break-word'; } + async function setValidLoras(inputEl) { + const apiRequest = await api.fetchApi(`${SettingUtils.API_PREFIX}/loras`, { + method: "GET", + }) + if (!apiRequest.ok) { + console.error('API request failed:', apiRequest.statusText); + } else { + const responseBody = await apiRequest.json(); + inputEl.validLoras = responseBody; + } + } + nodeType.prototype.onNodeCreated = function () { this.populate(); setTextColors(this.inputEl.inputEl, this.overlayEl); + setValidLoras(this.inputEl.inputEl) this.getTextboxText = this.getTextboxText.bind(this); }; } From 924a5ba1acf59c0476e61941baee0114766e05eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 20:38:56 +0200 Subject: [PATCH 07/13] Add embedding validation --- sn0w.py | 6 ++++++ web/js/textbox.js | 26 +++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/sn0w.py b/sn0w.py index 54082e8..8707ba1 100644 --- a/sn0w.py +++ b/sn0w.py @@ -339,3 +339,9 @@ class MessageHolder: async def get_loras(request): loras = folder_paths.get_filename_list("loras") return web.json_response(list(map(lambda a: os.path.splitext(a)[0], loras))) + + +@PromptServer.instance.routes.get("/sn0w/embeddings") +async def get_embeddings(request): + embeddings = folder_paths.get_filename_list("embeddings") + return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings))) diff --git a/web/js/textbox.js b/web/js/textbox.js index 786c7a2..e37e41f 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -203,13 +203,13 @@ app.registerExtension({ return loraName; } - function validateLoraName(inputEl, loraName) { - if (!inputEl.validLoras) { + function validateName(validFiles, name) { + if (!validFiles) { console.error('Valid LoRA names not defined or not an array.'); return false; } - if (inputEl.validLoras.includes(loraName)) { + if (validFiles.includes(name)) { return true; } else { return false; @@ -282,7 +282,7 @@ app.registerExtension({ lastIndex = i + 1; break; case 'e': - const embeddingColor = colors[0]; + let embeddingColor = colors[0]; const embeddingPrefix = "embedding:"; const startIndex = i; @@ -296,6 +296,9 @@ app.registerExtension({ // Get the full embedding text const embeddingText = text.slice(startIndex, endIndex); + if (validateName(inputEl.validEmbeddings, embeddingText.split(":")[1]) === false) { + embeddingColor = errorColor; + } const wrappedEmbedding = `${escapeHtml(embeddingText)}`; highlightedText += text.slice(lastIndex, i) + wrappedEmbedding; lastIndex = i + embeddingText.length; @@ -313,7 +316,7 @@ app.registerExtension({ // Extract and validate the LoRA name if (id.endsWith('lora')) { const loraName = extractLoraName(text, i); - if (validateLoraName(inputEl, loraName) === false) { + if (validateName(inputEl.validLoras, loraName) === false) { uniqueIdMap.set(id, [errorColor, originalColor]); lastIndex = i + 1; continue; @@ -400,22 +403,27 @@ app.registerExtension({ overlayEl.style.wordWrap = 'break-word'; } - async function setValidLoras(inputEl) { - const apiRequest = await api.fetchApi(`${SettingUtils.API_PREFIX}/loras`, { + async function setValidFiles(inputEl) { + inputEl.validLoras = await getValidFiles("loras"); + inputEl.validEmbeddings = await getValidFiles("embeddings"); + } + + async function getValidFiles(type) { + const apiRequest = await api.fetchApi(`${SettingUtils.API_PREFIX}/${type}`, { method: "GET", }) if (!apiRequest.ok) { console.error('API request failed:', apiRequest.statusText); } else { const responseBody = await apiRequest.json(); - inputEl.validLoras = responseBody; + return responseBody; } } nodeType.prototype.onNodeCreated = function () { this.populate(); setTextColors(this.inputEl.inputEl, this.overlayEl); - setValidLoras(this.inputEl.inputEl) + setValidFiles(this.inputEl.inputEl); this.getTextboxText = this.getTextboxText.bind(this); }; } From 7ee04b78c25b8e0118e095b53083b81b2acbb473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Mon, 15 Jul 2024 22:23:27 +0200 Subject: [PATCH 08/13] Fix error log --- web/js/textbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index e37e41f..39a6af3 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -205,7 +205,7 @@ app.registerExtension({ function validateName(validFiles, name) { if (!validFiles) { - console.error('Valid LoRA names not defined or not an array.'); + console.error('Valid names not defined or not an array.'); return false; } From e6fe4964b5b99114b542b0b542d5521da22aff8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Tue, 16 Jul 2024 01:43:55 +0200 Subject: [PATCH 09/13] Add api prefix --- sn0w.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sn0w.py b/sn0w.py index 8707ba1..6c72c5f 100644 --- a/sn0w.py +++ b/sn0w.py @@ -331,17 +331,20 @@ class MessageHolder: return int(message.strip()) except ValueError: - cls.logger.log(f"failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}", "ERROR") + cls.logger.log(f"failed to parse '{message}' as {'comma separated list of ints' if asList else 'int'}", "ERROR") return [1] if asList else 1 -@PromptServer.instance.routes.get("/sn0w/loras") +API_PREFIX = "/sn0w" + + +@PromptServer.instance.routes.get(f"{API_PREFIX}/loras") async def get_loras(request): loras = folder_paths.get_filename_list("loras") return web.json_response(list(map(lambda a: os.path.splitext(a)[0], loras))) -@PromptServer.instance.routes.get("/sn0w/embeddings") +@PromptServer.instance.routes.get(f"{API_PREFIX}/embeddings") async def get_embeddings(request): embeddings = folder_paths.get_filename_list("embeddings") return web.json_response(list(map(lambda a: os.path.splitext(a)[0], embeddings))) From 575b4fac9c3a1b60e78f2855e92e786598c6556f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Wed, 17 Jul 2024 16:03:23 +0200 Subject: [PATCH 10/13] Add duplicate tag highlighting --- web/js/textbox.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/web/js/textbox.js b/web/js/textbox.js index 39a6af3..a7a40a2 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -216,6 +216,34 @@ app.registerExtension({ } } + function processTags(tags) { + let returnArray = []; + tags.forEach(tag => { + returnArray.push(processTag(tag)); + }) + return returnArray; + } + + function processTag(tag) { + let trimmedTag = tag.trim(); + + // Remove HTML tags + trimmedTag = trimmedTag.replace(/<[^>]*>/g, ''); + + // Remove the first character if it is a parenthesis + if (trimmedTag.startsWith('(')) { + trimmedTag = trimmedTag.substring(1).trim(); + } + + // Remove everything after a colon + const colonIndex = trimmedTag.indexOf(':'); + if (colonIndex !== -1) { + trimmedTag = trimmedTag.substring(0, colonIndex).trim(); + } + + return trimmedTag; + } + const charPairs = { '(': ')', '<': '>', @@ -246,6 +274,20 @@ app.registerExtension({ let spanStack = []; const uniqueIdMap = new Map(); + const extractTags = (text) => { + let tags = text.split(',').map(tag => tag.trim()); + tags = processTags(tags); + const tagCounts = new Map(); + + tags.forEach(tag => { + tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); + }); + + return { tags, tagCounts }; + }; + + const { tags, tagCounts } = extractTags(text); + /* * This loop iterates over each character in the `text` string to apply syntax highlighting and handle special cases. * - lastIndex: index of the last processed character, used to slice text segments. @@ -368,6 +410,20 @@ app.registerExtension({ highlightedText = highlightedText.replace(`id="${id}" style="background-color: ${originalColor};"`, `id="${id}" style="background-color: ${newColor};"`); }); + // Highlight duplicate tags + highlightedText.split(/(,)/).map(segment => { + if (segment === ',') { + return segment; + } + + const trimmedSegment = processTag(segment) + + // Check for duplicates + if (tagCounts.get(trimmedSegment) > 1) { + highlightedText = highlightedText.replaceAll(trimmedSegment, `${trimmedSegment}`) + } + }).join(''); + overlayEl.innerHTML = highlightedText; } From 6daf7b566223efb777a2fb7488708e7b16435351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Thu, 18 Jul 2024 01:02:06 +0200 Subject: [PATCH 11/13] Small changes --- web/js/textbox.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/js/textbox.js b/web/js/textbox.js index a7a40a2..1dae834 100644 --- a/web/js/textbox.js +++ b/web/js/textbox.js @@ -305,7 +305,7 @@ app.registerExtension({ // Handle wrong escape characters if (char === '/' && i + 1 < text.length && (text[i + 1] === '(' || text[i + 1] === ')')) { highlightedText += text.slice(lastIndex, i) + `/`; - console.error(`Try replacing "${char}" at char ${i} with "\\"`); + console.error(`Replace "${char}" at char ${i} with "\\"`); lastIndex = i + 1; continue; } @@ -326,7 +326,6 @@ app.registerExtension({ case 'e': let embeddingColor = colors[0]; const embeddingPrefix = "embedding:"; - const startIndex = i; // Check if the text starts with "embedding:" at position i if (text.toLowerCase().startsWith(embeddingPrefix, i)) { @@ -337,7 +336,7 @@ app.registerExtension({ } // Get the full embedding text - const embeddingText = text.slice(startIndex, endIndex); + const embeddingText = text.slice(i, endIndex); if (validateName(inputEl.validEmbeddings, embeddingText.split(":")[1]) === false) { embeddingColor = errorColor; } @@ -416,7 +415,7 @@ app.registerExtension({ return segment; } - const trimmedSegment = processTag(segment) + const trimmedSegment = processTag(segment); // Check for duplicates if (tagCounts.get(trimmedSegment) > 1) { From 6f173e55c1de2ce42c4ee0b97f80ca975ab4f7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Thu, 18 Jul 2024 01:02:36 +0200 Subject: [PATCH 12/13] Change json loading --- src/character_select.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/character_select.py b/src/character_select.py index c5525c9..a4a7166 100644 --- a/src/character_select.py +++ b/src/character_select.py @@ -9,6 +9,10 @@ from ..sn0w import ConfigReader, Logger, Utility, MessageHolder routes = MessageHolder.routes API_PREFIX = MessageHolder.API_PREFIX +# File paths +CHARACTER_FILE_PATH = "web/settings/characters.json" +CUSTOM_CHARACTER_FILE_PATH = "web/settings/custom_characters.json" + @routes.post(f"{API_PREFIX}/update_characters") async def handle_update_characters(request): @@ -38,15 +42,32 @@ class CharacterSelectNode: cls.load_characters(dir_path) + @classmethod + def get_base_dir(cls): + dir_path = os.path.dirname(os.path.realpath(__file__)) + if os.path.basename(dir_path) == "src": + dir_path = os.path.dirname(dir_path) + return dir_path + + @classmethod + def check_initialize(cls): + if not cls.final_characters or cls.cached_default_character_setting != ConfigReader.get_setting( + "sn0w.DisableDefaultCharacters", False + ): + cls.cached_default_character_setting = ConfigReader.get_setting("sn0w.DisableDefaultCharacters", False) + cls.initialize() + elif cls.cached_sorting_setting != ConfigReader.get_setting("sn0w.SortBySeries", False): + cls.sort_characters() + @classmethod def load_characters(cls, dir_path): character_data = [] if not cls.cached_default_character_setting: - json_path = os.path.join(dir_path, "web/settings/characters.json") + json_path = os.path.join(dir_path, CHARACTER_FILE_PATH) with open(json_path, "r", encoding="utf-8") as file: character_data = json.load(file) - custom_json_path = os.path.join(dir_path, "web/settings/custom_characters.json") + custom_json_path = os.path.join(dir_path, CUSTOM_CHARACTER_FILE_PATH) if os.path.exists(custom_json_path): with open(custom_json_path, "r", encoding="utf-8") as file: custom_character_data = json.load(file) @@ -64,7 +85,10 @@ class CharacterSelectNode: @staticmethod def extract_series_name(character_name): - return character_name.split("(")[-1].split(")")[0].strip() + series = character_name.split("(")[-1].split(")")[0].strip() + if character_name == series: + Logger().log(f"{character_name} has no series name.", "WARNING") + return series @classmethod def sort_characters(cls, force_sort=False): @@ -84,13 +108,7 @@ class CharacterSelectNode: @classmethod def INPUT_TYPES(cls): - if not cls.final_characters: # Check if initialization is needed - cls.initialize() - elif cls.cached_default_character_setting != ConfigReader.get_setting("sn0w.DisableDefaultCharacters", False): - cls.cached_default_character_setting = not cls.cached_default_character_setting - cls.initialize() - else: - cls.sort_characters() + cls.check_initialize() character_names = ["None"] + list(cls.final_characters) return { "required": { From ede0eb43dbe7d1492e56e0c16d11f7a29fd819cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20=20Sk=C3=B6ld?= Date: Sat, 20 Jul 2024 02:16:10 +0200 Subject: [PATCH 13/13] Add known issues --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2ead150..21bf527 100644 --- a/README.md +++ b/README.md @@ -277,3 +277,7 @@ Note: Uses [ImagesGrid](https://github.com/LEv145/images-grid-comfy-plugin) for My general text to image workflow, note that it uses several nodes from different custom nodes. ![T2I](./imgs/normal_workflow.png) + +# Known Issues + +* Embedding highlighting does not work on linux.