Files
olivv-cs-ComfyUI-FunPack/modules/loaders/lora/nodes.py
T

155 lines
9.3 KiB
Python

"""The LoRA loader.
Up to eight LoRAs in one node, applied in order. Slot 1 keeps the original input
names (`lora_name`, `strength_model`, `strength_clip`) so every saved pipeline
still loads; slots 2..8 add `lora_name_N`, `strength_model_N`, `strength_clip_N`
as optional inputs, which is backwards-compatible with every saved graph.
Chaining several nodes still works and gives the same result.
The slots are numbered combos rather than ComfyUI's autogrow input, for the same
reason the text encoder loader uses them: autogrow turns a Combo template into a
row of sockets, so the file picker would stop being a picker. v4 used a JSON list
in a string widget and a dict subclass that lied to the validator; neither is
needed, and every strength here is a real, validated input.
"""
import comfy.utils
import folder_paths
from comfy_api.latest import io
from ..._core import dit_hooks, log, sysinfo
from .keys import match
NONE = "None"
MAX_LORAS = 8
def _picked(name):
"""A condition for the form (core/widgets, `funui_shows`): the file input `name` holds a LoRA."""
return {"input": name, "not": [NONE]}
class FunUILoraLoader(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
files = [NONE] + folder_paths.get_filename_list("loras")
inputs = [io.Model.Input("model")]
for n in range(1, MAX_LORAS + 1):
sfx = "" if n == 1 else f"_{n}"
first = n == 1
mine = {"funui_shows": [_picked(f"lora_name{sfx}")]} # a slot's settings are drawn once its LoRA is chosen
# A slot's picker is drawn once the one before it has a LoRA (or it has one itself, so a filled slot never hides).
ask = {} if first else {"funui_shows": [{"either": [_picked(f"lora_name_{n - 1}" if n > 2 else "lora_name"), _picked(f"lora_name{sfx}")]}]}
inputs += [
io.Combo.Input(f"lora_name{sfx}", options=files, default=NONE, optional=not first, extra_dict=ask or None,
tooltip="None leaves the model untouched." if first
else "Another LoRA, applied after the ones above it. None skips this slot."),
io.Float.Input(f"strength_model{sfx}", default=1.0, min=-100.0, max=100.0, step=0.01, optional=not first, extra_dict=mine,
tooltip="How strongly the LoRA changes the model. 0 = not at all, negative = the opposite."),
]
if first: # the original order: clip sits between slot 1's two strengths
inputs.append(io.Clip.Input("clip", optional=True, tooltip="Leave unwired for model-only LoRAs."))
inputs.append(io.Float.Input(f"strength_clip{sfx}", default=1.0, min=-100.0, max=100.0, step=0.01, optional=True, extra_dict=mine,
tooltip="How strongly the LoRA changes the text encoder. Needs a wired clip; not used for First or Second stage LoRAs."))
inputs.append(io.Combo.Input(f"apply_to_stage{sfx}", options=list(dit_hooks.STAGES), default="Both", optional=True,
extra_dict={**mine, "funui_needs_module": ["two_stage"]},
tooltip="Which part of a Two stages run this LoRA applies to. First: the smaller picture. Second: the full-size steps. Both only change the model."))
return io.Schema(
node_id="FunUILoraLoader",
display_name="FunUI LoRA Loader",
category="FunUI/Loaders",
description=f"Apply up to {MAX_LORAS} LoRAs to a model, and optionally its text encoder, in order.",
inputs=inputs,
outputs=[
io.Model.Output(display_name="model"),
io.Clip.Output(display_name="clip"),
io.String.Output(display_name="status"),
],
)
@classmethod
def execute(cls, model, lora_name=NONE, strength_model=1.0, clip=None, strength_clip=1.0, **kw) -> io.NodeOutput:
kw.update(lora_name=lora_name, strength_model=strength_model, strength_clip=strength_clip)
notes = []
for n in range(1, MAX_LORAS + 1):
sfx = "" if n == 1 else f"_{n}"
name = kw.get(f"lora_name{sfx}", NONE)
if name in (None, NONE):
if n == 1 and not any(kw.get(f"lora_name_{k}", NONE) not in (None, NONE) for k in range(2, MAX_LORAS + 1)):
notes.append("no LoRA picked")
continue
stage = kw.get(f"apply_to_stage{sfx}", "Both")
if stage in ("First", "Second"):
model, note = _apply_to_stage(model, name, kw.get(f"strength_model{sfx}", 1.0), stage)
else:
model, clip, note = _apply(model, clip, name, kw.get(f"strength_model{sfx}", 1.0),
kw.get(f"strength_clip{sfx}", 1.0))
notes.append(note)
return io.NodeOutput(model, clip, "\n".join(notes))
def _apply_to_stage(model, lora_name, strength_model, stage):
"""One LoRA for one stage of Two stages. The patches ride on the model, unapplied: Two stages puts them on the
model of the stage they belong to. Returns (model, status line)."""
if strength_model == 0:
log.alert("FunUI LoRA Loader", f"{lora_name} has its strength at 0 and was not applied")
return model, f"{lora_name}: strength is 0, not applied"
path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = comfy.utils.load_torch_file(path, safe_load=True)
patches, how, dropped, unplaced = match(model, None, lora)
if not patches:
log.alert("FunUI LoRA Loader", f"{lora_name} matched NOTHING in this model, so it does nothing: "
"it is for another model, or names its layers in a way not known here")
return model, f"{lora_name}: matched nothing, not applied"
if unplaced:
log.alert("FunUI LoRA Loader", f"{lora_name}: {unplaced} of its {len(patches) + dropped + unplaced} modules have no place in this model "
"and were left out, so the LoRA is only partly applied")
if dropped:
log.alert("FunUI LoRA Loader", f"{lora_name}: {dropped} of {len(patches) + dropped} weights do not fit this "
"model's shapes and were left out (trained on another variant of it)")
patched = model.clone()
held = patched.get_attachment(dit_hooks.STAGE_PATCHES) or ()
patched.set_attachments(dit_hooks.STAGE_PATCHES, tuple(held) + ((stage, patches, strength_model),))
found = f"{len(patches)} weights" + (f", keys {how}" if how != "as-is" else "") + (f", {dropped} dropped" if dropped else "") + (f", {unplaced} without a place" if unplaced else "")
log.info("FunUI LoRA Loader", f"{lora_name} kept for the {stage.lower()} stage of Two stages (model={strength_model}; {found})")
return patched, f"FunUI LoRA Loader | {lora_name} ({stage.lower()} stage only; model={strength_model}; {found})"
def _apply(model, clip, lora_name, strength_model, strength_clip):
"""One LoRA onto (model, clip). Returns (model, clip, status line)."""
if strength_model == 0 and (clip is None or strength_clip == 0):
# Doing nothing is a valid request, but it must return the SAME objects
# rather than clones: a clone here would silently drop patches a later
# loader in the chain applied to the original.
# Loud, because "applied a LoRA at strength 0" is indistinguishable
# from "forgot to set the strength" by looking at the picture.
log.alert("FunUI LoRA Loader", f"{lora_name} has both strengths at 0 and was not applied")
return model, clip, f"{lora_name}: both strengths are 0, not applied"
path = folder_paths.get_full_path_or_raise("loras", lora_name)
lora = comfy.utils.load_torch_file(path, safe_load=True)
patches, how, dropped, unplaced = match(model, clip, lora)
if not patches:
log.alert("FunUI LoRA Loader", f"{lora_name} matched NOTHING in this model, so it does nothing: "
"it is for another model, or names its layers in a way not known here")
return model, clip, f"{lora_name}: matched nothing, not applied"
patched_model = model.clone()
patched_model.add_patches(patches, strength_model)
patched_clip = None
if clip is not None:
patched_clip = clip.clone()
patched_clip.add_patches(patches, strength_clip)
if unplaced:
log.alert("FunUI LoRA Loader", f"{lora_name}: {unplaced} of its {len(patches) + dropped + unplaced} modules have no place in this model "
"and were left out, so the LoRA is only partly applied")
if dropped:
log.alert("FunUI LoRA Loader", f"{lora_name}: {dropped} of {len(patches) + dropped} weights do not fit this "
"model's shapes and were left out (trained on another variant of it)")
applied = [f"model={strength_model}"]
applied.append(f"clip={strength_clip}" if clip is not None else "clip=not wired")
found = f"{len(patches)} weights" + (f", keys {how}" if how != "as-is" else "") + (f", {dropped} dropped" if dropped else "") + (f", {unplaced} without a place" if unplaced else "")
log.info("FunUI LoRA Loader", f"{lora_name} applied ({', '.join(applied)}; {found}). {sysinfo.memory_line()}")
return patched_model, patched_clip, f"FunUI LoRA Loader | {lora_name} ({', '.join(applied)}; {found})"