opencut_ui: split_scenes cuts on expansion-keyed transitions (scene-drop fix)

The canonical generation/preview/key splitter (split_scenes) only treated a
shortcut as a scene cut when the shortcut's TRIGGER word was a key in the
transition DB. But split markers are commonly keyed by the EXPANSION phrase: a
"cut" shortcut expands to "Scene cut.", and the split marker is "scene cut.".
So the lookup `custom_map.get(_norm(raw))` missed, the shortcut was treated as
plain content, and NO cut was made — split_scenes returned one scene for
"A cut B cut C" while the verbatim editor split returned three. That disagreement
collapsed the timeline's middle scenes (and would have generated one scene).

Fix: when the trigger word isn't in the DB, also match the shortcut's EXPANSION
(`custom_map.get(_norm(exp))`). The generic "scene <N>" label is still NOT matched
on expansions, so an incidental "scene 2" inside a content shortcut never cuts.
Verified against the user's on-disk cut/qcut defs: split_scenes now agrees with
the verbatim split (2 scenes for cut x2 / qcut x2 / mixed). Added regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-20 17:03:28 +03:00
co-authored by Claude Opus 4.8
parent 7ed0717f73
commit 9d69f4bef8
2 changed files with 43 additions and 0 deletions
+8
View File
@@ -905,7 +905,15 @@ def split_scenes(prompt, placement="start"):
raw = text[pc.get("orig_start", 0):pc.get("orig_end", 0)]
exp = expanded_full[pc.get("exp_start", 0):pc.get("exp_end", 0)]
if pc.get("is_shortcut"):
# A shortcut is a transition when its TRIGGER word is in the DB (e.g. a literal
# "qcut" entry) OR when its EXPANSION resolves to a transition phrase (the common
# case: a "cut" shortcut expanding to the "Scene cut." split phrase). Match either,
# so an expansion-keyed split marker still cuts here exactly as it does in the
# verbatim editor split. Generic "scene <N>" is intentionally NOT matched on
# expansions — only user-defined custom triggers — so incidental labels never cut.
info = custom_map.get(_norm(raw))
if info is None:
info = custom_map.get(_norm(exp))
if info is not None: # transition-trigger shortcut
_transition(raw, exp, info)
else: # content shortcut: text + its key
+35
View File
@@ -83,6 +83,41 @@ def test_verbatim_split_shortcut_driven_split_still_works(monkeypatch):
assert "<break>" in v["anchor"]
def test_split_scenes_splits_on_expansion_keyed_transition(monkeypatch):
"""Regression: split_scenes (the canonical generation/preview/key splitter) must cut when a
shortcut's EXPANSION is a transition phrase (e.g. 'cut'->'Scene cut.'), not only when the
shortcut's TRIGGER word is itself in the transition DB. It previously matched only the raw
trigger, so an expansion-keyed split marker produced ONE scene here while the verbatim editor
split produced TWO — and the disagreement collapsed the timeline's middle scenes."""
db = _db({
"cut": (["cut"], ["Scene cut."], ""),
"qcut": (["qcut"], ["The video rapidly cuts, showing the next view."], ""),
})
monkeypatch.setattr(templates, "load_shortcut_db", lambda: db)
monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {
"scene cut.": {"placement": "silent", "visual_effect": None},
"the video rapidly cuts, showing the next view.": {"placement": "start", "visual_effect": None},
})
prompt = "a dog runs cut a cat sleeps cut a bird flies"
s = conditioning.split_scenes(prompt)
assert len(s["scenes"]) == 2 # silent 'cut' x2 -> anchor + 2 scenes
assert s["anchor_expanded"] == "a dog runs"
# The canonical split must agree with the verbatim editor split (no collapse).
v = conditioning.split_timeline_verbatim(prompt)
assert len(v["scenes"]) == len(s["scenes"])
# Non-silent 'qcut' too.
assert len(conditioning.split_scenes("a dog runs qcut a cat sleeps qcut a bird flies")["scenes"]) == 2
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)."""
db = _db({"desc": (["desc"], ["a wide shot like scene 2 of a film"], "")})
monkeypatch.setattr(templates, "load_shortcut_db", lambda: db)
monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {})
assert len(conditioning.split_scenes("a dog runs desc here")["scenes"]) == 1
def test_verbatim_split_still_splits_on_literal_cuts(monkeypatch):
"""The fix must not suppress real, user-typed scene cuts."""
db = _db({"closeup": (["<closeup>"], ["a tight close-up"], "closeupkey")}) # no cut in replacement