# Schedule against the recording

`src/recording-timing.ts` turns the separate-video timing contract into a checked
frame schedule. It does not infer a transcript, manufacture a pause, approve a
storyboard, or play the talking head. Its inputs must come from the actual
recording and a rehearsal of the intended playback setup.

## Gather evidence first

1. Preserve the supplied recording. Derive a timed transcript from its audio,
   including pauses. Watch it to mark the genuine ending wave/pause onset.
2. For each spoken beat, identify the exact recording time when its completed
   visual proof should be available. Record that as `recordingProofSeconds`.
3. Pick a recipe whose source-backed mechanic fits that beat. Specify the local
   output frame where its proof is complete, total duration, and a protected
   reading hold. Do not assume `settle` always means completed text: in
   `TitleCut`, for example, `change` completes the title and `settle` cuts away.
4. Place the recognizable content transition that cues the OBS scene switch.
   Measure reaction/startup delay in rehearsal; do not assume it is zero.
5. Choose the presentation end inside the actual talking-head tail, leaving
   enough real footage for the presenter to mute and stop video manually.

## Use the checked schedule

Import `alignToRecording` and the `RecordingClock` / `RecordingBeat` types from
`./recording-timing`. Supply a clock with these fields:

| Field | Evidence / units |
| --- | --- |
| `fps` | Chosen output frames per second, established at storyboard review |
| `cueSeconds` | Visible presentation cue time |
| `measuredReactionSeconds` | Rehearsed manual reaction plus OBS startup delay |
| `reactionSamplesSeconds` | Observed delays; the end must work across their min/max range, not just the nominal delay |
| `recordingDurationSeconds` | Actual supplied recording duration |
| `actualTailStartSeconds` | Watched onset of the genuine ending wave/pause |
| `actualTailEndSeconds` | Watched end of usable performance, no later than the file end; required even when equal to duration |
| `lastRequiredSpeechSeconds` | End of the last required spoken word, from the actual timed transcript |
| `requiredRemainingTailSeconds` | Positive remaining buffer chosen against footage and rehearsal |
| `presentationEndFrame` | Exclusive end of the presentation file, not its last frame index |

Each beat supplies `id`, `recordingProofSeconds`, `proofLocalFrame`,
`durationInFrames`, and `minimumReadingFrames`. All frame fields are integers.
Use the returned `from` and `durationInFrames` on a Remotion `Sequence`; its
child's `useCurrentFrame()` then reads the recipe's local time. Keep existing
recipe phase durations. The scheduler works backward from proof, not forward
from a guessed speech rate.

The function checks both sides of the final interval, not just the files'
durations. With `S = cueSeconds + measuredReactionSeconds`, recording time `h`
maps to presentation time `S + h`. The permitted end window is
`C + Rmax + max(T,L) <= presentation end <= C + Rmin + E - B`,
using the OBS worksheet's names. `E=actualTailEndSeconds` must be within the
recording. The helper rejects missing `E` or reaction samples. With one measured
delay the same rule is `S + max(T,L) <= P <= S + E - B`.
End boundaries round inward to whole output frames. Proof anchors round to
the nearest frame (at most half an output frame of quantization).

The earliest end also cannot precede `S + lastRequiredSpeechSeconds`. If the
presenter starts waving while still speaking, the wave does not authorize
ending the presentation before the required words finish.

`requiredPostPresentationWave: {startSeconds, endSeconds}` is optional only
when a whole post-ending wave is not required. When supplied, the presentation
must end before the observed wave starts for every sampled reaction delay.
The wave must fit inside the usable performance. A wave overlapping speech may
produce an empty window; it is not an invitation to alter the recording.

Example counterexample (source-equivalent clock, not a zero-delay rehearsal):
`H=786.133333, T=773.7, L=773.14, E=780.6, B=2`. Ending at `774`
retains `6.6` seconds of usable performance. Ending at `784` is rejected,
despite still being inside the file. Three observed starts are the OBS guide's
proposed operating practice, not a certified bound on future delay.

The [production packet](PRODUCTION_PACKET.md) imports `timing-private.json`
directly into this helper. There is no second timing worksheet.

Overlapping shots, missing reading time, a beat that cannot fit after the cue,
or an inadequate ending pause are errors. Shorten surplus holds, simplify the
visual, choose a direct crop instead of travel, or raise a recording conflict
in storyboard review. Do not automatically accelerate the whole edit.
Unfilled gaps need an explicit context/proof hold or chapter card; a schedule
with gaps is not itself a finished composition. A successful numerical check
does not prove that the chosen phrase matches the visible content.

Run the executable synthetic example and rejection checks:

```bash
node scripts/test-recording-timing.mjs
```

The numbers in that test are deliberately invented test fixtures, not timings
of any supplied footage. The test shows how the input contract works without
publishing a private recording or suggesting that its synchronization is done.

## Approval and paired playback

Consolidate the transcript-derived beat map, selected source examples, demo
list and current product verification, capture prerequisites, cue, measured
offset, ending buffer, and delivery settings into the single storyboard
review. Get approval before rendering the final presentation. Material
departures require review; routine approved scenes do not need new approvals.

Keep two assets: supplied talking head in OBS and the separate presentation in
Google Slides. Do not composite them, synthesize a voice, duplicate narration,
or add music. Verify the exported pair in the real setup. A stable Remotion
render cannot establish the real OBS startup delay or Slides playback behavior.
