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 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-07 14:39:23 +03:00
co-authored by Claude Opus 4.8
parent 50ef7edc57
commit 3ab367fb51
6 changed files with 129 additions and 0 deletions
+59
View File
@@ -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:
+1
View File
@@ -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) =>
+1
View File
@@ -89,6 +89,7 @@
<script src="models.js"></script>
<script src="menubar.js"></script>
<script src="mediabrowser.js"></script>
<script src="log.js"></script>
<script src="player.js"></script>
<script src="sampler_panel.js"></script>
<script src="inspector.js"></script>
+45
View File
@@ -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);
})();
+14
View File
@@ -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; }
+9
View File
@@ -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: