diff --git a/web/multi_lora_stack.js b/web/multi_lora_stack.js new file mode 100644 index 0000000..13cbb11 --- /dev/null +++ b/web/multi_lora_stack.js @@ -0,0 +1,186 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Get available LoRAs from the API +async function getAvailableLoras() { + try { + const response = await api.fetchApi("/object_info"); + const objectInfo = await response.json(); + + // Look for LoRA inputs in any loader node + for (const [nodeName, nodeInfo] of Object.entries(objectInfo)) { + if (nodeInfo.input?.required?.lora_name) { + return nodeInfo.input.required.lora_name[0] || ["None"]; + } + } + return ["None"]; + } catch (error) { + console.error("Error fetching LoRAs:", error); + return ["None"]; + } +} + +app.registerExtension({ + name: "MultiLoRAStack", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "MultiLoRAStack") { + console.log("Registering MultiLoRAStack node"); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + console.log("MultiLoRAStack node created"); + + const result = onNodeCreated?.apply(this, arguments); + + // Set wider default size + this.size = [450, 300]; + + // Initialize node data + this.loraData = []; + this.availableLoras = await getAvailableLoras(); + + // Add control buttons + this.addWidget("button", "➕ Add LoRA", null, () => { + this.addLoRA(); + }); + + this.addWidget("button", "Toggle All LoRAs", null, () => { + this.toggleAllLoRAs(); + }); + + // Add initial LoRA + this.addLoRA(); + + return result; + }; + + // Add methods to the node prototype + nodeType.prototype.addLoRA = function() { + const index = this.loraData.length; + console.log(`Adding LoRA ${index + 1}`); + + // Add LoRA data + this.loraData.push({ + on: true, + lora: "None", + strength: 1.0 + }); + + // Create widgets for this LoRA + const enableWidget = this.addWidget("toggle", `Enable LoRA ${index + 1}`, true, (value) => { + this.loraData[index].on = value; + this.updateLoraStack(); + }); + + const loraWidget = this.addWidget("combo", `LoRA ${index + 1}`, "None", (value) => { + this.loraData[index].lora = value; + this.updateLoraStack(); + }, { + values: this.availableLoras + }); + + const strengthWidget = this.addWidget("number", `Strength ${index + 1}`, 1.0, (value) => { + this.loraData[index].strength = value; + this.updateLoraStack(); + }, { + min: -2.0, + max: 2.0, + step: 0.1 + }); + + const removeWidget = this.addWidget("button", `Remove LoRA ${index + 1}`, null, () => { + this.removeLoRA(index); + }); + + // Store widget references + this.loraData[index]._widgets = { + enable: enableWidget, + lora: loraWidget, + strength: strengthWidget, + remove: removeWidget + }; + + this.updateLoraStack(); + this.setSize([this.size[0], this.computeSize()[1]]); + }; + + nodeType.prototype.removeLoRA = function(index) { + console.log(`Removing LoRA ${index + 1}`); + + // Get widgets to remove + const loraItem = this.loraData[index]; + if (loraItem && loraItem._widgets) { + // Remove widgets from the node + Object.values(loraItem._widgets).forEach(widget => { + const widgetIndex = this.widgets.indexOf(widget); + if (widgetIndex !== -1) { + this.widgets.splice(widgetIndex, 1); + } + }); + } + + // Remove from data array + this.loraData.splice(index, 1); + + // Update remaining widget names and indices + this.updateWidgetNames(); + this.updateLoraStack(); + this.setSize([this.size[0], this.computeSize()[1]]); + }; + + nodeType.prototype.updateWidgetNames = function() { + // Update widget names after removal + this.loraData.forEach((loraItem, index) => { + if (loraItem._widgets) { + loraItem._widgets.enable.name = `Enable LoRA ${index + 1}`; + loraItem._widgets.lora.name = `LoRA ${index + 1}`; + loraItem._widgets.strength.name = `Strength ${index + 1}`; + loraItem._widgets.remove.name = `Remove LoRA ${index + 1}`; + } + }); + }; + + nodeType.prototype.toggleAllLoRAs = function() { + const allEnabled = this.loraData.every(lora => lora.on); + const newState = !allEnabled; + + console.log(`Toggling all LoRAs to:`, newState); + + this.loraData.forEach((loraItem, index) => { + loraItem.on = newState; + if (loraItem._widgets && loraItem._widgets.enable) { + loraItem._widgets.enable.value = newState; + } + }); + + this.updateLoraStack(); + }; + + nodeType.prototype.updateLoraStack = function() { + // Find the lora_stack widget + const stackWidget = this.widgets.find(w => w.name === "lora_stack"); + if (stackWidget) { + const jsonData = JSON.stringify(this.loraData.map(item => ({ + on: item.on, + lora: item.lora, + strength: item.strength + }))); + stackWidget.value = jsonData; + console.log("Updated lora_stack:", jsonData); + } else { + console.warn("lora_stack widget not found"); + } + }; + + // Override computeSize to ensure minimum width + const originalComputeSize = nodeType.prototype.computeSize; + nodeType.prototype.computeSize = function(out) { + const size = originalComputeSize ? originalComputeSize.call(this, out) : [300, 200]; + // Ensure minimum width of 450px + size[0] = Math.max(size[0], 450); + return size; + }; + } + } +}); \ No newline at end of file diff --git a/web/multi_lora_stack_model_only.js b/web/multi_lora_stack_model_only.js new file mode 100644 index 0000000..0c3b988 --- /dev/null +++ b/web/multi_lora_stack_model_only.js @@ -0,0 +1,186 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Get available LoRAs from the API +async function getAvailableLoras() { + try { + const response = await api.fetchApi("/object_info"); + const objectInfo = await response.json(); + + // Look for LoRA inputs in any loader node + for (const [nodeName, nodeInfo] of Object.entries(objectInfo)) { + if (nodeInfo.input?.required?.lora_name) { + return nodeInfo.input.required.lora_name[0] || ["None"]; + } + } + return ["None"]; + } catch (error) { + console.error("Error fetching LoRAs:", error); + return ["None"]; + } +} + +app.registerExtension({ + name: "MultiLoRAStackModelOnly", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "MultiLoRAStackModelOnly") { + console.log("Registering MultiLoRAStackModelOnly node"); + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + console.log("MultiLoRAStackModelOnly node created"); + + const result = onNodeCreated?.apply(this, arguments); + + // Set wider default size + this.size = [450, 300]; + + // Initialize node data + this.loraData = []; + this.availableLoras = await getAvailableLoras(); + + // Add control buttons + this.addWidget("button", "➕ Add LoRA", null, () => { + this.addLoRA(); + }); + + this.addWidget("button", "Toggle All LoRAs", null, () => { + this.toggleAllLoRAs(); + }); + + // Add initial LoRA + this.addLoRA(); + + return result; + }; + + // Add methods to the node prototype + nodeType.prototype.addLoRA = function() { + const index = this.loraData.length; + console.log(`Adding LoRA ${index + 1} (Model Only)`); + + // Add LoRA data + this.loraData.push({ + on: true, + lora: "None", + strength: 1.0 + }); + + // Create widgets for this LoRA + const enableWidget = this.addWidget("toggle", `Enable LoRA ${index + 1}`, true, (value) => { + this.loraData[index].on = value; + this.updateLoraStack(); + }); + + const loraWidget = this.addWidget("combo", `LoRA ${index + 1}`, "None", (value) => { + this.loraData[index].lora = value; + this.updateLoraStack(); + }, { + values: this.availableLoras + }); + + const strengthWidget = this.addWidget("number", `Strength ${index + 1}`, 1.0, (value) => { + this.loraData[index].strength = value; + this.updateLoraStack(); + }, { + min: -2.0, + max: 2.0, + step: 0.1 + }); + + const removeWidget = this.addWidget("button", `Remove LoRA ${index + 1}`, null, () => { + this.removeLoRA(index); + }); + + // Store widget references + this.loraData[index]._widgets = { + enable: enableWidget, + lora: loraWidget, + strength: strengthWidget, + remove: removeWidget + }; + + this.updateLoraStack(); + this.setSize([this.size[0], this.computeSize()[1]]); + }; + + nodeType.prototype.removeLoRA = function(index) { + console.log(`Removing LoRA ${index + 1} (Model Only)`); + + // Get widgets to remove + const loraItem = this.loraData[index]; + if (loraItem && loraItem._widgets) { + // Remove widgets from the node + Object.values(loraItem._widgets).forEach(widget => { + const widgetIndex = this.widgets.indexOf(widget); + if (widgetIndex !== -1) { + this.widgets.splice(widgetIndex, 1); + } + }); + } + + // Remove from data array + this.loraData.splice(index, 1); + + // Update remaining widget names and indices + this.updateWidgetNames(); + this.updateLoraStack(); + this.setSize([this.size[0], this.computeSize()[1]]); + }; + + nodeType.prototype.updateWidgetNames = function() { + // Update widget names after removal + this.loraData.forEach((loraItem, index) => { + if (loraItem._widgets) { + loraItem._widgets.enable.name = `Enable LoRA ${index + 1}`; + loraItem._widgets.lora.name = `LoRA ${index + 1}`; + loraItem._widgets.strength.name = `Strength ${index + 1}`; + loraItem._widgets.remove.name = `Remove LoRA ${index + 1}`; + } + }); + }; + + nodeType.prototype.toggleAllLoRAs = function() { + const allEnabled = this.loraData.every(lora => lora.on); + const newState = !allEnabled; + + console.log(`Toggling all LoRAs to: ${newState} (Model Only)`); + + this.loraData.forEach((loraItem, index) => { + loraItem.on = newState; + if (loraItem._widgets && loraItem._widgets.enable) { + loraItem._widgets.enable.value = newState; + } + }); + + this.updateLoraStack(); + }; + + nodeType.prototype.updateLoraStack = function() { + // Find the lora_stack widget + const stackWidget = this.widgets.find(w => w.name === "lora_stack"); + if (stackWidget) { + const jsonData = JSON.stringify(this.loraData.map(item => ({ + on: item.on, + lora: item.lora, + strength: item.strength + }))); + stackWidget.value = jsonData; + console.log("Updated lora_stack (Model Only):", jsonData); + } else { + console.warn("lora_stack widget not found (Model Only)"); + } + }; + + // Override computeSize to ensure minimum width + const originalComputeSize = nodeType.prototype.computeSize; + nodeType.prototype.computeSize = function(out) { + const size = originalComputeSize ? originalComputeSize.call(this, out) : [300, 200]; + // Ensure minimum width of 450px + size[0] = Math.max(size[0], 450); + return size; + }; + } + } +}); \ No newline at end of file