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:
co-authored by
Claude Opus 4.8
parent
0b72037c5b
commit
58a4f9b52b
@@ -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.
|
||||
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user