ComfyUI-CheckpointNameCycler対応を追加

This commit is contained in:
るみなー先生
2026-05-10 00:29:02 +09:00
parent 1faf4c53a8
commit 90450c4fd5
2 changed files with 191 additions and 79 deletions
+28 -30
View File
@@ -20,13 +20,6 @@ def _clear_checkpoint_filename_cache():
def _scan_registered_checkpoint_folders():
"""
Scan only ComfyUI-registered checkpoint folders.
This intentionally does NOT accept arbitrary user-supplied paths.
It should pick up files added/removed under normal checkpoints paths,
including paths registered through extra_model_paths.yaml.
"""
results = set()
extensions = _get_supported_checkpoint_extensions()
@@ -54,10 +47,6 @@ def _scan_registered_checkpoint_folders():
def _get_fresh_checkpoints():
"""
Prefer ComfyUI's own list after clearing its filename cache.
Merge direct scanning as a safety net for stale model-library states.
"""
_clear_checkpoint_filename_cache()
results = set()
@@ -75,17 +64,7 @@ def _get_fresh_checkpoints():
return sorted(results, key=lambda value: value.lower())
def _patch_backend_checkpoint_widget_classes(checkpoints):
"""
Patch backend class contracts for checkpoint combo nodes.
Updating only the browser-side widget list is not enough.
ComfyUI backend prompt validation can still use stale INPUT_TYPES / RETURN_TYPES.
Targets:
- CheckpointNameSelector-style nodes
- CheckpointLoaderSimple
"""
def _patch_backend_checkpoint_classes(checkpoints):
patched = []
try:
@@ -125,6 +104,32 @@ def _patch_backend_checkpoint_widget_classes(checkpoints):
patched.append(name)
continue
if name == "CheckpointNameCycler":
def cycler_input_types(cls, _checkpoint_values=checkpoint_values):
values = list(_checkpoint_values) or [""]
return {
"required": {
"start_checkpoint": (values,),
"mode": (["fixed", "increment", "randomize", "shuffle_once"], {"default": "increment"}),
"change_every": ("INT", {"default": 1, "min": 1, "max": 999999}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
},
}
node_class.INPUT_TYPES = classmethod(cycler_input_types)
node_class.RETURN_TYPES = (
list(checkpoint_values),
"STRING",
"STRING",
"INT",
"INT",
"INT",
)
patched.append(name)
continue
except Exception as exc:
print(f"[CheckpointWidgetRefresh] failed to patch {name}: {exc}")
@@ -137,7 +142,7 @@ routes = PromptServer.instance.routes
@routes.get("/checkpoint_widget_refresh/checkpoints")
async def checkpoint_widget_refresh_checkpoints(_request):
checkpoints = _get_fresh_checkpoints()
patched = _patch_backend_checkpoint_widget_classes(checkpoints)
patched = _patch_backend_checkpoint_classes(checkpoints)
return web.json_response({
"checkpoints": checkpoints,
@@ -148,13 +153,6 @@ async def checkpoint_widget_refresh_checkpoints(_request):
class CheckpointWidgetRefreshPanel:
"""
UI-only panel node.
The refresh button itself is implemented in web/checkpoint_widget_refresh.js.
This backend node exists so the button can be placed inside a workflow as a small panel.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+163 -49
View File
@@ -23,12 +23,24 @@ function isRefresherNode(node) {
return getNodeTypeName(node) === REFRESHER_NODE_NAME;
}
function findWidget(node, name) {
return (node?.widgets ?? []).find((widget) => widget?.name === name);
}
function findComboWidget(node, name) {
const widget = findWidget(node, name);
if (widget && Array.isArray(widget?.options?.values)) {
return widget;
}
return null;
}
function findCheckpointWidget(node) {
const widgets = node?.widgets ?? [];
return widgets.find((widget) =>
widget?.name === "ckpt_name" &&
Array.isArray(widget?.options?.values)
);
return findComboWidget(node, "ckpt_name");
}
function findStartCheckpointWidget(node) {
return findComboWidget(node, "start_checkpoint");
}
function outputNames(node) {
@@ -47,7 +59,23 @@ function hasLoaderOutputs(node) {
return names.includes("model") && names.includes("clip") && names.includes("vae");
}
function shouldTargetNode(node) {
function isCyclerNode(node) {
const typeName = getNodeTypeName(node);
if (typeName === "CheckpointNameCycler") {
return true;
}
const names = outputNames(node);
return (
names.includes("ckpt_name") &&
names.includes("ckpt_name_str") &&
names.includes("ckpt_name_safe") &&
names.includes("index") &&
names.includes("count")
);
}
function shouldTargetCheckpointWidgetNode(node) {
if (!node || isRefresherNode(node)) {
return false;
}
@@ -79,7 +107,11 @@ function shouldPatchFutureNodeType(nodeData) {
return true;
}
return name.includes("CheckpointNameSelector") || name === "CheckpointLoaderSimple";
return (
name.includes("CheckpointNameSelector") ||
name === "CheckpointLoaderSimple" ||
name === "CheckpointNameCycler"
);
}
function chooseReplacement(oldValues, newValues, currentValue) {
@@ -94,7 +126,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
const oldIndex = Array.isArray(oldValues) ? oldValues.indexOf(currentValue) : -1;
if (oldIndex >= 0) {
// Prefer the next surviving item from the old list.
for (let index = oldIndex + 1; index < oldValues.length; index++) {
const candidate = oldValues[index];
if (newValues.includes(candidate)) {
@@ -102,7 +133,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
}
}
// If the deleted item was at the end, choose the previous survivor.
for (let index = oldIndex - 1; index >= 0; index--) {
const candidate = oldValues[index];
if (newValues.includes(candidate)) {
@@ -110,7 +140,6 @@ function chooseReplacement(oldValues, newValues, currentValue) {
}
}
// Last fallback: same numeric position in the new list if possible.
return newValues[Math.min(oldIndex, newValues.length - 1)] ?? newValues[0];
}
@@ -164,25 +193,7 @@ function patchCheckpointSlotTypes(node, checkpoints) {
}
}
function applyCheckpointListToNode(node, checkpoints) {
if (!shouldTargetNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findCheckpointWidget(node);
if (!widget) {
return {
matched: true,
changed: false,
valueChanged: false,
reason: "checkpoint widget not found",
};
}
function updateComboWidget(node, widget, checkpoints) {
if (!widget.options) {
widget.options = {};
}
@@ -197,7 +208,6 @@ function applyCheckpointListToNode(node, checkpoints) {
widget.options.values = newValues;
widget.value = newValue;
patchCheckpointSlotTypes(node, newValues);
const valuesChanged = !arraysEqual(oldValues, newValues);
const valueChanged = oldValue !== newValue;
@@ -215,7 +225,6 @@ function applyCheckpointListToNode(node, checkpoints) {
}
return {
matched: true,
changed: valuesChanged || valueChanged,
valueChanged,
oldValue,
@@ -223,6 +232,64 @@ function applyCheckpointListToNode(node, checkpoints) {
};
}
function applyCheckpointListToWidgetNode(node, checkpoints) {
if (!shouldTargetCheckpointWidgetNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findCheckpointWidget(node);
if (!widget) {
return {
matched: true,
changed: false,
valueChanged: false,
reason: "checkpoint widget not found",
};
}
const result = updateComboWidget(node, widget, checkpoints);
patchCheckpointSlotTypes(node, checkpoints);
return {
matched: true,
...result,
};
}
function applyCheckpointListToCyclerNode(node, checkpoints) {
if (!isCyclerNode(node)) {
return {
matched: false,
changed: false,
valueChanged: false,
};
}
const widget = findStartCheckpointWidget(node);
if (!widget) {
patchCheckpointSlotTypes(node, checkpoints);
node.setDirtyCanvas?.(true, true);
return {
matched: true,
changed: false,
valueChanged: false,
reason: "start_checkpoint widget not found",
};
}
const result = updateComboWidget(node, widget, checkpoints);
patchCheckpointSlotTypes(node, checkpoints);
return {
matched: true,
...result,
};
}
async function fetchCheckpointList() {
const response = await api.fetchApi("/checkpoint_widget_refresh/checkpoints", {
cache: "no-store",
@@ -240,6 +307,36 @@ async function fetchCheckpointList() {
};
}
async function resetCyclersViaApi() {
try {
const response = await api.fetchApi("/checkpoint_name_cycler/reset", {
method: "POST",
});
if (!response.ok) {
return {
ok: false,
status: response.status,
resetCount: 0,
};
}
const payload = await response.json();
return {
ok: Boolean(payload.ok),
status: response.status,
resetCount: payload.reset_count ?? 0,
};
} catch (error) {
// Cycler may not be installed. That is fine.
return {
ok: false,
error,
resetCount: 0,
};
}
}
function updatePanelTitle(panelNode, text) {
if (!panelNode) {
return;
@@ -257,35 +354,51 @@ async function refreshCheckpointWidgets(panelNode = null) {
const checkpoints = payload.checkpoints;
lastCheckpointList = checkpoints;
let matched = 0;
let changed = 0;
let widgetMatched = 0;
let widgetChanged = 0;
let valueChanged = 0;
let cyclerMatched = 0;
let cyclerChanged = 0;
let cyclerValueChanged = 0;
for (const node of app.graph?._nodes ?? []) {
const result = applyCheckpointListToNode(node, checkpoints);
if (!result.matched) {
continue;
const widgetResult = applyCheckpointListToWidgetNode(node, checkpoints);
if (widgetResult.matched) {
widgetMatched += 1;
if (widgetResult.changed) {
widgetChanged += 1;
}
if (widgetResult.valueChanged) {
valueChanged += 1;
console.log(
`[CheckpointWidgetRefresh] ${getNodeTypeName(node)} #${node.id}: ` +
`${widgetResult.oldValue} -> ${widgetResult.newValue}`
);
}
}
matched += 1;
if (result.changed) {
changed += 1;
}
if (result.valueChanged) {
valueChanged += 1;
console.log(
`[CheckpointWidgetRefresh] ${getNodeTypeName(node)} #${node.id}: ` +
`${result.oldValue} -> ${result.newValue}`
);
const cyclerResult = applyCheckpointListToCyclerNode(node, checkpoints);
if (cyclerResult.matched) {
cyclerMatched += 1;
if (cyclerResult.changed) {
cyclerChanged += 1;
}
if (cyclerResult.valueChanged) {
cyclerValueChanged += 1;
}
}
}
const title = `Updated ${changed}/${matched} ckpt widgets, ${checkpoints.length} ckpts`;
const resetResult = await resetCyclersViaApi();
const cyclerPart = cyclerMatched > 0 ? `, ${cyclerMatched} cyclers reset` : "";
const title = `Updated ${widgetChanged}/${widgetMatched} ckpt widgets${cyclerPart}, ${checkpoints.length} ckpts`;
updatePanelTitle(panelNode, title);
console.log(
`[CheckpointWidgetRefresh] ${title}. ` +
`valueChanged=${valueChanged}, backend patched=${payload.patchedClassCount}`,
`valueChanged=${valueChanged}, cyclerChanged=${cyclerChanged}, ` +
`cyclerValueChanged=${cyclerValueChanged}, backend patched=${payload.patchedClassCount}, ` +
`cyclerResetOk=${resetResult.ok}, cyclerResetCount=${resetResult.resetCount}`,
payload.patchedClasses
);
@@ -333,7 +446,8 @@ function installFuturePatchHook(nodeType, nodeData) {
if (lastCheckpointList) {
setTimeout(() => {
applyCheckpointListToNode(this, lastCheckpointList);
applyCheckpointListToWidgetNode(this, lastCheckpointList);
applyCheckpointListToCyclerNode(this, lastCheckpointList);
app.graph?.setDirtyCanvas?.(true, true);
}, 0);
}