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.
198 lines
7.6 KiB
JavaScript
198 lines
7.6 KiB
JavaScript
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: "MultiLoRAStack",
|
||
|
||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||
if (nodeData.name !== "MultiLoRAStack") 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;
|
||
};
|
||
}
|
||
});
|