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