LocalListへの同一項目複数回登録を許容し、ジョブ実行時の除去処理の不具合を修正
This commit is contained in:
+37
-21
@@ -21,7 +21,7 @@ from server import PromptServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# v8d: Local List execution/status separation, Tagger cleanup, and regression guardrails.
|
||||
# v8e: Local List queue consumption, UI restoration, and regression guardrails.
|
||||
EXTENSION_PREFIX = "checkpoint_handpicker_suite"
|
||||
PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview"
|
||||
CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler"
|
||||
@@ -413,7 +413,10 @@ def _cycler_state_payload(state: dict) -> dict:
|
||||
local_count = len(valid_local_items)
|
||||
filter_matches = _filter_match_count(active_filter)
|
||||
use_local = bool(state.get("use_local_list", True))
|
||||
display_source = "local_list" if use_local and state.get("last_source") == "local_list" and local_count else "cycle"
|
||||
# Current job title/source is a snapshot of the last resolved job. Local List
|
||||
# ON/OFF and Clear must not rewrite it; they only affect future selection and
|
||||
# the pending Local List display.
|
||||
display_source = "local_list" if state.get("last_source") == "local_list" else "cycle"
|
||||
hold_index = max(0, _parse_saved_int(state.get("last_hold_index"), 0))
|
||||
change_every = _valid_change_every(state.get("last_change_every", 1))
|
||||
title = _build_cycler_title(display_source, ckpt, hold_index, change_every, active_filter, False, local_count if use_local else 0) if ckpt else "Checkpoint Name Cycler"
|
||||
@@ -432,6 +435,7 @@ def _cycler_state_payload(state: dict) -> dict:
|
||||
"use_local_list": bool(state.get("use_local_list", True)),
|
||||
"active_filter": active_filter,
|
||||
"filter_matches": filter_matches,
|
||||
"filter_total": _checkpoint_total_count(),
|
||||
"settings_revision": _parse_saved_int(state.get("settings_revision"), 0),
|
||||
"local_list_count": len(valid_local_items),
|
||||
"local_list_items": valid_local_items,
|
||||
@@ -1104,14 +1108,17 @@ def _load_image_dir_preview(node_id, relpath, search_directory=None, tab_id="",
|
||||
|
||||
|
||||
def _valid_local_list(local_list: list[str] | None) -> list[str]:
|
||||
all_checkpoints = _get_checkpoint_list()
|
||||
all_set = set(all_checkpoints)
|
||||
"""Return valid Local List entries while preserving order and duplicates.
|
||||
|
||||
Local List is a manual per-Cycler queue, not a set. The same checkpoint may
|
||||
intentionally be pushed multiple times, and each entry must be consumed
|
||||
independently.
|
||||
"""
|
||||
all_set = set(_get_checkpoint_list())
|
||||
local = []
|
||||
seen = set()
|
||||
for item in local_list or []:
|
||||
rel = _normalize_relpath(item)
|
||||
if rel in all_set and rel not in seen:
|
||||
seen.add(rel)
|
||||
if rel in all_set:
|
||||
local.append(rel)
|
||||
return local
|
||||
|
||||
@@ -1123,6 +1130,10 @@ def _filter_match_count(active_filter: list[str]) -> int:
|
||||
return sum(1 for ckpt in all_checkpoints if _get_status(ckpt) in active_filter)
|
||||
|
||||
|
||||
def _checkpoint_total_count() -> int:
|
||||
return len(_get_checkpoint_list())
|
||||
|
||||
|
||||
def _build_local_list_lines(local_items: list[str], max_items: int = 20) -> list[str]:
|
||||
shown = list(local_items[:max_items])
|
||||
if len(local_items) > max_items:
|
||||
@@ -1138,7 +1149,7 @@ def _build_cycler_title(source, ckpt_name, hold_index, change_every, active_filt
|
||||
prefix += f" [{_status_icons_for_filter(active_filter)}]"
|
||||
if fallback_all:
|
||||
prefix += " [fallback all]"
|
||||
if source == "local_list":
|
||||
if source == "local_list" and local_count > 0:
|
||||
prefix += f" [local:{local_count}]"
|
||||
body = f"{icon} {ckpt_name}".strip() if ckpt_name else "(none)"
|
||||
if source == "local_list":
|
||||
@@ -1152,7 +1163,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: {display_mode}" if source == "local_list" else f"Mode: {mode} Hold: {hold_index}/{change_every}",
|
||||
f"Filter: {_filter_display(active_filter)} Matches: {match_count}",
|
||||
f"Filter: {_filter_display(active_filter)} Matches: {match_count} / {_checkpoint_total_count()}",
|
||||
]
|
||||
if fallback_all:
|
||||
lines.append("Filter fallback: all checkpoints")
|
||||
@@ -1177,12 +1188,12 @@ def _build_cycler_idle_status_text(action: str, state: dict, detail: str = "") -
|
||||
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)"
|
||||
use_local = bool(state.get("use_local_list", True))
|
||||
local_job_active = use_local and state.get("last_source") == "local_list" and bool(local_items)
|
||||
local_job_active = state.get("last_source") == "local_list"
|
||||
mode_line = "Mode: local list" if local_job_active else f"Mode: {mode} Hold: {hold_index}/{change_every}"
|
||||
lines = [
|
||||
current,
|
||||
mode_line,
|
||||
f"Filter: {_filter_display(active_filter)} Matches: {_filter_match_count(active_filter)}",
|
||||
f"Filter: {_filter_display(active_filter)} Matches: {_filter_match_count(active_filter)} / {_checkpoint_total_count()}",
|
||||
]
|
||||
if use_local:
|
||||
lines.append(f"Local List: {local_count} item(s)")
|
||||
@@ -1302,13 +1313,16 @@ class CheckpointNameCycler:
|
||||
|
||||
if use_local_source:
|
||||
# Local List is a manual per-Cycler queue. It must not advance the
|
||||
# normal change_every/repeat/shuffle state.
|
||||
local_index = max(0, _parse_saved_int(state.get("local_index"), 0))
|
||||
local_pos = local_index % len(local_items)
|
||||
ckpt_name = local_items[local_pos]
|
||||
state["local_index"] = (local_pos + 1) % len(local_items)
|
||||
# normal change_every/repeat/shuffle state, and it must consume one
|
||||
# queued entry per execution. Duplicates are allowed and consumed
|
||||
# independently.
|
||||
ckpt_name = local_items[0]
|
||||
remaining_local_items = local_items[1:]
|
||||
state["local_list"] = remaining_local_items
|
||||
state["override_queue"] = remaining_local_items
|
||||
state["local_index"] = 0
|
||||
selected_index = all_checkpoints.index(ckpt_name)
|
||||
title = _build_cycler_title("local_list", ckpt_name, 1, change_every, state.get("active_filter", []), False, len(local_items))
|
||||
title = _build_cycler_title("local_list", ckpt_name, 1, change_every, state.get("active_filter", []), False, len(remaining_local_items))
|
||||
status_text = _build_cycler_status_text(
|
||||
"local_list",
|
||||
ckpt_name,
|
||||
@@ -1318,11 +1332,12 @@ class CheckpointNameCycler:
|
||||
1,
|
||||
change_every,
|
||||
global_match_count,
|
||||
len(local_items),
|
||||
len(remaining_local_items),
|
||||
mode,
|
||||
local_items=local_items,
|
||||
local_items=remaining_local_items,
|
||||
use_local_list=True,
|
||||
)
|
||||
local_items = remaining_local_items
|
||||
else:
|
||||
repeat_count = max(0, int(state.get("repeat_count", 0)))
|
||||
last_ckpt = _normalize_relpath(state.get("last_normal_ckpt_name") or state.get("last_ckpt_name", ""))
|
||||
@@ -1447,7 +1462,9 @@ class CheckpointNameCycler:
|
||||
hold_index=hold_index,
|
||||
change_every=change_every,
|
||||
local_list_count=len(local_items),
|
||||
local_list_items=list(local_items),
|
||||
filter_matches=global_match_count,
|
||||
filter_total=_checkpoint_total_count(),
|
||||
execution_revision=state["execution_revision"],
|
||||
)
|
||||
return (ckpt_name, ckpt_name, _ckpt_name_safe_from_relpath(ckpt_name))
|
||||
@@ -1758,8 +1775,7 @@ async def cycler_local_list_append(request):
|
||||
state = _get_cycler_state(_state_key(tab_id, node_id))
|
||||
if state.get("use_local_list", True):
|
||||
local_list = state.setdefault("local_list", [])
|
||||
if relpath not in local_list:
|
||||
local_list.append(relpath)
|
||||
local_list.append(relpath)
|
||||
state["override_queue"] = local_list
|
||||
state["last_status_text"] = _build_cycler_idle_status_text("", state)
|
||||
updated += 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { app } from "../scripts/app.js";
|
||||
import { api } from "../scripts/api.js";
|
||||
|
||||
// v8c: v8b/v8a conservative rebuild with ListSelector/Cycler/Tagger UI-state fixes.
|
||||
// v8e: Local List queue consumption, UI restoration, and regression guardrails.
|
||||
const EXT = "ruminar.checkpoint_handpicker_suite";
|
||||
const PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview";
|
||||
const CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler";
|
||||
@@ -61,6 +61,7 @@ const STATUS_ORDER = ["favorite", "nice", "keep", "delete", "none"];
|
||||
const TAGGER_STATUS_ORDER = ["favorite", "nice", "keep", "delete"];
|
||||
const STATUS_ICON = { favorite: "💛", nice: "👍", keep: "✔", delete: "🗑", none: "—" };
|
||||
const STATUS_LABEL = { favorite: "favorite", nice: "nice", keep: "keep", delete: "delete", none: "none" };
|
||||
const STATUS_CURRENT_LABEL = { favorite: "favorite", nice: "nice", keep: "keep", delete: "Marked for deletion", none: "none" };
|
||||
|
||||
function getWidget(node, name) {
|
||||
return node.widgets?.find((w) => w.name === name);
|
||||
@@ -684,8 +685,8 @@ async function restoreNodeStateFromBackend(node, nodeClass) {
|
||||
node.__hpsTaggerPath = result.ckpt_name_str;
|
||||
node.__hpsTaggerStatus = result.status || "none";
|
||||
node.__hpsTaggerMessage = node.__hpsTaggerStatus === "none"
|
||||
? "Current: — none"
|
||||
: `Current: ${STATUS_ICON[node.__hpsTaggerStatus]} ${STATUS_LABEL[node.__hpsTaggerStatus]}`;
|
||||
? "Current: none"
|
||||
: `Current: ${STATUS_ICON[node.__hpsTaggerStatus]} ${STATUS_CURRENT_LABEL[node.__hpsTaggerStatus] || STATUS_LABEL[node.__hpsTaggerStatus]}`;
|
||||
node.title = node.__hpsTaggerStatus === "none"
|
||||
? `Tagger : ${result.ckpt_name_str}`
|
||||
: `Tagger : ${STATUS_ICON[node.__hpsTaggerStatus]} ${result.ckpt_name_str}`;
|
||||
@@ -829,9 +830,9 @@ function hideSelectorWidget(node) {
|
||||
function selectorRects(node) {
|
||||
const margin = 8;
|
||||
return {
|
||||
refreshAll: { x: margin, y: 8, w: 100, h: 24 },
|
||||
listOnly: { x: 114, y: 8, w: 80, h: 24 },
|
||||
pushLocalList: { x: 200, y: 8, w: 148, h: 24 },
|
||||
refreshAll: { x: margin, y: 8, w: 110, h: 24 },
|
||||
listOnly: { x: 124, y: 8, w: 104, h: 24 },
|
||||
pushLocalList: { x: 236, y: 8, w: 154, h: 24 },
|
||||
up: { x: 360, y: 8, w: 34, h: 24 },
|
||||
down: { x: 400, y: 8, w: 34, h: 24 },
|
||||
list: { x: margin, y: 104, w: node.size[0] - 16, h: ROW_H * SELECTOR_VISIBLE_ROWS },
|
||||
@@ -1201,7 +1202,12 @@ function drawSelectorRows(ctx, node) {
|
||||
ctx.fillStyle = active ? "rgba(75,125,190,0.58)" : (i % 2 ? "rgba(255,255,255,0.035)" : "rgba(0,0,0,0.06)");
|
||||
ctx.fillRect(r.list.x + 1, y + 1, r.list.w - 2, ROW_H - 2);
|
||||
ctx.fillStyle = active ? "#fff" : "#ddd";
|
||||
ctx.fillText(`${selectorStatusIcon(status)} ${ckpt}`, r.list.x + 6, y + ROW_H / 2, r.list.w - 20);
|
||||
const icon = selectorStatusIcon(status);
|
||||
// Keep the checkpoint-name column aligned even when status is none.
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(icon, r.list.x + 14, y + ROW_H / 2, 18);
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(ckpt, r.list.x + 28, y + ROW_H / 2, r.list.w - 42);
|
||||
}
|
||||
const sb = selectorScrollbar(node);
|
||||
if (sb) {
|
||||
@@ -1241,7 +1247,7 @@ function setupSelectorNode(nodeType) {
|
||||
if (hpsNodeCollapsed(this)) return;
|
||||
const r = selectorRects(this);
|
||||
drawButton(ctx, r.refreshAll, "🔄 Refresh All", !this.__hpsLoading);
|
||||
drawButton(ctx, r.listOnly, "List", !this.__hpsLoading);
|
||||
drawButton(ctx, r.listOnly, "📋 List Only", !this.__hpsLoading);
|
||||
drawButton(ctx, r.pushLocalList, selectorActionLabel(this), !this.__hpsLoading);
|
||||
drawButton(ctx, r.up, "▲", true);
|
||||
drawButton(ctx, r.down, "▼", true);
|
||||
@@ -1399,12 +1405,17 @@ function currentTaggerPath(node) {
|
||||
return node.__hpsTaggerPath || linkedInputValue(node, "ckpt_name_str") || "";
|
||||
}
|
||||
function taggerButtons(node) {
|
||||
// Keep the controls away from LiteGraph input pins/labels.
|
||||
// Keep controls right-aligned and away from LiteGraph input pins/labels.
|
||||
const buttonW = 76;
|
||||
const gap = 6;
|
||||
const totalW = TAGGER_STATUS_ORDER.length * buttonW + (TAGGER_STATUS_ORDER.length - 1) * gap;
|
||||
const right = Math.max(442, (node.size?.[0] || 450) - 8);
|
||||
const startX = right - totalW;
|
||||
return TAGGER_STATUS_ORDER.map((status, i) => ({
|
||||
status,
|
||||
x: 120 + i * 82,
|
||||
x: startX + i * (buttonW + gap),
|
||||
y: 3,
|
||||
w: 76,
|
||||
w: buttonW,
|
||||
h: 26,
|
||||
}));
|
||||
}
|
||||
@@ -1424,7 +1435,7 @@ async function setTaggerStatus(node, status) {
|
||||
const result = await response.json();
|
||||
if (result.ok) {
|
||||
node.__hpsTaggerStatus = result.status;
|
||||
node.__hpsTaggerMessage = result.status === "none" ? "Current: — none" : `Current: ${STATUS_ICON[result.status]} ${STATUS_LABEL[result.status]}`;
|
||||
node.__hpsTaggerMessage = result.status === "none" ? "Current: none" : `Current: ${STATUS_ICON[result.status]} ${STATUS_CURRENT_LABEL[result.status] || STATUS_LABEL[result.status]}`;
|
||||
node.title = result.status === "none" ? `Tagger : ${ckpt}` : `Tagger : ${STATUS_ICON[result.status]} ${ckpt}`;
|
||||
} else {
|
||||
node.__hpsTaggerMessage = result.error || "Failed";
|
||||
@@ -1482,7 +1493,7 @@ function setupTaggerNode(nodeType) {
|
||||
ctx.fillStyle = "#ddd";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillText(p ? p : "Execute once to bind current checkpoint.", 8, 50);
|
||||
const msg = this.__hpsTaggerMessage || (current === "none" ? "Current: — none" : `Current: ${STATUS_ICON[current]} ${STATUS_LABEL[current]}`);
|
||||
const msg = this.__hpsTaggerMessage || (current === "none" ? "Current: none" : `Current: ${STATUS_ICON[current]} ${STATUS_CURRENT_LABEL[current] || STATUS_LABEL[current]}`);
|
||||
ctx.fillStyle = current === "none" ? "#888" : "#ddd";
|
||||
ctx.fillText(msg, 8, 70);
|
||||
if (this.size[1] >= 124 && current !== "none" && current !== "delete") {
|
||||
@@ -1509,7 +1520,7 @@ api.addEventListener(TAGGER_EVENT, ({ detail }) => {
|
||||
if (!node) return;
|
||||
node.__hpsTaggerPath = detail.ckpt_name_str;
|
||||
node.__hpsTaggerStatus = detail.status;
|
||||
node.__hpsTaggerMessage = detail.status === "none" ? "Current: — none" : `Current: ${STATUS_ICON[detail.status]} ${STATUS_LABEL[detail.status]}`;
|
||||
node.__hpsTaggerMessage = detail.status === "none" ? "Current: none" : `Current: ${STATUS_ICON[detail.status]} ${STATUS_CURRENT_LABEL[detail.status] || STATUS_LABEL[detail.status]}`;
|
||||
if (detail.title) node.title = detail.title;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
@@ -1538,7 +1549,7 @@ api.addEventListener(STATUS_CHANGED_EVENT, ({ detail }) => {
|
||||
}
|
||||
if (isNodeClass(node, TAGGER_CLASS) && currentTaggerPath(node) === detail.ckpt_name_str) {
|
||||
node.__hpsTaggerStatus = detail.status || "none";
|
||||
node.__hpsTaggerMessage = node.__hpsTaggerStatus === "none" ? "Current: — none" : `Current: ${STATUS_ICON[node.__hpsTaggerStatus]} ${STATUS_LABEL[node.__hpsTaggerStatus]}`;
|
||||
node.__hpsTaggerMessage = node.__hpsTaggerStatus === "none" ? "Current: none" : `Current: ${STATUS_ICON[node.__hpsTaggerStatus]} ${STATUS_CURRENT_LABEL[node.__hpsTaggerStatus] || STATUS_LABEL[node.__hpsTaggerStatus]}`;
|
||||
patchCheckpointTitle(node, "Tagger", detail.ckpt_name_str, node.__hpsTaggerStatus);
|
||||
}
|
||||
if (isNodeClass(node, CYCLER_CLASS) && node.__hpsCyclerCkptName === detail.ckpt_name_str) {
|
||||
@@ -1555,8 +1566,8 @@ function cyclerRects(node) {
|
||||
const filter = [{ status: "all", x: 8, y: filterY, w: 54, h: 24 }];
|
||||
CYCLER_FILTER_STATUSES.forEach((status, i) => filter.push({ status, x: 68 + i * 48, y: filterY, w: 42, h: 24 }));
|
||||
return {
|
||||
localListToggle: { x: 8, y: 4, w: 122, h: 24 },
|
||||
clearLocalList: { x: 136, y: 4, w: 122, h: 24 },
|
||||
localListToggle: { x: 8, y: 4, w: 150, h: 24 },
|
||||
clearLocalList: { x: 164, y: 4, w: 122, h: 24 },
|
||||
filter,
|
||||
statusBox: { x: 8, y: 140, w: node.size[0] - 16, h: Math.max(80, node.size[1] - 150) },
|
||||
};
|
||||
@@ -1644,7 +1655,7 @@ function setupCyclerNode(nodeType) {
|
||||
const active = cyclerActiveFilter(this);
|
||||
ctx.save();
|
||||
|
||||
drawButton(ctx, r.localListToggle, this.__hpsUseLocalList === false ? "Local List OFF" : "Local List ON", true, this.__hpsUseLocalList !== false);
|
||||
drawButton(ctx, r.localListToggle, this.__hpsUseLocalList === false ? "☐ Use Local List" : "☑ Use Local List", true, this.__hpsUseLocalList !== false);
|
||||
drawButton(ctx, r.clearLocalList, "Clear Local List", true, false, "rgba(105,90,90,0.65)");
|
||||
for (const b of r.filter) {
|
||||
const isAll = b.status === "all";
|
||||
@@ -1652,11 +1663,6 @@ function setupCyclerNode(nodeType) {
|
||||
drawButton(ctx, b, isAll ? "all" : STATUS_ICON[b.status], true, on);
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#aaa";
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillText(`filter: ${statusIconDisplay(active)}`, 8, 78);
|
||||
ctx.fillText(`settings revision: ${this.__hpsSettingsRevision ?? 0}`, 8, 96);
|
||||
|
||||
ctx.fillStyle = "rgba(0,0,0,0.18)";
|
||||
ctx.fillRect(r.statusBox.x, r.statusBox.y, r.statusBox.w, r.statusBox.h);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.18)";
|
||||
|
||||
Reference in New Issue
Block a user