From 2fae9208399cb8daa0ed51e74950caf3eb35efd2 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:39:52 +0300 Subject: [PATCH] =?UTF-8?q?Restore=20generic=20'scene=20N'=20split=20?= =?UTF-8?q?=E2=80=94=20but=20literal-only=20(Movie=20Editor=20needs=20it)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping the generic scene-N pattern broke the Movie Editor: build_combined_prompt (for_generation) INJECTS 'scene N' markers into the generation prompt to force a split for any unit whose text lacks a leading transition trigger. Without that recognition the second unit never split -> 'scene 2 exists only on the timeline, not generated'. split_scenes now matches the generic scene-N label again, but ONLY in its literal-text scanner (never over shortcut expansions). So an editor-injected / user-typed 'scene 2' cuts, while a 'scene 2' buried inside a shortcut expansion does not — fixing both the user's phantom split AND the broken montage. For the reported prompt the piece-walker now returns 2 scenes (was 4), matching the old generation splitter's 2 -> the guard should pass and keys attribute while the chain/guides keep working. Co-Authored-By: Claude Opus 4.8 --- conditioning.py | 25 ++++++++++++++----------- tests/test_refinement_keys.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) 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)."""