diff --git a/README.md b/README.md index d30f6c6..4611191 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ python main.py --listen --port 8188 --enable-compress-response-body negligible compared to the bytes saved over the network. ## Notes +- **Loading status:** instead of ComfyUI's silent "Comfy" splash, a small status + line shows whether it's *serving from cache* or *building* (with node count + + elapsed time), so a long rebuild isn't a black screen with no feedback. It + removes itself once the app is ready. Status is also at `GET /tenaciousload/status`. - The disk cache lives in `./cache/` (git-ignored). Delete it, or use the refresh button, to force a rebuild. - An nginx reverse proxy can cache `object_info` at the HTTP layer too, but this diff --git a/__init__.py b/__init__.py index 124a3ef..f909226 100644 --- a/__init__.py +++ b/__init__.py @@ -412,6 +412,9 @@ def _check_node_signature(): _node_info_fn = None _node_info_resolved = False +# Live build progress, surfaced at /tenaciousload/status for the loading overlay. +_build_state = {"building": False, "started": 0.0, "done": 0, "total": 0, "last_ms": 0, "last_bytes": 0} + def _resolve_node_info_fn(): """Pull ComfyUI's own `node_info` closure off the /object_info route, so the @@ -441,14 +444,23 @@ def _resolve_node_info_fn(): def _build_object_info_bytes(): """Replicate ComfyUI's object_info build. Runs in a worker thread.""" import nodes + keys = list(nodes.NODE_CLASS_MAPPINGS.keys()) + _build_state.update(building=True, started=time.time(), done=0, total=len(keys)) out = {} - with folder_paths.cache_helper: - for x in list(nodes.NODE_CLASS_MAPPINGS.keys()): - try: - out[x] = _node_info_fn(x) - except Exception: # pragma: no cover - log.error("Tenaciousload: node_info failed for '%s'", x, exc_info=True) - return json.dumps(out).encode("utf-8") + try: + with folder_paths.cache_helper: + for i, x in enumerate(keys): + try: + out[x] = _node_info_fn(x) + except Exception: # pragma: no cover + log.error("Tenaciousload: node_info failed for '%s'", x, exc_info=True) + _build_state["done"] = i + 1 + raw = json.dumps(out).encode("utf-8") + _build_state["last_bytes"] = len(raw) + return raw + finally: + _build_state["building"] = False + _build_state["last_ms"] = int((time.time() - _build_state["started"]) * 1000) async def _build_object_info_off_loop(): @@ -533,6 +545,22 @@ _install_middleware() # --------------------------------------------------------------------------- # # Refresh API # --------------------------------------------------------------------------- # +@PromptServer.instance.routes.get("/tenaciousload/status") +async def _status(request): + st = { + "building": _build_state["building"], + "done": _build_state["done"], + "total": _build_state["total"], + "last_ms": _build_state["last_ms"], + "cached": _mem["raw"] is not None, + "cache_bytes": len(_mem["raw"]) if _mem["raw"] else 0, + "gz_bytes": len(_mem["gz"]) if _mem["gz"] else 0, + } + if _build_state["building"] and _build_state["started"]: + st["elapsed"] = round(time.time() - _build_state["started"], 1) + return web.json_response(st) + + @PromptServer.instance.routes.post("/tenaciousload/refresh") async def _refresh(request): try: diff --git a/web/loading-status.js b/web/loading-status.js new file mode 100644 index 0000000..c9e933e --- /dev/null +++ b/web/loading-status.js @@ -0,0 +1,100 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +// Shows a small status line on ComfyUI's "Comfy" loading splash so a long +// object_info build (or cache load) isn't a silent black screen. + +const START = performance.now(); +let overlay = null; +let ready = false; +let lastDone = -1; +let stallTicks = 0; + +function injectStyle() { + if (document.getElementById("tl-loading-style")) return; + const s = document.createElement("style"); + s.id = "tl-loading-style"; + s.textContent = ` +@keyframes tl-indet { 0%{left:-35%;width:35%} 50%{left:40%;width:45%} 100%{left:100%;width:35%} } +#tl-loading{position:fixed;left:0;right:0;top:57%;margin:0 auto;z-index:99999;width:340px; + text-align:center;font-family:system-ui,-apple-system,sans-serif;color:#a1a1aa;pointer-events:none; + transition:opacity .35s ease} +#tl-loading .tl-msg{font-size:12.5px;letter-spacing:.3px;line-height:1.5} +#tl-loading .tl-sub{font-size:11px;color:#71717a;margin-top:2px} +#tl-loading .tl-track{position:relative;margin:11px auto 0;width:240px;height:4px; + background:#27272a;border-radius:3px;overflow:hidden} +#tl-loading .tl-bar{position:absolute;top:0;height:100%;border-radius:3px; + background:linear-gradient(90deg,#60a5fa,#a78bfa,#ff9cf9)} +#tl-loading .tl-bar.indet{animation:tl-indet 1.15s ease-in-out infinite}`; + document.head.appendChild(s); +} + +function ensureOverlay() { + if (overlay || ready) return; + injectStyle(); + overlay = document.createElement("div"); + overlay.id = "tl-loading"; + overlay.innerHTML = + '
Tenaciousload
' + + '
' + + '
'; + (document.body || document.documentElement).appendChild(overlay); +} + +function fmt(s) { + s = Math.max(0, Math.round(s)); + const m = Math.floor(s / 60); + return m ? `${m}:${String(s % 60).padStart(2, "0")}` : `${s}s`; +} +const mb = (b) => (b / 1048576).toFixed(1) + " MB"; + +async function tick() { + if (ready) return; + if (!overlay && performance.now() - START > 700) ensureOverlay(); + if (!overlay) return; + + let st = null; + try { st = await (await api.fetchApi("/tenaciousload/status")).json(); } catch (e) { /* ignore */ } + const msg = overlay.querySelector("#tl-msg"); + const sub = overlay.querySelector("#tl-sub"); + const bar = overlay.querySelector("#tl-bar"); + if (!msg) return; + + if (st && st.building) { + const pct = st.total ? Math.round((100 * st.done) / st.total) : 0; + msg.textContent = `Building node definitions — ${st.done}/${st.total} (${pct}%)`; + // detect a stall (the model-folder walk) and explain it + stallTicks = st.done === lastDone ? stallTicks + 1 : 0; + lastDone = st.done; + sub.textContent = stallTicks >= 3 + ? `scanning model folders over the network… · ${fmt(st.elapsed || 0)}` + : `first build / refresh · ${fmt(st.elapsed || 0)}`; + bar.classList.add("indet"); + bar.style.width = "35%"; + } else { + msg.textContent = "Loading node definitions…"; + sub.textContent = st && st.cached ? `from cache · ${mb(st.gz_bytes || st.cache_bytes || 0)} gzipped` : ""; + bar.classList.remove("indet"); + bar.style.left = "0"; + bar.style.width = "90%"; + } +} + +const poll = setInterval(tick, 600); +tick(); + +app.registerExtension({ + name: "Tenaciousload.LoadingStatus", + // setup() fires once the app is ready (object_info loaded, nodes registered). + async setup() { + ready = true; + clearInterval(poll); + if (overlay) { + overlay.style.opacity = "0"; + setTimeout(() => overlay && overlay.remove(), 350); + } + }, +}); + +// hard safety: never let the overlay linger +setTimeout(() => { if (overlay && !ready) { clearInterval(poll); overlay.remove(); } }, 15 * 60 * 1000);