Files
UndefinedUo-ComfyUi-DuoNodes/web/multi_lora_stack_model_only.js
UndefinedUo 0ad8da9034 feat: multi LoRA stack nodes with Gram-Schmidt orthogonal stacking
Three ComfyUI nodes: Multi LoRA Stack (MODEL+CLIP), Model Only, and an
Ortho variant that orthogonalizes LoRA down-projection row spaces at load
time to reduce interference between stacked LoRAs. Includes strength_1-10
override inputs and dynamic JS UI for all nodes.
2026-07-01 19:34:21 -07:00

198 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
async function getAvailableLoras() {
try {
const response = await api.fetchApi("/object_info");
const objectInfo = await response.json();
for (const [, 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") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const result = onNodeCreated?.apply(this, arguments);
const stackWidget = this.widgets?.find(w => w.name === "lora_stack");
if (stackWidget) {
stackWidget.computeSize = () => [0, -4];
stackWidget.draw = () => {};
}
this.size = [450, 300];
this.loraData = [];
this.availableLoras = await getAvailableLoras();
if (!this._configureWasRun) {
for (let i = (this.inputs?.length ?? 0) - 1; i >= 0; i--) {
if (/^strength_\d+$/.test(this.inputs[i].name)) this.removeInput(i);
}
}
this.addWidget("button", "➕ Add LoRA", null, () => { this.addLoRA(); });
this.addWidget("button", "Toggle All LoRAs", null, () => { this.toggleAllLoRAs(); });
this.addLoRA();
return result;
};
nodeType.prototype.addLoRA = function(initialData = null) {
const index = this.loraData.length;
const on = initialData?.on ?? true;
const lora = initialData?.lora ?? "None";
const strength = initialData?.strength ?? 1.0;
this.loraData.push({ on, lora, strength });
const enableWidget = this.addWidget("toggle", `Enable LoRA ${index + 1}`, on, (value) => {
this.loraData[index].on = value;
this.updateLoraStack();
});
const loraWidget = this.addWidget("combo", `LoRA ${index + 1}`, lora, (value) => {
this.loraData[index].lora = value;
this.updateLoraStack();
}, { values: this.availableLoras });
const strengthWidget = this.addWidget("number", `Strength ${index + 1}`, strength, (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);
});
this.loraData[index]._widgets = {
enable: enableWidget,
lora: loraWidget,
strength: strengthWidget,
remove: removeWidget
};
const slotName = `strength_${index + 1}`;
const existing = this.inputs?.find(inp => inp.name === slotName);
if (existing) {
if (!existing.widget) existing.widget = { name: strengthWidget.name };
} else {
this.addInput(slotName, "FLOAT", { widget: { name: strengthWidget.name } });
}
this.updateLoraStack();
this.setSize([this.size[0], this.computeSize()[1]]);
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
this._configureWasRun = true;
const result = onConfigure?.apply(this, arguments);
const stackWidget = this.widgets?.find(w => w.name === "lora_stack");
const savedJson = stackWidget?.value;
const tryRestore = () => {
if (!this.availableLoras) { setTimeout(tryRestore, 50); return; }
if (!savedJson) return;
let saved;
try { saved = JSON.parse(savedJson); } catch (e) { return; }
if (!Array.isArray(saved) || saved.length === 0) return;
for (const item of this.loraData) {
if (item._widgets) {
Object.values(item._widgets).forEach(w => {
const i = this.widgets.indexOf(w);
if (i !== -1) this.widgets.splice(i, 1);
});
}
}
this.loraData = [];
for (const item of saved) {
this.addLoRA(item);
}
this.setSize([this.size[0], this.computeSize()[1]]);
};
setTimeout(tryRestore, 0);
return result;
};
nodeType.prototype.removeLoRA = function(index) {
const loraItem = this.loraData[index];
if (loraItem?._widgets) {
Object.values(loraItem._widgets).forEach(widget => {
const widgetIndex = this.widgets.indexOf(widget);
if (widgetIndex !== -1) this.widgets.splice(widgetIndex, 1);
});
}
const slotName = `strength_${index + 1}`;
const inputIdx = this.inputs?.findIndex(inp => inp.name === slotName) ?? -1;
if (inputIdx !== -1) this.removeInput(inputIdx);
this.loraData.splice(index, 1);
this.updateWidgetNames();
this.updateLoraStack();
this.setSize([this.size[0], this.computeSize()[1]]);
};
nodeType.prototype.updateWidgetNames = function() {
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}`;
}
});
let n = 1;
for (const inp of (this.inputs || [])) {
if (/^strength_\d+$/.test(inp.name)) {
inp.name = `strength_${n}`;
inp.widget = { name: `Strength ${n}` };
n++;
}
}
};
nodeType.prototype.toggleAllLoRAs = function() {
const allEnabled = this.loraData.every(lora => lora.on);
const newState = !allEnabled;
this.loraData.forEach((loraItem) => {
loraItem.on = newState;
if (loraItem._widgets?.enable) loraItem._widgets.enable.value = newState;
});
this.updateLoraStack();
};
nodeType.prototype.updateLoraStack = function() {
const stackWidget = this.widgets?.find(w => w.name === "lora_stack");
if (stackWidget) {
stackWidget.value = JSON.stringify(
this.loraData.map(item => ({ on: item.on, lora: item.lora, strength: item.strength }))
);
}
};
const originalComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function(out) {
const size = originalComputeSize ? originalComputeSize.call(this, out) : [300, 200];
size[0] = Math.max(size[0], 450);
return size;
};
}
});