From c70d046a14b8690a22cf593cd4acf64beb634d45 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:28:01 +0300 Subject: [PATCH] editor: show postfix in the prompt preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-scene prompt preview mirrors what Studio encodes, and the postfix is appended to every scene at generation — so it belongs there. Surface it once after the scenes as a 'postfix' badge, symmetric to the 'anchor' badge shown once above them. - bridge.expand_prompt_fragment: expands a standalone fragment (anchor/postfix) the same way generation does, so the previewed text matches exactly. - server preview: result["postfix"] = expanded effective_postfix(p). - inspector: postfix badge line; _promptPreviewKey includes postfix so editing it refreshes the preview live. Co-Authored-By: Claude Opus 4.8 --- movie_editor/backend/bridge.py | 19 +++++++++++++++++++ movie_editor/frontend/inspector.js | 8 ++++++++ movie_editor/frontend/store.js | 2 ++ movie_editor/frontend/styles.css | 1 + movie_editor/server.py | 4 ++++ tests/test_refinement_keys.py | 16 ++++++++++++++++ 6 files changed, 50 insertions(+) diff --git a/movie_editor/backend/bridge.py b/movie_editor/backend/bridge.py index 61ef2aec..546487c7 100644 --- a/movie_editor/backend/bridge.py +++ b/movie_editor/backend/bridge.py @@ -68,6 +68,25 @@ def parse_timeline(prompt: str, seed: int = 0) -> dict: return {"anchor": s.get("anchor_expanded", ""), "scenes": scenes, "transitions": transitions} +def expand_prompt_fragment(text: str) -> str: + """Expand shortcuts in a standalone fragment (anchor / postfix) the way generation does — + mirrors the _flatten(split_scenes(...)) the chain sampler runs on each, so the preview shows + exactly the expanded text Studio will append/prepend. Empty in → empty out.""" + if not str(text or "").strip(): + return "" + split_fn = _funpack_attr("conditioning", "split_scenes") + s = split_fn(str(text)) + parts = [] + ae = (s.get("anchor_expanded") or "").strip() + if ae: + parts.append(ae) + for sc in (s.get("scenes", []) or []): + e = (sc.get("expanded") or "").strip() + if e: + parts.append(e) + return " ".join(parts).strip() + + 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 diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index 2309058a..9b1dbbaf 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -580,6 +580,14 @@ if (t) l.append(el("span", "pv-badge trans", "→ split")); box.append(l); }); + // Postfix is appended to every scene at generation — show it once after the scenes, mirroring + // how the anchor (prepended) is shown once above them. + if (pv.postfix) { + const l = el("div", "pv-line"); + l.append(el("span", "pv-badge postfix", "postfix")); + l.append(el("span", null, pv.postfix)); + box.append(l); + } const raw = el("details", "pv-raw"); raw.append(el("summary", null, "generation prompt (sent to Studio)")); raw.append(el("pre", null, pv.combined_prompt || "")); box.append(raw); if (pv.display_prompt && pv.display_prompt !== pv.combined_prompt) { const disp = el("details", "pv-raw"); disp.append(el("summary", null, "display prompt (timeline view)")); disp.append(el("pre", null, pv.display_prompt)); box.append(disp); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index ea6042b9..eb85841e 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -125,6 +125,8 @@ if (!project) return ""; return JSON.stringify({ anchor: project.anchor, + postfix: project.postfix, + postfix_enabled: project.postfix_enabled, global_prompt: project.global_prompt, intro_transition: project.intro_transition, scenes: (project.scenes || []).map((s) => ({ diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index f6b82df8..2dee0570 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -550,6 +550,7 @@ input[type=number] { font-family: var(--font-mono); } .pv-line { display: flex; gap: 8px; align-items: baseline; padding: 3px 0; font-size: 12.5px; } .pv-badge { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .5px; text-transform: uppercase; color: var(--teal); border: 1px solid #57d0c655; border-radius: 4px; padding: 1px 5px; flex: 0 0 auto; } .pv-badge.anchor { color: var(--amber); border-color: #f3a93c55; } +.pv-badge.postfix { color: var(--amber); border-color: #f3a93c55; } .pv-badge.trans { color: var(--faint); border-color: transparent; font-size: 9px; } .pv-badge.rkey { color: #c79bf2; border-color: #c79bf255; text-transform: none; letter-spacing: 0; } .pv-badge.rkey.default { color: var(--faint); border-color: #ffffff22; } diff --git a/movie_editor/server.py b/movie_editor/server.py index f2b5ea20..c563ff11 100644 --- a/movie_editor/server.py +++ b/movie_editor/server.py @@ -29,6 +29,7 @@ from .backend.timeline import ( collapse_generative_units, continuity_media_refs, effective_negative_prompt, + effective_postfix, gen_unit_id, group_generative_units, is_video_clip, @@ -1188,6 +1189,9 @@ if web is not None and PromptServer is not None: result["scene_refinement_keys"] = bridge.scene_refinement_keys( prompt, p.refinement_key) result["refinement_key_pool"] = bridge.refinement_key_pool(prompt) + # Postfix is appended to every scene at generation but lives outside the prompt — + # surface it (expanded) so the preview mirrors what Studio actually encodes. + result["postfix"] = bridge.expand_prompt_fragment(effective_postfix(p)) except Exception as e: # noqa: BLE001 result["parse_error"] = str(e) return web.json_response(result) diff --git a/tests/test_refinement_keys.py b/tests/test_refinement_keys.py index c3d8f0ce..ce55f9ff 100644 --- a/tests/test_refinement_keys.py +++ b/tests/test_refinement_keys.py @@ -34,6 +34,7 @@ sys.modules.setdefault( import templates import conditioning from templates import apply_prompt_shortcuts, normalize_shortcut_item +from movie_editor.backend import bridge def _db(shortcuts): @@ -137,6 +138,21 @@ def test_postfix_shortcuts_expand_and_key_folds_into_every_scene(monkeypatch): assert "look" in scene["keys"] +def test_expand_prompt_fragment_matches_generation_postfix(monkeypatch): + """The preview's postfix text must be expanded exactly like generation appends it — same + shortcut expansion the chain sampler runs via split_scenes_from_segments.""" + db = _db({"cine": (["cine"], ["cinematic, shallow depth of field"], "look")}) + monkeypatch.setattr(templates, "load_shortcut_db", lambda: db) + monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {}) + + preview = bridge.expand_prompt_fragment("cine") + generation = conditioning.split_scenes_from_segments( + "", ["x"], postfix="cine")["postfix_expanded"] + assert preview == "cinematic, shallow depth of field" + assert preview == generation # preview mirrors generation exactly + assert bridge.expand_prompt_fragment("") == "" # disabled/empty postfix shows nothing + + def test_split_scenes_no_false_cut_from_generic_label_in_expansion(monkeypatch): """A content shortcut whose expansion merely CONTAINS 'scene 2' must NOT cut — only a user-defined custom transition phrase does (the generic label is never matched on expansions)."""