Merge pull request #7 from sn0w12/dev

Dev
This commit is contained in:
sn0w
2024-07-20 02:16:47 +02:00
committed by GitHub
5 changed files with 314 additions and 89 deletions
+4
View File
@@ -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.
+18 -1
View File
@@ -16,6 +16,8 @@ import torch
from server import PromptServer
from aiohttp import web
import folder_paths
class ConfigReader:
"""
@@ -329,5 +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
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(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)))
+28 -10
View File
@@ -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": {
+4 -2
View File
@@ -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) {
+260 -76
View File
@@ -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',
@@ -11,6 +12,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];
@@ -41,54 +49,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) {
@@ -159,7 +171,85 @@ app.registerExtension({
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
async function syncText(inputEl, overlayEl) {
function charToType(char) {
switch(char) {
case '<':
return '-lora';
case 'e':
return '-embedding';
default:
return '';
}
}
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 validateName(validFiles, name) {
if (!validFiles) {
console.error('Valid names not defined or not an array.');
return false;
}
if (validFiles.includes(name)) {
return true;
} else {
return false;
}
}
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 = {
'(': ')',
'<': '>',
};
async function syncText(inputEl, overlayEl, tries = 1) {
const text = inputEl.value;
overlayEl.textContent = text;
@@ -169,12 +259,14 @@ 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;
}
let uniqueIdCounter = 0;
const generateUniqueId = () => `unique-span-${uniqueIdCounter++}`;
const generateUniqueId = (type = "") => `span-${uniqueIdCounter++}${type}`;
let nestingLevel = 0;
let highlightedText = '';
@@ -182,8 +274,27 @@ 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.
* - 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] === ')')) {
@@ -194,53 +305,81 @@ app.registerExtension({
// Handle wrong escape characters
if (char === '/' && i + 1 < text.length && (text[i + 1] === '(' || text[i + 1] === ')')) {
highlightedText += text.slice(lastIndex, i) + `<span style="background-color: ${errorColor};">/</span>`;
console.error(`Try replacing "${char}" at char ${i} with "\\"`);
console.error(`Replace "${char}" at char ${i} with "\\"`);
lastIndex = i + 1;
continue;
}
let color = colors[nestingLevel % colors.length];
let uniqueId = generateUniqueId();
let uniqueId = generateUniqueId(charToType(char));
if (inputEl.highlightGradient === true) {
color = `id-${uniqueId}`;
}
switch(char) {
case '(':
case '<':
highlightedText += text.slice(lastIndex, i) + `<span id="${uniqueId}" style="background-color: ${color};">${escapeHtml(char)}`;
spanStack.push({ id: uniqueId, start: highlightedText.length, originalSpan: `<span id="${uniqueId}" style="background-color: ${color};">`, nestingLevel });
spanStack.push({ id: uniqueId, start: highlightedText.length, originalSpan: `<span id="${uniqueId}" style="background-color: ${color};">`, nestingLevel, originalColor: color, originalChar: char });
nestingLevel++;
lastIndex = i + 1;
break;
case '<':
uniqueId = `${uniqueId}-lora`;
color = `id-${uniqueId}`;
highlightedText += text.slice(lastIndex, i) + `<span id="${uniqueId}" style="background-color: ${color};">${escapeHtml(char)}`;
spanStack.push({ id: uniqueId, start: highlightedText.length - 3, originalSpan: `<span id="${uniqueId}" style="background-color: ${color};">`, nestingLevel });
nestingLevel++;
lastIndex = i + 1;
case 'e':
let embeddingColor = colors[0];
const embeddingPrefix = "embedding:";
// 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(i, endIndex);
if (validateName(inputEl.validEmbeddings, embeddingText.split(":")[1]) === false) {
embeddingColor = errorColor;
}
const wrappedEmbedding = `<span id="${uniqueId}" style="background-color: ${embeddingColor};">${escapeHtml(embeddingText)}</span>`;
highlightedText += text.slice(lastIndex, i) + wrappedEmbedding;
lastIndex = i + embeddingText.length;
}
break;
case ')':
case '>':
if (nestingLevel > 0) {
highlightedText += text.slice(lastIndex, i) + `${escapeHtml(char)}</span>`;
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)}</span>`;
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));
// Extract and validate the LoRA name
if (id.endsWith('lora')) {
const loraName = extractLoraName(text, i);
if (validateName(inputEl.validLoras, 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);
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;
}
@@ -257,8 +396,7 @@ app.registerExtension({
const errorSpanTag = `<span id="${id}" style="background-color: ${errorColor};">`;
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 += `</span>`;
}
}
@@ -266,10 +404,25 @@ 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};"`);
});
// 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, `<span style="background-color: ${errorColor};">${trimmedSegment}</span>`)
}
}).join('');
overlayEl.innerHTML = highlightedText;
}
@@ -305,9 +458,27 @@ app.registerExtension({
overlayEl.style.wordWrap = 'break-word';
}
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();
return responseBody;
}
}
nodeType.prototype.onNodeCreated = function () {
this.populate();
setTextColors(this.inputEl.inputEl, this.overlayEl);
setValidFiles(this.inputEl.inputEl);
this.getTextboxText = this.getTextboxText.bind(this);
};
}
@@ -321,6 +492,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 +505,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 +525,7 @@ const registerSetting = (settingDefinition) => {
defaultValue: settingDefinition.defaultValue,
tooltip: settingDefinition.tooltip,
attrs: settingDefinition.attrs,
onChange: settingDefinition.onChange,
});
},
};