Files
olivv-cs-ComfyUI-FunPack/hub.py
T
digital-garbage 9791cb8ccd Add FunPack Easy Gen (simplified single-scene UI) and a /funpack Hub
Easy Gen is a new Fooocus-style frontend at /funpack/easy — big preview,
one prompt box, Upload, Generate, and an Advanced settings panel — built
entirely on the Movie Editor's existing project format and API (no new
backend endpoints), so projects stay interchangeable between the two UIs.

The Hub at /funpack links to both UIs and surfaces version/branch/commit
plus an Update/Restart action reusing the Editor's git_update.py flow.

Startup now prints full host:port URLs for all three (Hub, Editor, Easy
Gen) via a new config.comfy_display_url() helper.

Also: settings_window.js's About section now labels itself via
window.FunPackAppName instead of hardcoding "Cutting Room", and Easy
Gen's boot-error banner accumulates distinct errors instead of
overwriting, so a cascading failure (e.g. dom.js not loading) surfaces
its root cause instead of just the last script in load order.
2026-07-23 16:37:26 +03:00

254 lines
11 KiB
Python

"""FunPack Hub — a small landing page at /funpack/ linking to the two FunPack
UIs (Movie Editor "Cutting Room" and "Easy Gen"), plus an About block (version/
branch/commit) and an Update/Restart action. Talks to the Movie Editor's
existing /funpack/movie/api/{health,git/status,git/update,restart} endpoints —
no new backend API, same git_update.py logic the Editor's own "Updates &
ComfyUI" settings section uses. Kept as a single self-contained inline page
(no shared JS files) since its surface is a handful of fetch calls, much
smaller than pulling in api.js/dom.js/git_actions.js/restart_ui.js would need.
"""
from __future__ import annotations
try:
from aiohttp import web
from server import PromptServer
except Exception: # pragma: no cover - only available inside ComfyUI
web = None
PromptServer = None
from .movie_editor.backend import config as movie_config
HUB_PREFIX = "/funpack"
_PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FunPack</title>
<style>
:root {
--ink-0: #0c0b09; --ink-1: #131210; --ink-2: #1a1814; --line: #2a261e;
--text: #ece7db; --muted: #918a7a; --faint: #645e51;
--amber: #f3a93c; --amber-2: #ffc36b; --teal: #57d0c6; --danger: #e8607a; --good: #79c479;
--radius: 12px; --shadow: 0 10px 30px -12px rgba(0,0,0,.7);
}
* { box-sizing: border-box; }
html, body {
margin: 0; min-height: 100%; background: var(--ink-0); color: var(--text);
font-family: "IBM Plex Sans", system-ui, sans-serif;
display: flex; align-items: center; justify-content: center; padding: 32px 16px;
}
.hub { text-align: center; max-width: 640px; width: 100%; }
.brand { font-weight: 800; font-size: 28px; letter-spacing: .2px; margin-bottom: 6px; }
.brand-mark { color: var(--amber); margin-right: 8px; }
.lead { color: var(--muted); margin: 0 0 20px; font-size: 14px; }
.about {
display: inline-flex; align-items: center; gap: 14px; flex-wrap: wrap; justify-content: center;
background: var(--ink-1); border: 1px solid var(--line); border-radius: var(--radius);
padding: 10px 16px; margin-bottom: 28px; font-size: 12.5px; color: var(--muted);
font-family: "IBM Plex Mono", ui-monospace, monospace;
}
.about .led { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); display: inline-block; margin-right: 5px; }
.about .led.ok { background: var(--good); box-shadow: 0 0 8px var(--good); }
.about .led.bad { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.about b { color: var(--text); font-weight: 600; }
.about .sep { color: var(--line); }
.update-row { margin-bottom: 28px; display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; }
.btn {
background: var(--ink-2); border: 1px solid var(--line); border-radius: 7px; padding: 7px 14px;
cursor: pointer; font-size: 13px; color: var(--text); transition: border-color .12s, background .12s;
}
.btn:hover:not(:disabled) { border-color: var(--amber); }
.btn:disabled { opacity: .45; cursor: default; }
.btn.primary { background: linear-gradient(#f3a93c, #e0972a); color: #1a1206; border-color: #f3a93c; font-weight: 700; }
.btn.primary:hover:not(:disabled) { filter: brightness(1.08); }
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover:not(:disabled) { color: var(--amber-2); background: var(--ink-3); }
.update-hint { color: var(--faint); font-size: 12px; }
.cards { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; }
a.card {
display: block; width: 260px; padding: 22px 20px; text-align: left;
background: var(--ink-1); border: 1px solid var(--line); border-radius: var(--radius);
text-decoration: none; color: inherit; box-shadow: var(--shadow);
transition: border-color .12s, transform .12s;
}
a.card:hover { border-color: var(--amber); transform: translateY(-2px); }
.card-title { font-weight: 700; font-size: 17px; margin-bottom: 6px; }
.card-title.easy { color: var(--amber-2); }
.card-title.editor { color: var(--teal); }
.card-sub { color: var(--faint); font-size: 12.5px; line-height: 1.5; }
.restart-overlay {
position: fixed; inset: 0; z-index: 500; background: rgba(6,5,4,.75); backdrop-filter: blur(3px);
display: flex; align-items: center; justify-content: center;
}
.restart-card {
background: #16140f; border: 1px solid var(--line); border-radius: 14px; padding: 26px 30px;
box-shadow: var(--shadow); text-align: center; max-width: 360px;
}
.restart-spin {
width: 26px; height: 26px; margin: 0 auto 14px; border-radius: 50%;
border: 3px solid var(--line); border-top-color: var(--amber); animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.restart-msg { color: var(--muted); font-size: 13px; white-space: pre-wrap; }
</style>
</head>
<body>
<div class="hub">
<div class="brand"><span class="brand-mark">◉</span>FunPack</div>
<p class="lead">Pick a UI.</p>
<div class="about" id="about">Loading…</div>
<div class="update-row" id="update-row"></div>
<div class="cards">
<a class="card" href="/funpack/easy/">
<div class="card-title easy">Easy Gen</div>
<div class="card-sub">One prompt, one Generate button, one big preview. Advanced settings tucked away.</div>
</a>
<a class="card" href="/funpack/movie/">
<div class="card-title editor">Cutting Room</div>
<div class="card-sub">The full multi-scene NLE — timeline, composer, overlays, audio.</div>
</a>
</div>
</div>
<script>
(function () {
const API = (p) => "/funpack/movie/api" + p;
const aboutEl = document.getElementById("about");
const updateEl = document.getElementById("update-row");
async function j(method, url, body) {
const opts = { method, headers: {} };
if (body !== undefined) { opts.headers["Content-Type"] = "application/json"; opts.body = JSON.stringify(body); }
const res = await fetch(url, opts);
if (!res.ok) {
let payload = null; try { payload = await res.json(); } catch (_) {}
throw new Error((payload && payload.detail) || res.statusText || "Request failed");
}
return res.status === 204 ? null : res.json();
}
function led(ok) { return `<span class="led ${ok ? "ok" : "bad"}"></span>`; }
function renderAbout(health, git) {
const parts = [];
parts.push(`${led(!!(health && health.ok))}${health && health.ok ? "online" : "offline"}`);
if (git && git.ok) {
parts.push(`<b>v${git.version || "?"}</b>`);
parts.push(`${git.branch}@${git.commit}${git.dirty ? " (local changes)" : ""}`);
} else {
parts.push(git && git.version ? `<b>v${git.version}</b>` : "");
parts.push("git unavailable");
}
aboutEl.innerHTML = parts.filter(Boolean).join(' <span class="sep">·</span> ');
}
function renderUpdateRow(git) {
updateEl.innerHTML = "";
if (!git || !git.ok) return;
const updateBtn = document.createElement("button");
updateBtn.className = "btn primary";
updateBtn.disabled = !!git.dirty;
updateBtn.textContent = git.behind > 0 ? `${git.behind}↓ Update` : "⬇⟳ Update";
updateBtn.onclick = doUpdate;
updateEl.appendChild(updateBtn);
const restartBtn = document.createElement("button");
restartBtn.className = "btn ghost";
restartBtn.textContent = "⟳ Restart ComfyUI";
restartBtn.onclick = doRestart;
updateEl.appendChild(restartBtn);
const hint = document.createElement("span");
hint.className = "update-hint";
hint.textContent = git.dirty
? "Local changes — commit or stash first"
: (git.behind > 0 ? `${git.behind} commit(s) behind origin/${git.branch}` : `origin/${git.branch} up to date`);
updateEl.appendChild(hint);
}
function showOverlay(message) {
document.querySelector(".restart-overlay")?.remove();
const ov = document.createElement("div"); ov.className = "restart-overlay";
const card = document.createElement("div"); card.className = "restart-card";
const spin = document.createElement("div"); spin.className = "restart-spin";
const msg = document.createElement("div"); msg.className = "restart-msg"; msg.textContent = message;
card.append(spin, msg); ov.append(card); document.body.append(ov);
return msg;
}
function waitForReload(msgEl, startMs) {
const start = startMs || Date.now();
const tick = async () => {
try {
const h = await j("GET", API("/health"));
if (h && h.ok) { location.reload(); return; }
} catch (_) { /* still down */ }
if (Date.now() - start > 90000) {
msgEl.textContent = "Still waiting on ComfyUI…\\nIt may need a manual restart - check the console.";
}
setTimeout(tick, 2000);
};
setTimeout(tick, 3500);
}
async function doUpdate() {
let git;
try { git = await j("GET", API("/git/status")); } catch (e) { alert("Could not read git status: " + e.message); return; }
if (!git.ok) { alert(git.detail || "Git unavailable for this install."); return; }
if (git.dirty) { alert("Local changes detected in the FunPack folder.\\nCommit or stash them before updating."); return; }
const behind = git.behind > 0 ? `\\n\\n${git.behind} commit(s) available on origin/${git.branch}.` : "";
if (!confirm(`Pull latest "${git.branch}" from origin and restart ComfyUI?\\n\\nAny running generation will be lost.${behind}`)) return;
const msg = showOverlay(`Pulling origin/${git.branch}…\\nComfyUI will restart when the pull finishes.`);
try {
const res = await j("POST", API("/git/update"), {});
msg.textContent = res.updated ? `Updated ${res.before} → ${res.after}.\\nRestarting ComfyUI…` : "Already up to date.\\nRestarting ComfyUI…";
} catch (e) {
document.querySelector(".restart-overlay")?.remove();
alert("Update failed: " + e.message);
return;
}
waitForReload(msg, Date.now());
}
async function doRestart() {
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 msg = showOverlay("Restarting ComfyUI…\\nThis page will reload when it's back.");
try { await j("POST", API("/restart"), {}); } catch (_) { /* connection drops as it execv's - expected */ }
waitForReload(msg, Date.now());
}
async function boot() {
let health = null, git = null;
try { health = await j("GET", API("/health")); } catch (_) {}
try { git = await j("GET", API("/git/status")); } catch (_) {}
renderAbout(health, git);
renderUpdateRow(git);
}
boot();
})();
</script>
</body>
</html>
"""
if web is not None and PromptServer is not None:
routes = PromptServer.instance.routes
@routes.get(HUB_PREFIX)
async def _hub_redirect(_req):
raise web.HTTPFound(HUB_PREFIX + "/")
@routes.get(HUB_PREFIX + "/")
async def _hub_index(_req):
return web.Response(body=_PAGE, content_type="text/html", headers={"Cache-Control": "no-store, max-age=0"})
print(f"[FunPack] Hub available at {movie_config.comfy_display_url()}{HUB_PREFIX}/")