From 9d69f4bef855e2bdd40062f9dd4fd1429cc0cf17 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:03:28 +0300 Subject: [PATCH] opencut_ui: split_scenes cuts on expansion-keyed transitions (scene-drop fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 " 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 --- conditioning.py | 8 ++++++++ tests/test_refinement_keys.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/conditioning.py b/conditioning.py index 384a9ceb..998d7951 100644 --- a/conditioning.py +++ b/conditioning.py @@ -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 " 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 diff --git a/tests/test_refinement_keys.py b/tests/test_refinement_keys.py index 9333b49f..1b2f21ac 100644 --- a/tests/test_refinement_keys.py +++ b/tests/test_refinement_keys.py @@ -83,6 +83,41 @@ def test_verbatim_split_shortcut_driven_split_still_works(monkeypatch): assert "" 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": ([""], ["a tight close-up"], "closeupkey")}) # no cut in replacement