初期ロード時に、CheckpointLoaderSimpleでクラッシュしないように修正
This commit is contained in:
+1
-1
@@ -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
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user