Allow traceable chapter beat expansion
This commit is contained in:
parent
8a6eab543f
commit
864f3e80e2
3 changed files with 69 additions and 13 deletions
|
|
@ -68,15 +68,19 @@ Target words: {target_words}
|
|||
|
||||
Return strict JSON in this shape:
|
||||
{{"purpose":"","pov_character":"","tense":"past","location":"","time_context":"",
|
||||
"present":[],"target_words":{target_words},"beats":[{{"text":"","required":true}}],
|
||||
"present":[],"target_words":{target_words},"beats":[{{"text":"","required":true,
|
||||
"supports_beat_id":"","kind":""}}],
|
||||
"exact_values":[],"constraints":[],"forbidden_events":[],"ending_state":"","final_image":"",
|
||||
"boundary_constraints":[],"continuity_questions":[]}}
|
||||
|
||||
Use 3-8 concrete beats. Mark only indispensable events required:true. Preserve unresolved continuity questions
|
||||
instead of guessing. The ending state and final image must define where the scene stops. For a book-bound
|
||||
scene, repeat the approved chapter beats without expanding their events or state changes. Include an exact
|
||||
value only when it appears explicitly in the approved chapter packet or scene brief; omit numbers and
|
||||
classifications found only in background source context."""
|
||||
scene, do not restate or replace the approved chapter beats. Propose subordinate execution beats instead.
|
||||
Every proposed beat must name one approved supports_beat_id, use kind "dramatization" or "transition", and
|
||||
remain non-required. It may stage action, resistance, dialogue pressure, sensory evidence, or movement that
|
||||
realizes its parent beat, but it cannot add an outcome, agreement, state change, explanation, or arc movement.
|
||||
Include an exact value only when it appears explicitly in the approved chapter packet or scene brief; omit
|
||||
numbers and classifications found only in background source context."""
|
||||
|
||||
SCENE_IDEA_TYPES = {
|
||||
"quiet_connection": "A short, low-stakes character moment whose meaning comes from attention or choice.",
|
||||
|
|
|
|||
|
|
@ -677,19 +677,43 @@ class StandaloneSceneService:
|
|||
)
|
||||
approved_beats = [
|
||||
{
|
||||
"beat_id": str(item.get("beat_id") or "").strip(),
|
||||
"text": str(item.get("text") or "").strip(),
|
||||
"required": bool(item.get("required")),
|
||||
"kind": "contract",
|
||||
}
|
||||
for item in chapter.get("beats") or []
|
||||
if isinstance(item, dict) and str(item.get("text") or "").strip()
|
||||
]
|
||||
approved_text = {item["text"].casefold() for item in approved_beats}
|
||||
discarded = [
|
||||
item
|
||||
for item in plan["beats"]
|
||||
if str(item.get("text") or "").strip().casefold() not in approved_text
|
||||
]
|
||||
plan["beats"] = approved_beats
|
||||
approved_by_text = {item["text"].casefold(): item for item in approved_beats}
|
||||
approved_ids = {item["beat_id"] for item in approved_beats}
|
||||
expansions_by_parent = {beat_id: [] for beat_id in approved_ids}
|
||||
discarded = []
|
||||
for item in plan["beats"]:
|
||||
text = str(item.get("text") or "").strip()
|
||||
if text.casefold() in approved_by_text:
|
||||
continue
|
||||
supports_beat_id = str(item.get("supports_beat_id") or "").strip()
|
||||
kind = str(item.get("kind") or "").strip().lower()
|
||||
if supports_beat_id not in approved_ids or kind not in {
|
||||
"dramatization",
|
||||
"transition",
|
||||
}:
|
||||
discarded.append(item)
|
||||
continue
|
||||
expansions_by_parent[supports_beat_id].append(
|
||||
{
|
||||
"text": text,
|
||||
"required": False,
|
||||
"supports_beat_id": supports_beat_id,
|
||||
"kind": kind,
|
||||
}
|
||||
)
|
||||
scoped_beats = []
|
||||
for approved in approved_beats:
|
||||
scoped_beats.append(approved)
|
||||
scoped_beats.extend(expansions_by_parent[approved["beat_id"]])
|
||||
plan["beats"] = scoped_beats
|
||||
plan["purpose"] = str(chapter.get("purpose") or "").strip()
|
||||
plan["ending_state"] = str(chapter.get("ending_state") or "").strip()
|
||||
return plan, discarded
|
||||
|
|
@ -1145,7 +1169,14 @@ class StandaloneSceneService:
|
|||
raw = {"text": raw, "required": False}
|
||||
text = str(raw.get("text") or "").strip()
|
||||
if text:
|
||||
beats.append({"text": text, "required": bool(raw.get("required"))})
|
||||
beat = {"text": text, "required": bool(raw.get("required"))}
|
||||
supports_beat_id = str(raw.get("supports_beat_id") or "").strip()
|
||||
kind = str(raw.get("kind") or "").strip().lower()
|
||||
if supports_beat_id:
|
||||
beat["supports_beat_id"] = supports_beat_id
|
||||
if kind:
|
||||
beat["kind"] = kind
|
||||
beats.append(beat)
|
||||
if not beats:
|
||||
raise ValueError("scene plan must contain at least one beat")
|
||||
plan = {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ class OutOfScopePlanningRouter:
|
|||
"present": ["Protagonist"],
|
||||
"beats": [
|
||||
{"text": "The chapter performs turn 1.", "required": True},
|
||||
{
|
||||
"text": "The protagonist opens the record and challenges its premise.",
|
||||
"required": True,
|
||||
"supports_beat_id": "beat-1",
|
||||
"kind": "dramatization",
|
||||
},
|
||||
{
|
||||
"text": "The planner adds a Floor ninety-six inspection.",
|
||||
"required": True,
|
||||
|
|
@ -395,10 +401,25 @@ def test_book_scene_plan_discards_exact_values_outside_approved_scope(work: Work
|
|||
{"label": "approved turn", "value": "turn 1"}
|
||||
]
|
||||
assert scene.plan["beats"] == [
|
||||
{"text": "The chapter performs turn 1.", "required": True}
|
||||
{
|
||||
"beat_id": "beat-1",
|
||||
"text": "The chapter performs turn 1.",
|
||||
"required": True,
|
||||
"kind": "contract",
|
||||
},
|
||||
{
|
||||
"text": "The protagonist opens the record and challenges its premise.",
|
||||
"required": False,
|
||||
"supports_beat_id": "beat-1",
|
||||
"kind": "dramatization",
|
||||
},
|
||||
]
|
||||
assert scene.plan["purpose"] == "Advance turn 1."
|
||||
assert scene.plan["ending_state"] == "Turn 1 is complete."
|
||||
beat_requirements = [
|
||||
item for item in scene.contract_requirements if item["type"] == "BEAT"
|
||||
]
|
||||
assert [item["blocking"] for item in beat_requirements] == [True, False]
|
||||
assert scene.generation_metadata["planning"][
|
||||
"discarded_out_of_scope_exact_values"
|
||||
] == [{"label": "background floor", "value": "Floor ninety-six"}]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue