From 3ab367fb51e3c33a47f14540a7800a7fb164ac5b Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:39:23 +0300 Subject: [PATCH] Movie Editor: ComfyUI backend log viewer Add a 'Log' button in the menubar (next to Saved) that opens a collapsible panel streaming ComfyUI's real backend log. The bridge tees stdout/stderr into a ring buffer (writes still pass through to the terminal; captures print + logging); /api/log returns the tail. Panel polls every 1.5s while open, auto-scrolls (pauses if you scroll up), with Copy. Far more informative than our wrapped error messages. Co-Authored-By: Claude Opus 4.8 --- movie_editor/backend/bridge.py | 59 ++++++++++++++++++++++++++++++++ movie_editor/frontend/api.js | 1 + movie_editor/frontend/index.html | 1 + movie_editor/frontend/log.js | 45 ++++++++++++++++++++++++ movie_editor/frontend/styles.css | 14 ++++++++ movie_editor/server.py | 9 +++++ 6 files changed, 129 insertions(+) create mode 100644 movie_editor/frontend/log.js diff --git a/movie_editor/backend/bridge.py b/movie_editor/backend/bridge.py index 44872434..1003ee0b 100644 --- a/movie_editor/backend/bridge.py +++ b/movie_editor/backend/bridge.py @@ -53,6 +53,65 @@ def parse_timeline_verbatim(prompt: str) -> dict: return split_timeline_verbatim(str(prompt or "")) +# ── ComfyUI log capture ─────────────────────────────────────────────────────── +# Tee stdout/stderr into a ring buffer so the editor can show ComfyUI's real backend +# log (writes still pass through to the terminal). Captures print() and logging. +import collections as _collections +import threading as _threading + +_LOG = _collections.deque(maxlen=5000) +_LOG_LOCK = _threading.Lock() +_log_installed = False + + +class _Tee: + def __init__(self, orig): + self._orig = orig + self._buf = "" + + def write(self, s): + try: + self._orig.write(s) + except Exception: + pass + try: + with _LOG_LOCK: + self._buf += s + while "\n" in self._buf: + line, self._buf = self._buf.split("\n", 1) + _LOG.append(line) + except Exception: + pass + return len(s) if isinstance(s, str) else 0 + + def flush(self): + try: + self._orig.flush() + except Exception: + pass + + def __getattr__(self, name): + return getattr(self._orig, name) + + +def install_log_capture(): + global _log_installed + if _log_installed: + return + import sys + try: + sys.stdout = _Tee(sys.stdout) + sys.stderr = _Tee(sys.stderr) + _log_installed = True + except Exception: + pass + + +def recent_log(limit: int = 500) -> list: + with _LOG_LOCK: + return list(_LOG)[-int(limit):] + + def rating_labels() -> dict: """FunPack Studio V2 rating labels (for the Scene rating dropdown).""" try: diff --git a/movie_editor/frontend/api.js b/movie_editor/frontend/api.js index 1a4d6444..b231657f 100644 --- a/movie_editor/frontend/api.js +++ b/movie_editor/frontend/api.js @@ -80,6 +80,7 @@ status: (id, promptId) => j("GET", API(`/projects/${id}/status/${promptId}`)), progress: () => j("GET", API("/progress")), ratingLabels: () => j("GET", API("/rating-labels")), + log: (limit) => j("GET", API("/log" + (limit ? `?limit=${limit}` : ""))), interrupt: () => j("POST", API("/interrupt")), renderFinal: (id, clips) => j("POST", API(`/projects/${id}/render`), { clips }), resultUrl: (id, m) => diff --git a/movie_editor/frontend/index.html b/movie_editor/frontend/index.html index 0937c134..962c1c15 100644 --- a/movie_editor/frontend/index.html +++ b/movie_editor/frontend/index.html @@ -89,6 +89,7 @@ + diff --git a/movie_editor/frontend/log.js b/movie_editor/frontend/log.js new file mode 100644 index 00000000..63d46918 --- /dev/null +++ b/movie_editor/frontend/log.js @@ -0,0 +1,45 @@ +// Menubar "Log" button + a collapsible panel showing ComfyUI's real backend log +// (tee'd stdout/stderr from the server). Polls only while open. +(function () { + const API = window.MovieEditorAPI; + const right = document.querySelector(".menubar-right"); + if (!right) return; + + const btn = document.createElement("button"); + btn.className = "log-btn"; btn.textContent = "Log"; btn.title = "Show the ComfyUI backend log"; + right.insertBefore(btn, right.firstChild); + + const panel = document.createElement("div"); + panel.className = "log-panel"; panel.hidden = true; + const head = document.createElement("div"); head.className = "log-head"; + const title = document.createElement("span"); title.textContent = "ComfyUI log"; + const actions = document.createElement("div"); actions.className = "log-actions"; + head.append(title, actions); + const bodyEl = document.createElement("pre"); bodyEl.className = "log-body"; + panel.append(head, bodyEl); + document.body.append(panel); + + let timer = null, autoscroll = true; + + async function refresh() { + try { + const r = await API.log(800); + bodyEl.textContent = (r.lines || []).join("\n"); + if (autoscroll) bodyEl.scrollTop = bodyEl.scrollHeight; + } catch (e) { + bodyEl.textContent = "(log unavailable: " + e.message + ")"; + } + } + bodyEl.addEventListener("scroll", () => { + autoscroll = bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight < 40; + }); + + function open() { panel.hidden = false; btn.classList.add("active"); autoscroll = true; refresh(); timer = setInterval(refresh, 1500); } + function close() { panel.hidden = true; btn.classList.remove("active"); clearInterval(timer); timer = null; } + btn.onclick = () => (panel.hidden ? open() : close()); + + const copy = document.createElement("button"); copy.className = "btn ghost tiny"; copy.textContent = "Copy"; + copy.onclick = () => { try { navigator.clipboard.writeText(bodyEl.textContent); copy.textContent = "Copied"; setTimeout(() => (copy.textContent = "Copy"), 1200); } catch (_) {} }; + const x = document.createElement("button"); x.className = "btn ghost tiny"; x.textContent = "✕"; x.onclick = close; + actions.append(copy, x); +})(); diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index e71ae418..1ba5f046 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -108,6 +108,20 @@ button, input, select, textarea { font-family: inherit; color: var(--text); } .save-chip { border-color: transparent; color: var(--faint); } .save-chip.dirty { color: var(--amber); } +/* backend log viewer */ +.log-btn { background: var(--ink-2); border: 1px solid var(--line); border-radius: 20px; color: var(--muted); + font-family: var(--font-mono); font-size: 11px; letter-spacing: .5px; padding: 3px 11px; cursor: pointer; } +.log-btn:hover { color: var(--amber-2); background: var(--ink-3); } +.log-btn.active { color: var(--amber-2); border-color: color-mix(in srgb, var(--amber) 45%, var(--line)); } +.log-panel { position: fixed; z-index: 9000; right: 14px; top: 48px; width: min(720px, 88vw); height: min(420px, 60vh); + background: #0c0b09f2; border: 1px solid var(--line); border-radius: 10px; display: flex; flex-direction: column; + box-shadow: 0 16px 40px -12px #000; overflow: hidden; } +.log-head { display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border-bottom: 1px solid var(--line); + font-family: var(--font-mono); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--amber); } +.log-actions { display: flex; gap: 6px; } +.log-body { flex: 1; margin: 0; padding: 8px 10px; overflow: auto; white-space: pre-wrap; word-break: break-word; + font-family: var(--font-mono); font-size: 11px; line-height: 1.45; color: var(--muted); background: #060606; } + /* ───────────────────────── WORKSPACE GRID ───────────────────────── */ .workspace { height: calc(100% - var(--menubar-h)); display: flex; flex-direction: column; } .row-main { flex: 1; display: flex; min-height: 0; } diff --git a/movie_editor/server.py b/movie_editor/server.py index 6b071781..838f5307 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -188,6 +188,7 @@ def _serve_static(tail: str) -> "web.Response": if web is not None and PromptServer is not None: routes = PromptServer.instance.routes config.ensure_dirs() + bridge.install_log_capture() # tee ComfyUI stdout/stderr for the Log viewer # --- API: projects --- @routes.get(UI_PREFIX + "/api/health") @@ -522,6 +523,14 @@ if web is not None and PromptServer is not None: async def _progress(_req): return web.json_response(bridge.current_progress()) + @routes.get(UI_PREFIX + "/api/log") + async def _log(req): + try: + limit = int(req.query.get("limit", "600")) + except (TypeError, ValueError): + limit = 600 + return web.json_response({"lines": bridge.recent_log(limit)}) + @routes.post(UI_PREFIX + "/api/interrupt") async def _interrupt(_req): try: