From 8b278c9515ebc76314656db80bdbeb9fb99c675d Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:13:26 +0300 Subject: [PATCH] Movie Editor: Restart ComfyUI button FunPack menu -> 'Restart ComfyUI' relaunches the server process in place (POST /funpack/movie/api/restart -> os.execv, mirroring ComfyUI-Manager's reboot incl. the comfy-cli session case; no Manager dependency). Restart is deferred 0.7s so the 200 flushes first. UI shows a blocking spinner overlay, polls /health, and auto-reloads when ComfyUI is back. Confirms first. Co-Authored-By: Claude Opus 4.8 --- movie_editor/frontend/api.js | 1 + movie_editor/frontend/menubar.js | 21 ++++++++++++++++++ movie_editor/frontend/styles.css | 10 +++++++++ movie_editor/server.py | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/movie_editor/frontend/api.js b/movie_editor/frontend/api.js index 67920c6a..d411cfbf 100644 --- a/movie_editor/frontend/api.js +++ b/movie_editor/frontend/api.js @@ -45,6 +45,7 @@ getModels: () => j("GET", API("/models")), saveModels: (data) => j("PUT", API("/models"), data), refreshModels: () => j("POST", API("/models/refresh")), + restart: () => j("POST", API("/restart")), // generate generate: (id, onlyScene) => j("POST", API(`/projects/${id}/generate`), { only_scene: onlyScene || null }), diff --git a/movie_editor/frontend/menubar.js b/movie_editor/frontend/menubar.js index 306967fb..3eb17f97 100644 --- a/movie_editor/frontend/menubar.js +++ b/movie_editor/frontend/menubar.js @@ -9,6 +9,26 @@ function sel() { return S.get().selectedSceneId; } function hasProject() { return !!S.get().project; } + async function restartComfy() { + if (!confirm("Restart ComfyUI now?\n\nThe server will be down for ~10–40s and any running generation will be lost. This page reloads automatically when it's back.")) return; + const ov = el("div", "restart-overlay"); + const card = el("div", "restart-card"); + card.append(el("div", "restart-spin")); + const msg = el("div", "restart-msg", "Restarting ComfyUI…\nThis page will reload when it's back."); + card.append(msg); ov.append(card); document.body.append(ov); + try { await window.MovieEditorAPI.restart(); } catch (_) { /* connection drops as it execv's — expected */ } + const start = Date.now(); + const tick = async () => { + try { + const h = await window.MovieEditorAPI.health(); + if (h && h.ok) { location.reload(); return; } + } catch (_) { /* still down */ } + if (Date.now() - start > 90000) msg.textContent = "Still waiting on ComfyUI…\nIt may need a manual restart — check the console."; + setTimeout(tick, 2000); + }; + setTimeout(tick, 3500); // give it a moment to actually go down first + } + function menuSpec() { const st = S.get(); const recent = (st.projects || []).slice(0, 8).map((p) => ({ @@ -53,6 +73,7 @@ FunPack: [ { menulabel: "Libraries (in ComfyUI Studio)" }, { label: "Open ComfyUI", hint: "↗", action: () => window.open("/", "_blank") }, + { label: "Restart ComfyUI", hint: "⟳", danger: true, action: restartComfy }, { sep: true }, { label: st.health?.reference_loaded ? "Pipeline reference: loaded" : "Pipeline reference: missing", disabled: true }, { label: `Configured nodes: ${st.health?.configured_slots ?? 0}`, disabled: true }, diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index dcf0fa36..72efc238 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -293,6 +293,16 @@ input[type=number] { font-family: var(--font-mono); } .wire-arrow { color: var(--teal); } .wire-select { flex: 1; } +/* restart overlay */ +.restart-overlay { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; + background: color-mix(in srgb, var(--ink-0) 88%, transparent); backdrop-filter: blur(3px); } +.restart-card { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 30px 40px; + background: var(--ink-1); border: 1px solid var(--line); border-radius: 14px; } +.restart-msg { color: var(--muted); text-align: center; white-space: pre-line; font-size: 13px; line-height: 1.5; } +.restart-spin { width: 30px; height: 30px; border-radius: 50%; border: 3px solid var(--line); + border-top-color: var(--amber); animation: spin .8s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } + /* staggered zone reveal on load */ .zone { animation: rise .5s both; } .media { animation-delay: .02s; } diff --git a/movie_editor/server.py b/movie_editor/server.py index 54f4a9dc..6f69ae98 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -25,6 +25,37 @@ from .backend.timeline import Project, build_combined_prompt UI_PREFIX = "/funpack/movie" +def _restart_comfy() -> None: + """Relaunch the ComfyUI process in place. Mirrors ComfyUI-Manager's reboot so it + works the same whether launched directly, as a module, or via comfy-cli.""" + import os + import sys + try: + sys.stdout.close_log() # type: ignore[attr-defined] # Manager's tee logger, if present + except Exception: + pass + # comfy-cli watches for a .reboot file and relaunches us itself. + if "__COMFY_CLI_SESSION__" in os.environ: + try: + open(os.environ["__COMFY_CLI_SESSION__"] + ".reboot", "w").close() + except Exception: + pass + print("\n[FunPack] Restarting ComfyUI...\n", flush=True) + os._exit(0) + sys_argv = sys.argv.copy() + if "--windows-standalone-build" in sys_argv: + sys_argv.remove("--windows-standalone-build") + if sys_argv and sys_argv[0].endswith("__main__.py"): # python -m comfy + module_name = os.path.basename(os.path.dirname(sys_argv[0])) + cmds = [sys.executable, "-m", module_name] + sys_argv[1:] + elif sys.platform.startswith("win32"): + cmds = ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:] + else: + cmds = [sys.executable] + sys_argv + print(f"\n[FunPack] Restarting ComfyUI... {cmds}\n", flush=True) + os.execv(sys.executable, cmds) + + # ── helpers ────────────────────────────────────────────────────────────────── def _media_from_history(hist_entry: dict) -> list[dict]: @@ -267,6 +298,13 @@ if web is not None and PromptServer is not None: raise web.HTTPNotFound(reason="Unknown node class") return web.json_response(spec) + @routes.post(UI_PREFIX + "/api/restart") + async def _restart(_req): + import asyncio + # defer so this 200 flushes to the browser before the process is replaced + asyncio.get_event_loop().call_later(0.7, _restart_comfy) + return web.json_response({"restarting": True}) + @routes.post(UI_PREFIX + "/api/models/refresh") async def _models_refresh(_req): try: