Initial release

This commit is contained in:
Szymon
2026-04-30 15:00:27 +02:00
commit 6c9f21b24d
10 changed files with 306 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
__pycache__/
*.pyc
*.pyo
*.pyd
.venv/
venv/
.vscode/
.idea/
.DS_Store
Thumbs.db
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## 1.0.0
- Initial release.
- Added `LoraRangeLoaderModelOnly`.
- Added `first`, `last`, `current`, and `mode` controls.
- Added frontend logic for `fixed`, `increment`, `decrement`, and `random` current switching.
- Added Comfy Registry metadata and documentation.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 szymonj
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+82
View File
@@ -0,0 +1,82 @@
# comfyui-lora-range-loader (ComfyUI)
Custom node for ComfyUI that loads a LoRA like the core `LoraLoaderModelOnly`, but adds a filename range sweep workflow for testing many LoRA checkpoints from the same training run.
## What this node does
- Adds the node **Load LoRA Range (Model Only)**.
- Loads the LoRA selected in `current` exactly like the core model-only LoRA loader.
- Adds `first` and `last` fields to define an allowed filename range.
- Adds `mode` to control how `current` changes after each queued run.
- Supports these modes:
- `fixed`: keep `current` unchanged
- `increment`: move to the next filename in the sorted `first..last` range and wrap to `first`
- `decrement`: move backward in the sorted `first..last` range and wrap to `last`
- `random`: pick a random filename from the sorted `first..last` range
## How it works
The node is split into two parts:
- Backend Python loads only the LoRA selected in `current`.
- Frontend JavaScript updates `current` after the workflow is queued.
This means the LoRA used for the current execution is always the value visible in `current` at queue time, and the next value is prepared for the following run.
## Range rules
- The list of LoRAs is sorted by filename in ascending order.
- `first` must come before or equal to `last` in that sorted list.
- If `first` comes after `last`, automatic switching is disabled.
- If `current` is outside the `first..last` range, the node still loads that LoRA, but `increment`, `decrement`, and `random` do not change it.
## LoRA loading behavior
- The node reuses the core `LoraLoader` implementation.
- It does not load `first` or `last` into memory.
- It only loads `current`.
- It inherits the same single-entry cache as the core loader, so repeating the same `current` value does not reload the LoRA file unnecessarily.
## Compatibility
Backend compatibility:
- The Python node is lightweight and relies only on ComfyUI core APIs.
- No extra Python packages are required.
Frontend compatibility:
- Full automatic switching of `current` requires a modern ComfyUI frontend that supports widget queue lifecycle hooks.
- If the frontend extension does not run, the backend node still works as a manual `current` LoRA loader.
## How to use
1. Add **Load LoRA Range (Model Only)** to the graph.
2. Connect your `MODEL` input.
3. Choose `first`, `last`, and `current` from the LoRA dropdowns.
4. Choose `mode`.
5. Run the workflow repeatedly.
6. Observe `current` advancing for the next run according to `mode`.
## Install via ComfyUI-Manager
1. Open **Manager** in ComfyUI.
2. Install this node from the list or by repository URL.
3. Restart ComfyUI or refresh node definitions.
## Manual install
1. Clone or copy this repository into `ComfyUI/custom_nodes/`.
2. Restart ComfyUI.
No additional Python dependencies are needed.
## Included files
- [__init__.py](__init__.py) - backend node definition
- [web/lora_range_loader.js](web/lora_range_loader.js) - frontend logic that advances `current`
## Notes for publishing
- The Registry package name in `pyproject.toml` is intended to be stable and should be treated as immutable after publishing.
- If you want a different final GitHub repository URL or package name, change it before the first Registry publish.
+57
View File
@@ -0,0 +1,57 @@
import folder_paths
from nodes import LoraLoader
class LoraRangeLoaderModelOnly(LoraLoader):
@classmethod
def INPUT_TYPES(cls):
loras = folder_paths.get_filename_list("loras")
return {
"required": {
"model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}),
"first": (loras, {"tooltip": "First LoRA in the allowed filename range."}),
"last": (loras, {"tooltip": "Last LoRA in the allowed filename range."}),
"current": (loras, {"tooltip": "The LoRA that will be loaded for the current run."}),
"mode": (
["fixed", "increment", "decrement", "random"],
{"default": "fixed", "tooltip": "How current should change after a run is queued."},
),
"strength_model": (
"FLOAT",
{
"default": 1.0,
"min": -100.0,
"max": 100.0,
"step": 0.01,
"tooltip": "How strongly to modify the diffusion model. This value can be negative.",
},
),
}
}
RETURN_TYPES = ("MODEL",)
OUTPUT_TOOLTIPS = ("The modified diffusion model.",)
FUNCTION = "load_lora_model_only"
CATEGORY = "loaders"
DESCRIPTION = "Loads the current LoRA like Load LoRA (Model Only) and lets the frontend advance through a filename range after each queued run."
SEARCH_ALIASES = ["lora range", "lora sweep", "load lora range", "checkpoint training lora"]
def load_lora_model_only(self, model, first, last, current, mode, strength_model):
del first
del last
del mode
return (self.load_lora(model, None, current, strength_model, 0)[0],)
NODE_CLASS_MAPPINGS = {
"LoraRangeLoaderModelOnly": LoraRangeLoaderModelOnly,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoraRangeLoaderModelOnly": "Load LoRA Range (Model Only)",
}
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

