Movie Editor: per-scene Studio rating + wire outputs into widget inputs
Rating: - Scene.rating (V2_RATING_LABELS); /api/rating-labels exposes the options. - Timeline toolbar 'Scene N ★' dropdown, shown ONLY when FunPack Studio is the conditioning provider AND the selected clip has a render. Rates that clip's own scene (cut-aware by scene id). - Generation injects the run's rating into Studio (studio_inputs.rating) so Studio refines the next generation from it; empty = use Studio's default. Wiring: - Widget inputs (INT/FLOAT/STRING/BOOLEAN, e.g. EmptyLatentVideo.width/height) are now offered as manual wire destinations (ComfyUI converts a widget to an input when wired) — so ImageTransform's width/height outputs can drive them. They're still not auto-wired, so widget values aren't clobbered unless wired explicitly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
77b4084a81
commit
c38ba2c09e
@@ -53,6 +53,15 @@ def parse_timeline_verbatim(prompt: str) -> dict:
|
||||
return split_timeline_verbatim(str(prompt or ""))
|
||||
|
||||
|
||||
def rating_labels() -> dict:
|
||||
"""FunPack Studio V2 rating labels (for the Scene rating dropdown)."""
|
||||
try:
|
||||
from conditioning import V2_RATING_LABELS
|
||||
except ImportError: # package submodule
|
||||
from ...conditioning import V2_RATING_LABELS # type: ignore
|
||||
return {"labels": list(V2_RATING_LABELS)}
|
||||
|
||||
|
||||
def transitions() -> dict:
|
||||
_, _, load_transition_db, transition_items = _funpack_imports()
|
||||
data = load_transition_db()
|
||||
|
||||
@@ -64,6 +64,9 @@ class Scene:
|
||||
width: Optional[int] = None
|
||||
height: Optional[int] = None
|
||||
source: SceneSource = field(default_factory=SceneSource)
|
||||
# FunPack Studio RLHF rating of this scene's last render (a V2_RATING_LABELS value).
|
||||
# Fed into Studio at the next generation of this scene's run so it refines. "" = unrated.
|
||||
rating: str = ""
|
||||
# Selective generation (UI-functional only in Phase 3 — see plan). `excluded`
|
||||
# scenes are skipped by a full Generate/Render; "generate this scene only" is a
|
||||
# transient request handled at the API/route level, not stored here.
|
||||
@@ -83,6 +86,7 @@ class Scene:
|
||||
width=d.get("width"),
|
||||
height=d.get("height"),
|
||||
source=SceneSource.from_dict(d.get("source")),
|
||||
rating=str(d.get("rating", "")),
|
||||
excluded=bool(d.get("excluded", False)),
|
||||
)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
j("POST", API(`/projects/${id}/generate`), { only_scene: onlyScene || null, scene_ids: sceneIds || null }),
|
||||
status: (id, promptId) => j("GET", API(`/projects/${id}/status/${promptId}`)),
|
||||
progress: () => j("GET", API("/progress")),
|
||||
ratingLabels: () => j("GET", API("/rating-labels")),
|
||||
interrupt: () => j("POST", API("/interrupt")),
|
||||
renderFinal: (id, clips) => j("POST", API(`/projects/${id}/render`), { clips }),
|
||||
resultUrl: (id, m) =>
|
||||
|
||||
@@ -315,6 +315,7 @@
|
||||
return card;
|
||||
}
|
||||
|
||||
const _KIND2T = { int: "INT", float: "FLOAT", string: "STRING", boolean: "BOOLEAN" };
|
||||
function destinations(slot, type) {
|
||||
const out = [{ value: "", label: "— unwired —" }];
|
||||
ports.filter((p) => p.type === type).forEach((p) => out.push({ value: "port:" + p.id, label: p.label }));
|
||||
@@ -322,6 +323,10 @@
|
||||
const c2 = specFor(s2);
|
||||
(c2?.connection_inputs || []).filter((ci) => ci.type === type).forEach((ci) =>
|
||||
out.push({ value: `node:${s2.id}:${ci.name}`, label: `${slotFullLabel(s2)} · ${ci.name}` }));
|
||||
// Widget inputs can also receive a connection (ComfyUI converts a widget to an
|
||||
// input when wired) — e.g. EmptyLatentVideo.width/height. Offer them as targets.
|
||||
(c2?.inputs || []).filter((w) => _KIND2T[w.kind] === type).forEach((w) =>
|
||||
out.push({ value: `node:${s2.id}:${w.name}`, label: `${slotFullLabel(s2)} · ${w.name} (widget)` }));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
mediaBin: [], // uploaded assets [{id,name,kind,...}]
|
||||
shortcuts: [], // prompt shortcut library
|
||||
imageTargets: [], // where an image asset can be wired [{value,label}]
|
||||
ratingLabels: [], // FunPack Studio V2 rating options
|
||||
};
|
||||
|
||||
const listeners = new Set();
|
||||
@@ -601,6 +602,7 @@
|
||||
try { const t = await API.transitions(); state.transitions = t.transitions || []; } catch (_) { state.transitions = []; }
|
||||
await loadShortcuts();
|
||||
await loadMedia();
|
||||
try { state.ratingLabels = (await API.ratingLabels()).labels || []; } catch (_) { state.ratingLabels = []; }
|
||||
await loadModels();
|
||||
window.addEventListener("funpack-models-changed", loadModels);
|
||||
await refreshProjectList();
|
||||
@@ -619,5 +621,6 @@
|
||||
loadMedia, uploadMedia, deleteMedia, assignMediaToScene,
|
||||
loadShortcuts, saveShortcut, deleteShortcut, importShortcuts, loadTransitions, saveTransition, deleteTransition, importTransitions,
|
||||
applyTransitionToSelection, insertShortcutIntoSelection,
|
||||
setSceneRating: (id, v) => patchScene(id, { rating: v }),
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -336,6 +336,7 @@ input[type=number] { font-family: var(--font-mono); }
|
||||
.tl-tc { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
|
||||
.tl-zlabel { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--faint); }
|
||||
.tl-keys { font-family: var(--font-mono); font-size: 10px; letter-spacing: .5px; color: var(--faint); margin-right: 10px; opacity: .8; }
|
||||
.tl-rating { font-size: 12px; padding: 3px 6px; background: var(--ink-2); border: 1px solid var(--line); border-radius: 6px; color: var(--amber-2); max-width: 200px; }
|
||||
|
||||
.tl-scroll { flex: 1; overflow-x: auto; overflow-y: hidden; position: relative; }
|
||||
.tl-content { position: relative; height: 100%; min-height: 100%; }
|
||||
|
||||
@@ -200,6 +200,22 @@
|
||||
exp.onclick = () => S.exportSelected();
|
||||
bar.append(split); bar.append(del); bar.append(exp);
|
||||
|
||||
// Scene rating — only when FunPack Studio is the conditioning provider AND the
|
||||
// selected clip has a render. Rates that clip's own scene (cut-aware by scene id);
|
||||
// fed into Studio at the next generation of its run.
|
||||
const studioCond = !st.project.conditioning_slot || st.project.conditioning_slot === "funpack";
|
||||
if (studioCond && hasSel && hasRender(st, st.selectedSceneId) && (st.ratingLabels || []).length) {
|
||||
const sc = S.scene(st.selectedSceneId);
|
||||
const sceneNo = p.scenes.indexOf(sc) + 1;
|
||||
const rlabel = el("span", "tl-keys", `★ Scene ${sceneNo}`);
|
||||
const rsel = el("select", "tl-rating");
|
||||
rsel.title = "Rate this scene's render — FunPack Studio refines from it next generation";
|
||||
rsel.append(new Option("— rate —", ""));
|
||||
(st.ratingLabels || []).forEach((l) => { const o = new Option(l, l); if (l === (sc.rating || "")) o.selected = true; rsel.append(o); });
|
||||
rsel.onchange = () => S.setSceneRating(sc.id, rsel.value);
|
||||
bar.append(rlabel); bar.append(rsel);
|
||||
}
|
||||
|
||||
const spacer = el("div", "tl-spacer"); bar.append(spacer);
|
||||
const keys = el("span", "tl-keys", "S split · ⌫ remove"); keys.title = "Select a clip, then: S splits it at the playhead · Delete/Backspace removes it";
|
||||
bar.append(keys);
|
||||
|
||||
+20
-1
@@ -128,6 +128,18 @@ def _solo(p: Project, only_scene: Optional[str]) -> Project:
|
||||
return clone
|
||||
|
||||
|
||||
def _run_studio_inputs(target: Project, active_scenes: list) -> dict:
|
||||
"""Studio widget overrides for this run, incl. the RLHF rating of the run's last
|
||||
render (Studio refines the next generation from it). Uses the run's first rated scene."""
|
||||
if target.conditioning_slot != "funpack":
|
||||
return {}
|
||||
si = dict(target.studio_inputs or {})
|
||||
rating = next((s.rating for s in active_scenes if (getattr(s, "rating", "") or "").strip()), None)
|
||||
if rating:
|
||||
si["rating"] = rating
|
||||
return si
|
||||
|
||||
|
||||
def _run_sampler_inputs(target: Project, scene_count: int) -> dict:
|
||||
"""Sampler widget overrides for one chain run. Carries inside a multi-scene run
|
||||
must overlap, so carry_i2v_guides is forced on; a 1-scene run leaves it alone."""
|
||||
@@ -453,7 +465,7 @@ if web is not None and PromptServer is not None:
|
||||
"width": target.width, "height": target.height,
|
||||
"negative_prompt": target.negative_prompt or None,
|
||||
"max_scenes": active_scene_count,
|
||||
"studio_inputs": target.studio_inputs if target.conditioning_slot == "funpack" else {},
|
||||
"studio_inputs": _run_studio_inputs(target, active_scenes),
|
||||
"sampler_inputs": _run_sampler_inputs(target, active_scene_count),
|
||||
}, media=_prepare_media(target))
|
||||
if report["blocking"]:
|
||||
@@ -497,6 +509,13 @@ if web is not None and PromptServer is not None:
|
||||
running = True
|
||||
return web.json_response({"state": "running" if running else "pending", "media": []})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/rating-labels")
|
||||
async def _rating_labels(_req):
|
||||
try:
|
||||
return web.json_response(bridge.rating_labels())
|
||||
except Exception as e: # noqa: BLE001
|
||||
return web.json_response({"labels": [], "error": str(e)})
|
||||
|
||||
@routes.get(UI_PREFIX + "/api/progress")
|
||||
async def _progress(_req):
|
||||
return web.json_response(bridge.current_progress())
|
||||
|
||||
Reference in New Issue
Block a user