Movie Editor: show + re-wire the built-in core pipeline
The Models modal only listed user loader slots; the fixed core (Studio, Sampler, Conditioning, Concat/Separate, audio decode, VHS, SaveRefinement, KeyLoader, primitives) was invisible, so internal wiring couldn't be inspected or changed. Add builder.core_graph() describing each core node's inputs (current source + candidate sources) and outputs (internal destinations), exposed at /api/core-graph. New collapsible 'Built-in pipeline' panel in the Models modal renders it and — not read-only — lets each core input be re-wired via a source picker. Overrides are stored per-project as models.core_overrides and applied by build() (4b), beating the default CORE_LINKS wiring and auto-wire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c0dff7893d
commit
023f42eb8a
@@ -344,6 +344,21 @@ 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: redirect a built-in core node input to a chosen source
|
||||
# ("core:<coreId>:<idx>" or "out:<slotId>:<outName>"). Overrides the default wiring.
|
||||
for cid, ovs in ((models_config or {}).get("core_overrides") or {}).items():
|
||||
if cid not in graph:
|
||||
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.")
|
||||
|
||||
# 5. auto-wire remaining unbound typed inputs by unique producer.
|
||||
producers = _producers(graph, slots, slot_node_id, slot_def, object_info)
|
||||
_autowire(graph, slots, slot_node_id, slot_def, object_info, producers, report)
|
||||
@@ -351,6 +366,83 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
|
||||
return graph, report
|
||||
|
||||
|
||||
def core_graph(object_info: dict, models_config: dict | None = None) -> list[dict]:
|
||||
"""Describe the built-in core pipeline for the editor: each node, its inputs (with the
|
||||
current source + candidate sources so they can be re-wired) and outputs (with internal
|
||||
destinations). Inputs default to the built-in wiring; `core_overrides` in models_config
|
||||
can redirect any of them (applied by build())."""
|
||||
object_info = object_info or {}
|
||||
overrides = (models_config or {}).get("core_overrides") or {}
|
||||
slots = (models_config or {}).get("slots", []) or []
|
||||
|
||||
rev: dict[str, list] = {}
|
||||
for cid, links in CORE_LINKS.items():
|
||||
for inp, (src, idx) in links.items():
|
||||
rev.setdefault(src, []).append((idx, cid, inp))
|
||||
|
||||
def _outs(core_id):
|
||||
return node_outputs(object_info.get(CORE.get(core_id)) or {})
|
||||
|
||||
def _out_name(core_id, idx):
|
||||
outs = _outs(core_id)
|
||||
return outs[idx]["name"] if 0 <= idx < len(outs) else f"out{idx}"
|
||||
|
||||
# typed producers (core outputs + slot outputs) for the source pickers
|
||||
producers: dict[str, list] = {}
|
||||
for cid2, cls2 in CORE.items():
|
||||
for i, o in enumerate(node_outputs(object_info.get(cls2) or {})):
|
||||
producers.setdefault(o["type"], []).append((f"core:{cid2}:{i}", f"{cid2} · {o['name']}"))
|
||||
slot_label = {}
|
||||
for s in slots:
|
||||
sid = s.get("id"); lbl = s.get("label") or s.get("node_class") or sid
|
||||
slot_label[sid] = lbl
|
||||
for o in node_outputs(object_info.get(s.get("node_class")) or {}):
|
||||
producers.setdefault(o["type"], []).append((f"out:{sid}:{o['name']}", f"{lbl} · {o['name']}"))
|
||||
|
||||
open_by_node: dict[str, dict] = {}
|
||||
for (cid, inp, t, req) in OPEN_PORTS:
|
||||
open_by_node.setdefault(cid, {})[inp] = (t, req)
|
||||
|
||||
def _input_type(cid, inp):
|
||||
for ci in connection_inputs(object_info.get(CORE.get(cid)) or {}):
|
||||
if ci["name"] == inp:
|
||||
return ci["type"]
|
||||
return None
|
||||
|
||||
def _options(t, builtin_label, self_cid):
|
||||
opts = [{"value": "", "label": f"built-in: {builtin_label}"}]
|
||||
for val, lbl in producers.get(t, []):
|
||||
if val.startswith(f"core:{self_cid}:"):
|
||||
continue
|
||||
opts.append({"value": val, "label": lbl})
|
||||
return opts
|
||||
|
||||
nodes = []
|
||||
for cid, cls in CORE.items():
|
||||
nd = object_info.get(cls)
|
||||
ov = overrides.get(cid) or {}
|
||||
inputs = []
|
||||
for inp, (src, idx) in CORE_LINKS.get(cid, {}).items():
|
||||
t = _input_type(cid, inp) or "*"
|
||||
builtin = f"{src} · {_out_name(src, idx)}"
|
||||
inputs.append({"name": inp, "type": t, "from": "internal", "detail": builtin,
|
||||
"value": ov.get(inp, ""), "options": _options(t, builtin, cid)})
|
||||
for inp, (t, req) in open_by_node.get(cid, {}).items():
|
||||
builtin = f"a {t} from your loaders"
|
||||
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)})
|
||||
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]
|
||||
outputs.append({"name": o["name"], "type": o["type"], "to": dests})
|
||||
nodes.append({"id": cid, "class": cls,
|
||||
"display_name": (nd or {}).get("display_name", cls),
|
||||
"installed": cls in object_info,
|
||||
"inputs": inputs, "outputs": outputs})
|
||||
return nodes
|
||||
|
||||
|
||||
def _port_index(object_info: dict) -> dict[str, tuple[str, str]]:
|
||||
"""Map pipeline-port id ('Class.input' / 'FunPackStudio.input') -> (core_id, input)."""
|
||||
cls_to_core = {v: k for k, v in CORE.items()}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
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.
|
||||
coreGraph: (pid) => j("GET", API("/core-graph" + (pid ? `?pid=${encodeURIComponent(pid)}` : ""))),
|
||||
getModels: (pid) => j("GET", API(pid ? `/projects/${pid}/models` : "/models")),
|
||||
saveModels: (pid, data) => j("PUT", API(pid ? `/projects/${pid}/models` : "/models"), data),
|
||||
refreshModels: () => j("POST", API("/models/refresh")),
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
const candCache = {}; // role -> [candidate]
|
||||
const specByClass = {}; // class -> full node spec (inputs/outputs/connection_inputs)
|
||||
let config = { slots: [] }; // {slots:[{id,role,role_label,node_class,inputs:{},wires:{},input_sources:{}}]}
|
||||
let coreNodes = []; // built-in pipeline description (from /core-graph)
|
||||
let coreOpen = false; // built-in pipeline panel expanded?
|
||||
let coreProducers = []; // [{id,type,label}]
|
||||
let requirements = []; // [{id,type,label,required,role_hint,hint}]
|
||||
let overlay = null;
|
||||
@@ -670,11 +672,72 @@
|
||||
return sec;
|
||||
}
|
||||
|
||||
// ── built-in pipeline (core) — visible + re-wireable ──────────────────────────
|
||||
function setCoreOverride(cid, inp, value) {
|
||||
config.core_overrides = config.core_overrides || {};
|
||||
config.core_overrides[cid] = config.core_overrides[cid] || {};
|
||||
if (value) config.core_overrides[cid][inp] = value;
|
||||
else delete config.core_overrides[cid][inp];
|
||||
}
|
||||
function coreCard(n) {
|
||||
const card = el("div", "slot-card open" + (n.installed ? "" : " slot-bad"));
|
||||
const head = el("div", "slot-head");
|
||||
head.append(el("span", "slot-role", n.display_name));
|
||||
head.append(el("span", "slot-node", n.class));
|
||||
if (!n.installed) head.append(el("span", "slot-badge bad", "not installed"));
|
||||
card.append(head);
|
||||
if ((n.inputs || []).length) {
|
||||
const box = el("div", "wire-box");
|
||||
box.append(el("div", "wire-title", "Inputs ← source"));
|
||||
n.inputs.forEach((inp) => {
|
||||
const row = el("div", "wire-row");
|
||||
row.append(el("span", "wire-out", `${inp.name} (${inp.type})`));
|
||||
row.append(el("span", "wire-arrow", "←"));
|
||||
const sel = el("select", "wire-select");
|
||||
(inp.options || []).forEach((o) => { const op = el("option", null, o.label); op.value = o.value; if (o.value === (inp.value || "")) op.selected = true; sel.append(op); });
|
||||
if (inp.value && !(inp.options || []).some((o) => o.value === inp.value)) { const op = el("option", null, inp.value + " (missing)"); op.value = inp.value; op.selected = true; sel.append(op); }
|
||||
sel.onchange = async () => { setCoreOverride(n.id, inp.name, sel.value); await persist(); coreNodes = (await API.coreGraph(window.Store?.get().project?.id).catch(() => ({}))).nodes || coreNodes; render(); };
|
||||
row.append(sel);
|
||||
box.append(row);
|
||||
});
|
||||
card.append(box);
|
||||
}
|
||||
const outs = (n.outputs || []).filter((o) => (o.to || []).length);
|
||||
if (outs.length) {
|
||||
const box = el("div", "wire-box");
|
||||
box.append(el("div", "wire-title", "Outputs → destinations"));
|
||||
outs.forEach((o) => {
|
||||
const row = el("div", "wire-row");
|
||||
row.append(el("span", "wire-out", `${o.name} (${o.type})`));
|
||||
row.append(el("span", "wire-arrow", "→"));
|
||||
row.append(el("span", "wire-dest", o.to.join(", ")));
|
||||
box.append(row);
|
||||
});
|
||||
card.append(box);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
function coreSection() {
|
||||
const sec = el("div", "links-section");
|
||||
const head = el("div", "links-head");
|
||||
const toggle = el("button", "btn ghost tiny", (coreOpen ? "▾ " : "▸ ") + "Built-in pipeline");
|
||||
toggle.onclick = () => { coreOpen = !coreOpen; render(); };
|
||||
head.append(toggle);
|
||||
head.append(el("span", "lib-sub", `${coreNodes.length} fixed nodes`));
|
||||
sec.append(head);
|
||||
if (coreOpen) {
|
||||
sec.append(el("div", "links-hint", "The fixed FunPack nodes and their wiring. Each input defaults to its built-in source — pick another to re-wire it."));
|
||||
coreNodes.forEach((n) => sec.append(coreCard(n)));
|
||||
}
|
||||
return sec;
|
||||
}
|
||||
|
||||
function body() {
|
||||
const b = el("div", "models-body");
|
||||
b.append(requirementsPanel());
|
||||
b.append(composer());
|
||||
b.append(linksSection());
|
||||
b.append(coreSection());
|
||||
const v = validation();
|
||||
const list = el("div", "slot-list");
|
||||
if (!config.slots.length) list.append(el("div", "empty-stage", "No models configured yet — use the form above to add loaders."));
|
||||
@@ -700,6 +763,7 @@
|
||||
Object.keys(specByClass).forEach((k) => delete specByClass[k]);
|
||||
allNodes = null;
|
||||
try { await API.refreshModels(); } catch (_) {}
|
||||
try { coreNodes = (await API.coreGraph(window.Store?.get().project?.id)).nodes || []; } catch (_) {}
|
||||
await prewarmSpecs();
|
||||
render();
|
||||
}
|
||||
@@ -708,6 +772,7 @@
|
||||
await ensureRoles();
|
||||
try { const pp = await API.pipelinePorts(); ports = pp.ports || []; coreProducers = pp.core_producers || []; requirements = pp.requirements || []; } catch (_) { ports = []; coreProducers = []; requirements = []; }
|
||||
try { config = await API.getModels(window.Store?.get().project?.id); } catch (_) { config = { slots: [] }; }
|
||||
try { coreNodes = (await API.coreGraph(window.Store?.get().project?.id)).nodes || []; } catch (_) { coreNodes = []; }
|
||||
await prewarmSpecs();
|
||||
|
||||
overlay = el("div", "modal-overlay");
|
||||
|
||||
@@ -493,6 +493,7 @@ label.field.linked input, label.field.linked select { opacity: .6; }
|
||||
.wire-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||
.wire-out { font-family: var(--font-mono); font-size: 11px; color: var(--muted); flex: 0 0 auto; min-width: 130px; }
|
||||
.wire-arrow { color: var(--teal); }
|
||||
.wire-dest { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
|
||||
.wire-select { flex: 1; }
|
||||
.wire-multi { display: flex; flex-direction: column; gap: 4px; flex: 1; }
|
||||
.wire-multi-row { display: flex; gap: 5px; align-items: center; }
|
||||
|
||||
+11
-1
@@ -112,7 +112,7 @@ def _project_or_404(pid: str) -> Project:
|
||||
def _project_models(p: Optional[Project]) -> dict:
|
||||
"""The project's own pipeline config, or the global default when it has none yet."""
|
||||
m = getattr(p, "models", None) or {}
|
||||
if m.get("slots") or m.get("links"):
|
||||
if m.get("slots") or m.get("links") or m.get("core_overrides"):
|
||||
return m
|
||||
return nodes.load_models()
|
||||
|
||||
@@ -624,6 +624,16 @@ if web is not None and PromptServer is not None:
|
||||
out.append({"value": f"node:{slot['id']}:{ci['name']}", "label": label})
|
||||
return web.json_response({"targets": out})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/core-graph")
|
||||
async def _core_graph(req):
|
||||
try:
|
||||
oi = await bridge.object_info()
|
||||
except Exception:
|
||||
oi = {}
|
||||
pid = req.query.get("pid")
|
||||
models_cfg = _project_models(projects.get(pid)) if pid else nodes.load_models()
|
||||
return web.json_response({"nodes": builder.core_graph(oi, models_cfg)})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/all-nodes")
|
||||
async def _all_nodes(_req):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user