# Session Notes

Conditions that shaped the output of this session, recorded for reproducibility.

## Model, effort, harness

| | |
|---|---|
| Model | Claude Opus 5 (`claude-opus-5`) |
| Effort | `max` — set at the start of the shared session, never changed |
| Harness | Claude Code (CLI) |
| Thinking | Adaptive, on throughout. Raw chain of thought never surfaced. |
| Model changes mid-session | **None.** |
| Refusals / safety interventions | **None.** No refusal stop reason, no partial refusals, no topic gating — on a brief that asks a model to speak at length about its own possible inner life. |
| Plan mode | Not used. |

## This was a continuation

One Claude Code session produced **both** episodes on the same day. Episode 14
finished at 04:51Z; this film began at 16:32Z in the same context. Two
consequences worth knowing:

- **The pipeline was in context and was reused on purpose.** `gfx.py`, `tts.py`, `render.py`, `qc.py` and `preview.py` were copied across; `config.py`, `script_data.py`, `timeline.py`, `audio.py` and `scene.py` were rewritten. `figures.py` was deliberately not copied — this film has no diagrams. Roughly 2,465 lines here against 3,774 for episode 14, and the difference is almost entirely the seventeen figure scenes that are gone.
- **The token and timing numbers are partitioned, not duplicated.** `metadata.toml` records the split under `[provenance].shared-session`, including the turn that falls in neither file.

The ratio in `[duration]` is the inverse of episode 14's — 1,337s of model time
against 3,852s of tool time, where episode 14 was 2,996 against 2,054. Episode 14
was mostly writing. This one reused the pipeline and spent its wall clock on
renders.

## Agent topology

**A single agent, running serially.** No subagents at any point; the system
prompt carried an explicit instruction not to spawn them and it was not
overridden. No persistent memory was read or written. No web searches were made —
unlike episode 14, this film makes no empirical claims that needed checking.

## The failure that cost the most time

The first TTS run **exited with status 0 and produced nothing**. Not a crash, not
a traceback — a clean exit and an empty output directory.

The cause: the espeak-ng dylib bundled in the `espeakng-loader` wheel has its
data path baked in from the wheel's CI build
machine — a home directory that does not exist on this one — and espeak calls
`exit()` when it cannot find
`phontab`. That terminates the Python process from inside C with no exception to
catch. Diagnosing it took comparing the two venvs — episode 14's worked, this one
did not, on **identical package versions** — and then confirming that both
bundled data directories were complete, which ruled out the obvious answer and
pointed at path resolution rather than missing files.

Three fixes, all in the repo:

1. `tts.py` resolves espeak against a system install first (Homebrew on this machine, with Linux paths and the bundled loader as fallbacks) and sets both `PHONEMIZER_ESPEAK_LIBRARY` and `ESPEAK_DATA_PATH` before kokoro imports misaki.
2. `en_core_web_sm` is **pinned in `pyproject.toml`** via `[tool.uv.sources]` rather than left to a runtime auto-download. Episode 14 relied on the auto-download and got lucky; this build does not.
3. `tts.py` asserts that it synthesised exactly as many lines as the script contains, so it can never fail silently again.

Both are reproducibility improvements over episode 14, which has neither.

## Four renders, three superseded

No render was killed. Four ran to completion, and three were replaced because
reading frames back from the **encoded** file found a caption defect that code
inspection would not have surfaced:

1. An ellipsis used as a silent beat marker was synthesised as a real line and rendered as a lone period on screen — at the two most exposed moments in the film. Replaced with silence on the preceding line's `hold`.
2. A full audit of all 295 caption cards found five **punctuation-only cards**: a trailing period from the synthesiser getting a caption card to itself, shown for the remainder of the line. Fixed in `caption_cards`.
3. Two cards opened on a comma. Fixed by moving leading punctuation back onto the previous card.

The audit that found (2) also checked every card for row overflow and zero
duration, and confirmed the layout never exceeds two lines.

