mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-18 19:41:26 -03:00
feat(loaders): add control_after_generate random model selection to checkpoint/unet loaders
The Checkpoint/Unet Loader (LoraManager) nodes now support ComfyUI's built-in control_after_generate mechanism on the ckpt_name/unet_name combos, letting users pick a random model on every queue with the selected model written back into the widget (visible, and lockable via the 'fixed' mode). A base_model input narrows the random pool: a front-end extension fetches the name/base_model mapping from the new /api/lm/checkpoints/loader-pool endpoint and filters the combo options, wired through the node callback, the refreshComboInNodes extension hook, and a graph.onConfigure hook installed from onAdded (onNodeCreated fires before the node is attached to a graph, so the graph reference is unavailable there).
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const NODE_CONFIGS = {
|
||||
"Checkpoint Loader (LoraManager)": {
|
||||
modelWidget: "ckpt_name",
|
||||
subType: "checkpoint",
|
||||
},
|
||||
"Unet Loader (LoraManager)": {
|
||||
modelWidget: "unet_name",
|
||||
subType: "diffusion_model",
|
||||
},
|
||||
};
|
||||
|
||||
const poolCache = new Map();
|
||||
|
||||
async function fetchPool(subType) {
|
||||
try {
|
||||
const response = await api.fetchApi(
|
||||
`/api/lm/checkpoints/loader-pool?sub_type=${encodeURIComponent(subType)}`
|
||||
);
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return Array.isArray(data.items) ? data.items : [];
|
||||
} catch (error) {
|
||||
console.error("LoRA Manager: failed to fetch loader pool", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPoolCache() {
|
||||
const subTypes = new Set(Object.values(NODE_CONFIGS).map((c) => c.subType));
|
||||
await Promise.all(
|
||||
[...subTypes].map(async (subType) => {
|
||||
poolCache.set(subType, await fetchPool(subType));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function applyBaseModelFilter(node, config) {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(widget) => widget.name === config.modelWidget
|
||||
);
|
||||
const baseModelWidget = node.widgets?.find(
|
||||
(widget) => widget.name === "base_model"
|
||||
);
|
||||
if (!modelWidget || !baseModelWidget) return;
|
||||
|
||||
const wired = node.inputs?.some(
|
||||
(input) =>
|
||||
input.widget?.name === config.modelWidget && input.link != null
|
||||
);
|
||||
if (wired) return;
|
||||
|
||||
const pool = poolCache.get(config.subType) ?? [];
|
||||
const filter = baseModelWidget.value;
|
||||
const filtered =
|
||||
filter === "Any"
|
||||
? pool
|
||||
: pool.filter((model) => model.base_model === filter);
|
||||
const names = filtered.map((model) => model.name);
|
||||
|
||||
modelWidget.options.values = names;
|
||||
if (!names.includes(modelWidget.value)) {
|
||||
modelWidget.value = names[0];
|
||||
}
|
||||
}
|
||||
|
||||
function applyToAllNodes() {
|
||||
app.graph?.nodes?.forEach((node) => {
|
||||
const config = NODE_CONFIGS[node.comfyClass];
|
||||
if (config) applyBaseModelFilter(node, config);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureGraphConfigureHook(graph) {
|
||||
if (!graph || graph.__loraManagerConfigureHooked) return;
|
||||
graph.__loraManagerConfigureHooked = true;
|
||||
|
||||
const originalConfigure = graph.onConfigure;
|
||||
graph.onConfigure = function (data) {
|
||||
const result = originalConfigure?.call(this, data);
|
||||
// Workflow reload restores widget values after onNodeCreated fires, so the
|
||||
// per-node hook runs too early; re-apply the filter once the whole graph
|
||||
// has been configured.
|
||||
setTimeout(() => applyToAllNodes(), 0);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "LoraManager.RandomLoaderControl",
|
||||
|
||||
async setup() {
|
||||
await refreshPoolCache();
|
||||
},
|
||||
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
const config = NODE_CONFIGS[nodeType.comfyClass];
|
||||
if (!config) return;
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
|
||||
const baseModelWidget = this.widgets?.find(
|
||||
(widget) => widget.name === "base_model"
|
||||
);
|
||||
if (baseModelWidget) {
|
||||
const originalCallback = baseModelWidget.callback;
|
||||
baseModelWidget.callback = (value, canvas, node, pos, event) => {
|
||||
applyBaseModelFilter(node ?? this, config);
|
||||
return originalCallback?.call(this, value, canvas, node, pos, event);
|
||||
};
|
||||
}
|
||||
|
||||
applyBaseModelFilter(this, config);
|
||||
return result;
|
||||
};
|
||||
|
||||
// onNodeCreated fires inside LGraph.createNode, before the node is added to
|
||||
// a graph (this.graph is null there), so the graph-level configure hook
|
||||
// must be installed from onAdded, where the graph reference is available.
|
||||
const onAdded = nodeType.prototype.onAdded;
|
||||
nodeType.prototype.onAdded = function () {
|
||||
const result = onAdded?.apply(this, arguments);
|
||||
ensureGraphConfigureHook(this.graph);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
||||
async refreshComboInNodes() {
|
||||
await refreshPoolCache();
|
||||
applyToAllNodes();
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user