diff --git a/conditioning.py b/conditioning.py index be5ce56e..3f463c86 100644 --- a/conditioning.py +++ b/conditioning.py @@ -886,17 +886,20 @@ def split_scenes(prompt, placement="start"): _open(effect) segs[-1]["raw"] += raw_t; segs[-1]["exp"] += exp_t - # Literal-text trigger matcher (custom triggers only; never the generic scene-N pattern). - triggers = [t for t in custom_map.keys() if t] - lit_pat = None - if triggers: - def _tp(t): - end = r"\b" if re.search(r"\w$", t) else r"(?=\s|$)" - return r"\b" + re.escape(t) + end - try: - lit_pat = re.compile("|".join(_tp(t) for t in sorted(triggers, key=len, reverse=True)), re.IGNORECASE) - except re.error: - lit_pat = None + # Literal-text trigger matcher: the user's custom triggers PLUS the generic `scene ` label. + # Scoped to LITERAL text only (this matcher never runs over shortcut expansions), so a "scene 2" + # the user types — or, crucially, the one the Movie Editor INJECTS into the generation prompt to + # force a split for a scene with no leading trigger (see build_combined_prompt for_generation) — + # still cuts, while a "scene 2" buried inside a shortcut's expansion does NOT. + def _tp(t): + end = r"\b" if re.search(r"\w$", t) else r"(?=\s|$)" + return r"\b" + re.escape(t) + end + _pats = [_tp(t) for t in sorted((t for t in custom_map.keys() if t), key=len, reverse=True)] + _pats.append(_GENERIC_SCENE_LABEL_PATTERN) + try: + lit_pat = re.compile("|".join(_pats), re.IGNORECASE) + except re.error: + lit_pat = None for pc in pieces: raw = text[pc.get("orig_start", 0):pc.get("orig_end", 0)] diff --git a/tests/test_refinement_keys.py b/tests/test_refinement_keys.py index b7225aa7..9333b49f 100644 --- a/tests/test_refinement_keys.py +++ b/tests/test_refinement_keys.py @@ -245,6 +245,25 @@ def test_split_scenes_ignores_triggers_inside_expansions(monkeypatch): assert "Scene cut." not in out["scenes"][1]["expanded"] # silent trigger dropped +def test_split_scenes_literal_scene_label_still_splits(monkeypatch): + """The generic 'scene N' label MUST still split when it's LITERAL text — the Movie Editor + injects it into the generation prompt to force a split for a unit with no leading trigger + (build_combined_prompt for_generation). But the SAME label inside a shortcut expansion must NOT + split (that was the user's phantom 'scene 2'). One rule: literal cuts, expansion-content doesn't.""" + db = _db({"qcut": (["qcut"], ["rapid cut"], ""), + "tth": (["tth"], ["a backflip"], ""), + "cof": (["cof"], ["sitting, scene 9 of many"], "cs")}) # 'scene 9' INSIDE expansion + monkeypatch.setattr(templates, "load_shortcut_db", lambda: db) + monkeypatch.setattr(templates, "load_custom_transition_triggers", + lambda: {"qcut": {"placement": "start", "visual_effect": "none"}}) + # explicit qcut leads scene 0; the editor-injected literal 'scene 2' leads scene 1 + out = conditioning.split_scenes("CHAR a woman man qcut tth scene 2 cof") + assert len(out["scenes"]) == 2 + assert out["scenes"][0]["keys"] == set() # tth has no key + assert out["scenes"][1]["keys"] == {"cs"} # cof, despite 'scene 9' in its expansion + assert out["anchor"] == "CHAR a woman man" + + def test_single_scene_with_custom_key_is_detected(monkeypatch): """Regression: a custom key fired in a single-logical-scene prompt MUST be detected (was silently dropped to empty when the old code compared scene counts and fell back)."""