Cyclerのランタイム制御と実行スナップショットを追加し、関連するUIウィジェットの更新を実装
This commit is contained in:
+5
-2
@@ -1,5 +1,8 @@
|
||||
try:
|
||||
from .suite_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
except Exception:
|
||||
from suite_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
from .suite_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
+283
-85
@@ -21,7 +21,7 @@ from server import PromptServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# v8e: Local List queue consumption, UI restoration, and regression guardrails.
|
||||
# v8f: live Cycler runtime controls and execution snapshots.
|
||||
EXTENSION_PREFIX = "checkpoint_handpicker_suite"
|
||||
PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview"
|
||||
CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler"
|
||||
@@ -364,17 +364,35 @@ def _valid_change_every(value) -> int:
|
||||
|
||||
def _get_default_cycler_state() -> dict:
|
||||
return {
|
||||
"use_local_list": True,
|
||||
"accept_queue": True,
|
||||
# runtime_controls are the live controls used by the next execution.
|
||||
# Python function arguments are only a fallback when the frontend state
|
||||
# has not been initialized yet.
|
||||
"runtime_controls": {
|
||||
"mode": "increment",
|
||||
"change_every": 1,
|
||||
"start_checkpoint": "",
|
||||
"active_filter": [],
|
||||
"use_local_list": True,
|
||||
"settings_revision": 0,
|
||||
},
|
||||
"runtime_controls_initialized": False,
|
||||
# runtime_state: mutable execution inventory/decks/counters.
|
||||
"local_list": [],
|
||||
"local_index": 0,
|
||||
"override_queue": [],
|
||||
"active_filter": [],
|
||||
"settings_revision": 0,
|
||||
"current_index": 0,
|
||||
"repeat_count": 0,
|
||||
"cycle_count": 0,
|
||||
"shuffle_deck": [],
|
||||
# last_execution_snapshot is the original record of the last checkpoint
|
||||
# that was actually returned downstream. Local List contents are NOT
|
||||
# stored here; they are a separate runtime_state ledger.
|
||||
"last_execution_snapshot": None,
|
||||
# legacy mirrors kept for old JS/payload compatibility.
|
||||
"use_local_list": True,
|
||||
"accept_queue": True,
|
||||
"active_filter": [],
|
||||
"settings_revision": 0,
|
||||
"last_ckpt_name": "",
|
||||
"last_normal_ckpt_name": "",
|
||||
"last_source": "cycle",
|
||||
@@ -397,6 +415,72 @@ def _get_cycler_state(key: str) -> dict:
|
||||
return state
|
||||
|
||||
|
||||
def _runtime_controls_from_state(state: dict) -> dict:
|
||||
raw = state.get("runtime_controls")
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
controls = {
|
||||
"mode": _valid_mode(raw.get("mode", state.get("last_mode", "increment"))),
|
||||
"change_every": _valid_change_every(raw.get("change_every", state.get("last_change_every", 1))),
|
||||
"start_checkpoint": _normalize_relpath(raw.get("start_checkpoint", state.get("last_start_checkpoint", ""))),
|
||||
"active_filter": _normalize_status_list(raw.get("active_filter", state.get("active_filter", []))),
|
||||
"use_local_list": bool(raw.get("use_local_list", state.get("use_local_list", True))),
|
||||
"settings_revision": _parse_saved_int(raw.get("settings_revision", state.get("settings_revision", 0)), 0),
|
||||
}
|
||||
state["runtime_controls"] = controls
|
||||
# Keep legacy mirrors in sync. These mirrors are runtime controls, not last
|
||||
# execution facts.
|
||||
state["last_mode"] = controls["mode"]
|
||||
state["last_change_every"] = controls["change_every"]
|
||||
state["last_start_checkpoint"] = controls["start_checkpoint"]
|
||||
state["active_filter"] = list(controls["active_filter"])
|
||||
state["use_local_list"] = controls["use_local_list"]
|
||||
state["accept_queue"] = controls["use_local_list"]
|
||||
state["settings_revision"] = controls["settings_revision"]
|
||||
return controls
|
||||
|
||||
|
||||
def _set_runtime_controls(state: dict, *, mode=None, change_every=None, start_checkpoint=None, active_filter=None, use_local_list=None, settings_revision=None, mark_initialized=True) -> dict:
|
||||
controls = _runtime_controls_from_state(state)
|
||||
if mode is not None:
|
||||
controls["mode"] = _valid_mode(mode)
|
||||
if change_every is not None:
|
||||
controls["change_every"] = _valid_change_every(change_every)
|
||||
if start_checkpoint is not None:
|
||||
controls["start_checkpoint"] = _normalize_relpath(start_checkpoint)
|
||||
if active_filter is not None:
|
||||
controls["active_filter"] = _normalize_status_list(active_filter)
|
||||
if use_local_list is not None:
|
||||
controls["use_local_list"] = bool(use_local_list)
|
||||
if settings_revision is not None:
|
||||
controls["settings_revision"] = max(0, _parse_saved_int(settings_revision, controls.get("settings_revision", 0)))
|
||||
state["runtime_controls"] = controls
|
||||
if mark_initialized:
|
||||
state["runtime_controls_initialized"] = True
|
||||
return _runtime_controls_from_state(state)
|
||||
|
||||
|
||||
def _last_execution_snapshot(state: dict) -> dict:
|
||||
snap = state.get("last_execution_snapshot")
|
||||
if isinstance(snap, dict) and snap.get("ckpt_name"):
|
||||
return snap
|
||||
# Migrate old last_* fields if present.
|
||||
ckpt = state.get("last_ckpt_name", "") or ""
|
||||
if not ckpt:
|
||||
return {}
|
||||
return {
|
||||
"ckpt_name": ckpt,
|
||||
"ckpt_name_safe": _ckpt_name_safe_from_relpath(ckpt),
|
||||
"source": state.get("last_source", "cycle"),
|
||||
"mode_used": state.get("last_mode", "increment"),
|
||||
"base_mode": state.get("last_mode", "increment"),
|
||||
"change_every_used": _valid_change_every(state.get("last_change_every", 1)),
|
||||
"hold_index": max(0, _parse_saved_int(state.get("last_hold_index"), 0)),
|
||||
"filter_used": list(state.get("active_filter", [])),
|
||||
"fallback_all": False,
|
||||
}
|
||||
|
||||
|
||||
def _should_accept_settings_update(state: dict, revision: int) -> tuple[bool, int]:
|
||||
revision = max(0, _parse_saved_int(revision, 0))
|
||||
current = max(0, _parse_saved_int(state.get("settings_revision"), 0))
|
||||
@@ -406,23 +490,41 @@ def _should_accept_settings_update(state: dict, revision: int) -> tuple[bool, in
|
||||
|
||||
|
||||
def _cycler_state_payload(state: dict) -> dict:
|
||||
ckpt = state.get("last_ckpt_name", "") or ""
|
||||
controls = _runtime_controls_from_state(state)
|
||||
snap = _last_execution_snapshot(state)
|
||||
ckpt = snap.get("ckpt_name", "") or ""
|
||||
status = _get_status(ckpt) if ckpt else "none"
|
||||
active_filter = list(state.get("active_filter", []))
|
||||
active_filter = list(controls.get("active_filter", []))
|
||||
valid_local_items = _valid_local_list(state.get("local_list", []))
|
||||
local_count = len(valid_local_items)
|
||||
filter_matches = _filter_match_count(active_filter)
|
||||
use_local = bool(state.get("use_local_list", True))
|
||||
# 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"
|
||||
# Keep status_text derived from the current UI state so Local List ON/OFF
|
||||
# immediately shows/hides the list without waiting for the next queue run.
|
||||
status_text = _build_cycler_idle_status_text("", state)
|
||||
use_local = bool(controls.get("use_local_list", True))
|
||||
|
||||
if ckpt:
|
||||
display_source = snap.get("source", "cycle")
|
||||
hold_index = max(0, _parse_saved_int(snap.get("hold_index"), 0))
|
||||
change_every = _valid_change_every(snap.get("change_every_used", controls.get("change_every", 1)))
|
||||
mode_used = snap.get("mode_used", controls.get("mode", "increment"))
|
||||
fallback_all = bool(snap.get("fallback_all", False))
|
||||
title = _build_cycler_title(display_source, ckpt, hold_index, change_every, active_filter, fallback_all, local_count if use_local else 0)
|
||||
status_text = _build_cycler_status_text(
|
||||
display_source,
|
||||
ckpt,
|
||||
active_filter,
|
||||
fallback_all,
|
||||
[],
|
||||
hold_index,
|
||||
change_every,
|
||||
filter_matches,
|
||||
local_count,
|
||||
mode_used,
|
||||
local_items=valid_local_items,
|
||||
use_local_list=use_local,
|
||||
)
|
||||
else:
|
||||
title = "Checkpoint Name Cycler"
|
||||
status_text = _build_cycler_idle_status_text("", state)
|
||||
|
||||
state["last_title"] = title
|
||||
state["last_status_text"] = status_text
|
||||
return {
|
||||
@@ -432,16 +534,25 @@ def _cycler_state_payload(state: dict) -> dict:
|
||||
"status_icon": STATUS_ICON[status],
|
||||
"title": title,
|
||||
"status_text": status_text,
|
||||
"use_local_list": bool(state.get("use_local_list", True)),
|
||||
"use_local_list": bool(controls.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),
|
||||
"settings_revision": _parse_saved_int(controls.get("settings_revision"), 0),
|
||||
"local_list_count": len(valid_local_items),
|
||||
"local_list_items": valid_local_items,
|
||||
"mode": state.get("last_mode", "increment"),
|
||||
"change_every": state.get("last_change_every", 1),
|
||||
"start_checkpoint": state.get("last_start_checkpoint", ""),
|
||||
"runtime_controls": {
|
||||
"mode": controls.get("mode", "increment"),
|
||||
"change_every": controls.get("change_every", 1),
|
||||
"start_checkpoint": controls.get("start_checkpoint", ""),
|
||||
"active_filter": active_filter,
|
||||
"use_local_list": bool(controls.get("use_local_list", True)),
|
||||
"settings_revision": _parse_saved_int(controls.get("settings_revision"), 0),
|
||||
},
|
||||
"mode": controls.get("mode", "increment"),
|
||||
"change_every": controls.get("change_every", 1),
|
||||
"start_checkpoint": controls.get("start_checkpoint", ""),
|
||||
"last_execution_snapshot": snap,
|
||||
"execution_revision": state.get("execution_revision", 0),
|
||||
}
|
||||
|
||||
@@ -1135,7 +1246,11 @@ def _checkpoint_total_count() -> int:
|
||||
|
||||
|
||||
def _build_local_list_lines(local_items: list[str], max_items: int = 20) -> list[str]:
|
||||
shown = list(local_items[:max_items])
|
||||
shown = []
|
||||
for item in list(local_items[:max_items]):
|
||||
status = _get_status(item)
|
||||
icon = STATUS_ICON[status] if status != "none" else " "
|
||||
shown.append(f"{icon} {item}")
|
||||
if len(local_items) > max_items:
|
||||
shown.append(f"... and {len(local_items) - max_items} more")
|
||||
return shown
|
||||
@@ -1145,8 +1260,8 @@ def _build_cycler_title(source, ckpt_name, hold_index, change_every, active_filt
|
||||
status = _get_status(ckpt_name) if ckpt_name else "none"
|
||||
icon = STATUS_ICON[status] if status != "none" else ""
|
||||
prefix = "Cycler"
|
||||
if active_filter:
|
||||
prefix += f" [{_status_icons_for_filter(active_filter)}]"
|
||||
# Filter state is shown by the filter buttons and the status panel. Do not
|
||||
# duplicate it in the title.
|
||||
if fallback_all:
|
||||
prefix += " [fallback all]"
|
||||
if source == "local_list" and local_count > 0:
|
||||
@@ -1159,9 +1274,10 @@ def _build_cycler_title(source, ckpt_name, hold_index, change_every, active_filt
|
||||
|
||||
def _build_cycler_status_text(source, ckpt_name, active_filter, fallback_all, queue, hold_index, change_every, match_count, local_count, mode, local_items=None, use_local_list=True) -> str:
|
||||
status = _get_status(ckpt_name) if ckpt_name else "none"
|
||||
current = f"Current: {STATUS_ICON[status]} {ckpt_name}" if status != "none" and ckpt_name else (f"Current: {ckpt_name}" if ckpt_name else "Current: (none)")
|
||||
display_mode = "local list" if source == "local_list" else mode
|
||||
lines = [
|
||||
f"Current: {STATUS_ICON[status]} {ckpt_name}" if ckpt_name else "Current: (none)",
|
||||
current,
|
||||
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} / {_checkpoint_total_count()}",
|
||||
]
|
||||
@@ -1177,19 +1293,29 @@ def _build_cycler_status_text(source, ckpt_name, active_filter, fallback_all, qu
|
||||
|
||||
|
||||
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 ""
|
||||
"""Build a status panel for UI-only operations such as Push/Clear Local List.
|
||||
|
||||
This is a synthesized display: last_execution_snapshot provides the current
|
||||
job, runtime_controls provide the live filter/mode settings, and Local List
|
||||
comes from runtime_state.
|
||||
"""
|
||||
controls = _runtime_controls_from_state(state)
|
||||
snap = _last_execution_snapshot(state)
|
||||
ckpt_name = snap.get("ckpt_name", "") or ""
|
||||
status = _get_status(ckpt_name) if ckpt_name else "none"
|
||||
active_filter = list(state.get("active_filter", []))
|
||||
active_filter = list(controls.get("active_filter", []))
|
||||
local_items = _valid_local_list(state.get("local_list", []))
|
||||
local_count = len(local_items)
|
||||
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)"
|
||||
use_local = bool(state.get("use_local_list", True))
|
||||
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}"
|
||||
mode = controls.get("mode", "increment")
|
||||
current = f"Current: {STATUS_ICON[status]} {ckpt_name}" if status != "none" and ckpt_name else (f"Current: {ckpt_name}" if ckpt_name else "Current: (not executed yet)")
|
||||
use_local = bool(controls.get("use_local_list", True))
|
||||
local_job_active = snap.get("source") == "local_list"
|
||||
if local_job_active:
|
||||
mode_line = "Mode: local list"
|
||||
else:
|
||||
hold_index = max(0, _parse_saved_int(snap.get("hold_index"), 0))
|
||||
change_every = _valid_change_every(snap.get("change_every_used", controls.get("change_every", 1)))
|
||||
mode_line = f"Mode: {snap.get('mode_used', mode)} Hold: {hold_index}/{change_every}" if ckpt_name else f"Mode: {mode}"
|
||||
lines = [
|
||||
current,
|
||||
mode_line,
|
||||
@@ -1270,18 +1396,38 @@ class CheckpointNameCycler:
|
||||
node_id = str(unique_id or "")
|
||||
key = _state_key(tab_id, node_id)
|
||||
state = _get_cycler_state(key)
|
||||
mode = _valid_mode(mode)
|
||||
change_every = _valid_change_every(change_every)
|
||||
active_filter = _normalize_status_list(hps_filter_statuses)
|
||||
use_local_list = _parse_saved_bool(hps_use_local_list, bool(state.get("use_local_list", True)))
|
||||
revision = _parse_saved_int(hps_settings_revision, _parse_saved_int(state.get("settings_revision"), 0))
|
||||
|
||||
accepted, revision = _should_accept_settings_update(state, revision)
|
||||
if accepted:
|
||||
state["settings_revision"] = revision
|
||||
state["active_filter"] = active_filter
|
||||
state["use_local_list"] = use_local_list
|
||||
state["accept_queue"] = use_local_list
|
||||
prompt_mode = _valid_mode(mode)
|
||||
prompt_change_every = _valid_change_every(change_every)
|
||||
prompt_filter = _normalize_status_list(hps_filter_statuses)
|
||||
prompt_use_local_list = _parse_saved_bool(hps_use_local_list, bool(_runtime_controls_from_state(state).get("use_local_list", True)))
|
||||
prompt_start_checkpoint = _normalize_relpath(start_checkpoint)
|
||||
prompt_revision = _parse_saved_int(hps_settings_revision, _runtime_controls_from_state(state).get("settings_revision", 0))
|
||||
|
||||
controls = _runtime_controls_from_state(state)
|
||||
current_revision = _parse_saved_int(controls.get("settings_revision"), 0)
|
||||
# First execution can seed runtime_controls from prompt arguments. After
|
||||
# that, prompt JSON is treated as a stale fallback unless its revision is
|
||||
# newer than the backend runtime state.
|
||||
if not state.get("runtime_controls_initialized") or prompt_revision > current_revision:
|
||||
controls = _set_runtime_controls(
|
||||
state,
|
||||
mode=prompt_mode,
|
||||
change_every=prompt_change_every,
|
||||
start_checkpoint=prompt_start_checkpoint,
|
||||
active_filter=prompt_filter,
|
||||
use_local_list=prompt_use_local_list,
|
||||
settings_revision=max(prompt_revision, current_revision),
|
||||
mark_initialized=True,
|
||||
)
|
||||
else:
|
||||
controls = _runtime_controls_from_state(state)
|
||||
|
||||
mode = _valid_mode(controls.get("mode", prompt_mode))
|
||||
change_every = _valid_change_every(controls.get("change_every", prompt_change_every))
|
||||
active_filter = _normalize_status_list(controls.get("active_filter", prompt_filter))
|
||||
use_local_list = bool(controls.get("use_local_list", prompt_use_local_list))
|
||||
start_checkpoint_used = _normalize_relpath(controls.get("start_checkpoint") or prompt_start_checkpoint)
|
||||
|
||||
all_checkpoints = _get_checkpoint_list()
|
||||
if not all_checkpoints:
|
||||
@@ -1292,9 +1438,9 @@ class CheckpointNameCycler:
|
||||
_send_cycler_update(node_id, title, status_text, tab_id=tab_id)
|
||||
return ("", "", "checkpoint")
|
||||
|
||||
global_candidates, fallback_all, global_match_count = _candidate_checkpoints(state.get("active_filter", []))
|
||||
global_candidates, fallback_all, global_match_count = _candidate_checkpoints(active_filter)
|
||||
local_items = _valid_local_list(state.get("local_list", []))
|
||||
use_local_source = bool(state.get("use_local_list", True)) and bool(local_items)
|
||||
use_local_source = use_local_list and bool(local_items)
|
||||
candidates = local_items if use_local_source else global_candidates
|
||||
if not candidates:
|
||||
candidates = all_checkpoints
|
||||
@@ -1302,7 +1448,7 @@ class CheckpointNameCycler:
|
||||
candidate_set = set(candidates)
|
||||
|
||||
def start_index() -> int:
|
||||
requested = _normalize_relpath(start_checkpoint)
|
||||
requested = _normalize_relpath(start_checkpoint_used)
|
||||
if requested in all_checkpoints:
|
||||
return all_checkpoints.index(requested)
|
||||
return 0
|
||||
@@ -1312,28 +1458,26 @@ class CheckpointNameCycler:
|
||||
selected_index = 0
|
||||
|
||||
if use_local_source:
|
||||
# Local List is a manual per-Cycler queue. It must not advance the
|
||||
# normal change_every/repeat/shuffle state, and it must consume one
|
||||
# queued entry per execution. Duplicates are allowed and consumed
|
||||
# independently.
|
||||
# Manual Local List queue. It is a runtime_state ledger, not part of
|
||||
# last_execution_snapshot. Consume exactly one item per execution.
|
||||
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(remaining_local_items))
|
||||
title = _build_cycler_title("local_list", ckpt_name, 1, change_every, active_filter, False, len(remaining_local_items))
|
||||
status_text = _build_cycler_status_text(
|
||||
"local_list",
|
||||
ckpt_name,
|
||||
state.get("active_filter", []),
|
||||
active_filter,
|
||||
False,
|
||||
[],
|
||||
1,
|
||||
change_every,
|
||||
global_match_count,
|
||||
len(remaining_local_items),
|
||||
mode,
|
||||
"local list",
|
||||
local_items=remaining_local_items,
|
||||
use_local_list=True,
|
||||
)
|
||||
@@ -1370,7 +1514,7 @@ class CheckpointNameCycler:
|
||||
selected_index = all_checkpoints.index(ckpt_name)
|
||||
else:
|
||||
if mode == "fixed":
|
||||
requested = _normalize_relpath(start_checkpoint)
|
||||
requested = _normalize_relpath(start_checkpoint_used)
|
||||
ckpt_name = requested if requested in candidate_set else candidates[0]
|
||||
selected_index = all_checkpoints.index(ckpt_name)
|
||||
elif mode == "randomize":
|
||||
@@ -1398,11 +1542,11 @@ class CheckpointNameCycler:
|
||||
state["current_index"] = selected_index
|
||||
|
||||
state["last_normal_ckpt_name"] = ckpt_name
|
||||
title = _build_cycler_title("cycle", ckpt_name, hold_index, change_every, state.get("active_filter", []), fallback_all and mode != "fixed", len(local_items) if state.get("use_local_list", True) else 0)
|
||||
title = _build_cycler_title("cycle", ckpt_name, hold_index, change_every, active_filter, fallback_all and mode != "fixed", len(local_items) if use_local_list else 0)
|
||||
status_text = _build_cycler_status_text(
|
||||
"cycle",
|
||||
ckpt_name,
|
||||
state.get("active_filter", []),
|
||||
active_filter,
|
||||
fallback_all and mode != "fixed",
|
||||
[],
|
||||
hold_index,
|
||||
@@ -1411,20 +1555,42 @@ class CheckpointNameCycler:
|
||||
len(local_items),
|
||||
mode,
|
||||
local_items=local_items,
|
||||
use_local_list=bool(state.get("use_local_list", True)),
|
||||
use_local_list=use_local_list,
|
||||
)
|
||||
if mode == "shuffle_once":
|
||||
status_text += f"\nShuffle Deck Remaining: {len(state.get('shuffle_deck', []))}"
|
||||
|
||||
status = _get_status(ckpt_name)
|
||||
snapshot = {
|
||||
"ckpt_name": ckpt_name,
|
||||
"ckpt_name_safe": _ckpt_name_safe_from_relpath(ckpt_name),
|
||||
"source": source,
|
||||
"mode_used": "local_list" if use_local_source else mode,
|
||||
"base_mode": mode,
|
||||
"change_every_used": change_every,
|
||||
"filter_used": list(active_filter),
|
||||
"use_local_list_used": use_local_list,
|
||||
"fallback_all": False if use_local_source else (fallback_all and mode != "fixed"),
|
||||
"matches_at_resolve": global_match_count,
|
||||
"local_list_count_after": len(local_items),
|
||||
"hold_index": hold_index,
|
||||
"hold_total": change_every,
|
||||
"status": status,
|
||||
"status_icon": STATUS_ICON[status],
|
||||
"title": title,
|
||||
"status_text": status_text,
|
||||
}
|
||||
state["last_execution_snapshot"] = snapshot
|
||||
state["last_ckpt_name"] = ckpt_name
|
||||
state["last_source"] = source
|
||||
state["last_title"] = title
|
||||
state["last_status_text"] = status_text
|
||||
state["last_hold_index"] = hold_index
|
||||
# Mirrors of runtime controls, not queue-prompt arguments.
|
||||
state["last_change_every"] = change_every
|
||||
state["last_mode"] = mode
|
||||
state["last_start_checkpoint"] = _normalize_relpath(start_checkpoint)
|
||||
status = _get_status(ckpt_name)
|
||||
state["last_start_checkpoint"] = start_checkpoint_used
|
||||
|
||||
exec_state = _store_tab_execution_state(tab_id, {
|
||||
"node": int(unique_id) if unique_id is not None else None,
|
||||
"node_class": "CheckpointNameCycler",
|
||||
@@ -1437,16 +1603,19 @@ class CheckpointNameCycler:
|
||||
"mode_used": "local_list" if use_local_source else mode,
|
||||
"base_mode": mode,
|
||||
"change_every": change_every,
|
||||
"change_every_used": 1 if use_local_source else change_every,
|
||||
"filter_used": list(state.get("active_filter", [])),
|
||||
"change_every_used": change_every,
|
||||
"filter_used": list(active_filter),
|
||||
"fallback_all": False if use_local_source else (fallback_all and mode != "fixed"),
|
||||
"matches_at_resolve": global_match_count,
|
||||
"local_list_count": len(local_items),
|
||||
"hold_index": hold_index,
|
||||
"title": title,
|
||||
"status_text": status_text,
|
||||
"last_execution_snapshot": snapshot,
|
||||
})
|
||||
state["execution_revision"] = exec_state.get("execution_revision", 0)
|
||||
snapshot["execution_revision"] = state["execution_revision"]
|
||||
state["last_execution_snapshot"] = snapshot
|
||||
_send_cycler_update(
|
||||
node_id,
|
||||
title,
|
||||
@@ -1465,6 +1634,10 @@ class CheckpointNameCycler:
|
||||
local_list_items=list(local_items),
|
||||
filter_matches=global_match_count,
|
||||
filter_total=_checkpoint_total_count(),
|
||||
active_filter=list(active_filter),
|
||||
use_local_list=use_local_list,
|
||||
runtime_controls=controls,
|
||||
last_execution_snapshot=snapshot,
|
||||
execution_revision=state["execution_revision"],
|
||||
)
|
||||
return (ckpt_name, ckpt_name, _ckpt_name_safe_from_relpath(ckpt_name))
|
||||
@@ -1711,6 +1884,31 @@ async def tagger_set_status(request):
|
||||
})
|
||||
|
||||
|
||||
@routes.post(f"/{EXTENSION_PREFIX}/cycler/set_runtime_controls")
|
||||
async def cycler_set_runtime_controls(request):
|
||||
data = await request.json()
|
||||
node_id = str(data.get("node_id", ""))
|
||||
tab_id = _clean_tab_id(data.get("tab_id", ""))
|
||||
state = _get_cycler_state(_state_key(tab_id, node_id))
|
||||
revision = _parse_saved_int(data.get("settings_revision"), _runtime_controls_from_state(state).get("settings_revision", 0))
|
||||
accepted, revision = _should_accept_settings_update(state, revision)
|
||||
if accepted:
|
||||
_set_runtime_controls(
|
||||
state,
|
||||
mode=data.get("mode") if "mode" in data else None,
|
||||
change_every=data.get("change_every") if "change_every" in data else None,
|
||||
start_checkpoint=data.get("start_checkpoint", "") if "start_checkpoint" in data else None,
|
||||
active_filter=data.get("active_filter") if "active_filter" in data else None,
|
||||
use_local_list=bool(data.get("use_local_list")) if "use_local_list" in data else None,
|
||||
settings_revision=revision,
|
||||
mark_initialized=True,
|
||||
)
|
||||
_send_cycler_state_update(node_id, state, tab_id=tab_id)
|
||||
payload = _cycler_state_payload(state)
|
||||
payload.update({"ok": True, "accepted": accepted, "node_id": node_id})
|
||||
return web.json_response(payload)
|
||||
|
||||
|
||||
@routes.post(f"/{EXTENSION_PREFIX}/cycler/set_flags")
|
||||
async def cycler_set_flags(request):
|
||||
data = await request.json()
|
||||
@@ -1720,16 +1918,15 @@ async def cycler_set_flags(request):
|
||||
revision = _parse_saved_int(data.get("settings_revision"), _parse_saved_int(state.get("settings_revision"), 0))
|
||||
accepted, revision = _should_accept_settings_update(state, revision)
|
||||
if accepted:
|
||||
if "use_local_list" in data:
|
||||
state["use_local_list"] = bool(data.get("use_local_list"))
|
||||
state["accept_queue"] = state["use_local_list"]
|
||||
if "mode" in data:
|
||||
state["last_mode"] = _valid_mode(data.get("mode"))
|
||||
if "change_every" in data:
|
||||
state["last_change_every"] = _valid_change_every(data.get("change_every"))
|
||||
if "start_checkpoint" in data:
|
||||
state["last_start_checkpoint"] = _normalize_relpath(data.get("start_checkpoint", ""))
|
||||
state["settings_revision"] = revision
|
||||
_set_runtime_controls(
|
||||
state,
|
||||
mode=data.get("mode") if "mode" in data else None,
|
||||
change_every=data.get("change_every") if "change_every" in data else None,
|
||||
start_checkpoint=data.get("start_checkpoint", "") if "start_checkpoint" in data else None,
|
||||
use_local_list=bool(data.get("use_local_list")) if "use_local_list" in data else None,
|
||||
settings_revision=revision,
|
||||
mark_initialized=True,
|
||||
)
|
||||
_send_cycler_state_update(node_id, state, tab_id=tab_id)
|
||||
payload = _cycler_state_payload(state)
|
||||
payload.update({"ok": True, "accepted": accepted, "use_local_list": state["use_local_list"]})
|
||||
@@ -1746,18 +1943,19 @@ async def cycler_set_filter(request):
|
||||
revision = _parse_saved_int(data.get("settings_revision"), _parse_saved_int(state.get("settings_revision"), 0))
|
||||
accepted, revision = _should_accept_settings_update(state, revision)
|
||||
if accepted:
|
||||
state["active_filter"] = statuses.copy()
|
||||
if "mode" in data:
|
||||
state["last_mode"] = _valid_mode(data.get("mode"))
|
||||
if "change_every" in data:
|
||||
state["last_change_every"] = _valid_change_every(data.get("change_every"))
|
||||
if "start_checkpoint" in data:
|
||||
state["last_start_checkpoint"] = _normalize_relpath(data.get("start_checkpoint", ""))
|
||||
state["settings_revision"] = revision
|
||||
_set_runtime_controls(
|
||||
state,
|
||||
mode=data.get("mode") if "mode" in data else None,
|
||||
change_every=data.get("change_every") if "change_every" in data else None,
|
||||
start_checkpoint=data.get("start_checkpoint", "") if "start_checkpoint" in data else None,
|
||||
active_filter=statuses.copy(),
|
||||
settings_revision=revision,
|
||||
mark_initialized=True,
|
||||
)
|
||||
_send_cycler_state_update(node_id, state, tab_id=tab_id)
|
||||
logger.info("[CheckpointHandpickerSuite] Updated cycler filter for tab %s node %s: %s accepted=%s", tab_id, node_id, statuses or ["All"], accepted)
|
||||
payload = _cycler_state_payload(state)
|
||||
payload.update({"ok": True, "accepted": accepted, "node_id": node_id, "statuses": state.get("active_filter", [])})
|
||||
payload.update({"ok": True, "accepted": accepted, "node_id": node_id, "statuses": _runtime_controls_from_state(state).get("active_filter", [])})
|
||||
return web.json_response(payload)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
// v8e: Local List queue consumption, UI restoration, and regression guardrails.
|
||||
// v8f: live Cycler runtime controls and execution snapshots.
|
||||
const EXT = "ruminar.checkpoint_handpicker_suite";
|
||||
const PREVIEW_EVENT = "ruminar.checkpoint_handpicker_suite.preview";
|
||||
const CYCLER_EVENT = "ruminar.checkpoint_handpicker_suite.cycler";
|
||||
@@ -412,14 +412,28 @@ function selectorStatusIcon(status) {
|
||||
|
||||
function applyCyclerStatePayload(node, detail) {
|
||||
if (!node || !detail) return;
|
||||
if (detail.active_filter !== undefined) {
|
||||
node.__hpsFilterStatuses = normalizeCyclerFilterStatuses(detail.active_filter);
|
||||
const controls = detail.runtime_controls || {};
|
||||
if (detail.active_filter !== undefined || controls.active_filter !== undefined) {
|
||||
node.__hpsFilterStatuses = normalizeCyclerFilterStatuses(detail.active_filter ?? controls.active_filter);
|
||||
}
|
||||
if (detail.use_local_list !== undefined) {
|
||||
node.__hpsUseLocalList = !!detail.use_local_list;
|
||||
if (detail.use_local_list !== undefined || controls.use_local_list !== undefined) {
|
||||
node.__hpsUseLocalList = !!(detail.use_local_list ?? controls.use_local_list);
|
||||
}
|
||||
if (detail.settings_revision !== undefined) {
|
||||
node.__hpsSettingsRevision = parseSavedInt(detail.settings_revision, node.__hpsSettingsRevision ?? 0);
|
||||
if (detail.settings_revision !== undefined || controls.settings_revision !== undefined) {
|
||||
node.__hpsSettingsRevision = parseSavedInt(detail.settings_revision ?? controls.settings_revision, node.__hpsSettingsRevision ?? 0);
|
||||
}
|
||||
// Only apply visible runtime widgets when the backend explicitly returns them
|
||||
// as runtime_controls. This keeps restore coherent while avoiding prompt-time
|
||||
// stale values becoming authoritative.
|
||||
if (detail.runtime_controls) {
|
||||
node.__hpsApplyingRuntimePayload = true;
|
||||
try {
|
||||
if (controls.mode !== undefined) setWidgetValueIfDifferent(node, "mode", controls.mode);
|
||||
if (controls.change_every !== undefined) setWidgetValueIfDifferent(node, "change_every", controls.change_every);
|
||||
if (controls.start_checkpoint !== undefined && controls.start_checkpoint !== "") setWidgetValueIfDifferent(node, "start_checkpoint", controls.start_checkpoint);
|
||||
} finally {
|
||||
node.__hpsApplyingRuntimePayload = false;
|
||||
}
|
||||
}
|
||||
if (detail.ckpt_name_str) {
|
||||
node.__hpsCyclerCkptName = detail.ckpt_name_str;
|
||||
@@ -829,21 +843,42 @@ function hideSelectorWidget(node) {
|
||||
}
|
||||
function selectorRects(node) {
|
||||
const margin = 8;
|
||||
const gap = 6;
|
||||
const buttonY = 8;
|
||||
const buttonH = 24;
|
||||
const arrowW = 34;
|
||||
const downX = Math.max(430, (node.size?.[0] || 520) - margin - arrowW);
|
||||
const upX = downX - gap - arrowW;
|
||||
const refreshW = 108;
|
||||
const listOnlyW = 96;
|
||||
const pushX = margin + refreshW + gap + listOnlyW + gap;
|
||||
const pushW = Math.max(118, upX - gap - pushX);
|
||||
return {
|
||||
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 },
|
||||
refreshAll: { x: margin, y: buttonY, w: refreshW, h: buttonH },
|
||||
listOnly: { x: margin + refreshW + gap, y: buttonY, w: listOnlyW, h: buttonH },
|
||||
pushLocalList: { x: pushX, y: buttonY, w: pushW, h: buttonH },
|
||||
up: { x: upX, y: buttonY, w: arrowW, h: buttonH },
|
||||
down: { x: downX, y: buttonY, w: arrowW, h: buttonH },
|
||||
list: { x: margin, y: 104, w: node.size[0] - 16, h: ROW_H * SELECTOR_VISIBLE_ROWS },
|
||||
};
|
||||
}
|
||||
function selectorItems(node) { return node.__hpsItems || []; }
|
||||
function selectorSelected(node) { return selectorWidget(node)?.value || node.__hpsSelected || ""; }
|
||||
function selectorStatusFor(node, ckptName) {
|
||||
const item = selectorItems(node).find((entry) => entry.ckpt_name_str === ckptName);
|
||||
return item?.status || "none";
|
||||
}
|
||||
|
||||
function selectorTitleText(node, ckptName) {
|
||||
if (!ckptName) return "Checkpoint List Selector";
|
||||
const status = selectorStatusFor(node, ckptName);
|
||||
return `Selector : ${titleDisplayForCheckpoint(ckptName, status)}`;
|
||||
}
|
||||
|
||||
function setSelectorSelected(node, value) {
|
||||
const w = selectorWidget(node); if (w) w.value = value;
|
||||
node.__hpsSelected = value;
|
||||
node.title = value ? `Selector : ${value}` : "Checkpoint List Selector";
|
||||
node.title = selectorTitleText(node, value);
|
||||
}
|
||||
function selectorStatusText(result, prefix = "") {
|
||||
const s = result?.summary || {};
|
||||
@@ -1006,6 +1041,8 @@ async function refreshSelector(node, all = false) {
|
||||
const selected = selectorSelected(node);
|
||||
if (selected && !node.__hpsItems.find((x) => x.ckpt_name_str === selected) && node.__hpsItems.length) {
|
||||
setSelectorSelected(node, node.__hpsItems[0].ckpt_name_str);
|
||||
} else if (selectorSelected(node)) {
|
||||
node.title = selectorTitleText(node, selectorSelected(node));
|
||||
}
|
||||
|
||||
if (all) {
|
||||
@@ -1542,6 +1579,9 @@ api.addEventListener(STATUS_CHANGED_EVENT, ({ detail }) => {
|
||||
for (const item of selectorItems(node)) {
|
||||
if (item.ckpt_name_str === detail.ckpt_name_str) item.status = detail.status || "none";
|
||||
}
|
||||
if (selectorSelected(node) === detail.ckpt_name_str) {
|
||||
node.title = selectorTitleText(node, detail.ckpt_name_str);
|
||||
}
|
||||
}
|
||||
if (PREVIEW_CLASSES.has(node.type || node.comfyClass) && node.__hpsPreviewCkptName === detail.ckpt_name_str) {
|
||||
node.__hpsPreviewStatus = detail.status || "none";
|
||||
@@ -1553,7 +1593,12 @@ api.addEventListener(STATUS_CHANGED_EVENT, ({ detail }) => {
|
||||
patchCheckpointTitle(node, "Tagger", detail.ckpt_name_str, node.__hpsTaggerStatus);
|
||||
}
|
||||
if (isNodeClass(node, CYCLER_CLASS) && node.__hpsCyclerCkptName === detail.ckpt_name_str) {
|
||||
patchCheckpointTitle(node, "Cycler", detail.ckpt_name_str, detail.status || "none");
|
||||
node.__hpsCyclerStatusValue = detail.status || "none";
|
||||
const oldTitle = String(node.title || "");
|
||||
const idx = oldTitle.indexOf(detail.ckpt_name_str);
|
||||
if (idx >= 0) {
|
||||
node.title = oldTitle.slice(0, idx) + titleDisplayForCheckpoint(detail.ckpt_name_str, detail.status || "none") + oldTitle.slice(idx + detail.ckpt_name_str.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
scheduleSelectorGlobalRefresh();
|
||||
@@ -1605,6 +1650,49 @@ async function pushCyclerFilter(node) {
|
||||
const result = await response.json();
|
||||
if (result?.ok) applyCyclerStatePayload(node, result);
|
||||
}
|
||||
function schedulePushCyclerRuntimeControls(node) {
|
||||
if (!node || node.__hpsApplyingRuntimePayload) return;
|
||||
clearTimeout(node.__hpsRuntimePushTimer);
|
||||
node.__hpsRuntimePushTimer = setTimeout(() => pushCyclerRuntimeControls(node), 120);
|
||||
}
|
||||
|
||||
async function pushCyclerRuntimeControls(node) {
|
||||
if (!node || node.__hpsApplyingRuntimePayload) return;
|
||||
syncCyclerSettingsWidgets(node);
|
||||
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/set_runtime_controls`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(tabPayload({
|
||||
node_id: node.id,
|
||||
settings_revision: node.__hpsSettingsRevision ?? 0,
|
||||
...getCyclerRuntimeControls(node),
|
||||
})),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result?.ok) applyCyclerStatePayload(node, result);
|
||||
}
|
||||
|
||||
function installCyclerRuntimeControlHooks(node) {
|
||||
if (!node) return;
|
||||
let installedAny = false;
|
||||
for (const name of ["start_checkpoint", "mode", "change_every"]) {
|
||||
const widget = getWidget(node, name);
|
||||
if (!widget || widget.__hpsRuntimeHookInstalled) continue;
|
||||
widget.__hpsRuntimeHookInstalled = true;
|
||||
installedAny = true;
|
||||
const originalCallback = widget.callback;
|
||||
widget.callback = function (...args) {
|
||||
const r = originalCallback ? originalCallback.apply(this, args) : undefined;
|
||||
if (!node.__hpsApplyingRuntimePayload) {
|
||||
bumpCyclerSettingsRevision(node);
|
||||
schedulePushCyclerRuntimeControls(node);
|
||||
}
|
||||
return r;
|
||||
};
|
||||
}
|
||||
if (installedAny) node.__hpsRuntimeHooksInstalled = true;
|
||||
}
|
||||
|
||||
async function clearLocalList(node) {
|
||||
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/cycler/clear_local_list`, {
|
||||
method: "POST",
|
||||
@@ -1635,6 +1723,7 @@ function setupCyclerNode(nodeType) {
|
||||
const r = origCreated ? origCreated.apply(this, arguments) : undefined;
|
||||
ensureSize(this, 560, 260);
|
||||
restoreCyclerSettingsFromWidgets(this);
|
||||
installCyclerRuntimeControlHooks(this);
|
||||
setTimeout(() => restoreNodeStateFromBackend(this, CYCLER_CLASS), 0);
|
||||
return r;
|
||||
};
|
||||
@@ -1642,6 +1731,7 @@ function setupCyclerNode(nodeType) {
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
const r = origConfigure ? origConfigure.apply(this, arguments) : undefined;
|
||||
restoreCyclerSettingsFromWidgets(this);
|
||||
installCyclerRuntimeControlHooks(this);
|
||||
setTimeout(() => restoreNodeStateFromBackend(this, CYCLER_CLASS), 0);
|
||||
return r;
|
||||
};
|
||||
@@ -1650,6 +1740,13 @@ function setupCyclerNode(nodeType) {
|
||||
if (origDraw) origDraw.apply(this, arguments);
|
||||
ensureHiddenTabIdWidget(this);
|
||||
restoreCyclerSettingsFromWidgets(this);
|
||||
installCyclerRuntimeControlHooks(this);
|
||||
const runtimeKey = JSON.stringify(getCyclerRuntimeControls(this));
|
||||
if (!this.__hpsApplyingRuntimePayload && this.__hpsLastRuntimeControlsKey && this.__hpsLastRuntimeControlsKey !== runtimeKey) {
|
||||
bumpCyclerSettingsRevision(this);
|
||||
schedulePushCyclerRuntimeControls(this);
|
||||
}
|
||||
this.__hpsLastRuntimeControlsKey = runtimeKey;
|
||||
if (hpsNodeCollapsed(this)) return;
|
||||
const r = cyclerRects(this);
|
||||
const active = cyclerActiveFilter(this);
|
||||
|
||||
Reference in New Issue
Block a user