opencut_ui: fix image-drop anchor, preview stick, t2v removal, full rewiring
1. Image drop → i2v anchor: re-wire S.assignMediaToScene onto the timeline clip
(it was orphaned when the Plan card, its only caller, was removed).
2. Full rewiring in Full control: nodes.core_producers() now enumerates every
installed core node's outputs (Studio model/conditioning/sigmas, sampler
latent/images…), so a custom slot (e.g. a replacement sampler) can source
them via input_sources. core_graph() tags each output with source_id + lists
slot consumers; Models shows all core outputs (tappable in Full control).
3. Preview stuck on the media-bin preview ('anchor'): clear the transient
mediaPreviewId on transport scrub/seek (_seekNow), play (_play), and clip
selection (selectScene) so the program resumes following the playhead.
4. Remove t2v ('empty') as a selectable scene source; default new scenes to i2v
anchor. Anchorless scenes still fall back to t2v in the engine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
114e20ac48
commit
a1bf407e39
@@ -537,6 +537,19 @@ def core_graph(object_info: dict, models_config: dict | None = None) -> list[dic
|
||||
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']}"))
|
||||
|
||||
# Slot inputs that pull from a core output (input_sources "core:<cid>:<idx>") so each
|
||||
# core output can show where it's tapped — e.g. Studio · cond → MyCustomSampler · positive.
|
||||
slot_consumers: dict[tuple[str, int], list[str]] = {}
|
||||
for s in slots:
|
||||
lbl = slot_label.get(s.get("id"), s.get("id"))
|
||||
for inp, src in (s.get("input_sources") or {}).items():
|
||||
if isinstance(src, str) and src.startswith("core:"):
|
||||
try:
|
||||
_, ccid, cidx = src.split(":", 2)
|
||||
slot_consumers.setdefault((ccid, int(cidx)), []).append(f"{lbl} · {inp}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
open_by_node: dict[str, dict] = {}
|
||||
for (cid, inp, t, req) in OPEN_PORTS:
|
||||
open_by_node.setdefault(cid, {})[inp] = (t, req)
|
||||
@@ -575,7 +588,9 @@ def core_graph(object_info: dict, models_config: dict | None = None) -> list[dic
|
||||
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})
|
||||
dests += slot_consumers.get((cid, i), [])
|
||||
outputs.append({"name": o["name"], "type": o["type"], "to": dests,
|
||||
"source_id": f"core:{cid}:{i}"})
|
||||
nodes.append({"id": cid, "class": cls,
|
||||
"display_name": (nd or {}).get("display_name", cls),
|
||||
"installed": cls in object_info,
|
||||
|
||||
@@ -329,13 +329,35 @@ def pipeline_requirements() -> list[dict]:
|
||||
return PIPELINE_REQUIREMENTS
|
||||
|
||||
|
||||
def core_producers() -> list[dict]:
|
||||
"""Typed outputs from the fixed core that slot nodes can source from."""
|
||||
return [
|
||||
def core_producers(object_info: dict | None = None) -> list[dict]:
|
||||
"""Typed outputs from the fixed core that slot nodes can source from.
|
||||
|
||||
The project primitives are always available. When ``object_info`` is supplied, every
|
||||
installed core node's outputs are exposed too — so in Full control a custom node (e.g.
|
||||
a replacement sampler) can be fed Studio's model / conditioning / sigmas, or the
|
||||
sampler's latent. The slot picker only offers these in Full control (see allowedSources);
|
||||
build() resolves the chosen ``core:<id>:<idx>`` via _resolve_source."""
|
||||
out = [
|
||||
{"id": "core:frames:0", "type": "INT", "label": "Project frames (primitive)"},
|
||||
{"id": "core:fps:0", "type": "FLOAT", "label": "Project FPS (primitive)"},
|
||||
{"id": "core:f2i:0", "type": "INT", "label": "Project FPS as int (primitive)"},
|
||||
]
|
||||
if not object_info:
|
||||
return out
|
||||
from . import builder # lazy: avoid import cycle at module load
|
||||
seen = {p["id"] for p in out}
|
||||
for cid, cls in builder.CORE.items():
|
||||
nd = object_info.get(cls)
|
||||
if not nd:
|
||||
continue
|
||||
node_label = nd.get("display_name", cls)
|
||||
for i, o in enumerate(node_outputs(nd)):
|
||||
pid = f"core:{cid}:{i}"
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
out.append({"id": pid, "type": o["type"], "label": f"{node_label} → {o['name']}"})
|
||||
return out
|
||||
|
||||
|
||||
def pipeline_ports(object_info: dict | None = None) -> list[dict]:
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
const body = document.getElementById("inspector-body");
|
||||
const title = document.getElementById("inspector-title");
|
||||
|
||||
// t2v ("empty") is intentionally not selectable — scenes are i2v/v2v/carry. An
|
||||
// anchorless scene still falls back to t2v in the engine (effectiveSourceType).
|
||||
const SRC = [
|
||||
["empty", "Empty · text-to-video"],
|
||||
["image", "Image · i2v anchor"],
|
||||
["generated_frame", "From generated frame"],
|
||||
["v2v", "Video · v2v source"],
|
||||
@@ -130,12 +131,15 @@
|
||||
|
||||
function availableSourceOptions(st) {
|
||||
if (PC()?.usesChainSampler(st)) return SRC;
|
||||
return SRC.filter(([v]) => v === "empty" || v === "image");
|
||||
return SRC.filter(([v]) => v === "image");
|
||||
}
|
||||
|
||||
function renderSourceField(st, root, scene) {
|
||||
const chain = PC()?.usesChainSampler(st);
|
||||
const stored = root.source?.type || (chain ? "carry" : "empty");
|
||||
// t2v is removed as a mode — surface a legacy/default "empty" scene as the i2v
|
||||
// anchor option (no committed change until the user edits or assigns an anchor).
|
||||
let stored = root.source?.type || (chain ? "carry" : "image");
|
||||
if (stored === "empty") stored = "image";
|
||||
const opts = availableSourceOptions(st);
|
||||
if (!opts.some(([v]) => v === stored) && PC()?.isChainOnlySource(stored)) {
|
||||
const note = el("div", "insp-hint");
|
||||
@@ -146,7 +150,7 @@
|
||||
opts.forEach(([v, label]) => {
|
||||
const o = el("option", null, label);
|
||||
o.value = v;
|
||||
if (v === stored || (!opts.some(([vv]) => vv === stored) && v === "empty")) o.selected = true;
|
||||
if (v === stored) o.selected = true;
|
||||
src.append(o);
|
||||
});
|
||||
if (stored && !opts.some(([v]) => v === stored)) {
|
||||
|
||||
@@ -979,7 +979,10 @@
|
||||
});
|
||||
card.append(box);
|
||||
}
|
||||
const outs = (n.outputs || []).filter((o) => (o.to || []).length);
|
||||
// Show every core output. In Full control these are tappable as slot input sources
|
||||
// (pick "<node> → <output>" in a slot's Input sources), so the user can, e.g., feed a
|
||||
// replacement sampler with Studio's conditioning. In guided mode they're informational.
|
||||
const outs = n.outputs || [];
|
||||
if (outs.length) {
|
||||
const box = el("div", "wire-box");
|
||||
box.append(el("div", "wire-title", "Outputs → destinations"));
|
||||
@@ -987,7 +990,12 @@
|
||||
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(", ")));
|
||||
const dest = (o.to || []).length
|
||||
? o.to.join(", ")
|
||||
: (config.full_control ? "available — pick in a slot's Input sources" : "—");
|
||||
const destEl = el("span", "wire-dest", dest);
|
||||
if (!(o.to || []).length && !config.full_control) destEl.classList.add("wire-muted");
|
||||
row.append(destEl);
|
||||
box.append(row);
|
||||
});
|
||||
card.append(box);
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
}
|
||||
|
||||
function defaultSceneSourceType(st) {
|
||||
return usesChainSampler(st) ? "carry" : "empty";
|
||||
// t2v ("empty") is no longer a user-facing mode — new scenes default to an i2v
|
||||
// anchor (image). Anchorless scenes still fall back to t2v in the engine.
|
||||
return usesChainSampler(st) ? "carry" : "image";
|
||||
}
|
||||
|
||||
function sourceLabel(type) {
|
||||
|
||||
@@ -737,6 +737,9 @@
|
||||
|
||||
// ── transport actions ──────────────────────────────────────────────────────
|
||||
function _seekNow(sec) {
|
||||
// Any transport scrub/seek exits the transient media-bin preview so the program
|
||||
// resumes following the playhead (otherwise it stays frozen on the previewed image).
|
||||
if (S.get().mediaPreviewId) S.clearMediaPreview();
|
||||
_phSec = Math.max(0, sec);
|
||||
const clip = _clipAt(_phSec);
|
||||
if (!clip) {
|
||||
@@ -808,6 +811,7 @@
|
||||
}
|
||||
|
||||
function _play() {
|
||||
if (S.get().mediaPreviewId) S.clearMediaPreview();
|
||||
// At (or past) the end of the timeline → loop the playhead back to the start.
|
||||
if (_phSec >= (_totalSecCur || 0) - 0.05 || !_clipFrom(_phSec)) _phSec = 0;
|
||||
const clip = _clipFrom(_phSec);
|
||||
|
||||
@@ -1016,6 +1016,7 @@
|
||||
if (!scene(id)) return;
|
||||
state.selectedOverlayId = null;
|
||||
state.selectedAudioTrackId = null;
|
||||
state.mediaPreviewId = null; // tapping a clip exits the transient media-bin preview
|
||||
|
||||
if (range && _selectionAnchorId && order.includes(_selectionAnchorId)) {
|
||||
const a = order.indexOf(_selectionAnchorId);
|
||||
|
||||
@@ -1237,6 +1237,7 @@ label.field.linked input, label.field.linked select { opacity: .6; }
|
||||
.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-dest.wire-muted { color: var(--faint); opacity: .7; }
|
||||
.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; }
|
||||
|
||||
@@ -601,9 +601,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
// i2v anchors are set on the PLAN, not here — the timeline is the rendered result.
|
||||
// (Drop an image onto a Plan card to anchor that scene.) Video clips still drop on the
|
||||
// timeline track below.
|
||||
// Drop an image (or video) from the Media bin onto a generative clip to set its i2v
|
||||
// anchor. This used to live on the Plan card; the single-timeline layout moved it here.
|
||||
if (!subclip && S.isGenerativeScene(scene)) {
|
||||
clip.addEventListener("dragover", (e) => {
|
||||
if (!e.dataTransfer.types.includes("application/funpack-media")) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
clip.classList.add("drop-target");
|
||||
});
|
||||
clip.addEventListener("dragleave", (e) => {
|
||||
if (!clip.contains(e.relatedTarget)) clip.classList.remove("drop-target");
|
||||
});
|
||||
clip.addEventListener("drop", (e) => {
|
||||
clip.classList.remove("drop-target");
|
||||
const id = e.dataTransfer.getData("application/funpack-media");
|
||||
if (!id) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
S.assignMediaToScene(scene.id, id);
|
||||
});
|
||||
}
|
||||
|
||||
// i2v anchor thumbnail (image + mixed + generated_frame)
|
||||
const mref = anchorMediaRef(scene, p);
|
||||
|
||||
@@ -1789,7 +1789,7 @@ if web is not None and PromptServer is not None:
|
||||
oi = None
|
||||
return web.json_response({
|
||||
"ports": nodes.pipeline_ports(oi),
|
||||
"core_producers": nodes.core_producers(),
|
||||
"core_producers": nodes.core_producers(oi),
|
||||
"requirements": nodes.pipeline_requirements(),
|
||||
"wiring": pipeline_wiring.wiring_rules_payload(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user