+14
View File
@@ -0,0 +1,14 @@
[project]
name = "comfyui-lora-range-loader"
description = "ComfyUI custom node: load a LoRA from current selection and automatically advance current within a first/last filename range."
version = "1.0.0"
license = { file = "LICENSE.txt" }
dependencies = []
[project.urls]
Repository = "https://github.com/esp-dev/comfyui-lora-range-loader"
[tool.comfy]
PublisherId = "szymonj"
DisplayName = "comfyui-lora-range-loader"
Icon = "https://raw.githubusercontent.com/esp-dev/comfyui-lora-range-loader/main/comfyui-lora-range-loader-icon-256.png"
+1
View File
@@ -0,0 +1 @@
# No extra Python dependencies are required for this node.
+109
View File
@@ -0,0 +1,109 @@
import { app } from "../../scripts/app.js";
const NODE_CLASS = "LoraRangeLoaderModelOnly";
function getWidget(node, name) {
return node.widgets?.find((widget) => widget.name === name);
}
function getSortedLoras(currentWidget) {
const values = Array.isArray(currentWidget?.options?.values) ? [...currentWidget.options.values] : [];
values.sort((left, right) => String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: "base" }));
return values;
}
function getAllowedRange(node) {
const firstWidget = getWidget(node, "first");
const lastWidget = getWidget(node, "last");
const currentWidget = getWidget(node, "current");
if (!firstWidget || !lastWidget || !currentWidget) {
return null;
}
const sorted = getSortedLoras(currentWidget);
const firstIndex = sorted.indexOf(firstWidget.value);
const lastIndex = sorted.indexOf(lastWidget.value);
if (firstIndex === -1 || lastIndex === -1 || firstIndex > lastIndex) {
return null;
}
return sorted.slice(firstIndex, lastIndex + 1);
}
function advanceCurrent(node) {
const currentWidget = getWidget(node, "current");
const modeWidget = getWidget(node, "mode");
if (!currentWidget || !modeWidget) {
return;
}
const allowedRange = getAllowedRange(node);
if (!allowedRange || allowedRange.length === 0) {
return;
}
const currentIndex = allowedRange.indexOf(currentWidget.value);
if (currentIndex === -1) {
return;
}
let nextValue = currentWidget.value;
switch (modeWidget.value) {
case "fixed":
return;
case "increment":
nextValue = allowedRange[(currentIndex + 1) % allowedRange.length];
break;
case "decrement":
nextValue = allowedRange[(currentIndex - 1 + allowedRange.length) % allowedRange.length];
break;
case "random":
nextValue = allowedRange[Math.floor(Math.random() * allowedRange.length)];
break;
default:
return;
}
currentWidget.value = nextValue;
currentWidget.callback?.(nextValue);
node.setDirtyCanvas(true, true);
}
function installAfterQueuedHandler(node) {
if (node.__loraRangeAfterQueuedInstalled) {
return;
}
const currentWidget = getWidget(node, "current");
if (!currentWidget) {
return;
}
const originalAfterQueued = currentWidget.afterQueued?.bind(currentWidget);
currentWidget.afterQueued = function afterQueued(event = {}) {
originalAfterQueued?.(event);
if (!event.isPartialExecution) {
advanceCurrent(node);
}
};
node.__loraRangeAfterQueuedInstalled = true;
}
app.registerExtension({
name: "Comfy.LoraRangeLoader",
nodeCreated(node) {
if (node.constructor.comfyClass !== NODE_CLASS) {
return;
}
installAfterQueuedHandler(node);
},
});