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:
co-authored by
Claude Opus 4.8
parent
50ef7edc57
commit
3ab367fb51
@@ -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:
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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; }
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user