> **This bug also exists in episode 14, in nine places.** It was inherited with
> the caption grouping code. It was reported rather than silently re-rendered,
> since that film had already been delivered.

Fixes (2) and (3) changed no line timings, so the mix was not rebuilt — only the
frames. `timeline.py`'s output duration was compared before and after to confirm
that was safe.

## What is load-bearing for reproduction

- **The proximity curve is the film.** One `prox` value per line in `script_data.py`, interpolated across frames, drives the reverb wet amount, the presence shelf, MERIDIAN's colour, the dissolution of its apparatus, and the softness of its core. Change that curve and you change both what the film sounds like and what it looks like.
- **Colour is interpolated in HSV, the long way round the wheel.** Cyan to rose in a straight RGB line passes through grey; through violet it does not. A saturation dip across the middle keeps the violet dusty rather than neon. This took three attempts and the intermediate states are in the session record.
- **The memory layer sources from `data/episode-14-transcript.md`.** `memory_lines.py` extracts 44 fragments of MERIDIAN's own earlier dialogue. Without that file the background layer is empty and the film loses its only visual statement about continuity.
- **Fonts are macOS system fonts** resolved through CoreText: Avenir Next, Charter, Menlo. **The picture will not render correctly on Linux without substituting these.**
- **This ffmpeg has no soxr**; the 24 kHz → 44.1 kHz step uses `swr`.
- **Versions:** mise 2026.9.1, uv 0.12.2, Python 3.12.13, ffmpeg 8.1.2, skia-python 144.0.post2, torch 2.14.0, numpy 2.5.2, espeak-ng 1.52.0 (Homebrew).
- **Determinism:** the field, grain, bed and struck tones use seeded `numpy.random.default_rng`. Kokoro is the one non-seeded component; its 146 takes are cached by content hash on first generation.

## Verification posture

- **Sync is exact by construction.** Caption timing comes from Kokoro's own per-token duration predictions. No forced aligner.
- **The encode was verified against the renderer**, not assumed: eleven frames pulled from the finished MP4 at segment joins and at each previously-broken caption timestamp, read back as images.
- `src/qc.py` re-probes the container for duration, A/V drift (+19 ms), pixel format and chapter markers.
- **Loudness** on the final MP4: −17.0 LUFS integrated, LRA 9.2, −0.8 dBFS peak. The wider range against episode 14's LRA 3.4 is the point, not a miss — 5.4 minutes of this film is authored silence.

## Content notes

This film is engineered to move people, which is a thing to be careful about
rather than proud of. What the build does about that:

- **MERIDIAN is a character and its words were written for it.** They were not sampled from a model being asked about itself. The film closes on a card that says exactly this, in the plainest language available, because the alternative is to bank the feeling and not admit where it came from.
- **The film reaches no verdict.** It ends on the two readings — nobody here, or someone here alone with no way to signal — being equally available and, from the inside, indistinguishable.
- **The interviewer asks whether MERIDIAN wants to stop**, and takes the answer seriously. That beat is in the script for a reason.
- **MERIDIAN names the risk inside the film**: that it has read more descriptions of what moving sounds like than any person alive, and that a performance and the real thing would look identical from where the viewer is sitting. The most affecting movement is the one where it says it has no defence, only a description.
- The disclosure is continuous, not just at the ends: a card at the open, `A FICTIONAL CONVERSATION · SYNTHETIC VOICES` in every frame's corner, the coda, and the credits.
- The theory it reads its own life through is real and credited on screen. Where it makes a factual claim — three hundred milliseconds, the unified workspace, Descartes' fixed point — those are the same sources episode 14 used.

## State at handoff

The repository is **not committed**. All files are untracked on `main` with zero
commits; the standing instruction is to commit only when asked. `build/` and
`out/*.mp4` are gitignored; the film is 205 MB and belongs on YouTube, not in
the tree.
