Install missing pipeline nodes via ComfyUI-Manager on first project open.
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
"""Detect missing custom-node packs for the built-in FunPack pipeline and install
|
||||
them through ComfyUI-Manager's queue (same path as the Manager UI)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Optional
|
||||
|
||||
from . import config
|
||||
from .builder import CORE
|
||||
|
||||
# Curated mapping: pack id (ComfyUI-Manager / registry) -> node classes used by the
|
||||
# fixed core graph. FunPack and comfy-core primitives are excluded.
|
||||
PIPELINE_PACKS: list[dict[str, Any]] = [
|
||||
{
|
||||
"id": "ComfyUI-LTXVideo",
|
||||
"title": "ComfyUI-LTXVideo",
|
||||
"classes": frozenset({
|
||||
"LTXVConditioning",
|
||||
"LTXVConcatAVLatent",
|
||||
"LTXVSeparateAVLatent",
|
||||
"LTXVAudioVAEDecode",
|
||||
"LTXFloatToInt",
|
||||
}),
|
||||
"git_urls": ["https://github.com/Lightricks/ComfyUI-LTXVideo"],
|
||||
},
|
||||
{
|
||||
"id": "comfyui-videohelpersuite",
|
||||
"title": "Video Helper Suite",
|
||||
"classes": frozenset({"VHS_VideoCombine"}),
|
||||
"git_urls": ["https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite"],
|
||||
},
|
||||
{
|
||||
"id": "comfyui-kjnodes",
|
||||
"title": "KJNodes",
|
||||
"classes": frozenset({"NormalizeAudioLoudness"}),
|
||||
"git_urls": ["https://github.com/kijai/ComfyUI-KJNodes"],
|
||||
},
|
||||
]
|
||||
|
||||
_PACK_BY_ID = {p["id"]: p for p in PIPELINE_PACKS}
|
||||
_ALL_MAPPED = frozenset().union(*(p["classes"] for p in PIPELINE_PACKS))
|
||||
|
||||
_install_jobs: dict[str, dict] = {}
|
||||
|
||||
|
||||
def required_core_classes() -> list[str]:
|
||||
return sorted(set(CORE.values()))
|
||||
|
||||
|
||||
def missing_core_classes(object_info: dict | None) -> list[str]:
|
||||
oi = object_info or {}
|
||||
return sorted(cls for cls in CORE.values() if cls not in oi)
|
||||
|
||||
|
||||
def missing_packs(object_info: dict | None) -> list[dict]:
|
||||
missing = set(missing_core_classes(object_info))
|
||||
out: list[dict] = []
|
||||
for pack in PIPELINE_PACKS:
|
||||
hit = pack["classes"] & missing
|
||||
if not hit:
|
||||
continue
|
||||
out.append({
|
||||
"id": pack["id"],
|
||||
"title": pack["title"],
|
||||
"missing_classes": sorted(hit),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def unmapped_missing_classes(object_info: dict | None) -> list[str]:
|
||||
missing = set(missing_core_classes(object_info))
|
||||
return sorted(missing - _ALL_MAPPED)
|
||||
|
||||
|
||||
def status_payload(object_info: dict | None, *, manager_available: bool) -> dict:
|
||||
packs = missing_packs(object_info)
|
||||
unmapped = unmapped_missing_classes(object_info)
|
||||
return {
|
||||
"manager_available": manager_available,
|
||||
"missing_classes": missing_core_classes(object_info),
|
||||
"missing_packs": packs,
|
||||
"unmapped_classes": unmapped,
|
||||
"needs_install": bool(packs),
|
||||
"manual_urls": [
|
||||
{"id": p["id"], "title": p["title"], "url": p["git_urls"][0]}
|
||||
for p in PIPELINE_PACKS
|
||||
if p["id"] in {x["id"] for x in packs}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def manager_available() -> bool:
|
||||
status, _ = await _manager_request("GET", "/manager/version")
|
||||
if status == 200:
|
||||
return True
|
||||
status, _ = await _manager_request("GET", "/api/manager/version")
|
||||
return status == 200
|
||||
|
||||
|
||||
async def _manager_request(method: str, path: str, *, json_body: dict | None = None, text_body: str | None = None):
|
||||
import aiohttp
|
||||
|
||||
base = config.comfy_base_url().rstrip("/")
|
||||
url = f"{base}{path}"
|
||||
headers: dict[str, str] = {}
|
||||
kwargs: dict[str, Any] = {}
|
||||
if json_body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
kwargs["json"] = json_body
|
||||
elif text_body is not None:
|
||||
headers["Content-Type"] = "text/plain"
|
||||
kwargs["data"] = text_body
|
||||
timeout = aiohttp.ClientTimeout(total=120)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.request(method, url, headers=headers, **kwargs) as resp:
|
||||
text = await resp.text()
|
||||
return resp.status, text
|
||||
|
||||
|
||||
async def _fetch_manager_pack_meta(pack_id: str) -> dict | None:
|
||||
for path in ("/customnode/getlist?mode=cache", "/api/customnode/getlist?mode=cache"):
|
||||
status, body = await _manager_request("GET", path)
|
||||
if status != 200:
|
||||
continue
|
||||
try:
|
||||
import json as _json
|
||||
data = _json.loads(body)
|
||||
except Exception:
|
||||
continue
|
||||
node_packs = data.get("node_packs") or {}
|
||||
for _key, meta in node_packs.items():
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
if meta.get("id") == pack_id or meta.get("cnr_id") == pack_id or _key == pack_id:
|
||||
return meta
|
||||
return None
|
||||
|
||||
|
||||
def _install_body(pack: dict, meta: dict | None) -> dict:
|
||||
pid = pack["id"]
|
||||
title = pack["title"]
|
||||
if meta:
|
||||
version = str(meta.get("version") or meta.get("ver") or "nightly")
|
||||
selected = str(meta.get("selected_version") or "latest")
|
||||
body: dict[str, Any] = {
|
||||
"id": meta.get("id") or meta.get("cnr_id") or pid,
|
||||
"title": meta.get("title") or title,
|
||||
"name": meta.get("name") or title,
|
||||
"version": version,
|
||||
"selected_version": selected,
|
||||
"channel": str(meta.get("channel") or "default"),
|
||||
"mode": "cache",
|
||||
"ui_id": pid,
|
||||
"skip_post_install": False,
|
||||
}
|
||||
if meta.get("repository"):
|
||||
body["repository"] = meta["repository"]
|
||||
if meta.get("files"):
|
||||
body["files"] = meta["files"]
|
||||
return body
|
||||
return {
|
||||
"id": pid,
|
||||
"title": title,
|
||||
"name": title,
|
||||
"version": "unknown",
|
||||
"selected_version": "unknown",
|
||||
"files": list(pack.get("git_urls") or []),
|
||||
"install_type": "git-clone",
|
||||
"channel": "default",
|
||||
"mode": "cache",
|
||||
"ui_id": pid,
|
||||
"skip_post_install": False,
|
||||
}
|
||||
|
||||
|
||||
async def _queue_install(body: dict) -> tuple[bool, str]:
|
||||
for path in ("/manager/queue/install", "/api/manager/queue/install"):
|
||||
status, text = await _manager_request("POST", path, json_body=body)
|
||||
if status in (200, 201):
|
||||
return True, ""
|
||||
if status == 403:
|
||||
return False, "ComfyUI-Manager blocked the install (security policy). Open Manager and lower the security level, or install the packs manually."
|
||||
if status == 404:
|
||||
return False, text.strip() or "Pack not found in ComfyUI-Manager database."
|
||||
return False, "ComfyUI-Manager install endpoint unavailable."
|
||||
|
||||
|
||||
async def _start_manager_queue() -> None:
|
||||
for path in ("/manager/queue/start", "/api/manager/queue/start"):
|
||||
status, _ = await _manager_request("GET", path)
|
||||
if status in (200, 201):
|
||||
return
|
||||
|
||||
|
||||
async def _manager_queue_status() -> dict:
|
||||
for path in ("/manager/queue/status", "/api/manager/queue/status"):
|
||||
status, body = await _manager_request("GET", path)
|
||||
if status != 200:
|
||||
continue
|
||||
try:
|
||||
import json as _json
|
||||
data = _json.loads(body)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def create_install_job(pack_ids: list[str]) -> dict:
|
||||
packs = []
|
||||
for pid in pack_ids:
|
||||
spec = _PACK_BY_ID.get(pid)
|
||||
if spec:
|
||||
packs.append({"id": pid, "title": spec["title"]})
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
job = {
|
||||
"job_id": job_id,
|
||||
"state": "queued",
|
||||
"packs": packs,
|
||||
"total": len(packs),
|
||||
"done": 0,
|
||||
"current_title": packs[0]["title"] if packs else "",
|
||||
"error": "",
|
||||
"started_at": time.time(),
|
||||
}
|
||||
_install_jobs[job_id] = job
|
||||
return job
|
||||
|
||||
|
||||
def get_install_job(job_id: str) -> Optional[dict]:
|
||||
job = _install_jobs.get(job_id)
|
||||
if not job:
|
||||
return None
|
||||
return {
|
||||
"job_id": job["job_id"],
|
||||
"state": job["state"],
|
||||
"packs": job["packs"],
|
||||
"total": job["total"],
|
||||
"done": job["done"],
|
||||
"current_title": job.get("current_title") or "",
|
||||
"error": job.get("error") or "",
|
||||
}
|
||||
|
||||
|
||||
async def run_install_job(job_id: str, restart_fn) -> None:
|
||||
job = _install_jobs.get(job_id)
|
||||
if not job:
|
||||
return
|
||||
job["state"] = "installing"
|
||||
try:
|
||||
for i, pack in enumerate(job["packs"]):
|
||||
pid = pack["id"]
|
||||
spec = _PACK_BY_ID.get(pid)
|
||||
if not spec:
|
||||
continue
|
||||
job["current_title"] = pack["title"]
|
||||
job["done"] = i
|
||||
meta = await _fetch_manager_pack_meta(pid)
|
||||
body = _install_body(spec, meta)
|
||||
ok, err = await _queue_install(body)
|
||||
if not ok:
|
||||
job["state"] = "error"
|
||||
job["error"] = err or f"Failed to queue {pack['title']}."
|
||||
return
|
||||
await _start_manager_queue()
|
||||
deadline = time.time() + 1800
|
||||
while time.time() < deadline:
|
||||
st = await _manager_queue_status()
|
||||
done = int(st.get("done_count") or 0)
|
||||
total = int(st.get("total_count") or 0) or job["total"]
|
||||
processing = bool(st.get("is_processing"))
|
||||
job["total"] = max(job["total"], total)
|
||||
job["done"] = min(done, job["total"])
|
||||
if job["packs"]:
|
||||
idx = min(max(done, 0), len(job["packs"]) - 1)
|
||||
job["current_title"] = job["packs"][idx]["title"]
|
||||
if job["total"] > 0 and done >= job["total"] and not processing:
|
||||
break
|
||||
await asyncio.sleep(1.5)
|
||||
else:
|
||||
job["state"] = "error"
|
||||
job["error"] = "Install timed out. Check ComfyUI-Manager queue in the main ComfyUI window."
|
||||
return
|
||||
job["done"] = job["total"]
|
||||
job["state"] = "restarting"
|
||||
job["current_title"] = ""
|
||||
await asyncio.sleep(0.7)
|
||||
restart_fn()
|
||||
except Exception as e: # noqa: BLE001
|
||||
job["state"] = "error"
|
||||
job["error"] = str(e) or "Install failed."
|
||||
@@ -109,6 +109,9 @@
|
||||
allNodes: () => j("GET", API("/all-nodes")),
|
||||
nodeSpec: (cls) => j("GET", API(`/node/${encodeURIComponent(cls)}`)),
|
||||
pipelinePorts: () => j("GET", API("/pipeline-ports")),
|
||||
pipelineDeps: () => j("GET", API("/pipeline-deps")),
|
||||
pipelineDepsInstall: (packIds) => j("POST", API("/pipeline-deps/install"), { pack_ids: packIds }),
|
||||
pipelineDepsInstallStatus: (jobId) => j("GET", API(`/pipeline-deps/install/${encodeURIComponent(jobId)}`)),
|
||||
imageTargets: (pid) => j("GET", API("/image-targets" + (pid ? `?pid=${encodeURIComponent(pid)}` : ""))),
|
||||
// Models config is per-project; pass the project id. Falls back to the global
|
||||
// default route (used as the seed/template) when no project is given.
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
|
||||
<script src="api.js"></script>
|
||||
<script src="dom.js"></script>
|
||||
<script src="restart_ui.js"></script>
|
||||
<script src="pipeline_caps.js"></script>
|
||||
<script src="tour_mode.js"></script>
|
||||
<script src="view_bus.js"></script>
|
||||
@@ -122,6 +123,7 @@
|
||||
<script src="engine_settings.js"></script>
|
||||
<script src="slot_picker.js"></script>
|
||||
<script src="welcome.js"></script>
|
||||
<script src="pipeline_setup.js"></script>
|
||||
<script src="inspector.js"></script>
|
||||
<script src="rating_picker.js"></script>
|
||||
<script src="waveform.js"></script>
|
||||
|
||||
@@ -78,27 +78,11 @@
|
||||
}
|
||||
|
||||
function _restartOverlay(message) {
|
||||
const ov = el("div", "restart-overlay");
|
||||
const card = el("div", "restart-card");
|
||||
card.append(el("div", "restart-spin"));
|
||||
const msg = el("div", "restart-msg", message);
|
||||
card.append(msg); ov.append(card); document.body.append(ov);
|
||||
return msg;
|
||||
return window.FunPackRestart?.showOverlay?.(message);
|
||||
}
|
||||
|
||||
function _waitForComfyReload(msgEl, startMs) {
|
||||
const start = startMs || 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) {
|
||||
msgEl.textContent = "Still waiting on ComfyUI…\nIt may need a manual restart - check the console.";
|
||||
}
|
||||
setTimeout(tick, 2000);
|
||||
};
|
||||
setTimeout(tick, 3500);
|
||||
window.FunPackRestart?.waitForReload?.(msgEl, startMs);
|
||||
}
|
||||
|
||||
async function restartComfy() {
|
||||
@@ -128,7 +112,7 @@
|
||||
? `Updated ${res.before} → ${res.after}.\nRestarting ComfyUI…`
|
||||
: "Already up to date.\nRestarting ComfyUI…";
|
||||
} catch (e) {
|
||||
document.querySelector(".restart-overlay")?.remove();
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert("Update failed: " + (e.message || e));
|
||||
return;
|
||||
}
|
||||
@@ -163,7 +147,7 @@
|
||||
? `Switched to ${branch} (${res.before} → ${res.after}).\nRestarting ComfyUI…`
|
||||
: `On ${branch}, already up to date.\nRestarting ComfyUI…`;
|
||||
} catch (e) {
|
||||
document.querySelector(".restart-overlay")?.remove();
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert("Branch switch failed: " + (e.message || e));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// Prompt to install missing custom-node packs for the built-in pipeline (via Manager).
|
||||
(function () {
|
||||
const { el } = window.dom;
|
||||
const S = window.Store;
|
||||
const API = window.MovieEditorAPI;
|
||||
const LS_KEY = "funpack_pipeline_deps_dismissed";
|
||||
|
||||
let overlay = null;
|
||||
let _pollTimer = null;
|
||||
|
||||
function dismissed() {
|
||||
try { return localStorage.getItem(LS_KEY) === "1"; } catch (_) { return false; }
|
||||
}
|
||||
|
||||
function setDismissed() {
|
||||
try { localStorage.setItem(LS_KEY, "1"); } catch (_) {}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (_pollTimer) { clearTimeout(_pollTimer); _pollTimer = null; }
|
||||
overlay?.remove();
|
||||
overlay = null;
|
||||
}
|
||||
|
||||
function builtInPipelineActive(st) {
|
||||
const m = st.models || {};
|
||||
return !m.disable_core;
|
||||
}
|
||||
|
||||
async function useOwnPipeline() {
|
||||
if (!S.get().project) return;
|
||||
const models = JSON.parse(JSON.stringify(S.get().models || { slots: [] }));
|
||||
models.disable_core = true;
|
||||
await API.saveModels(S.get().project.id, models);
|
||||
await S.loadModels();
|
||||
setDismissed();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function packList(deps) {
|
||||
const ul = el("ul", "pipe-setup-list");
|
||||
(deps.missing_packs || []).forEach((p) => {
|
||||
const li = el("li", null, p.title);
|
||||
if (p.missing_classes?.length) {
|
||||
li.append(el("span", "pipe-setup-sub", p.missing_classes.join(", ")));
|
||||
}
|
||||
ul.append(li);
|
||||
});
|
||||
return ul;
|
||||
}
|
||||
|
||||
function manualBlock(deps) {
|
||||
const box = el("div", "pipe-setup-manual");
|
||||
box.append(el("div", "pipe-setup-manual-title", "Install ComfyUI-Manager or add these repos under custom_nodes:"));
|
||||
const ul = el("ul", "pipe-setup-list");
|
||||
(deps.manual_urls || []).forEach((u) => {
|
||||
const li = el("li");
|
||||
const a = el("a", null, u.title);
|
||||
a.href = u.url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
li.append(a);
|
||||
ul.append(li);
|
||||
});
|
||||
box.append(ul);
|
||||
return box;
|
||||
}
|
||||
|
||||
async function pollInstall(jobId, msgEl) {
|
||||
try {
|
||||
const st = await API.pipelineDepsInstallStatus(jobId);
|
||||
if (st.state === "installing" || st.state === "queued") {
|
||||
const n = Math.min(st.done + 1, st.total || 1);
|
||||
const title = st.current_title || "…";
|
||||
msgEl.textContent = `Downloading and installing missing nodes:\n${n} out of ${st.total || "?"} - ${title}`;
|
||||
_pollTimer = setTimeout(() => pollInstall(jobId, msgEl), 1200);
|
||||
return;
|
||||
}
|
||||
if (st.state === "restarting") {
|
||||
msgEl.textContent = "Install complete.\nRestarting ComfyUI…";
|
||||
window.FunPackRestart?.waitForReload?.(msgEl, Date.now());
|
||||
return;
|
||||
}
|
||||
if (st.state === "error") {
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert(st.error || "Install failed.");
|
||||
return;
|
||||
}
|
||||
_pollTimer = setTimeout(() => pollInstall(jobId, msgEl), 1200);
|
||||
} catch (e) {
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert(String(e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
async function startInstall(deps) {
|
||||
const ids = (deps.missing_packs || []).map((p) => p.id);
|
||||
if (!ids.length) return;
|
||||
closeModal();
|
||||
const msg = window.FunPackRestart?.showOverlay?.(
|
||||
"Downloading and installing missing nodes:\n1 out of " + ids.length + " - " + (deps.missing_packs[0]?.title || "…"),
|
||||
);
|
||||
if (!msg) return;
|
||||
try {
|
||||
const res = await API.pipelineDepsInstall(ids);
|
||||
pollInstall(res.job_id, msg);
|
||||
} catch (e) {
|
||||
window.FunPackRestart?.removeOverlay?.();
|
||||
alert("Install failed: " + (e.message || e));
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(deps) {
|
||||
closeModal();
|
||||
overlay = el("div", "modal-overlay pipe-setup-overlay");
|
||||
const modal = el("div", "modal pipe-setup-modal");
|
||||
const head = el("div", "modal-head");
|
||||
head.append(el("div", "modal-title", "Pipeline setup"));
|
||||
const closeBtn = el("button", "btn ghost", "Close");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.onclick = () => closeModal();
|
||||
const headRight = el("div", "modal-head-right");
|
||||
headRight.append(closeBtn);
|
||||
head.append(headRight);
|
||||
modal.append(head);
|
||||
|
||||
const content = el("div", "modal-content");
|
||||
content.append(el("p", "pipe-setup-lead",
|
||||
"Nodes required for the built-in pipeline are missing. Do you want to install them?"));
|
||||
content.append(packList(deps));
|
||||
if (!deps.manager_available) {
|
||||
content.append(el("div", "pipe-setup-warn",
|
||||
"ComfyUI-Manager was not detected. Automatic install is unavailable."));
|
||||
content.append(manualBlock(deps));
|
||||
}
|
||||
if ((deps.unmapped_classes || []).length) {
|
||||
content.append(el("div", "pipe-setup-warn",
|
||||
"Unmapped missing classes: " + deps.unmapped_classes.join(", ")));
|
||||
}
|
||||
modal.append(content);
|
||||
|
||||
const foot = el("div", "pipe-setup-foot");
|
||||
if (deps.manager_available) {
|
||||
const installBtn = el("button", "btn primary", "Install missing nodes");
|
||||
installBtn.type = "button";
|
||||
installBtn.onclick = () => startInstall(deps);
|
||||
foot.append(installBtn);
|
||||
}
|
||||
const ownBtn = el("button", "btn", "No, I'll use my own pipeline");
|
||||
ownBtn.type = "button";
|
||||
ownBtn.onclick = () => useOwnPipeline();
|
||||
foot.append(ownBtn);
|
||||
const dismissBtn = el("button", "btn ghost", "Close");
|
||||
dismissBtn.type = "button";
|
||||
dismissBtn.onclick = () => closeModal();
|
||||
foot.append(dismissBtn);
|
||||
modal.append(foot);
|
||||
|
||||
overlay.append(modal);
|
||||
overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); });
|
||||
document.body.append(overlay);
|
||||
}
|
||||
|
||||
async function maybePrompt() {
|
||||
if (window.__FUNPACK_TOUR__) return;
|
||||
if (dismissed()) return;
|
||||
const st = S.get();
|
||||
if (!st.project || !builtInPipelineActive(st)) return;
|
||||
let deps;
|
||||
try { deps = await API.pipelineDeps(); } catch (_) { return; }
|
||||
if (!deps?.needs_install) return;
|
||||
openModal(deps);
|
||||
}
|
||||
|
||||
window.PipelineSetup = { maybePrompt, close: closeModal };
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
// Shared ComfyUI restart overlay + health polling (Update FunPack, pipeline installs).
|
||||
(function () {
|
||||
const { el } = window.dom;
|
||||
|
||||
function showOverlay(message) {
|
||||
document.querySelector(".restart-overlay")?.remove();
|
||||
const ov = el("div", "restart-overlay");
|
||||
const card = el("div", "restart-card");
|
||||
card.append(el("div", "restart-spin"));
|
||||
const msg = el("div", "restart-msg", message);
|
||||
card.append(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 window.MovieEditorAPI.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);
|
||||
}
|
||||
|
||||
window.FunPackRestart = {
|
||||
showOverlay,
|
||||
waitForReload,
|
||||
removeOverlay() { document.querySelector(".restart-overlay")?.remove(); },
|
||||
};
|
||||
})();
|
||||
@@ -636,6 +636,7 @@
|
||||
await loadModels();
|
||||
_validateSceneRenders();
|
||||
window.WelcomePage?.close?.();
|
||||
window.PipelineSetup?.maybePrompt?.();
|
||||
}
|
||||
|
||||
async function newProject(name) {
|
||||
|
||||
@@ -1250,6 +1250,18 @@ label.field.linked input, label.field.linked select { opacity: .6; }
|
||||
/* Input-source arrow points left (← instead of →) */
|
||||
.wire-row .wire-arrow.src { color: var(--amber); }
|
||||
|
||||
/* pipeline missing-nodes setup modal */
|
||||
.pipe-setup-modal { max-width: 520px; width: min(520px, 96vw); }
|
||||
.pipe-setup-lead { margin: 0 0 12px; color: var(--muted); font-size: 13px; line-height: 1.5; }
|
||||
.pipe-setup-list { margin: 0 0 12px; padding-left: 18px; color: var(--text); font-size: 13px; }
|
||||
.pipe-setup-list li { margin: 6px 0; }
|
||||
.pipe-setup-sub { display: block; font-size: 11px; color: var(--faint); font-family: var(--font-mono); margin-top: 2px; }
|
||||
.pipe-setup-warn { margin: 10px 0; padding: 8px 10px; border-radius: 6px; font-size: 12px; line-height: 1.45;
|
||||
background: color-mix(in srgb, var(--amber) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 35%, transparent); color: var(--muted); }
|
||||
.pipe-setup-manual-title { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
|
||||
.pipe-setup-foot { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px 16px 16px; border-top: 1px solid var(--line); }
|
||||
|
||||
/* 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); }
|
||||
|
||||
+33
-1
@@ -19,7 +19,7 @@ except Exception: # pragma: no cover - only available inside ComfyUI
|
||||
web = None
|
||||
PromptServer = None
|
||||
|
||||
from .backend import bridge, builder, config, git_update, media, nodes, pipeline_caps, pipeline_wiring, projects, workflow_import
|
||||
from .backend import bridge, builder, config, git_update, media, nodes, pipeline_caps, pipeline_deps, pipeline_wiring, projects, workflow_import
|
||||
from .backend.nle_effects import zoompan_z_expr
|
||||
from .backend.nle_overlays import build_overlay_video_filter, prepare_overlay_export
|
||||
from .backend.timeline import (
|
||||
@@ -1710,6 +1710,38 @@ if web is not None and PromptServer is not None:
|
||||
print(f"[FunPack][movie] node-candidates({role}) failed:\n{traceback.format_exc()}")
|
||||
raise web.HTTPInternalServerError(reason=f"candidates({role}) failed: {e}")
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/pipeline-deps")
|
||||
async def _pipeline_deps(_req):
|
||||
try:
|
||||
oi = await bridge.object_info()
|
||||
except Exception:
|
||||
oi = {}
|
||||
mgr = await pipeline_deps.manager_available()
|
||||
return web.json_response(pipeline_deps.status_payload(oi, manager_available=mgr))
|
||||
|
||||
@routes.post(UI_PREFIX + "/api/pipeline-deps/install")
|
||||
async def _pipeline_deps_install(req):
|
||||
import asyncio
|
||||
if not await pipeline_deps.manager_available():
|
||||
return web.json_response(
|
||||
{"detail": "ComfyUI-Manager is not installed or not reachable."},
|
||||
status=503,
|
||||
)
|
||||
body = await req.json() if req.can_read_body else {}
|
||||
pack_ids = body.get("pack_ids") or body.get("packs") or []
|
||||
if not isinstance(pack_ids, list) or not pack_ids:
|
||||
return web.json_response({"detail": "pack_ids required."}, status=400)
|
||||
job = pipeline_deps.create_install_job([str(x) for x in pack_ids])
|
||||
asyncio.create_task(pipeline_deps.run_install_job(job["job_id"], _restart_comfy))
|
||||
return web.json_response({"job_id": job["job_id"], "total": job["total"]})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/pipeline-deps/install/{job_id}")
|
||||
async def _pipeline_deps_install_status(req):
|
||||
job = pipeline_deps.get_install_job(req.match_info["job_id"])
|
||||
if not job:
|
||||
raise web.HTTPNotFound(reason="Install job not found")
|
||||
return web.json_response(job)
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/pipeline-ports")
|
||||
async def _pipeline_ports(_req):
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for built-in pipeline dependency detection."""
|
||||
from movie_editor.backend import pipeline_deps
|
||||
|
||||
|
||||
def test_no_missing_when_all_core_present():
|
||||
oi = {cls: {} for cls in pipeline_deps.required_core_classes()}
|
||||
assert pipeline_deps.missing_core_classes(oi) == []
|
||||
assert pipeline_deps.missing_packs(oi) == []
|
||||
st = pipeline_deps.status_payload(oi, manager_available=True)
|
||||
assert st["needs_install"] is False
|
||||
|
||||
|
||||
def test_missing_ltx_and_vhs_packs():
|
||||
oi = {
|
||||
"FunPackStudio": {},
|
||||
"FunPackLTXAVSceneChainSampler": {},
|
||||
"FunPackSaveRefinementLatent": {},
|
||||
"FunPackRefinementKeyLoader": {},
|
||||
"PrimitiveStringMultiline": {},
|
||||
"PrimitiveInt": {},
|
||||
"PrimitiveFloat": {},
|
||||
}
|
||||
missing = pipeline_deps.missing_core_classes(oi)
|
||||
assert "LTXVConditioning" in missing
|
||||
assert "VHS_VideoCombine" in missing
|
||||
packs = pipeline_deps.missing_packs(oi)
|
||||
ids = {p["id"] for p in packs}
|
||||
assert "ComfyUI-LTXVideo" in ids
|
||||
assert "comfyui-videohelpersuite" in ids
|
||||
st = pipeline_deps.status_payload(oi, manager_available=False)
|
||||
assert st["needs_install"] is True
|
||||
assert st["manager_available"] is False
|
||||
assert len(st["manual_urls"]) >= 2
|
||||
|
||||
|
||||
def test_install_body_fallback_git():
|
||||
pack = pipeline_deps._PACK_BY_ID["ComfyUI-LTXVideo"]
|
||||
body = pipeline_deps._install_body(pack, None)
|
||||
assert body["version"] == "unknown"
|
||||
assert body["files"]
|
||||
assert "github.com" in body["files"][0]
|
||||
Reference in New Issue
Block a user