editor: show postfix in the prompt preview
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c5ead80ae8
commit
c70d046a14
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user