multiple lora node

This commit is contained in:
Kelly Jerrell
2025-08-16 13:20:29 -07:00
parent bc7931ebd1
commit db60ca6acc
8 changed files with 273 additions and 80 deletions
+15 -4
View File
@@ -396,16 +396,25 @@ class DrawThingsLoRA:
@classmethod
def INPUT_TYPES(s):
# fmt: off
return {
first_model_widget = ("DT_MODEL", { "model_type": "loras", "tooltip": "The model used." })
model_widget = ("DT_MODEL", { "model_type": "loras", "tooltip": "The model used.", "hidden": True, "default": None })
first_weight_widget = ("FLOAT", { "default": 1.00, "min": -5.00, "max": 5.00, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative."})
weight_widget = ("FLOAT", { "default": 1.00, "min": -5.00, "max": 5.00, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative.", "hidden": True})
types = {
"required": {
"lora_name": ("DT_MODEL", { "model_type": "loras", "tooltip": "The model used." }),
"lora_weight": ("FLOAT", { "default": 1.00, "min": -5.00, "max": 5.00, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative."}),
"buttons": ("DT_BUTTONS", { "default": None }),
"model": first_model_widget,
"weight": first_weight_widget,
},
"optional": {
# "lora": ("DT_LORA",),
},
}
for i in range(2, 9):
types["required"]["lora_" + str(i)] = model_widget
types["required"]["weight_" + str(i)] = weight_widget
# fmt: on
return types
RETURN_TYPES = ("DT_LORA",)
RETURN_NAMES = ("LORA",)
@@ -413,7 +422,9 @@ class DrawThingsLoRA:
DESCRIPTION = "LoRAs are used to modify diffusion and CLIP models, altering the way in which latents are denoised such as applying styles. Multiple LoRA nodes can be linked together."
FUNCTION = "add_to_pipeline"
def add_to_pipeline(self, lora_name, lora_weight, **kwargs) -> LoraStack:
def add_to_pipeline(self, **kwargs) -> LoraStack:
print(kwargs)
return ([],)
prev_lora: LoraStack = kwargs.get("lora", None)
control_image = kwargs.get("control_image", None)
+2
View File
@@ -1,6 +1,7 @@
import { DtModelTypeHandler } from "./models.js"
import { updateProto, setCallback } from "./util.js"
import { findPropertyJson, findPropertyPython } from "./configProperties.js"
import { DtButtonsTypeHandler } from './lora.js'
export const nodePackVersion = "1.5.0"
@@ -16,6 +17,7 @@ export default {
getCustomWidgets(app) {
return {
DT_MODEL: DtModelTypeHandler,
"DT_BUTTONS": DtButtonsTypeHandler
}
},
+16
View File
@@ -6,3 +6,19 @@
-webkit-mask-image: url(/dt_grpc/logo.svg);
mask-image: url(/dt_grpc/logo.svg);
}
.dt-buttons-container {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: stretch;
gap: 4px;
width: 100%;
height: 100%;
padding: 0px 16px;
}
.dt-button {
/* font-weight: bold; */
/* font-size: 0.5rem; */
}
+80 -74
View File
@@ -58,8 +58,8 @@ const dtModelNodeProto = {
onConfigure(serialised) {
this._lastSelectedModel = serialised._lastSelectedModel || {}
},
getModelWidget() {
return this.widgets.find((w) => w.options?.modelType)
getModelWidgets() {
return this.widgets.filter((w) => w.options?.modelType)
},
onAdded() {
modelService.updateNodes()
@@ -96,29 +96,31 @@ const dtServerNodeProto = {
* @param {string} version
*/
updateModels(models, version) {
const widget = this.getModelWidget()
if (!widget) return
if (models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
const widgets = this.getModelWidgets()
for (const widget of widgets) {
if (!widget) return
if (models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
widget.options.values = ["(None selected)", ...models.models.map(m => getMenuItem(m)).sort((a, b) => a.content.localeCompare(b.content))]
widget.options.values = ["(None selected)", ...models.models.map(m => getMenuItem(m)).sort((a, b) => a.content.localeCompare(b.content))]
if (widget.value === "Click to retry" || widget.value === "Not connected") {
if (this._lastSelectedModel?.model) widget.value = this._lastSelectedModel.model
else widget.value = "(None selected)"
}
if (widget.value === "Click to retry" || widget.value === "Not connected") {
if (this._lastSelectedModel?.model) widget.value = this._lastSelectedModel.model
else widget.value = "(None selected)"
}
if (widget.value?.toString() === "[object Object]") {
const value = {
...widget.value,
toString() {
return this.value.name
},
if (widget.value?.toString() === "[object Object]") {
const value = {
...widget.value,
toString() {
return this.value.name
},
}
widget.value = value
}
widget.value = value
}
}
}
@@ -136,40 +138,42 @@ const dtModelStandardNodeProto = {
*/
updateModels(models, version) {
/** @type {import('@comfyorg/litegraph').IWidget} */
const widget = this.getModelWidget()
if (!widget) return
const widgets = this.getModelWidgets()
for (const widget of widgets) {
if (!widget) return
const type = widget?.options?.modelType
const type = widget?.options?.modelType
if (models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
if (models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
if (!(type in models)) return
if (!(type in models)) return
widget.options.values = ["(None selected)", ...models[type]
.map((m) => getMenuItem(m, version && m.version && m.version !== version))
.sort((a, b) => {
if (a.disabled && !b.disabled) return 1
if (!a.disabled && b.disabled) return -1
return a.content.toUpperCase().localeCompare(b.content.toUpperCase())
})]
widget.options.values = ["(None selected)", ...models[type]
.map((m) => getMenuItem(m, version && m.version && m.version !== version))
.sort((a, b) => {
if (a.disabled && !b.disabled) return 1
if (!a.disabled && b.disabled) return -1
return a.content.toUpperCase().localeCompare(b.content.toUpperCase())
})]
if (widget.value === "Click to retry" || widget.value === "Not connected") {
if (this._lastSelectedModel?.[widget.name]) widget.value = this._lastSelectedModel[widget.name]
else widget.value = "(None selected)"
}
if (widget.value === "Click to retry" || widget.value === "Not connected") {
if (this._lastSelectedModel?.[widget.name]) widget.value = this._lastSelectedModel[widget.name]
else widget.value = "(None selected)"
}
if (widget.value?.toString() === "[object Object]") {
const value = {
...widget.value,
toString() {
return this.value.name
},
if (widget.value?.toString() === "[object Object]") {
const value = {
...widget.value,
toString() {
return this.value.name
},
}
widget.value = value
}
widget.value = value
}
}
}
@@ -187,35 +191,37 @@ const dtModelPromptNodeProto = {
updateOptions() {
/** @type {import('@comfyorg/litegraph').IWidget} */
const widget = this.getModelWidget()
if (!widget) return
const widgets = this.getModelWidgets()
for (const widget of widgets) {
if (!widget) return
if (this._models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
if (this._models === null) {
widget.options.values = ["Not connected", "Click to retry"]
widget.value = "Not connected"
return
}
/** @type {string} */
const promptText = this.widgets.find((w) => w.name === "prompt")?.value
const matches = [...promptText.matchAll(/<(.*?)>/gm)]
const tags = matches.map(m => m[1])
widget.options.values = ["...", ...this._models
.map((m) => getMenuItem(m, this._version && m.version && m.version !== this._version && !tags.includes(m.keyword)))
.map(m => {
Object.defineProperty(m, 'content', {
get() {
return `${tags.includes(m.value.keyword) ? "✓ " : ""}${m.value.name} (${m.value.version})`
}
/** @type {string} */
const promptText = this.widgets.find((w) => w.name === "prompt")?.value
const matches = [...promptText.matchAll(/<(.*?)>/gm)]
const tags = matches.map(m => m[1])
widget.options.values = ["...", ...this._models
.map((m) => getMenuItem(m, this._version && m.version && m.version !== this._version && !tags.includes(m.keyword)))
.map(m => {
Object.defineProperty(m, 'content', {
get() {
return `${tags.includes(m.value.keyword) ? "✓ " : ""}${m.value.name} (${m.value.version})`
}
})
return m
})
return m
})
.sort((a, b) => {
if (a.disabled && !b.disabled) return 1
if (!a.disabled && b.disabled) return -1
return a.content.toUpperCase().localeCompare(b.content.toUpperCase())
})]
.sort((a, b) => {
if (a.disabled && !b.disabled) return 1
if (!a.disabled && b.disabled) return -1
return a.content.toUpperCase().localeCompare(b.content.toUpperCase())
})]
widget.value = "..."
widget.value = "..."
}
},
}
+2 -1
View File
@@ -3,13 +3,14 @@ import dtCore, { nodePackVersion } from "./ComfyUI-DrawThings-gRPC.js"
import dtModelNodes from "./dtModelNodes.js"
import dtDynamicInputs from "./dynamicInputs.js"
import dtWidgets from "./widgets.js"
import loraNode from "./lora.js"
import * as App from "../../scripts/app.js"
/** @type {import("@comfyorg/comfyui-frontend-types").ComfyApp} */
const app = App.app
const modules = [dtCore, dtPrompt, dtModelNodes, dtDynamicInputs, dtWidgets]
const modules = [dtCore, dtPrompt, dtModelNodes, /* dtDynamicInputs, */ dtWidgets, loraNode]
// different features of the nodepack extension are implemented in different modules
// here we combine them and register a single extension
+156
View File
@@ -0,0 +1,156 @@
import { DtModelTypeHandler } from './models.js'
import { updateProto } from "./util.js"
import { showWidget } from './widgets.js'
/**
* so I don't really see an official way to remove widgets other than .splice
* which seems weird, because then you have to call onRemove() yourself
* So I'm going to go with "create 8 of them and hide the ones you don't need"
* If someone needs more than 8 lora, well, they are crazy, and also they can
* just chain another node
*
* we'll use model = null for loras above count
* distinct from model = "(None selected)"
*
* .loraCount prop will 'add' and 'remove' (hide/show) widgets when changed
* serialize and deserialize it with the node
*
* and we'll need a callback on each of the lora models so we can add control
* image inputs if necessary
*
* the buttons widget is a custom widget type, so it's defined in the python
* code, event listeners attached by the type handler
*/
/**
* @param node {LGraphNode}
* @param inputName {string}
* @param inputData {["DT_MODEL", {model_type: string}]}
*/
export function DtButtonsTypeHandler(node, inputName, inputData, app) {
const { container, buttons } = createButtons([{
label: "Less",
callback: () => {
node.loraCount -= 1
},
}, {
label: "More",
callback: () => {
node.loraCount += 1
},
}])
const options = {
hideOnZoom: false,
getValue: () => undefined,
setValue: (value) => { },
getMinHeight: () => 36,
getMaxHeight: () => 36,
getHeight: () => 36,
margin: 4
}
const widget = node.addDOMWidget("something", 'DT_BUTTONS', container, options)
widget._buttonElements = buttons
return { widget }
}
/** @type {import("@comfyorg/litegraph").LGraphNode} */
const loraProto = {
onNodeCreated(graph) {
this.loraCount = 1
},
onConfigure(serialised) {
if ("loraCount" in serialised)
this.loraCount = serialised.loraCount
console.log(serialised)
},
onSerialize(serialised) {
serialised.loraCount = this._loraCount
},
loraCount: {
get() {
return this._loraCount
},
set(count) {
if (this._loraCount === count) return
this._loraCount = Math.max(0, Math.min(count, 8))
this.updateWidgets()
const buttons = this.widgets[0]._buttonElements
buttons[0].disabled = this._loraCount <= 1
buttons[1].disabled = this._loraCount >= 8
},
enumerable: true
},
updateWidgets() {
console.log("updating lora widgets")
for (let i = 0; i < 8; i++) {
const modelIndex = i * 2 + 1
const weightIndex = i * 2 + 2
if (i < this.loraCount) {
console.log("show", this.widgets[modelIndex].name)
showWidget(this, this.widgets[modelIndex].name, true)
showWidget(this, this.widgets[weightIndex].name, true)
if (!this.widgets[modelIndex].value) {
this.widgets[modelIndex].value = "(None selected)"
}
} else {
console.log("hide", this.widgets[modelIndex].name)
showWidget(this, this.widgets[modelIndex].name, false)
showWidget(this, this.widgets[weightIndex].name, false)
this.widgets[modelIndex].value = null
}
}
}
}
/** @type {import("@comfyorg/comfyui-frontend-types").ComfyExtension}*/
export default {
name: "loraNode",
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === "DrawThingsLoRA") {
updateProto(nodeType, loraProto)
}
}
}
/**
* @typedef ButtonDef
* @property {string?} label
* @property {node => void} callback
* @property {string?} style
* @property {string[]?} classList
*/
/** @param {ButtonDef[]} buttonsDefs */
function createButtons(buttonsDefs) {
const container = document.createElement("div")
container.classList.add("dt-buttons-container")
const buttons = []
for (const buttonDef of buttonsDefs) {
const button = document.createElement("button")
buttons.push(button)
button.classList.add("dt-button")
if (buttonDef.label)
button.innerText = buttonDef.label
button.addEventListener("click", () => {
buttonDef.callback()
})
if (buttonDef.style) {
button.style.cssText = buttonDef.style
}
if (buttonDef.classList) {
button.classList.add(...buttonDef.classList)
}
container.appendChild(button)
}
return { container, buttons }
}
+1
View File
@@ -76,6 +76,7 @@ export const modelService = new ModelService()
* @param inputData {["DT_MODEL", {model_type: string}]}
*/
export function DtModelTypeHandler(node, inputName, inputData, app) {
console.log("add dt model widget")
const widget = node.addWidget(
"combo",
inputName,
+1 -1
View File
@@ -104,7 +104,7 @@ function updateInput(input, node) {
}
/** @param node {LGraphNode} */
function showWidget(node, widgetName, show = false, suffix = "") {
export function showWidget(node, widgetName, show = false, suffix = "") {
const widget = findWidgetByName(node, widgetName);
if (!widget) return;
if (!origProps[widget.name]) {