初期ロード時に、CheckpointLoaderSimpleでクラッシュしないように修正

This commit is contained in:
るみなー先生
2026-06-06 12:03:55 +09:00
parent 55058c0a90
commit c9b6d62f25
3 changed files with 116 additions and 36 deletions
+1 -1
View File
@@ -2,4 +2,4 @@ WEB_DIRECTORY = "./web"
from .suite_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+53 -8
View File
@@ -21,6 +21,7 @@ from server import PromptServer
logger = logging.getLogger(__name__)
# v8c: conservative rebuild from v8b/v8a with Cycler/ListSelector UI-state fixes.
EXTENSION_PREFIX = "checkpoint_handpicker_suite"
PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview"
CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler"
@@ -203,6 +204,13 @@ def _checkpoint_roots() -> list[Path]:
def _get_checkpoint_list() -> list[str]:
"""Return ComfyUI's normal checkpoint list order.
This order is intentionally not re-sorted here. Before Refresh All has patched
every checkpoint combo type, CheckpointNameCycler must expose the same combo
ordering as ComfyUI's standard CheckpointLoaderSimple, otherwise combo output
types can diverge and fail when the queue starts.
"""
try:
names = folder_paths.get_filename_list("checkpoints")
except Exception:
@@ -216,7 +224,7 @@ def _get_checkpoint_list() -> list[str]:
continue
seen.add(relpath)
out.append(relpath)
return sorted(out, key=lambda x: x.lower())
return out
def _scan_registered_checkpoint_folders() -> list[str]:
@@ -296,6 +304,11 @@ def _status_icons_for_filter(active_statuses: list[str]) -> str:
return "".join(STATUS_ICON[s] for s in ["favorite", "nice", "keep", "delete", "none"] if s in active_statuses)
def _filter_display(active_statuses: list[str]) -> str:
icons = _status_icons_for_filter(active_statuses)
return icons or "all"
def _log_widget_refresh(updated_count: int):
logger.info("[CheckpointHandpickerSuite] Updated checkpoint widgets: %s", updated_count)
@@ -1091,7 +1104,7 @@ def _build_cycler_status_text(source, ckpt_name, active_filter, fallback_all, qu
lines = [
f"Current: {STATUS_ICON[status]} {ckpt_name}" if ckpt_name else "Current: (none)",
f"Mode: {mode} Hold: {hold_index}/{change_every}",
f"Filter: {', '.join(active_filter) if active_filter else 'all'} Matches: {match_count}",
f"Filter: {_filter_display(active_filter)} Matches: {match_count}",
]
if fallback_all:
lines.append("Filter fallback: all checkpoints")
@@ -1102,6 +1115,29 @@ def _build_cycler_status_text(source, ckpt_name, active_filter, fallback_all, qu
return "\n".join(lines)
def _build_cycler_idle_status_text(action: str, state: dict, detail: str = "") -> str:
"""Build a status panel for UI-only operations such as Push/Clear Local List."""
ckpt_name = state.get("last_ckpt_name", "") or ""
status = _get_status(ckpt_name) if ckpt_name else "none"
active_filter = list(state.get("active_filter", []))
local_count = len(state.get("local_list", []))
mode = state.get("last_mode", "increment")
hold_index = max(0, _parse_saved_int(state.get("last_hold_index"), 0))
change_every = _valid_change_every(state.get("last_change_every", 1))
current = f"Current: {STATUS_ICON[status]} {ckpt_name}" if ckpt_name else "Current: (not executed yet)"
lines = [
current,
f"Mode: {mode} Hold: {hold_index}/{change_every}",
f"Filter: {_filter_display(active_filter)}",
f"Local List: {local_count} item(s)",
]
if action:
lines.append(action)
if detail:
lines.append(detail)
return "\n".join(lines)
def _candidate_checkpoints(active_filter: list[str], local_list: list[str] | None = None, use_local_list: bool = True) -> tuple[list[str], bool, int]:
all_checkpoints = _get_checkpoint_list()
all_set = set(all_checkpoints)
@@ -1146,7 +1182,7 @@ class CheckpointListSelector:
class CheckpointNameCycler:
@classmethod
def INPUT_TYPES(cls):
values = _get_fresh_checkpoint_values() or [""]
values = _get_checkpoint_list() or [""]
return {
"required": {
"start_checkpoint": (values,),
@@ -1162,7 +1198,7 @@ class CheckpointNameCycler:
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = (_get_fresh_checkpoint_values() or [""], "STRING", "STRING")
RETURN_TYPES = (_get_checkpoint_list() or [""], "STRING", "STRING")
RETURN_NAMES = ("ckpt_name", "ckpt_name_str", "ckpt_name_safe")
FUNCTION = "cycle"
CATEGORY = "checkpoint/handpicker"
@@ -1621,15 +1657,21 @@ async def cycler_local_list_append(request):
tab_id = _clean_tab_id(data.get("tab_id", ""))
target_node_ids = [str(x) for x in data.get("target_node_ids", []) if str(x).isdigit()]
updated = 0
states = []
for node_id in target_node_ids:
state = _get_cycler_state(_state_key(tab_id, node_id))
if state.get("use_local_list", True):
state.setdefault("local_list", []).append(relpath)
state["override_queue"] = state["local_list"]
local_list = state.setdefault("local_list", [])
local_list.append(relpath)
state["override_queue"] = local_list
state["last_status_text"] = _build_cycler_idle_status_text("Pushed to Local List", state, relpath)
updated += 1
_send_cycler_state_update(node_id, state, tab_id=tab_id)
payload = _cycler_state_payload(state)
payload.update({"node_id": node_id})
states.append(payload)
logger.info("[CheckpointHandpickerSuite] Pushed checkpoint to %s Local List(s) in tab %s: %s", updated, tab_id, relpath)
return web.json_response({"ok": True, "updated": updated})
return web.json_response({"ok": True, "updated": updated, "states": states})
@routes.post(f"/{EXTENSION_PREFIX}/cycler/clear_local_list")
@@ -1641,9 +1683,12 @@ async def cycler_clear_local_list(request):
cleared = len(state.get("local_list", []))
state["local_list"] = []
state["override_queue"] = []
state["last_status_text"] = _build_cycler_idle_status_text("Cleared Local List", state, f"Cleared: {cleared} item(s)")
_send_cycler_state_update(node_id, state, tab_id=tab_id)
logger.info("[CheckpointHandpickerSuite] Cleared Local List for tab %s node %s: %s item(s)", tab_id, node_id, cleared)
return web.json_response({"ok": True, "cleared": cleared})
payload = _cycler_state_payload(state)
payload.update({"node_id": node_id})
return web.json_response({"ok": True, "cleared": cleared, "state": payload})
@routes.post(f"/{EXTENSION_PREFIX}/review/sync_checkpoint")
+62 -27
View File
@@ -1,7 +1,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// v8a: v7-based stable UI rebuild. Restores ListSelector page scroll, wheel capture, and scrollbar drag.
// v8c: v8b/v8a conservative rebuild with ListSelector/Cycler/Tagger UI-state fixes.
const EXT = "ruminar.checkpoint_handpicker_suite";
const PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview";
const CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler";
@@ -399,6 +399,36 @@ function serializeCyclerFilterStatuses(statuses) {
return JSON.stringify(normalizeCyclerFilterStatuses(statuses));
}
function statusIconDisplay(statuses) {
const normalized = normalizeCyclerFilterStatuses(statuses);
return normalized.length ? normalized.map((status) => STATUS_ICON[status] || status).join("") : "all";
}
function applyCyclerStatePayload(node, detail) {
if (!node || !detail) return;
if (detail.active_filter !== undefined) {
node.__hpsFilterStatuses = normalizeCyclerFilterStatuses(detail.active_filter);
}
if (detail.use_local_list !== undefined) {
node.__hpsUseLocalList = !!detail.use_local_list;
}
if (detail.settings_revision !== undefined) {
node.__hpsSettingsRevision = parseSavedInt(detail.settings_revision, node.__hpsSettingsRevision ?? 0);
}
if (detail.ckpt_name_str) {
node.__hpsCyclerCkptName = detail.ckpt_name_str;
node.__hpsCyclerStatusValue = detail.status || "none";
setExecutionState(detail);
}
if (detail.status_text !== undefined) {
node.__hpsCyclerStatus = detail.status_text;
}
if (detail.title) {
node.title = detail.title;
}
syncCyclerSettingsWidgets(node);
}
function parseSavedBool(value, fallback) {
const text = String(value ?? "").trim().toLowerCase();
if (!text) return fallback;
@@ -647,12 +677,7 @@ async function restoreNodeStateFromBackend(node, nodeClass) {
: `Tagger : ${STATUS_ICON[node.__hpsTaggerStatus]} ${result.ckpt_name_str}`;
}
} else if (nodeClass === CYCLER_CLASS) {
if (result.ckpt_name_str) {
node.__hpsCyclerCkptName = result.ckpt_name_str;
node.__hpsCyclerStatusValue = result.status || "none";
}
if (result.status_text) node.__hpsCyclerStatus = result.status_text;
if (result.title) node.title = result.title;
applyCyclerStatePayload(node, result);
}
app.graph?.setDirtyCanvas?.(true, true);
} catch (error) {
@@ -994,11 +1019,18 @@ async function pushSelectedToLocalList(node) {
body: JSON.stringify(tabPayload({
ckpt_name_str: selected,
target_node_ids: (app.graph?._nodes || [])
.filter((n) => isNodeClass(n, CYCLER_CLASS) && n.__hpsUseLocalList !== false)
.filter((n) => isNodeClass(n, CYCLER_CLASS))
.map((n) => n.id),
})),
});
const result = await response.json();
if (result.ok && Array.isArray(result.states)) {
for (const state of result.states) {
const cycler = app.graph?.getNodeById?.(Number(state.node_id));
if (cycler && isNodeClass(cycler, CYCLER_CLASS)) applyCyclerStatePayload(cycler, state);
}
}
console.info(`[INFO] [CheckpointHandpickerSuite] Push Local List: ${selected} -> ${result.updated ?? 0} Cycler(s)`);
node.__hpsStatus = result.ok
? `pushed : ${selected} (${result.updated} Cycler)`
: (result.error || "Push failed");
@@ -1194,7 +1226,7 @@ function setupSelectorNode(nodeType) {
hideSelectorWidget(this);
if (hpsNodeCollapsed(this)) return;
const r = selectorRects(this);
drawButton(ctx, r.refreshAll, "Refresh All", !this.__hpsLoading);
drawButton(ctx, r.refreshAll, "🔄 Refresh All", !this.__hpsLoading);
drawButton(ctx, r.listOnly, "List", !this.__hpsLoading);
drawButton(ctx, r.pushLocalList, selectorActionLabel(this), !this.__hpsLoading);
drawButton(ctx, r.up, "▲", true);
@@ -1353,9 +1385,10 @@ function currentTaggerPath(node) {
return node.__hpsTaggerPath || linkedInputValue(node, "ckpt_name_str") || "";
}
function taggerButtons(node) {
// Keep the controls below LiteGraph input pins/labels.
return STATUS_ORDER.map((status, i) => ({
status,
x: 8 + i * 84,
x: 128 + i * 84,
y: 8,
w: 78,
h: 26,
@@ -1395,13 +1428,13 @@ function taggerCursorAt(node, local) {
}
function setupTaggerNode(nodeType) {
installMinSize(nodeType, 450, 100);
installMinSize(nodeType, 450, 130);
installTabIdSupport(nodeType);
installCursorCapture();
const origCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = origCreated ? origCreated.apply(this, arguments) : undefined;
ensureSize(this, 450, 100);
ensureSize(this, 450, 130);
setTimeout(() => restoreNodeStateFromBackend(this, TAGGER_CLASS), 0);
return r;
};
@@ -1434,13 +1467,13 @@ function setupTaggerNode(nodeType) {
const p = currentTaggerPath(this);
ctx.fillStyle = "#ddd";
ctx.font = "12px sans-serif";
ctx.fillText(p ? p : "Execute once to bind current checkpoint.", 8, 54);
ctx.fillText(p ? p : "Execute once to bind current checkpoint.", 8, 78);
const msg = this.__hpsTaggerMessage || (current === "none" ? "Current: — none" : `Current: ${STATUS_ICON[current]} ${STATUS_LABEL[current]}`);
ctx.fillStyle = current === "none" ? "#888" : "#ddd";
ctx.fillText(msg, 8, 72);
if (this.size[1] >= 110 && current !== "none" && current !== "delete") {
ctx.fillText(msg, 8, 96);
if (this.size[1] >= 124 && current !== "none" && current !== "delete") {
ctx.fillStyle = "#aaa";
ctx.fillText("Delete is available only from none.", 8, 90);
ctx.fillText("Delete is available only from none.", 8, 114);
}
ctx.restore();
};
@@ -1519,7 +1552,7 @@ function cyclerActiveFilter(node) {
}
async function pushCyclerFlags(node) {
syncCyclerSettingsWidgets(node);
await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/set_flags`, {
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/set_flags`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tabPayload({
@@ -1529,10 +1562,12 @@ async function pushCyclerFlags(node) {
...getCyclerRuntimeControls(node),
})),
});
const result = await response.json();
if (result?.ok) applyCyclerStatePayload(node, result);
}
async function pushCyclerFilter(node) {
syncCyclerSettingsWidgets(node);
await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/set_filter`, {
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/set_filter`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tabPayload({
@@ -1542,13 +1577,19 @@ async function pushCyclerFilter(node) {
...getCyclerRuntimeControls(node),
})),
});
const result = await response.json();
if (result?.ok) applyCyclerStatePayload(node, result);
}
async function clearLocalList(node) {
await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/clear_local_list`, {
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/clear_local_list`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(tabPayload({ node_id: node.id })),
});
const result = await response.json();
if (result?.ok && result.state) applyCyclerStatePayload(node, result.state);
console.info(`[INFO] [CheckpointHandpickerSuite] Clear Local List: ${result?.cleared ?? 0} item(s)`);
app.graph?.setDirtyCanvas?.(true, true);
}
function cyclerCursorAt(node, local) {
if (!local) return "";
@@ -1599,7 +1640,7 @@ function setupCyclerNode(nodeType) {
ctx.fillStyle = "#aaa";
ctx.font = "12px sans-serif";
ctx.fillText(`filter: ${active.length ? active.join(", ") : "all"}`, 8, 78);
ctx.fillText(`filter: ${statusIconDisplay(active)}`, 8, 78);
ctx.fillText(`settings revision: ${this.__hpsSettingsRevision ?? 0}`, 8, 96);
ctx.fillStyle = "rgba(0,0,0,0.18)";
@@ -1649,13 +1690,7 @@ function setupCyclerNode(nodeType) {
api.addEventListener(CYCLER_EVENT, ({ detail }) => {
const node = nodeFromEvent(detail, CYCLER_CLASS);
if (!node) return;
if (detail.ckpt_name_str) {
node.__hpsCyclerCkptName = detail.ckpt_name_str;
node.__hpsCyclerStatusValue = detail.status || "none";
setExecutionState(detail);
}
if (detail.title) node.title = detail.title;
node.__hpsCyclerStatus = detail.status_text;
applyCyclerStatePayload(node, detail);
app.graph.setDirtyCanvas(true, true);
});