From 85ddc7868aed10ee6c14bdf8d7e0bc84c5860517 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:34:54 +0300 Subject: [PATCH] Keep global and scene prompts in sync; hide internal rating labels. Scene edits rebuild the global prompt immediately; global prompt edits auto-parse back to scenes. Filter __funpack_* ratings from the editor dropdown. --- movie_editor/backend/bridge.py | 13 +- movie_editor/frontend/inspector.js | 23 +++- movie_editor/frontend/store.js | 192 +++++++++++++++++++++++------ movie_editor/tests/test_bridge.py | 17 +++ 4 files changed, 206 insertions(+), 39 deletions(-) diff --git a/movie_editor/backend/bridge.py b/movie_editor/backend/bridge.py index 28af66d7..fffa7646 100644 --- a/movie_editor/backend/bridge.py +++ b/movie_editor/backend/bridge.py @@ -173,7 +173,18 @@ def recent_log(limit: int = 500) -> list: def rating_labels() -> dict: """FunPack Studio V2 rating labels (for the Scene rating dropdown).""" labels = _funpack_attr("conditioning", "V2_RATING_LABELS") - return {"labels": list(labels)} + try: + internal = { + _funpack_attr("conditioning", "MOVIE_EDITOR_CONTINUE_RATING"), + _funpack_attr("conditioning", "MOVIE_EDITOR_FRESH_PROMPT_RATING"), + } + except Exception: + internal = {"__funpack_continue__", "__funpack_fresh_prompt__"} + out = [ + str(l) for l in (labels or []) + if l and str(l) not in internal and not str(l).startswith("__funpack_") + ] + return {"labels": out} def transitions() -> dict: diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index f2f2f5d0..ae65bf9f 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -573,11 +573,11 @@ const ta = el("textarea", "insp-global-ta"); ta.rows = 3; ta.value = val; ta.dataset.k = "global-prompt"; ta.placeholder = "Anchor, scene texts, and split markers — one combined montage prompt for generation."; - ta.oninput = () => { gpDraft = ta.value; apply.disabled = !ta.value.trim(); }; + ta.oninput = () => { gpDraft = ta.value; apply.disabled = !ta.value.trim(); S.scheduleGlobalPromptApply(ta.value); }; sec.append(ta); sec.append(el("div", "insp-hint", dirty - ? "Edited — press Apply to (re)split onto the timeline." - : "Primary way to control how long videos divide into scenes. Per-seam tweaks: Split dropdown on timeline seams (video blends are separate, on the same seam).")); + ? "Syncing to timeline…" + : "Stays in sync with per-scene prompts. Per-seam tweaks: Split dropdown on timeline seams.")); body.append(sec); } @@ -638,11 +638,28 @@ // manual render when there was nothing pending. setTimeout(() => { if (_editing) return; + const k = t.dataset?.k; + if (k === "global-prompt") { + const draft = gpDraft; + S.flushSave(); + if (draft != null) S.applyGlobalPromptQuiet(draft); + gpDraft = null; + render(S.get()); + return; + } if (!S.flushSave()) render(S.get()); }, 60); }); window.addEventListener("funpack-invalidate-global-prompt", () => { gpDraft = null; }); + window.addEventListener("funpack-global-prompt-updated", (e) => { + if (_editing && document.activeElement?.dataset?.k === "global-prompt") return; + gpDraft = null; + const ta = body.querySelector('[data-k="global-prompt"]'); + if (ta && e.detail?.text != null) ta.value = e.detail.text; + const hint = body.querySelector(".insp-global .insp-hint"); + if (hint) hint.textContent = "Stays in sync with per-scene prompts. Per-seam tweaks: Split dropdown on timeline seams."; + }); if (window.ViewBus) window.ViewBus.subscribeInspector(render); else S.subscribe(render); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index 9016f538..7b65b3a0 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -120,6 +120,142 @@ try { window.dispatchEvent(new CustomEvent("funpack-invalidate-global-prompt")); } catch (_) {} } + function _dispatchGlobalPromptUpdated(text) { + try { + window.dispatchEvent(new CustomEvent("funpack-global-prompt-updated", { detail: { text: text || "" } })); + } catch (_) {} + } + + function _groupGenerativeUnits(scenes) { + const units = []; + for (const sc of scenes || []) { + const uid = genUnitId(sc); + if (units.length && units[units.length - 1][0] === uid) units[units.length - 1][1].push(sc); + else units.push([uid, [sc]]); + } + return units; + } + + function _rootFromGroup(group) { + return (group || []).find((s) => !isGenSubclip(s)) || group[0] || null; + } + + // Verbatim montage text rebuilt from timeline fields (display mode — transitions between scenes). + function buildGlobalPromptFromTimeline(project) { + if (!project) return ""; + const parts = []; + const anchor = (project.anchor || "").trim(); + if (anchor) parts.push(anchor); + const active = (project.scenes || []).filter((s) => !s.excluded); + const units = _groupGenerativeUnits(active); + units.forEach(([, group], i) => { + const root = _rootFromGroup(group); + if (!root) return; + if (i === 0) { + const intro = (project.intro_transition || "").trim(); + if (intro && anchor) parts.push(intro); + } else { + const prevRoot = _rootFromGroup(units[i - 1][1]); + const tr = (prevRoot?.transition_to_next || "").trim(); + if (tr) parts.push(tr); + } + const text = (root.text || "").trim(); + if (text) parts.push(text); + }); + return parts.join(" ").trim(); + } + + function _patchAffectsCombinedPrompt(patch) { + if (!patch || typeof patch !== "object") return false; + return patch.text != null + || patch.transition_to_next != null + || patch.excluded != null + || patch.anchor != null + || patch.intro_transition != null + || patch.scenes != null; + } + + let _timelinePromptTimer = null; + function syncGlobalPromptFromTimeline() { + if (!state.project) return; + const built = buildGlobalPromptFromTimeline(state.project); + const prev = (state.project.global_prompt || "").trim(); + if (built === prev && (state.preview?.display_prompt || "") === built) return; + state.project.global_prompt = built; + state.preview = { + ...(state.preview || {}), + display_prompt: built, + combined_prompt: built, + }; + _lastPreviewKey = _promptPreviewKey(state.project); + _invalidateGlobalPromptDraft(); + _dispatchGlobalPromptUpdated(built); + notify(); + clearTimeout(_timelinePromptTimer); + _timelinePromptTimer = setTimeout(() => refreshPreview(true), 300); + } + + let _globalApplyTimer = null; + function scheduleGlobalPromptApply(text) { + clearTimeout(_globalApplyTimer); + _globalApplyTimer = setTimeout(() => { applyGlobalPromptQuiet(text); }, 500); + } + + async function _distributeGlobalPrompt(text, res) { + const v = res.parsed_verbatim || res.parsed_raw || res.parsed || {}; + if (!(v.scenes || []).length) return false; + state.project.global_prompt = text; + state.project.anchor = v.anchor || ""; + const old = state.project.scenes || []; + const next = (v.scenes || []).map((ps, i) => { + const base = old[i] ? JSON.parse(JSON.stringify(old[i])) : { source: { type: "carry" }, excluded: false }; + base.text = ps.text || ""; + base.transition_to_next = ""; + return base; + }); + _applyDetectedTransitions(next, v.transitions, text); + state.project.scenes = next; + state.sceneGhosts = []; + const firstId = next[0]?.id || null; + state.selectedSceneId = firstId; + state.selectedSceneIds = firstId ? [firstId] : []; + _selectionAnchorId = firstId; + state.preview = { + ...(state.preview || {}), + combined_prompt: text, + display_prompt: text, + parsed: res.parsed, + parsed_raw: res.parsed_raw, + parsed_verbatim: res.parsed_verbatim, + for_generation: false, + }; + _lastPreviewKey = _promptPreviewKey(state.project); + _invalidateGlobalPromptDraft(); + _dispatchGlobalPromptUpdated(text); + notify(); + return true; + } + + async function applyGlobalPromptQuiet(text) { + if (!state.project) return false; + const trimmed = String(text || "").trim(); + if (!trimmed) return false; + if (trimmed === buildGlobalPromptFromTimeline(state.project)) return true; + let res; + try { res = await API.parsePrompt(state.project.id, trimmed); } + catch (e) { + console.warn("Global prompt parse failed:", e); + return false; + } + if (!await _distributeGlobalPrompt(trimmed, res)) return false; + clearTimeout(saveTimer); + saveTimer = null; + _localDirty = true; + scheduleSaveSilent(); + refreshPreview(true); + return true; + } + function _syncPromptPreview() { return (async () => { try { @@ -197,6 +333,7 @@ _removedDuringGen.clear(); if (window.EditorHistory) window.EditorHistory.clear(); notify(); + syncGlobalPromptFromTimeline(); refreshPreview(); loadModels(); _validateSceneRenders(); @@ -344,10 +481,16 @@ if (!state.project) return; _historyRecord(); Object.assign(state.project, patch); + if (_patchAffectsCombinedPrompt(patch)) syncGlobalPromptFromTimeline(); notify(); scheduleSave(); } - function patchProjectQuiet(patch) { if (!state.project) return; Object.assign(state.project, patch); scheduleSaveSilent(); } + function patchProjectQuiet(patch) { + if (!state.project) return; + Object.assign(state.project, patch); + if (_patchAffectsCombinedPrompt(patch)) syncGlobalPromptFromTimeline(); + scheduleSaveSilent(); + } function scene(id) { return state.project?.scenes.find((s) => s.id === id) || null; } @@ -429,6 +572,10 @@ } else if (charsChanged) { notify(); _syncPromptPreview(); + } else if (_patchAffectsCombinedPrompt(merged)) { + syncGlobalPromptFromTimeline(); + if (quiet) scheduleSaveSilent(); + else { notify(); scheduleSave(); } } else if (quiet) scheduleSaveSilent(); else { notify(); scheduleSave(); } } @@ -445,7 +592,10 @@ if (anchorChanged) { clearTimeout(saveTimer); saveTimer = null; _localDirty = true; notify(); commit(); - } else scheduleSaveSilent(); + } else { + if (_patchAffectsCombinedPrompt(merged)) syncGlobalPromptFromTimeline(); + scheduleSaveSilent(); + } } function _sceneOrder() { return (state.project?.scenes || []).map((s) => s.id); } @@ -929,7 +1079,6 @@ async function applyGlobalPrompt(text) { if (!state.project) return false; _historyRecord(); - state.project.global_prompt = text; let res; try { res = await API.parsePrompt(state.project.id, text); } catch (e) { @@ -937,37 +1086,10 @@ alert("Could not parse the global prompt: " + msg); return false; } - // The verbatim split is authoritative: correct (shortcut-aware) boundaries, scene - // text kept exactly as typed, anchor + scenes reproduce the global prompt. - const v = res.parsed_verbatim || res.parsed_raw || res.parsed || {}; - if (!(v.scenes || []).length) { alert("Nothing parsed — no scenes detected."); return false; } - - state.project.anchor = v.anchor || ""; - const old = state.project.scenes || []; - const next = (v.scenes || []).map((ps, i) => { - const base = old[i] ? JSON.parse(JSON.stringify(old[i])) : { source: { type: "carry" }, excluded: false }; - base.text = ps.text || ""; // verbatim chunk of the global prompt - base.transition_to_next = ""; - return base; - }); - _applyDetectedTransitions(next, v.transitions, text); - state.project.scenes = next; - state.sceneGhosts = []; // scene ids changed — old ghosts no longer anchor correctly - const firstId = next[0]?.id || null; - state.selectedSceneId = firstId; - state.selectedSceneIds = firstId ? [firstId] : []; - _selectionAnchorId = firstId; - // Refresh the inspector immediately — don't wait for save debounce + preview round-trip. - state.preview = { - ...(state.preview || {}), - combined_prompt: text, - display_prompt: text, - parsed: res.parsed, - parsed_raw: res.parsed_raw, - parsed_verbatim: res.parsed_verbatim, - for_generation: false, - }; - notify(); + if (!(await _distributeGlobalPrompt(text, res))) { + alert("Nothing parsed — no scenes detected."); + return false; + } clearTimeout(saveTimer); saveTimer = null; state.saving = true; @@ -1766,7 +1888,7 @@ addAudioTrack, updateAudioTrack, removeAudioTrack, resizeScene, splitScene, snapFrames, setSourceTrim, trimSceneLeft, slipScene, applyEnginePreset, ENGINE_PRESETS, undo, redo, - refreshPreview, syncFromPreview, applyGlobalPrompt, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, clearNotice, + refreshPreview, syncFromPreview, applyGlobalPrompt, applyGlobalPromptQuiet, scheduleGlobalPromptApply, buildGlobalPromptFromTimeline, syncGlobalPromptFromTimeline, generate, generateMontage, generateSelected, selectedSceneCount, renderFinal, exportSelected, interrupt, loadModels, loadImageTargets, setModelInput, setModelLink, clearNotice, setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, unsetSamplerInput, setStudioInput, setStudioInputNow, loadMedia, uploadMedia, deleteMedia, previewMedia, clearMediaPreview, assignMediaToScene, loadShortcuts, saveShortcut, deleteShortcut, importShortcuts, diff --git a/movie_editor/tests/test_bridge.py b/movie_editor/tests/test_bridge.py index 0862254f..d0af80d1 100644 --- a/movie_editor/tests/test_bridge.py +++ b/movie_editor/tests/test_bridge.py @@ -18,3 +18,20 @@ def test_ensure_funpack_path_adds_repo_root(): sys.path[:] = [p for p in sys.path if p != root] bridge._ensure_funpack_path() assert root in sys.path + + +def test_rating_labels_hide_internal_editor_values(monkeypatch): + def _fake_attr(_mod, name): + table = { + "V2_RATING_LABELS": ["Perfect", "__funpack_continue__", "__funpack_fresh_prompt__"], + "MOVIE_EDITOR_CONTINUE_RATING": "__funpack_continue__", + "MOVIE_EDITOR_FRESH_PROMPT_RATING": "__funpack_fresh_prompt__", + } + return table[name] + + monkeypatch.setattr(bridge, "_funpack_attr", _fake_attr) + labels = bridge.rating_labels().get("labels") or [] + assert "__funpack_continue__" not in labels + assert "__funpack_fresh_prompt__" not in labels + assert all(not str(l).startswith("__funpack_") for l in labels) + assert "Perfect" in labels