From ebe80e30abc0bf37af8b84ef506e2eff723c700c Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sat, 1 Mar 2025 09:10:14 +0800 Subject: [PATCH] Enhance lora loader --- py/nodes/lora_loader.py | 8 +- web/comfyui/lora_loader.js | 86 ++++ web/comfyui/loras_widget.js | 366 ++++++++++++++++++ web/comfyui/{lm_widgets.js => tags_widget.js} | 6 +- ...-word-toggle.js => trigger_word_toggle.js} | 5 +- 5 files changed, 467 insertions(+), 4 deletions(-) create mode 100644 web/comfyui/lora_loader.js create mode 100644 web/comfyui/loras_widget.js rename web/comfyui/{lm_widgets.js => tags_widget.js} (96%) rename web/comfyui/{trigger-word-toggle.js => trigger_word_toggle.js} (94%) diff --git a/py/nodes/lora_loader.py b/py/nodes/lora_loader.py index 9fa50935..e0094181 100644 --- a/py/nodes/lora_loader.py +++ b/py/nodes/lora_loader.py @@ -19,7 +19,8 @@ class LoraManagerLoader: "text": (IO.STRING, { "multiline": True, "dynamicPrompts": True, - "tooltip": "Format: separated by spaces or punctuation" + "tooltip": "Format: separated by spaces or punctuation", + "placeholder": "LoRA syntax input: " }), }, } @@ -47,8 +48,11 @@ class LoraManagerLoader: return relative_path, trigger_words return lora_name, [] # Fallback if not found - def load_loras(self, model, clip, text): + def load_loras(self, model, clip, text, **kwargs): """Loads multiple LoRAs based on the text input format.""" + for key, value in kwargs.items(): + print(f"{key}: {value}") + lora_pattern = r'' lora_matches = re.finditer(lora_pattern, text) diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js new file mode 100644 index 00000000..3a19ed44 --- /dev/null +++ b/web/comfyui/lora_loader.js @@ -0,0 +1,86 @@ +import { app } from "../../scripts/app.js"; +import { addLorasWidget } from "./loras_widget.js"; +import { hideWidgetForGood } from "./utils.js"; + +function mergeLoras(lorasText, lorasJson) { + const result = []; + const pattern = //g; + let match; + + // Parse text input and create initial entries + while ((match = pattern.exec(lorasText)) !== null) { + const name = match[1]; + const inputStrength = Number(match[2]); + + // Find if this lora exists in the JSON data + const existingLora = lorasJson.find(l => l.name === name); + + result.push({ + name: name, + // Use existing strength if available, otherwise use input strength + strength: existingLora ? existingLora.strength : inputStrength, + active: existingLora ? existingLora.active : true + }); + } + + return result; +} + +app.registerExtension({ + name: "LoraManager.LoraLoader", + + setup(...args) { + console.log("LoraLoader setup args:", args); + }, + + async nodeCreated(node) { + if (node.comfyClass === "Lora Loader (LoraManager)") { + // Enable widget serialization + node.serialize_widgets = true; + + // Wait for node to be properly initialized + requestAnimationFrame(() => { + // Restore saved value if exists + let existingLoras = []; + if (node.widgets_values && node.widgets_values.length > 0) { + // 0 is input, 1 is loras widget + try { + existingLoras = JSON.parse(node.widgets_values[1]); + } catch (e) { + console.warn("Failed to parse loras data:", e); + existingLoras = []; + } + } + // Merge the loras data + const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras); + + // Get the widget object directly from the returned object + const result = addLorasWidget(node, "loras", { + defaultVal: mergedLoras // Pass object directly + }, (value) => { + // TODO + }); + + node.lorasWidget = result.widget; + + // get the input widget and set a callback + const inputWidget = node.widgets[0]; + inputWidget.callback = (value) => { + // Merge the loras data with widget value + const currentLoras = node.lorasWidget.value || []; + const mergedLoras = mergeLoras(value, currentLoras); + + node.lorasWidget.value = mergedLoras; + }; + + console.log("node: ", node); + }); + } + }, + + async nodeRemoved(node) { + if (node.comfyClass === "Lora Loader (LoraManager)") { + // TODO: Remove widget from node + } + }, +}); \ No newline at end of file diff --git a/web/comfyui/loras_widget.js b/web/comfyui/loras_widget.js new file mode 100644 index 00000000..4818895b --- /dev/null +++ b/web/comfyui/loras_widget.js @@ -0,0 +1,366 @@ +export function addLorasWidget(node, name, opts, callback) { + // Create container for loras + const container = document.createElement("div"); + container.className = "comfy-loras-container"; + Object.assign(container.style, { + display: "flex", + flexDirection: "column", + gap: "8px", + padding: "6px", + backgroundColor: "rgba(40, 44, 52, 0.6)", + borderRadius: "6px", + width: "100%", + }); + + // Initialize default value + const defaultValue = opts?.defaultVal || ""; + + // Parse LoRA entries from value + const parseLoraValue = (value) => { + if (!value) return []; + return Array.isArray(value) ? value : []; + }; + + // Format LoRA data + const formatLoraValue = (loras) => { + return loras; + }; + + // Function to create toggle element + const createToggle = (active, onChange) => { + const toggle = document.createElement("div"); + toggle.className = "comfy-lora-toggle"; + + updateToggleStyle(toggle, active); + + toggle.addEventListener("click", (e) => { + e.stopPropagation(); + onChange(!active); + }); + + return toggle; + }; + + // Helper function to update toggle style + function updateToggleStyle(toggleEl, active) { + Object.assign(toggleEl.style, { + width: "18px", + height: "18px", + borderRadius: "4px", + cursor: "pointer", + transition: "all 0.2s ease", + backgroundColor: active ? "rgba(66, 153, 225, 0.9)" : "rgba(45, 55, 72, 0.7)", + border: `1px solid ${active ? "rgba(66, 153, 225, 0.9)" : "rgba(226, 232, 240, 0.2)"}`, + }); + + // Add hover effect + toggleEl.onmouseenter = () => { + toggleEl.style.transform = "scale(1.05)"; + toggleEl.style.boxShadow = "0 2px 4px rgba(0,0,0,0.15)"; + }; + + toggleEl.onmouseleave = () => { + toggleEl.style.transform = "scale(1)"; + toggleEl.style.boxShadow = "none"; + }; + } + + // Create arrow button for strength adjustment + const createArrowButton = (direction, onClick) => { + const button = document.createElement("div"); + button.className = `comfy-lora-arrow comfy-lora-arrow-${direction}`; + + Object.assign(button.style, { + width: "16px", + height: "16px", + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + userSelect: "none", + fontSize: "12px", + color: "rgba(226, 232, 240, 0.8)", + transition: "all 0.2s ease", + }); + + button.textContent = direction === "left" ? "◀" : "▶"; + + button.addEventListener("click", (e) => { + e.stopPropagation(); + onClick(); + }); + + // Add hover effect + button.onmouseenter = () => { + button.style.color = "white"; + button.style.transform = "scale(1.2)"; + }; + + button.onmouseleave = () => { + button.style.color = "rgba(226, 232, 240, 0.8)"; + button.style.transform = "scale(1)"; + }; + + return button; + }; + + // Function to render loras from data + const renderLoras = (value, widget) => { + // Clear existing content + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + // Parse the loras data + const lorasData = parseLoraValue(value); + + if (lorasData.length === 0) { + // Show message when no loras are added + const emptyMessage = document.createElement("div"); + emptyMessage.textContent = "No LoRAs added"; + Object.assign(emptyMessage.style, { + textAlign: "center", + padding: "20px 0", + color: "rgba(226, 232, 240, 0.8)", + fontStyle: "italic" + }); + container.appendChild(emptyMessage); + return; + } + + // Create header + const header = document.createElement("div"); + header.className = "comfy-loras-header"; + Object.assign(header.style, { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "4px 8px", + borderBottom: "1px solid rgba(226, 232, 240, 0.2)", + marginBottom: "8px" + }); + + // Add toggle all control + const allActive = lorasData.every(lora => lora.active); + const toggleAll = createToggle(allActive, (active) => { + // Update all loras active state + const lorasData = parseLoraValue(widget.value); + lorasData.forEach(lora => lora.active = active); + + // Update value and trigger widget callback + const newValue = formatLoraValue(lorasData); + widget.value = newValue; + widget.callback?.(newValue); + + // Re-render + renderLoras(newValue, widget); + }); + + // Add label to toggle all + const toggleLabel = document.createElement("div"); + toggleLabel.textContent = "Toggle All"; + Object.assign(toggleLabel.style, { + color: "rgba(226, 232, 240, 0.8)", + fontSize: "13px", + marginLeft: "8px", + }); + + const toggleContainer = document.createElement("div"); + Object.assign(toggleContainer.style, { + display: "flex", + alignItems: "center", + }); + toggleContainer.appendChild(toggleAll); + toggleContainer.appendChild(toggleLabel); + + // Strength label + const strengthLabel = document.createElement("div"); + strengthLabel.textContent = "Strength"; + Object.assign(strengthLabel.style, { + color: "rgba(226, 232, 240, 0.8)", + fontSize: "13px", + marginRight: "8px" + }); + + header.appendChild(toggleContainer); + header.appendChild(strengthLabel); + container.appendChild(header); + + // Render each lora entry + lorasData.forEach((loraData) => { + const { name, strength, active } = loraData; + + const loraEl = document.createElement("div"); + loraEl.className = "comfy-lora-entry"; + Object.assign(loraEl.style, { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "8px", + borderRadius: "6px", + backgroundColor: active ? "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)", + transition: "all 0.2s ease", + marginBottom: "6px", + }); + + // Create toggle for this lora + const toggle = createToggle(active, (newActive) => { + // Update this lora's active state + const lorasData = parseLoraValue(widget.value); + const loraIndex = lorasData.findIndex(l => l.name === name); + + if (loraIndex >= 0) { + lorasData[loraIndex].active = newActive; + + // Update value and trigger widget callback + const newValue = formatLoraValue(lorasData); + widget.value = newValue; + widget.callback?.(newValue); + + // Re-render + renderLoras(newValue, widget); + } + }); + + // Create name display + const nameEl = document.createElement("div"); + nameEl.textContent = name; + nameEl.title = name; // Set tooltip for full name + Object.assign(nameEl.style, { + marginLeft: "10px", + flex: "1", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)", + fontSize: "13px", + }); + + // Create strength control + const strengthControl = document.createElement("div"); + Object.assign(strengthControl.style, { + display: "flex", + alignItems: "center", + gap: "8px", + }); + + // Left arrow + const leftArrow = createArrowButton("left", () => { + // Decrease strength + const lorasData = parseLoraValue(widget.value); + const loraIndex = lorasData.findIndex(l => l.name === name); + + if (loraIndex >= 0) { + lorasData[loraIndex].strength = Math.max(0, lorasData[loraIndex].strength - 0.05).toFixed(2); + + // Update value and trigger widget callback + const newValue = formatLoraValue(lorasData); + widget.value = newValue; + widget.callback?.(newValue); + + // Re-render + renderLoras(newValue, widget); + } + }); + + // Strength display + const strengthEl = document.createElement("div"); + const displayStrength = typeof strength === 'number' ? strength.toFixed(2) : Number(strength).toFixed(2); + strengthEl.textContent = displayStrength; + Object.assign(strengthEl.style, { + minWidth: "36px", + textAlign: "center", + color: active ? "rgba(226, 232, 240, 0.9)" : "rgba(226, 232, 240, 0.6)", + fontSize: "13px", + }); + + // Right arrow + const rightArrow = createArrowButton("right", () => { + // Increase strength + const lorasData = parseLoraValue(widget.value); + const loraIndex = lorasData.findIndex(l => l.name === name); + + if (loraIndex >= 0) { + lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) + 0.05).toFixed(2); + + // Update value and trigger widget callback + const newValue = formatLoraValue(lorasData); + widget.value = newValue; + widget.callback?.(newValue); + + // Re-render + renderLoras(newValue, widget); + } + }); + + strengthControl.appendChild(leftArrow); + strengthControl.appendChild(strengthEl); + strengthControl.appendChild(rightArrow); + + // Assemble entry + const leftSection = document.createElement("div"); + Object.assign(leftSection.style, { + display: "flex", + alignItems: "center", + flex: "1", + minWidth: "0", // Allow shrinking + }); + + leftSection.appendChild(toggle); + leftSection.appendChild(nameEl); + + loraEl.appendChild(leftSection); + loraEl.appendChild(strengthControl); + + // Add hover effect to the lora entry + loraEl.onmouseenter = () => { + loraEl.style.backgroundColor = active ? "rgba(50, 60, 80, 0.8)" : "rgba(40, 45, 55, 0.6)"; + }; + + loraEl.onmouseleave = () => { + loraEl.style.backgroundColor = active ? "rgba(45, 55, 72, 0.7)" : "rgba(35, 40, 50, 0.5)"; + }; + + container.appendChild(loraEl); + }); + }; + + // Store the value in a variable to avoid recursion + let widgetValue = defaultValue; + + // Create widget with initial properties + const widget = node.addDOMWidget(name, "loras", container, { + getValue: function() { + return widgetValue; + }, + setValue: function(v) { + widgetValue = v || ""; + renderLoras(widgetValue, widget); + }, + getHeight: function() { + // Calculate height based on content + const lorasCount = parseLoraValue(widgetValue).length; + return Math.max( + 100, + lorasCount > 0 ? 60 + lorasCount * 44 : 60 // Header + entries or minimum height + ); + }, + onDraw: function() { + // Empty function + } + }); + + // Initialize widget value using options methods + widget.options.setValue(defaultValue); + + widget.callback = callback; + + // Render initial state + renderLoras(widgetValue, widget); + + widget.onRemove = () => { + container.remove(); + }; + + return { minWidth: 400, minHeight: 200, widget }; +} diff --git a/web/comfyui/lm_widgets.js b/web/comfyui/tags_widget.js similarity index 96% rename from web/comfyui/lm_widgets.js rename to web/comfyui/tags_widget.js index 1f79241a..b2f41278 100644 --- a/web/comfyui/lm_widgets.js +++ b/web/comfyui/tags_widget.js @@ -187,6 +187,10 @@ export function addTagsWidget(node, name, opts, callback) { widget.options.setValue(defaultValue); widget.callback = callback; + widget.serializeValue = function(workflowNode, widgetIndex) { + console.log("Serializing tags widget", widget.value); + return widget.value; + }; // Render initial state renderTags(widgetValue, widget); @@ -195,5 +199,5 @@ export function addTagsWidget(node, name, opts, callback) { container.remove(); }; - return { minWidth: 300, minHeight: 30, widget }; + return { minWidth: 300, minHeight: 150, widget }; } diff --git a/web/comfyui/trigger-word-toggle.js b/web/comfyui/trigger_word_toggle.js similarity index 94% rename from web/comfyui/trigger-word-toggle.js rename to web/comfyui/trigger_word_toggle.js index 97b24994..579bd3e4 100644 --- a/web/comfyui/trigger-word-toggle.js +++ b/web/comfyui/trigger_word_toggle.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -import { addTagsWidget } from "./lm_widgets.js"; +import { addTagsWidget } from "./tags_widget.js"; import { hideWidgetForGood } from "./utils.js"; // TriggerWordToggle extension for ComfyUI @@ -19,6 +19,7 @@ app.registerExtension({ if (node.comfyClass === "TriggerWord Toggle (LoraManager)") { // Enable widget serialization node.serialize_widgets = true; + node.size = [400, 200]; // Wait for node to be properly initialized requestAnimationFrame(() => { @@ -46,6 +47,8 @@ app.registerExtension({ result.widget.value = savedValue; } } + + console.log("trigger word toggle node: ", node); }); } },