From 59dd76670465adeda552a7fce7cd80cafadbfb77 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:05:39 +0300 Subject: [PATCH] Fix preview segments and pipeline persistence; add git update UI. Pass live render params to preview-segment URLs so playback works right after generate, sync scene renders and models on save/generate so wiring and deleted nodes stick, and expose FunPack pull/checkout from the menu. Co-authored-by: Cursor --- movie_editor/backend/builder.py | 33 +++-- movie_editor/backend/git_update.py | 160 +++++++++++++++++++++ movie_editor/backend/pipeline_wiring.py | 26 ++++ movie_editor/frontend/api.js | 19 ++- movie_editor/frontend/menubar.js | 111 ++++++++++++-- movie_editor/frontend/models.js | 85 ++++++++++- movie_editor/frontend/player.js | 73 +++++++++- movie_editor/frontend/store.js | 11 +- movie_editor/server.py | 69 ++++++++- movie_editor/tests/test_git_update.py | 118 +++++++++++++++ movie_editor/tests/test_pipeline_wiring.py | 36 ++++- movie_editor/tests/test_server.py | 30 +++- 12 files changed, 722 insertions(+), 49 deletions(-) create mode 100644 movie_editor/backend/git_update.py create mode 100644 movie_editor/tests/test_git_update.py diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index 49c24e49..f0fd5a63 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -406,20 +406,24 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No graph[dnode]["inputs"][dinput] = [sid, oidx] report["wired"].append(f"{s.get('node_class')}.{out_name} -> {dnode}.{dinput}") - # 4b. core input overrides (full-control only — guided mode keeps the fixed core path). - if not pipeline_wiring.wiring_locked(models_config): - for cid, ovs in ((models_config or {}).get("core_overrides") or {}).items(): - if cid not in graph: + # 4b. core input overrides. In guided mode only loader-facing open ports may be + # overridden (e.g. Studio · latent from a custom node); internal core links stay fixed. + for cid, ovs in ((models_config or {}).get("core_overrides") or {}).items(): + if cid not in graph: + continue + locked = pipeline_wiring.wiring_locked(models_config) + for inp, source in (ovs or {}).items(): + if not source: continue - for inp, source in (ovs or {}).items(): - if not source: - continue - src = _resolve_source(source, slot_node_id, slot_def, object_info) - if src: - graph[cid]["inputs"][inp] = list(src) - report["wired"].append(f"{source} -> {cid}.{inp} (core override)") - else: - report["unsatisfied"].append(f"core override {cid}.{inp}: '{source}' could not be resolved.") + if locked and (cid, inp) not in pipeline_wiring.OPEN_CORE_INPUTS: + continue + src = _resolve_source(source, slot_node_id, slot_def, object_info) + if src: + graph[cid]["inputs"][inp] = list(src) + report["wired"].append(f"{source} -> {cid}.{inp} (core override)") + else: + report["unsatisfied"].append( + f"core override {cid}.{inp}: '{source}' could not be resolved.") # 5. auto-wire remaining unbound typed inputs by unique producer. producers = _producers(graph, slots, slot_node_id, slot_def, object_info) @@ -519,7 +523,6 @@ def core_graph(object_info: dict, models_config: dict | None = None) -> list[dic for inp, (src, idx) in CORE_LINKS.get(cid, {}).items(): t = _input_type(cid, inp) or "*" builtin = f"{src} · {_out_name(src, idx)}" - is_internal = True inputs.append({"name": inp, "type": t, "from": "internal", "detail": builtin, "value": ov.get(inp, ""), "options": _options(t, builtin, cid), "locked": locked}) @@ -528,7 +531,7 @@ def core_graph(object_info: dict, models_config: dict | None = None) -> list[dic inputs.append({"name": inp, "type": t, "from": "loader", "required": req, "detail": builtin + ("" if req else " (optional)"), "value": ov.get(inp, ""), "options": _options(t, "(auto-wire from loaders)", cid), - "locked": locked}) + "locked": False}) outputs = [] for i, o in enumerate(node_outputs(nd or {})): dests = [f"{d} · {di}" for (oi, d, di) in rev.get(cid, []) if oi == i] diff --git a/movie_editor/backend/git_update.py b/movie_editor/backend/git_update.py new file mode 100644 index 00000000..43d91702 --- /dev/null +++ b/movie_editor/backend/git_update.py @@ -0,0 +1,160 @@ +"""Git pull / branch switch for the FunPack repo (ComfyUI custom node root).""" +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +_BRANCH_PRIORITY = ("dev", "cutting_edge", "main") + + +class GitUpdateError(RuntimeError): + pass + + +def _run_git(*args: str, timeout: int = 120) -> subprocess.CompletedProcess[str]: + if not shutil.which("git"): + raise GitUpdateError("git not found on PATH.") + if not (REPO_ROOT / ".git").exists(): + raise GitUpdateError("FunPack is not a git checkout (no .git directory).") + proc = subprocess.run( + ["git", *args], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=timeout, + ) + return proc + + +def _current_branch() -> str: + proc = _run_git("rev-parse", "--abbrev-ref", "HEAD") + if proc.returncode != 0: + raise GitUpdateError((proc.stderr or proc.stdout or "git rev-parse failed").strip()) + return (proc.stdout or "").strip() + + +def _current_commit() -> str: + proc = _run_git("rev-parse", "--short", "HEAD") + if proc.returncode != 0: + raise GitUpdateError((proc.stderr or proc.stdout or "git rev-parse failed").strip()) + return (proc.stdout or "").strip() + + +def _is_dirty() -> bool: + proc = _run_git("status", "--porcelain") + if proc.returncode != 0: + raise GitUpdateError((proc.stderr or "git status failed").strip()) + return bool((proc.stdout or "").strip()) + + +def _ahead_behind(branch: str) -> tuple[int, int]: + proc = _run_git("rev-list", "--left-right", "--count", f"HEAD...origin/{branch}") + if proc.returncode != 0: + return 0, 0 + parts = (proc.stdout or "").strip().split() + if len(parts) != 2: + return 0, 0 + try: + return int(parts[0]), int(parts[1]) + except ValueError: + return 0, 0 + + +def _list_branches() -> list[str]: + proc = _run_git("branch", "-a", "--format=%(refname:short)") + if proc.returncode != 0: + raise GitUpdateError((proc.stderr or "git branch failed").strip()) + names: set[str] = set() + for raw in (proc.stdout or "").splitlines(): + line = raw.strip() + if not line or line == "HEAD" or line.endswith("/HEAD"): + continue + if line.startswith("origin/"): + names.add(line[7:]) + elif not line.startswith("remotes/"): + names.add(line) + ordered = [b for b in _BRANCH_PRIORITY if b in names] + rest = sorted(b for b in names if b not in _BRANCH_PRIORITY) + return ordered + rest + + +def status() -> dict: + """Current branch, switchable branches, and update availability.""" + try: + branch = _current_branch() + commit = _current_commit() + dirty = _is_dirty() + branches = _list_branches() + fetch = _run_git("fetch", "--prune", "origin") + fetch_ok = fetch.returncode == 0 + ahead, behind = _ahead_behind(branch) if fetch_ok else (0, 0) + return { + "ok": True, + "branch": branch, + "commit": commit, + "dirty": dirty, + "branches": branches, + "ahead": ahead, + "behind": behind, + "fetch_ok": fetch_ok, + "repo": str(REPO_ROOT), + } + except GitUpdateError as e: + return {"ok": False, "detail": str(e)} + + +def pull(branch: str | None = None) -> dict: + """Fast-forward pull from origin on the given branch (current if omitted).""" + branch = (branch or _current_branch()).strip() + if not branch: + raise GitUpdateError("Could not determine current branch.") + if _is_dirty(): + raise GitUpdateError("Working tree has local changes. Commit or stash them before updating.") + before = _current_commit() + fetch = _run_git("fetch", "--prune", "origin") + if fetch.returncode != 0: + raise GitUpdateError((fetch.stderr or fetch.stdout or "git fetch failed").strip()) + if branch != _current_branch(): + co = _run_git("checkout", branch) + if co.returncode != 0: + raise GitUpdateError((co.stderr or co.stdout or "git checkout failed").strip()) + pull_proc = _run_git("pull", "--ff-only", "origin", branch) + if pull_proc.returncode != 0: + msg = (pull_proc.stderr or pull_proc.stdout or "git pull failed").strip() + raise GitUpdateError(msg) + after = _current_commit() + return { + "branch": branch, + "before": before, + "after": after, + "updated": before != after, + "output": (pull_proc.stdout or "").strip(), + } + + +def checkout(branch: str, *, pull_after: bool = True) -> dict: + """Switch branch, optionally pull, return combined result.""" + branch = (branch or "").strip() + if not branch: + raise GitUpdateError("Branch name is required.") + branches = _list_branches() + if branch not in branches: + raise GitUpdateError(f'Branch "{branch}" is not available locally or on origin.') + if _is_dirty(): + raise GitUpdateError("Working tree has local changes. Commit or stash them before switching branches.") + before_branch = _current_branch() + before_commit = _current_commit() + if branch != before_branch: + co = _run_git("checkout", branch) + if co.returncode != 0: + raise GitUpdateError((co.stderr or co.stdout or "git checkout failed").strip()) + result = {"branch": branch, "before_branch": before_branch, "before": before_commit} + if pull_after: + pulled = pull(branch) + result.update(pulled) + else: + result["after"] = _current_commit() + result["updated"] = result["before"] != result["after"] + return result diff --git a/movie_editor/backend/pipeline_wiring.py b/movie_editor/backend/pipeline_wiring.py index e754ae8c..73297094 100644 --- a/movie_editor/backend/pipeline_wiring.py +++ b/movie_editor/backend/pipeline_wiring.py @@ -67,6 +67,28 @@ PORT_LABELS: dict[str, str] = { "LTXVAudioVAEDecode.audio_vae": "Audio VAE Decode · audio_vae", } +# Built-in core inputs fed by user loaders (not fixed internal core links). +# core_overrides for these apply even in guided mode; internal links stay fixed. +OPEN_CORE_INPUTS: frozenset[tuple[str, str]] = frozenset({ + ("studio", "model"), + ("studio", "clip"), + ("studio", "source_image"), + ("studio", "latent"), + ("sampler", "vae"), + ("concat", "audio_latent"), + ("audiodec", "audio_vae"), +}) + +PORT_TO_OPEN_CORE: dict[str, tuple[str, str]] = { + "FunPackStudio.model": ("studio", "model"), + "FunPackStudio.clip": ("studio", "clip"), + "FunPackStudio.source_image": ("studio", "source_image"), + "FunPackStudio.latent": ("studio", "latent"), + "FunPackLTXAVSceneChainSampler.vae": ("sampler", "vae"), + "LTXVConcatAVLatent.audio_latent": ("concat", "audio_latent"), + "LTXVAudioVAEDecode.audio_vae": ("audiodec", "audio_vae"), +} + def models_dict(models: Any) -> dict: return models if isinstance(models, dict) else {} @@ -223,6 +245,10 @@ def wiring_rules_payload() -> dict[str, Any]: "default_wires": DEFAULT_WIRES_BY_ROLE, "default_input_sources": DEFAULT_INPUT_SOURCES_BY_ROLE, "port_labels": PORT_LABELS, + "open_core_ports": [ + {"core_id": cid, "input": inp, "port": port} + for port, (cid, inp) in PORT_TO_OPEN_CORE.items() + ], } diff --git a/movie_editor/frontend/api.js b/movie_editor/frontend/api.js index 21547eaf..0ae6685d 100644 --- a/movie_editor/frontend/api.js +++ b/movie_editor/frontend/api.js @@ -104,6 +104,9 @@ applyWorkflow: (pid, workflow, bindings) => j("POST", API(`/projects/${pid}/workflow/apply`), { workflow, bindings }), restart: () => j("POST", API("/restart")), + gitStatus: () => j("GET", API("/git/status")), + gitUpdate: (branch) => j("POST", API("/git/update"), branch ? { branch } : {}), + gitCheckout: (branch) => j("POST", API("/git/checkout"), { branch }), // generate (a single scene, or an explicit run of scene ids = one chain request) generate: (id, onlyScene, sceneIds, resetSession) => @@ -121,8 +124,20 @@ resultUrl: (id, m) => API(`/projects/${id}/result?filename=${encodeURIComponent(m.filename)}`) + `&subfolder=${encodeURIComponent(m.subfolder || "")}&type=${encodeURIComponent(m.type || "output")}`, - previewSegmentUrl: (id, sceneId) => - API(`/projects/${id}/preview-segment/${encodeURIComponent(sceneId)}`), + previewSegmentUrl: (id, sceneId, spec) => { + let u = API(`/projects/${id}/preview-segment/${encodeURIComponent(sceneId)}`); + const m = spec?.media; + if (m?.filename) { + const q = new URLSearchParams({ + filename: m.filename, + subfolder: m.subfolder || "", + type: m.type || "output", + render_in: String(spec.renderIn != null ? spec.renderIn : 0), + }); + u += "?" + q.toString(); + } + return u; + }, }; window.MovieEditorAPI = ClientAPI; diff --git a/movie_editor/frontend/menubar.js b/movie_editor/frontend/menubar.js index 180a0bd0..1382c0cb 100644 --- a/movie_editor/frontend/menubar.js +++ b/movie_editor/frontend/menubar.js @@ -18,26 +18,109 @@ return inp; })(); - 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; + async function refreshGitStatus() { + try { _gitStatus = await window.MovieEditorAPI.gitStatus(); } + catch (_) { _gitStatus = { ok: false }; } + if (openName === "FunPack") render(); + } + + 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", "Restarting ComfyUI…\nThis page will reload when it's back."); + const msg = el("div", "restart-msg", message); 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(); + return msg; + } + + 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) msg.textContent = "Still waiting on ComfyUI…\nIt may need a manual restart — check the console."; + 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); // give it a moment to actually go down first + setTimeout(tick, 3500); } + 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 msg = _restartOverlay("Restarting ComfyUI…\nThis page will reload when it's back."); + try { await window.MovieEditorAPI.restart(); } catch (_) { /* connection drops as it execv's - expected */ } + _waitForComfyReload(msg, Date.now()); + } + + async function updateFunpackAndReload() { + let gs = _gitStatus; + if (!gs?.ok) { + try { gs = await window.MovieEditorAPI.gitStatus(); _gitStatus = gs; } catch (e) { alert(String(e.message || e)); return; } + } + if (!gs?.ok) { alert(gs?.detail || "Git status unavailable for this install."); return; } + if (gs.dirty) { + alert("Local changes detected in the FunPack folder.\nCommit or stash them before updating from git."); + return; + } + const branch = gs.branch || "dev"; + const behind = gs.behind > 0 ? `\n\n${gs.behind} commit(s) available on origin/${branch}.` : ""; + if (!confirm(`Pull latest "${branch}" from origin and restart ComfyUI?\n\nAny running generation will be lost.${behind}`)) return; + const msg = _restartOverlay(`Pulling origin/${branch}…\nComfyUI will restart when the pull finishes.`); + try { + const res = await window.MovieEditorAPI.gitUpdate(branch); + 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 || e)); + return; + } + _waitForComfyReload(msg, Date.now()); + } + + async function switchFunpackBranch() { + closeAll(); + let gs = _gitStatus; + if (!gs?.ok) { + try { gs = await window.MovieEditorAPI.gitStatus(); _gitStatus = gs; } catch (e) { alert(String(e.message || e)); return; } + } + if (!gs?.ok) { alert(gs?.detail || "Git status unavailable for this install."); return; } + if (gs.dirty) { + alert("Local changes detected in the FunPack folder.\nCommit or stash them before switching branches."); + return; + } + const branches = gs.branches || []; + if (!branches.length) { alert("No git branches found."); return; } + const cur = gs.branch; + if (!window.SlotPicker) return; + window.SlotPicker.open({ + title: "Switch FunPack branch", + options: branches.map((b) => ({ value: b, label: b, hint: b === cur ? "current" : "" })), + onPick: async (branch) => { + if (branch === cur) return; + if (!confirm(`Switch to "${branch}", pull from origin, and restart ComfyUI?\n\nAny running generation will be lost.`)) return; + const msg = _restartOverlay(`Switching to ${branch}…\nComfyUI will restart when ready.`); + try { + const res = await window.MovieEditorAPI.gitCheckout(branch); + msg.textContent = res.updated + ? `Switched to ${branch} (${res.before} → ${res.after}).\nRestarting ComfyUI…` + : `On ${branch}, already up to date.\nRestarting ComfyUI…`; + } catch (e) { + document.querySelector(".restart-overlay")?.remove(); + alert("Branch switch failed: " + (e.message || e)); + return; + } + _waitForComfyReload(msg, Date.now()); + }, + }); + } + + let _gitStatus = null; + function promptNewProject() { if (window.SlotPicker?.openPrompt) { window.SlotPicker.openPrompt({ @@ -60,6 +143,12 @@ const recent = (st.projects || []).slice(0, 8).map((p) => ({ label: p.name, hint: `${p.scene_count}▦`, action: () => S.loadProject(p.id), })); + const git = _gitStatus; + const gitOk = !!git?.ok; + const gitBranchLine = gitOk + ? `Branch: ${git.branch}${git.commit ? " @ " + git.commit : ""}${git.dirty ? " (local changes)" : ""}` + : (git?.detail ? `Git: ${git.detail}` : "Git: unavailable"); + const updateHint = gitOk && git.behind > 0 ? `${git.behind}↓` : "⬇⟳"; return { File: [ { label: "New Project", hint: "⌘N", action: promptNewProject }, @@ -111,6 +200,12 @@ { label: st.resetSessionArmed ? "Reset Studio session ✓ armed — click to cancel" : "Reset Studio session", disabled: !hasProject() || !studioOn, action: () => S.resetStudioSession() }, { sep: true }, + { menulabel: "Code updates" }, + { label: gitBranchLine, disabled: true }, + { label: "Switch branch…", disabled: !gitOk || git?.dirty, action: switchFunpackBranch }, + { label: gitOk && git.behind > 0 ? `Update FunPack and reload (${git.behind} new)` : "Update FunPack and reload", + hint: updateHint, disabled: !gitOk || git?.dirty, action: updateFunpackAndReload }, + { sep: true }, { menulabel: "Libraries (in ComfyUI Studio)" }, { label: "Open ComfyUI", hint: "↗", action: () => window.open("/", "_blank") }, { label: "Restart ComfyUI", hint: "⟳", danger: true, action: restartComfy }, @@ -147,7 +242,7 @@ function closeAll() { openName = null; veil.hidden = true; render(); } - function openMenu(name) { openName = name; veil.hidden = false; render(); } + function openMenu(name) { openName = name; veil.hidden = false; if (name === "FunPack") refreshGitStatus(); render(); } function render() { const spec = menuSpec(); diff --git a/movie_editor/frontend/models.js b/movie_editor/frontend/models.js index 8ffa888e..90e975db 100644 --- a/movie_editor/frontend/models.js +++ b/movie_editor/frontend/models.js @@ -70,6 +70,63 @@ }); } + function portToOpenCore(portId) { + const hit = (wiringRules.open_core_ports || []).find((p) => p.port === portId); + return hit ? [hit.core_id, hit.input] : null; + } + + function _clearPortWires(portId, exceptSlotId, exceptOut) { + const portTarget = "port:" + portId; + for (const slot of config.slots) { + slot.wires = slot.wires || {}; + for (const outName of Object.keys(slot.wires)) { + if (slot.id === exceptSlotId && outName === exceptOut) continue; + slot.wires[outName] = wireTargets(slot.wires[outName]).filter((t) => t !== portTarget); + } + } + } + + // Keep core_overrides and slot port wires in sync so loader wiring survives reload. + function reconcileOpenPortWiring() { + config.core_overrides = config.core_overrides || {}; + for (const slot of config.slots) { + for (const [outName, raw] of Object.entries(slot.wires || {})) { + for (const t of wireTargets(raw)) { + if (!t.startsWith("port:")) continue; + const portId = t.slice(5); + const map = portToOpenCore(portId); + if (!map) continue; + const [cid, inp] = map; + config.core_overrides[cid] = config.core_overrides[cid] || {}; + config.core_overrides[cid][inp] = `out:${slot.id}:${outName}`; + } + } + } + for (const entry of wiringRules.open_core_ports || []) { + const portId = entry.port; + const cid = entry.core_id; + const inp = entry.input; + const src = config.core_overrides?.[cid]?.[inp]; + const parsed = _parseOutSource(src); + if (!parsed) continue; + _clearPortWires(portId, parsed.slotId, parsed.out); + _addWire(parsed.slotId, parsed.out, "port:" + portId); + } + } + + function clearInternalCoreOverrides() { + if (!config.core_overrides) return; + const open = new Set( + (wiringRules.open_core_ports || []).map((p) => `${p.core_id}.${p.input}`), + ); + for (const cid of Object.keys(config.core_overrides)) { + for (const inp of Object.keys(config.core_overrides[cid] || {})) { + if (!open.has(`${cid}.${inp}`)) delete config.core_overrides[cid][inp]; + } + if (!Object.keys(config.core_overrides[cid] || {}).length) delete config.core_overrides[cid]; + } + } + function allowedSources(slot, ci) { const all = sources(slot, ci.type); if (!pipelineLocked()) return all; @@ -476,10 +533,10 @@ const sel = el("select", "wire-select"); dests.forEach((d) => { const o = el("option", null, d.label); o.value = d.value; if (d.value === t) o.selected = true; sel.append(o); }); if (t && !dests.some((d) => d.value === t)) { const o = el("option", null, t + " (not allowed)"); o.value = t; o.selected = true; sel.append(o); } - sel.onchange = async () => { _setWireTarget(slot, out.name, targets[i], sel.value); targets[i] = sel.value; await persist(); render(); }; + sel.onchange = async () => { _setWireTarget(slot, out.name, targets[i], sel.value); targets[i] = sel.value; reconcileOpenPortWiring(); await persist(); render(); }; const rm = el("button", "btn ghost tiny wire-rm", "×"); rm.title = "Remove this wire"; - rm.onclick = async () => { _setWireTarget(slot, out.name, targets[i], ""); targets.splice(i, 1); await persist(); render(); }; + rm.onclick = async () => { _setWireTarget(slot, out.name, targets[i], ""); targets.splice(i, 1); reconcileOpenPortWiring(); await persist(); render(); }; row.append(sel, rm); wrap.append(row); }); @@ -790,8 +847,29 @@ function setCoreOverride(cid, inp, value) { config.core_overrides = config.core_overrides || {}; config.core_overrides[cid] = config.core_overrides[cid] || {}; + const prev = config.core_overrides[cid][inp]; if (value) config.core_overrides[cid][inp] = value; else delete config.core_overrides[cid][inp]; + const parsed = _parseOutSource(value); + if (parsed) { + for (const entry of wiringRules.open_core_ports || []) { + if (entry.core_id === cid && entry.input === inp) { + _clearPortWires(entry.port, parsed.slotId, parsed.out); + _addWire(parsed.slotId, parsed.out, "port:" + entry.port); + break; + } + } + } else if (prev) { + const old = _parseOutSource(prev); + if (old) { + for (const entry of wiringRules.open_core_ports || []) { + if (entry.core_id === cid && entry.input === inp) { + _removeWire(old.slotId, old.out, "port:" + entry.port); + break; + } + } + } + } } function coreCard(n) { const card = el("div", "slot-card open" + (n.installed ? "" : " slot-bad")); @@ -852,7 +930,7 @@ : "Guided wiring ON — loaders wire only to their built-in ports. Click for full manual control."; fc.onclick = async () => { config.full_control = !config.full_control; - if (!config.full_control) config.core_overrides = {}; + if (!config.full_control) clearInternalCoreOverrides(); await persist(); try { coreNodes = (await API.coreGraph(window.Store?.get().project?.id)).nodes || coreNodes; } catch (_) {} render(); @@ -935,6 +1013,7 @@ wiringRules = pp.wiring || {}; } catch (_) { ports = []; coreProducers = []; requirements = []; wiringRules = {}; } try { config = await API.getModels(window.Store?.get().project?.id); } catch (_) { config = { slots: [] }; } + reconcileOpenPortWiring(); try { coreNodes = (await API.coreGraph(window.Store?.get().project?.id)).nodes || []; } catch (_) { coreNodes = []; } await prewarmSpecs(); diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index 3c9f80ff..d2523e13 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -17,6 +17,7 @@ // ── preloaded video pool ───────────────────────────────────────────────────── const stage = document.createElement("div"); stage.className = "pm-stage"; const pool = new Map(); // url ->