From f024492f81fce4f2ac0650e3e356e618559da8d6 Mon Sep 17 00:00:00 2001 From: Talmaj Date: Thu, 9 Jul 2026 15:29:06 +0200 Subject: [PATCH] New Lora nodes using DynamicGroup widget (CORE-186) (#14655) --- comfy_extras/nodes_lora_stack.py | 107 +++++++++++++++++++++++++++++++ nodes.py | 1 + 2 files changed, 108 insertions(+) create mode 100644 comfy_extras/nodes_lora_stack.py diff --git a/comfy_extras/nodes_lora_stack.py b/comfy_extras/nodes_lora_stack.py new file mode 100644 index 000000000..61ce1c0b8 --- /dev/null +++ b/comfy_extras/nodes_lora_stack.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing_extensions import override + +import comfy.sd +import comfy.utils +import folder_paths +from comfy_api.latest import ComfyExtension, io + + +def _load_lora_file(lora_name: str): + lora_path = folder_paths.get_full_path_or_raise("loras", lora_name) + return comfy.utils.load_torch_file(lora_path, safe_load=True, return_metadata=True) + + +def _lora_template() -> list[io.Input]: + return [ + io.Combo.Input("lora_name", options=folder_paths.get_filename_list("loras"), + tooltip="The name of the LoRA file to apply."), + io.Float.Input("strength", default=1.0, min=-100.0, max=100.0, step=0.01, + tooltip="How strongly to apply this LoRA. 0 = off, negative inverts the effect."), + ] + + +class LoadLoraModel(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LoadLoraModel", + display_name="Load LoRA (Model)", + search_aliases=["lora", "load lora", "apply lora", "lora model", "lora stack"], + category="model/loaders", + description="Apply a stack of LoRAs to a diffusion model. Add one row per LoRA; " + "each row picks a LoRA file and its strength.", + inputs=[ + io.Model.Input("model", tooltip="The diffusion model the LoRAs will be applied to."), + io.DynamicGroup.Input( + "loras", + template=_lora_template(), + min=1, + max=50, + tooltip="Each row applies one LoRA to the model.", + group_name="LoRA", + ), + ], + outputs=[io.Model.Output(tooltip="The modified diffusion model.")], + ) + + @classmethod + def execute(cls, model, loras: list[dict]) -> io.NodeOutput: + for row in loras: + lora_name = row.get("lora_name") + strength = row.get("strength", 1.0) + if not lora_name or lora_name == "none" or strength == 0: + continue + lora, metadata = _load_lora_file(lora_name) + model, _ = comfy.sd.load_lora_for_models(model, None, lora, strength, 0, lora_metadata=metadata) + return io.NodeOutput(model) + + +class LoadLoraTextEncoder(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LoadLoraTextEncoder", + display_name="Load LoRA (Text Encoder)", + search_aliases=["lora", "load lora", "apply lora", "clip lora", "lora stack"], + category="model/loaders", + description="Apply a stack of LoRAs to a CLIP text encoder. Add one row per LoRA; " + "each row picks a LoRA file and its strength.", + inputs=[ + io.Clip.Input("clip", tooltip="The CLIP text encoder the LoRAs will be applied to."), + io.DynamicGroup.Input( + "loras", + template=_lora_template(), + min=1, + max=50, + tooltip="Each row applies one LoRA to the text encoder.", + group_name="LoRA", + ), + ], + outputs=[io.Clip.Output(tooltip="The modified CLIP text encoder.")], + ) + + @classmethod + def execute(cls, clip, loras: list[dict]) -> io.NodeOutput: + for row in loras: + lora_name = row.get("lora_name") + strength = row.get("strength", 1.0) + if not lora_name or lora_name == "none" or strength == 0: + continue + lora, metadata = _load_lora_file(lora_name) + _, clip = comfy.sd.load_lora_for_models(None, clip, lora, 0, strength, lora_metadata=metadata) + return io.NodeOutput(clip) + + +class LoraStackExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + LoadLoraModel, + LoadLoraTextEncoder, + ] + + +async def comfy_entrypoint() -> LoraStackExtension: + return LoraStackExtension() diff --git a/nodes.py b/nodes.py index 9043a8d0a..c881444fc 100644 --- a/nodes.py +++ b/nodes.py @@ -2502,6 +2502,7 @@ async def init_builtin_extra_nodes(): "nodes_triposplat.py", "nodes_depth_anything_3.py", "nodes_seed.py", + "nodes_lora_stack.py", ] import_failed = []