From 58a4f9b52bac041106ff5121ffaf4e40a07745f8 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:41:09 +0300 Subject: [PATCH] Stage 1a: canonical split_scenes (additive, not yet repointed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One rich scene model {anchor, scenes:[{index,raw,keys,effect}]} that every consumer will derive from — editing (raw), preview (raw->expanded), attribution (keys), reset (pool = union of keys), generation. Built on the existing shortcut-aware verbatim splitter + refinement_keys_in_text, so it can't disagree with itself. Nothing repointed yet; consumers still on their own splitters until the staged repointing. Co-Authored-By: Claude Opus 4.8 --- conditioning.py | 57 +++++++++++++++++++++++++++++++++++ tests/test_refinement_keys.py | 28 +++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/conditioning.py b/conditioning.py index c0f79c96..63852091 100644 --- a/conditioning.py +++ b/conditioning.py @@ -903,6 +903,63 @@ def refinement_keys_in_text(text): if pc.get("is_shortcut") and str(pc.get("refinement_key") or "")} +def split_scenes(prompt): + """THE canonical scene split — one source of truth for editing, preview, key attribution, + session reset and generation. + + Splits `prompt` into an anchor + scenes at shortcut-aware transition boundaries (detected on + BOTH the expanded and the raw text, then projected onto the raw words so nothing is expanded + or dropped), and for each scene reports everything any consumer needs: + + { + "anchor": , + "scenes": [ + {"index": i, + "raw": , # editing / lossless mapping + "keys": , # attribution / reset + "effect": }, # generation + ... + ], + } + + Derive, never re-split: per-scene keys = entry["keys"]; the reset pool = union of all keys; + the preview/generation scene texts come from these raw chunks (expanded by the caller). Because + every consumer reads ONE split, scene counts and boundaries can never disagree.""" + verbatim = split_timeline_verbatim(prompt) + anchor = verbatim.get("anchor", "") or "" + anchor_keys = refinement_keys_in_text(anchor) + # transitions[i] = {"after_scene": k, "visual_effect": e} means effect e precedes scene k+1. + effect_before = {} + for t in verbatim.get("transitions", []) or []: + try: + effect_before[int(t.get("after_scene", -1)) + 1] = t.get("visual_effect") + except (TypeError, ValueError): + continue + scenes = [] + for s in verbatim.get("scenes", []) or []: + try: + idx = int(s.get("index", len(scenes))) + except (TypeError, ValueError): + idx = len(scenes) + raw = s.get("text", "") or "" + scenes.append({ + "index": idx, + "raw": raw, + "keys": anchor_keys | refinement_keys_in_text(raw), + "effect": effect_before.get(idx), + }) + return {"anchor": anchor, "scenes": scenes} + + +def refinement_key_pool_for(prompt): + """Every non-default refinement key the prompt activates (anchor + all scenes) — the exact + set a session reset wipes. Derived from the one canonical split.""" + pool = set() + for scene in split_scenes(prompt).get("scenes", []) or []: + pool |= scene.get("keys") or set() + return pool + + def resolve_scene_refinement_keys(raw_positive, scene_count): """Per-scene non-default refinement keys, read straight off each scene's text. diff --git a/tests/test_refinement_keys.py b/tests/test_refinement_keys.py index f6ed1745..a2b2db94 100644 --- a/tests/test_refinement_keys.py +++ b/tests/test_refinement_keys.py @@ -233,6 +233,34 @@ def test_reset_prompt_keys_skips_when_primary_only(monkeypatch): assert studio._v2_reset_prompt_keys("default", "plain prompt") == [] +def test_split_scenes_canonical_model(monkeypatch): + """The one canonical split reports per-scene raw text + keys; anchor keys fold into every + scene; the reset pool is the union.""" + db = _db({"anc": (["zeta"], ["ZZZ"], "anchorKey"), + "a": (["alpha"], ["AAA"], "keyA"), + "b": (["beta"], ["BBB"], "keyB")}) + monkeypatch.setattr(templates, "load_shortcut_db", lambda: db) + monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {}) + out = conditioning.split_scenes("zeta intro scene 1 alpha here scene 2 beta here") + scenes = out["scenes"] + assert len(scenes) == 2 + assert scenes[0]["keys"] == {"anchorKey", "keyA"} + assert scenes[1]["keys"] == {"anchorKey", "keyB"} + assert "alpha" in scenes[0]["raw"] # verbatim text preserved + assert conditioning.refinement_key_pool_for( + "zeta intro scene 1 alpha here scene 2 beta here") == {"anchorKey", "keyA", "keyB"} + + +def test_split_scenes_single_scene(monkeypatch): + db = _db({"hero": (["hero"], ["a knight"], "mykey")}) + monkeypatch.setattr(templates, "load_shortcut_db", lambda: db) + monkeypatch.setattr(templates, "load_custom_transition_triggers", lambda: {}) + out = conditioning.split_scenes("hero stands in a field") + assert len(out["scenes"]) == 1 + assert out["scenes"][0]["keys"] == {"mykey"} + assert conditioning.refinement_key_pool_for("hero stands in a field") == {"mykey"} + + 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)."""