| """ |
| Narration / dialogue / CTA generator. |
| |
| ``fill_scripts(graph, intent, profile)`` walks every node in a |
| BranchGraph and populates its ``narration`` + ``title`` fields |
| according to the node's kind and position in the graph. Also |
| writes ``image_prompt`` / ``video_prompt`` strings that the asset |
| adapter layer consumes downstream. |
| |
| Phase 1 uses deterministic templates per node kind so tests are |
| reproducible. Phase 2 swaps the template calls for LLM prompts |
| behind the same signature. |
| """ |
| from __future__ import annotations |
|
|
| from typing import Optional |
|
|
| from ..branching.graph import BranchGraph, GraphNode |
| from ..planner.intent import Intent |
| from ..policy.profiles import PolicyProfile, load_profile |
| from .safety_rewriter import rewrite_for_safety |
| from .tone import ToneSpec, apply_tone, default_tone_for_mode, estimate_duration_sec |
|
|
|
|
| |
| _TEMPLATES = { |
| "scene": "Welcome. {objective}. Here's what happens next in the journey.", |
| "decision": "You have a choice. Pick the path that feels right.", |
| "merge": "Paths converge here. Let's continue.", |
| "assessment": "Quick check β what do you think about {topic}?", |
| "remediation": "Let's look at that one more time. {topic} is important becauseβ¦", |
| "ending": "Thanks for playing through {objective}. Come back anytime.", |
| } |
|
|
|
|
| |
| |
| |
|
|
| def generate_narration_for_node( |
| node: GraphNode, |
| intent: Intent, |
| *, |
| tone: Optional[ToneSpec] = None, |
| profile: Optional[PolicyProfile] = None, |
| ) -> str: |
| """Produce narration for a single node. |
| |
| Applies (1) template substitution, (2) tone shaping, |
| (3) safety rewriter using the active policy profile. |
| """ |
| tone = tone or default_tone_for_mode(intent.mode) |
| profile = profile or load_profile(intent.mode) or load_profile("sfw_general") |
| |
| |
| assert profile is not None |
|
|
| template = _TEMPLATES.get(node.kind, _TEMPLATES["scene"]) |
| raw = template.format( |
| objective=intent.objective or "our story", |
| topic=intent.topic or "this topic", |
| title=node.title or "this step", |
| ) |
| shaped = apply_tone(raw, tone) |
| safe = rewrite_for_safety(shaped, profile).text |
| return safe |
|
|
|
|
| def _title_for(node: GraphNode, intent: Intent) -> str: |
| if node.title: |
| return node.title |
| if node.is_entry: |
| return "Introduction" |
| if node.kind == "ending": |
| return "Epilogue" |
| if node.kind == "decision": |
| return "Choose" |
| return f"{node.kind.title()} step" |
|
|
|
|
| def _image_prompt(node: GraphNode, intent: Intent) -> str: |
| base = f"cinematic still, {intent.mode.replace('_', ' ')} scene" |
| if intent.topic: |
| base += f", about {intent.topic}" |
| if node.kind == "decision": |
| base += ", character facing two choices" |
| elif node.kind == "ending": |
| base += ", resolution, warm lighting" |
| elif node.kind == "assessment": |
| base += ", thoughtful look" |
| elif node.kind == "remediation": |
| base += ", teaching gesture" |
| return base |
|
|
|
|
| def _video_prompt(node: GraphNode, intent: Intent) -> str: |
| return _image_prompt(node, intent) + ", short 4-second clip, subtle motion" |
|
|
|
|
| |
| |
| |
|
|
| def fill_scripts( |
| graph: BranchGraph, |
| intent: Intent, |
| *, |
| profile: Optional[PolicyProfile] = None, |
| ) -> BranchGraph: |
| """Populate narration/title/image_prompt/video_prompt on every |
| node. Idempotent β nodes that already have non-empty narration |
| are skipped so designer edits are preserved when re-running the |
| generator after a partial manual edit. |
| """ |
| tone = default_tone_for_mode(intent.mode) |
| profile = profile or load_profile(intent.mode) or load_profile("sfw_general") |
| assert profile is not None |
|
|
| for node in graph.nodes: |
| if not node.title: |
| node.title = _title_for(node, intent) |
|
|
| if not node.narration: |
| node.narration = generate_narration_for_node( |
| node, intent, tone=tone, profile=profile, |
| ) |
|
|
| |
| |
| |
| node.metadata.setdefault("image_prompt", _image_prompt(node, intent)) |
| node.metadata.setdefault("video_prompt", _video_prompt(node, intent)) |
| node.metadata.setdefault( |
| "duration_sec", |
| estimate_duration_sec(node.narration, tone), |
| ) |
|
|
| return graph |
|
|