Stage 1a: canonical split_scenes (additive, not yet repointed)

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 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-15 09:41:09 +03:00
co-authored by Claude Opus 4.8
parent 0b72037c5b
commit 58a4f9b52b
2 changed files with 85 additions and 0 deletions
+57
View File
@@ -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": <raw anchor text, prepended to every scene>,
"scenes": [
{"index": i,
"raw": <verbatim scene text>, # editing / lossless mapping
"keys": <set of refinement keys firing in this scene + anchor>, # attribution / reset
"effect": <transition visual effect before this scene | None>}, # 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.
+28
View File
@@ -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)."""