Movie Editor: keep shortcuts verbatim outside preview/generation
Global prompt and 'Sync from preview' were writing shortcut-EXPANDED scene text into scenes (parse_timeline expands before splitting), so the verbatim combined prompt then showed expanded shortcuts. Add bridge.parse_timeline_raw() (split without expansion) exposed as parsed_raw on /parse and /preview; apply/sync now take scene text from the raw split (shortcuts preserved) and use the expanded split only to DETECT transitions (incl. shortcut->transition), mapping them to the library by visual_effect. Generation and the split preview still expand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1ffb7abe29
commit
e1e3261ca0
@@ -27,12 +27,21 @@ def _funpack_imports():
|
||||
|
||||
|
||||
def parse_timeline(prompt: str, seed: int = 0) -> dict:
|
||||
"""{anchor, scenes, transitions} — the canonical split Studio will see."""
|
||||
"""{anchor, scenes, transitions} — the canonical split Studio will see.
|
||||
Shortcuts are expanded first (as generation does)."""
|
||||
parse_timeline_segments, apply_prompt_shortcuts, *_ = _funpack_imports()
|
||||
expanded, _applied = apply_prompt_shortcuts(str(prompt or ""), seed=int(seed or 0))
|
||||
return parse_timeline_segments(expanded)
|
||||
|
||||
|
||||
def parse_timeline_raw(prompt: str) -> dict:
|
||||
"""Split WITHOUT expanding shortcuts — scene texts stay verbatim (shortcuts and
|
||||
transition triggers preserved). Used by editing views so the user keeps operating
|
||||
on shortcuts unchanged; only the full preview / generation expand them."""
|
||||
parse_timeline_segments, *_ = _funpack_imports()
|
||||
return parse_timeline_segments(str(prompt or ""))
|
||||
|
||||
|
||||
def transitions() -> dict:
|
||||
_, _, load_transition_db, transition_items = _funpack_imports()
|
||||
data = load_transition_db()
|
||||
|
||||
@@ -213,30 +213,28 @@
|
||||
// ── sync scenes from preview (distribute parsed anchor/transitions back) ──────
|
||||
function syncFromPreview() {
|
||||
if (!state.project || !state.preview) return;
|
||||
const parsed = state.preview.parsed || {};
|
||||
const parsedScenes = parsed.scenes || [];
|
||||
const parsedTrans = parsed.transitions || [];
|
||||
// raw = verbatim split (shortcuts preserved); exp = expanded (transition detection)
|
||||
const raw = state.preview.parsed_raw || state.preview.parsed || {};
|
||||
const exp = state.preview.parsed || raw;
|
||||
const parsedScenes = raw.scenes || [];
|
||||
if (!parsedScenes.length) return;
|
||||
|
||||
// Sync anchor
|
||||
if (parsed.anchor != null) state.project.anchor = parsed.anchor;
|
||||
if (raw.anchor != null) state.project.anchor = raw.anchor;
|
||||
|
||||
// Sync scene texts (only if count matches to avoid destructive mismatches)
|
||||
// Sync scene texts verbatim (only if count matches to avoid destructive mismatches)
|
||||
const activeScenes = state.project.scenes.filter((s) => !s.excluded);
|
||||
if (parsedScenes.length === activeScenes.length) {
|
||||
parsedScenes.forEach((ps, i) => { if (ps.text) activeScenes[i].text = ps.text; });
|
||||
}
|
||||
|
||||
// Sync transitions: parsed.transitions[i] → scene[i].transition_to_next
|
||||
// We derive the trigger from the library by matching visual_effect.
|
||||
parsedTrans.forEach((t) => {
|
||||
// Transitions: only fill seams the user hasn't set, mapped via the library.
|
||||
const trans = (((exp.scenes || []).length === parsedScenes.length ? exp.transitions : raw.transitions) || []);
|
||||
trans.forEach((t) => {
|
||||
const idx = t.after_scene;
|
||||
const sceneIdx = idx >= 0 ? idx : -1;
|
||||
if (sceneIdx >= 0 && sceneIdx < activeScenes.length) {
|
||||
const s = activeScenes[sceneIdx];
|
||||
if (!s.transition_to_next && t.visual_effect) {
|
||||
const lib = (state.transitions || []).find((tr) =>
|
||||
(tr.visual_effect || "none") === t.visual_effect);
|
||||
if (idx >= 0 && idx < activeScenes.length && t.visual_effect) {
|
||||
const s = activeScenes[idx];
|
||||
if (!s.transition_to_next) {
|
||||
const lib = (state.transitions || []).find((tr) => (tr.visual_effect || "none") === t.visual_effect);
|
||||
if (lib) s.transition_to_next = lib.trigger || lib.name || "";
|
||||
}
|
||||
}
|
||||
@@ -253,31 +251,42 @@
|
||||
async function applyGlobalPrompt(text) {
|
||||
if (!state.project) return;
|
||||
state.project.global_prompt = text;
|
||||
let parsed;
|
||||
try { parsed = (await API.parsePrompt(state.project.id, text)).parsed; }
|
||||
let res;
|
||||
try { res = await API.parsePrompt(state.project.id, text); }
|
||||
catch (e) { alert("Could not parse the global prompt: " + e.message); return; }
|
||||
if (!parsed || !(parsed.scenes || []).length) { alert("Nothing parsed — no scenes detected."); return; }
|
||||
// raw = verbatim split (shortcuts kept in scene text); exp = expanded split, used
|
||||
// ONLY to detect transitions (incl. those a shortcut expands into).
|
||||
const raw = res.parsed_raw || res.parsed;
|
||||
const exp = res.parsed || raw;
|
||||
if (!raw || !(raw.scenes || []).length) { alert("Nothing parsed — no scenes detected."); return; }
|
||||
|
||||
if (parsed.anchor != null) state.project.anchor = parsed.anchor;
|
||||
if (raw.anchor != null) state.project.anchor = raw.anchor;
|
||||
const old = state.project.scenes || [];
|
||||
const next = (parsed.scenes || []).map((ps, i) => {
|
||||
const next = (raw.scenes || []).map((ps, i) => {
|
||||
const base = old[i] ? JSON.parse(JSON.stringify(old[i])) : { source: { type: "empty" }, excluded: false };
|
||||
base.text = ps.text || "";
|
||||
base.text = ps.text || ""; // verbatim — shortcuts preserved
|
||||
base.transition_to_next = "";
|
||||
return base;
|
||||
});
|
||||
(parsed.transitions || []).forEach((t) => {
|
||||
const idx = t.after_scene;
|
||||
if (idx >= 0 && idx < next.length && t.visual_effect) {
|
||||
const lib = (state.transitions || []).find((tr) => (tr.visual_effect || "none") === t.visual_effect);
|
||||
if (lib) next[idx].transition_to_next = lib.trigger || lib.name || "";
|
||||
}
|
||||
});
|
||||
_applyDetectedTransitions(next, raw, exp);
|
||||
state.project.scenes = next;
|
||||
state.selectedSceneId = null; // show project view so the result is visible
|
||||
notify(); scheduleSave();
|
||||
}
|
||||
|
||||
// Map detected transitions (visual_effect) onto scene seams via the library. Prefers
|
||||
// the expanded split (catches shortcut→transition) when its scene count matches.
|
||||
function _applyDetectedTransitions(scenes, raw, exp) {
|
||||
const trans = (((exp.scenes || []).length === scenes.length ? exp.transitions : raw.transitions) || []);
|
||||
trans.forEach((t) => {
|
||||
const idx = t.after_scene;
|
||||
if (idx >= 0 && idx < scenes.length && t.visual_effect) {
|
||||
const lib = (state.transitions || []).find((tr) => (tr.visual_effect || "none") === t.visual_effect);
|
||||
if (lib) scenes[idx].transition_to_next = lib.trigger || lib.name || "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── preview ──────────────────────────────────────────────────────────────────
|
||||
let previewTimer = null;
|
||||
function refreshPreview() {
|
||||
|
||||
@@ -245,6 +245,7 @@ if web is not None and PromptServer is not None:
|
||||
try:
|
||||
parsed = bridge.parse_timeline(prompt, seed=p.seed)
|
||||
result["parsed"] = parsed
|
||||
result["parsed_raw"] = bridge.parse_timeline_raw(prompt)
|
||||
expected = len([s for s in p.scenes if include_excluded or not s.excluded])
|
||||
got = len(parsed.get("scenes", []))
|
||||
result["expected_scenes"] = expected
|
||||
@@ -266,7 +267,8 @@ if web is not None and PromptServer is not None:
|
||||
prompt = str(body.get("prompt", ""))
|
||||
try:
|
||||
parsed = bridge.parse_timeline(prompt, seed=p.seed)
|
||||
return web.json_response({"parsed": parsed, "combined_prompt": prompt})
|
||||
parsed_raw = bridge.parse_timeline_raw(prompt)
|
||||
return web.json_response({"parsed": parsed, "parsed_raw": parsed_raw, "combined_prompt": prompt})
|
||||
except Exception as e: # noqa: BLE001
|
||||
return web.json_response({"detail": f"Parse failed: {e}"}, status=502)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user