Files
x-stride fa673ce0d4 Initial release: ComfyUI XS DARE TIES custom nodes.
SDXL checkpoint merging with DARE/TIES deltas, bezier UNet profiles, stack visualizer, example workflow PNG, and API smoke tests.
2026-05-24 13:17:56 +01:00

160 lines
5.6 KiB
Python

import folder_paths
from .dare_ties_profile import PROFILE_TYPE
STACK_TYPE = "DARE_TIES_MODEL_STACK"
def checkpoint_list_with_none():
return ["None"] + folder_paths.get_filename_list("checkpoints")
class DARETIESModelStack:
"""
Chainable 3-slot checkpoint stack.
This node does not load or merge anything.
It only builds a list of child checkpoints plus model/clip weights.
Chain multiple stack nodes for 3 / 6 / 9 / 12... models.
"""
DESCRIPTION = (
"Builds a chainable list of SDXL/Illustrious-derived child checkpoints with per-slot "
"model and CLIP weights and optional per-slot bezier UNet profiles.\n"
"\n"
"Profile mapping along the SDXL UNet (left to right): 9 IN (composition) -> 3 MID "
"(global coherence) -> 9 OUT (style/details). A profile multiplies each child's delta "
"per block.\n"
"\n"
"No checkpoints are loaded here; loading and merging happen in DARE TIES Merge Models. "
"Chain multiple stack nodes for 3/6/9/12+ children. Disabled, 'None', or zero-zero slots "
"are dropped.\n"
"\n"
"Stack order is preserved in the list sent to Apply: slot index i is used in "
"stable_key_seed(seed, key, i) for DARE and affects TIES sign election, so reordering "
"slots changes merge output when using dare_delta, ties_delta, or dare_ties_delta."
)
@classmethod
def INPUT_TYPES(cls):
ckpts = checkpoint_list_with_none()
weight_cfg = {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.01,
"tooltip": (
"Pre-normalization weight for this child. With normalize_*_weights=True in the "
"Apply node, weights are rescaled so they sum to 1 across all stack entries."
),
}
clip_weight_cfg = dict(weight_cfg)
clip_weight_cfg["tooltip"] = (
"Pre-normalization weight for this child's CLIP/text-encoder delta. Only used when "
"merge_clip is not 'off' in the Apply node."
)
ckpt_tooltip = (
"Child checkpoint to mix into the reference. Set to 'None' to skip this slot."
)
enabled_tooltip = (
"Disable to skip this slot without changing its values. Disabled slots contribute "
"nothing to the merge."
)
return {
"required": {
"ckpt_name_1": (ckpts, {"tooltip": ckpt_tooltip}),
"model_weight_1": ("FLOAT", weight_cfg),
"clip_weight_1": ("FLOAT", clip_weight_cfg),
"enabled_1": ("BOOLEAN", {"default": True, "tooltip": enabled_tooltip}),
"ckpt_name_2": (ckpts, {"tooltip": ckpt_tooltip}),
"model_weight_2": ("FLOAT", weight_cfg),
"clip_weight_2": ("FLOAT", clip_weight_cfg),
"enabled_2": ("BOOLEAN", {"default": True, "tooltip": enabled_tooltip}),
"ckpt_name_3": (ckpts, {"tooltip": ckpt_tooltip}),
"model_weight_3": ("FLOAT", weight_cfg),
"clip_weight_3": ("FLOAT", clip_weight_cfg),
"enabled_3": ("BOOLEAN", {"default": True, "tooltip": enabled_tooltip}),
},
"optional": {
"model_stack": (
STACK_TYPE,
{"tooltip": "Optional upstream stack to extend. Entries from this input come first in the output list."},
),
"profile_1": (
PROFILE_TYPE,
{"tooltip": "Optional UNet block curve for slot 1 (21 samples). Multiplies that child's delta per block."},
),
"profile_2": (
PROFILE_TYPE,
{"tooltip": "Optional UNet block curve for slot 2."},
),
"profile_3": (
PROFILE_TYPE,
{"tooltip": "Optional UNet block curve for slot 3."},
),
},
}
RETURN_TYPES = (STACK_TYPE,)
RETURN_NAMES = ("model_stack",)
OUTPUT_TOOLTIPS = (
"List of child checkpoints with model_weight, clip_weight, and optional profile for UNet shaping.",
)
FUNCTION = "make_stack"
CATEGORY = "advanced/model_merging/dare_ties"
def make_stack(
self,
ckpt_name_1,
model_weight_1,
clip_weight_1,
enabled_1,
ckpt_name_2,
model_weight_2,
clip_weight_2,
enabled_2,
ckpt_name_3,
model_weight_3,
clip_weight_3,
enabled_3,
model_stack=None,
profile_1=None,
profile_2=None,
profile_3=None,
):
stack = []
if model_stack is not None:
stack.extend(list(model_stack))
slots = [
(ckpt_name_1, model_weight_1, clip_weight_1, enabled_1, profile_1),
(ckpt_name_2, model_weight_2, clip_weight_2, enabled_2, profile_2),
(ckpt_name_3, model_weight_3, clip_weight_3, enabled_3, profile_3),
]
for ckpt_name, model_weight, clip_weight, enabled, profile in slots:
if not enabled:
continue
if ckpt_name is None or ckpt_name == "None":
continue
if float(model_weight) == 0.0 and float(clip_weight) == 0.0:
continue
entry = {
"ckpt_name": ckpt_name,
"model_weight": float(model_weight),
"clip_weight": float(clip_weight),
}
if profile is not None:
entry["profile"] = profile
stack.append(entry)
return (stack,)