Files
DemonGatanjieu-Anomalous_Mo…/api/usage_ledger.py
T
DemonGatanjieuandClaude Opus 5.5 bf9c46d728 Tidy: a run record, careful judging and a node pack page
ComfyUI keeps no lasting note of what a run used, so AMB now notes each
queued run's node types and model files (on_prompt hook, user/anomalous/
usage_ledger.json). "Not used lately" reads it with the outputs, now
sounds too, and the saved workflows, and judges nothing that came in the
last 30 days; while the record is younger than the group shown, the page
says so.

Node packs get their own page: installed for how long, whether they add
to the page, which packs take what only they make, and a button opening
ComfyUI Manager's node list searched for the pack. Packs without nodes
are listed apart, as nothing can tell whether they are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:18:37 +08:00

125 lines
4.6 KiB
Python

"""What each run used, kept from the day this started (the Tidy tab's "Not used lately").
ComfyUI keeps no lasting note of which nodes and models a run used: its history is gone after a
restart and its log names neither. So every prompt queued here passes `note_prompt` (ComfyUI's
on_prompt hook) on its way in, whether it makes an image, a sound, a video or nothing kept: its
node types and model files go into user/anomalous/usage_ledger.json, with when each was last
used and how often ({started, types: {type: [last, runs]}, models: {ref: [last, runs]}}).
It stays on this computer, is saved a few seconds after a run, and noting never stops a run.
"""
import json
import logging
import os
import threading
import time
import folder_paths
from .image_search import MODEL_FILE
from .path_utils import atomic_write_json
SAVE_DELAY = 5.0 # seconds after a run: several runs in a row are saved once
MAX_NAMES = 20_000 # per kind; the longest unused go first
_lock = threading.Lock()
_ledger = None
_timer = None
def ledger_path():
user_dir = folder_paths.get_user_directory() if hasattr(folder_paths, "get_user_directory") else None
if not user_dir:
user_dir = os.path.join(folder_paths.base_path, "user")
directory = os.path.join(user_dir, "anomalous")
os.makedirs(directory, exist_ok=True)
return os.path.join(directory, "usage_ledger.json")
def _load():
global _ledger
if _ledger is None:
data = {}
try:
with open(ledger_path(), encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
pass
if not isinstance(data, dict):
data = {}
_ledger = {
"started": data.get("started") if isinstance(data.get("started"), (int, float)) else None,
"types": data.get("types") if isinstance(data.get("types"), dict) else {},
"models": data.get("models") if isinstance(data.get("models"), dict) else {},
}
return _ledger
def _save():
global _timer
with _lock:
_timer = None
ledger = _load()
for kind in ("types", "models"):
if len(ledger[kind]) > MAX_NAMES:
kept = sorted(ledger[kind].items(), key=lambda entry: entry[1][0], reverse=True)[:MAX_NAMES]
ledger[kind] = dict(kept)
snapshot = {"started": ledger["started"], "types": dict(ledger["types"]), "models": dict(ledger["models"])}
try:
atomic_write_json(ledger_path(), snapshot)
except (OSError, ValueError) as error:
logging.warning("[Anomalous Browser] Could not save the usage record: %s", error)
def prompt_uses(prompt):
"""(node types, model file references) a queued prompt names."""
types, models = set(), set()
for node in prompt.values():
if not isinstance(node, dict):
continue
if isinstance(node.get("class_type"), str) and node["class_type"]:
types.add(node["class_type"])
for value in (node.get("inputs") or {}).values():
if isinstance(value, dict) and isinstance(value.get("lora"), str):
value = value["lora"] # a row of rgthree's Power Lora Loader
if isinstance(value, str) and MODEL_FILE.search(value):
models.add(value.replace("\\", "/").strip().lower())
return types, models
def note(prompt, when=None):
"""Note what `prompt` (ComfyUI's API prompt) uses, as of `when`; saved shortly after."""
global _timer
if not isinstance(prompt, dict):
return
when = time.time() if when is None else when
types, models = prompt_uses(prompt)
with _lock:
ledger = _load()
if ledger["started"] is None:
ledger["started"] = when
for kind, names in (("types", types), ("models", models)):
for name in names:
runs = ledger[kind].get(name, [0, 0])[1]
ledger[kind][name] = [when, runs + 1]
if _timer is None:
_timer = threading.Timer(SAVE_DELAY, _save)
_timer.daemon = True
_timer.start()
def note_prompt(json_data):
"""ComfyUI's on_prompt hook: notes the run and hands the request on unchanged."""
try:
note((json_data or {}).get("prompt"))
except Exception as error: # noqa: BLE001 - a note must never stop a run
logging.warning("[Anomalous Browser] Could not note a run's nodes and models: %s", error)
return json_data
def read():
"""{started, types, models} as noted so far (a copy)."""
with _lock:
ledger = _load()
return {"started": ledger["started"], "types": dict(ledger["types"]), "models": dict(ledger["models"])}