diff --git a/nodes/lora_merger.py b/nodes/lora_merger.py index 751a76a..ccbd9b4 100644 --- a/nodes/lora_merger.py +++ b/nodes/lora_merger.py @@ -936,34 +936,52 @@ class LoRAMultiMerge(io.ComfyNode): tooltip="Number of LoRAs to merge"), # LoRA 1 io.Combo.Input("lora_1", options=folder_paths.get_filename_list("loras"), tooltip="First LoRA"), - io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 1's represented delta before merging; negative values subtract it."), # LoRA 2 - io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 2; used only when included by LoRA Count."), + io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 2's represented delta before merging."), # LoRA 3 - io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 3; used only when included by LoRA Count."), + io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 3's represented delta before merging."), # LoRA 4 - io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 4; used only when included by LoRA Count."), + io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 4's represented delta before merging."), # LoRA 5 - io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 5; used only when included by LoRA Count."), + io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 5's represented delta before merging."), # LoRA 6 - io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 6; used only when included by LoRA Count."), + io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 6's represented delta before merging."), # LoRA 7 - io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 7; used only when included by LoRA Count."), + io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 7's represented delta before merging."), # LoRA 8 - io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None", + tooltip="Optional LoRA 8; used only when included by LoRA Count."), + io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 8's represented delta before merging."), io.Combo.Input("merge_mode", options=["concatenate", "weighted_sum"], default="concatenate", tooltip="concatenate: safe, increases rank. weighted_sum: fixed rank (to max input rank), mathematically lossy but standard in Kohya."), - io.String.Input("output_filename", default="merged_lora"), - io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), - io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.String.Input("output_filename", default="merged_lora", + tooltip="Output filename without extension, written under ComfyUI's LoRA directory."), + io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16", + tooltip="Requested dtype for generated factors; each logical layer's source factor precision is considered independently of scalar alpha."), + io.Combo.Input("device", options=["cuda", "cpu"], default="cuda", + tooltip="Device for per-target arithmetic; a CUDA OOM retries only the affected target on CPU."), io.Boolean.Input("include_1d_diffs", default=False, tooltip="Include and merge 1D direct-diff tensors as FP32."), ], @@ -1025,32 +1043,34 @@ class LoRAMultiMergeDARE(io.ComfyNode): inputs=[ io.Combo.Input("base_model", options=["None"] + folder_paths.get_filename_list("diffusion_models"), default="None", tooltip="Optional reference model to resolve key naming issues across formats. If None, input keys are preserved verbatim."), - io.Combo.Input("lora_count", options=[str(i) for i in range(1, 9)], default="2"), + io.Combo.Input("lora_count", options=[str(i) for i in range(1, 9)], default="2", + tooltip="Use the first N LoRA selectors; selectors beyond this count are ignored."), # LoRA 1 io.Combo.Input("lora_1", options=folder_paths.get_filename_list("loras"), tooltip="First LoRA"), - io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01, + tooltip="Scale LoRA 1's represented delta before DARE-Ties merging; negative values subtract it."), # ... - io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 2; used only when included by LoRA Count."), + io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 2's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 3; used only when included by LoRA Count."), + io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 3's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 4; used only when included by LoRA Count."), + io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 4's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 5; used only when included by LoRA Count."), + io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 5's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 6; used only when included by LoRA Count."), + io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 6's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 7; used only when included by LoRA Count."), + io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 7's represented delta before DARE-Ties merging."), + io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 8; used only when included by LoRA Count."), + io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 8's represented delta before DARE-Ties merging."), - io.Float.Input("drop_rate", default=0.1, min=0.0, max=1.0, step=0.01, tooltip="DARE drop rate"), - io.Float.Input("trim_quantile", default=0.2, min=0.0, max=1.0, step=0.01, tooltip="TIES trim quantile (drops smallest values)"), - io.Int.Input("seed", default=42, min=0, max=0xffffffffffffffff), - io.String.Input("output_filename", default="merged_lora_dare"), - io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), - io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Float.Input("drop_rate", default=0.1, min=0.0, max=1.0, step=0.01, tooltip="Fraction of factor values randomly dropped before rescaling; higher values increase sparsification."), + io.Float.Input("trim_quantile", default=0.2, min=0.0, max=1.0, step=0.01, tooltip="Remove the lowest-magnitude fraction before resolving sign conflicts with TIES."), + io.Int.Input("seed", default=42, min=0, max=0xffffffffffffffff, tooltip="Random seed for reproducible DARE masks."), + io.String.Input("output_filename", default="merged_lora_dare", tooltip="Output filename without extension, written under ComfyUI's LoRA directory."), + io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16", tooltip="Requested dtype for generated factors; scalar alpha tensors do not determine factor precision."), + io.Combo.Input("device", options=["cuda", "cpu"], default="cuda", tooltip="Device for per-target arithmetic; a CUDA OOM retries only the affected target on CPU."), io.Boolean.Input("include_1d_diffs", default=False, tooltip="Include and merge 1D direct-diff tensors as FP32."), ], @@ -1143,34 +1163,34 @@ class LoRAMultiMergeDAREEnhanced(io.ComfyNode): inputs=[ io.Combo.Input("base_model", options=["None"] + folder_paths.get_filename_list("diffusion_models"), default="None", tooltip="Optional reference model to resolve key naming issues across formats. If None, input keys are preserved verbatim."), - io.Combo.Input("lora_count", options=[str(i) for i in range(1, 9)], default="2"), + io.Combo.Input("lora_count", options=[str(i) for i in range(1, 9)], default="2", tooltip="Use the first N LoRA selectors; selectors beyond this count are ignored."), # LoRA 1 io.Combo.Input("lora_1", options=folder_paths.get_filename_list("loras"), tooltip="First LoRA"), - io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Float.Input("weight_1", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 1's represented delta before enhanced DARE-Ties merging; negative values subtract it."), # ... - io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01), - io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None"), - io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01), + io.Combo.Input("lora_2", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 2; used only when included by LoRA Count."), + io.Float.Input("weight_2", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 2's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_3", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 3; used only when included by LoRA Count."), + io.Float.Input("weight_3", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 3's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_4", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 4; used only when included by LoRA Count."), + io.Float.Input("weight_4", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 4's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_5", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 5; used only when included by LoRA Count."), + io.Float.Input("weight_5", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 5's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_6", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 6; used only when included by LoRA Count."), + io.Float.Input("weight_6", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 6's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_7", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 7; used only when included by LoRA Count."), + io.Float.Input("weight_7", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 7's represented delta before enhanced DARE-Ties merging."), + io.Combo.Input("lora_8", options=["None"] + folder_paths.get_filename_list("loras"), default="None", tooltip="Optional LoRA 8; used only when included by LoRA Count."), + io.Float.Input("weight_8", default=1.0, min=-10.0, max=10.0, step=0.01, tooltip="Scale LoRA 8's represented delta before enhanced DARE-Ties merging."), - io.Float.Input("mask_power", default=2.0, min=0.001, max=10.0, step=0.01, tooltip="Mask power (Curve. 2.0 = quadratic)"), - io.Float.Input("min_keep_prob", default=0.01, min=0.0, max=1.0, step=0.01, tooltip="Minimum keep probability (Floor to prevent explosion)"), - io.Float.Input("mask_smooth", default=0.0, min=0.0, max=1.0, step=0.01, tooltip="Mask smoothness factor (0.0 = pure dropout, 1.0 = soft scale)"), - io.Float.Input("trim_quantile", default=0.2, min=0.0, max=1.0, step=0.01, tooltip="TIES trim quantile (drops smallest values)"), - io.Int.Input("seed", default=42, min=0, max=0xffffffffffffffff), - io.String.Input("output_filename", default="merged_lora_dare_enhanced"), - io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), - io.Combo.Input("device", options=["cuda", "cpu"], default="cuda"), + io.Float.Input("mask_power", default=2.0, min=0.001, max=10.0, step=0.01, tooltip="Exponent shaping magnitude-based keep probabilities; larger values favor high-magnitude values more strongly."), + io.Float.Input("min_keep_prob", default=0.01, min=0.0, max=1.0, step=0.01, tooltip="Lower bound for each value's keep probability, limiting inverse-probability rescaling."), + io.Float.Input("mask_smooth", default=0.0, min=0.0, max=1.0, step=0.01, tooltip="Blend between stochastic masking at 0 and smooth probability scaling at 1."), + io.Float.Input("trim_quantile", default=0.2, min=0.0, max=1.0, step=0.01, tooltip="Remove the lowest-magnitude fraction before resolving sign conflicts with TIES."), + io.Int.Input("seed", default=42, min=0, max=0xffffffffffffffff, tooltip="Random seed for reproducible enhanced DARE masks."), + io.String.Input("output_filename", default="merged_lora_dare_enhanced", tooltip="Output filename without extension, written under ComfyUI's LoRA directory."), + io.Combo.Input("save_dtype", options=["fp16", "bf16", "fp32"], default="fp16", tooltip="Requested dtype for generated factors; scalar alpha tensors do not determine factor precision."), + io.Combo.Input("device", options=["cuda", "cpu"], default="cuda", tooltip="Device for per-target arithmetic; a CUDA OOM retries only the affected target on CPU."), io.Boolean.Input("include_1d_diffs", default=False, tooltip="Include and merge 1D direct-diff tensors as FP32."), ], diff --git a/tests/test_schema_tooltips.py b/tests/test_schema_tooltips.py new file mode 100644 index 0000000..e708820 --- /dev/null +++ b/tests/test_schema_tooltips.py @@ -0,0 +1,42 @@ +import ast +from pathlib import Path + + +NODE_DIR = Path(__file__).resolve().parents[1] / "nodes" + + +def test_every_schema_input_has_a_direct_static_tooltip(): + missing = [] + non_static = [] + + for path in sorted(NODE_DIR.glob("*.py")): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for call in ast.walk(tree): + if not ( + isinstance(call, ast.Call) + and isinstance(call.func, ast.Attribute) + and call.func.attr == "Input" + ): + continue + + name = ( + call.args[0].value + if call.args and isinstance(call.args[0], ast.Constant) + else "" + ) + tooltip = next( + (keyword.value for keyword in call.keywords if keyword.arg == "tooltip"), + None, + ) + location = f"{path.name}:{call.lineno}:{name}" + if tooltip is None: + missing.append(location) + elif not ( + isinstance(tooltip, ast.Constant) + and isinstance(tooltip.value, str) + and tooltip.value.strip() + ): + non_static.append(location) + + assert not missing, "Inputs without tooltips:\n" + "\n".join(missing) + assert not non_static, "Tooltips must be direct non-empty string literals:\n" + "\n".join(non_static)