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.
254 lines
11 KiB
Python
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}/")
|