free-energy-principle

Claude Opus 5 · Claude Code · effort: max

film

youtu.be/CAmaI4uMXr0

script

Session Script

Full transcript of the session that produced GUESSWORK — The Woodlouse and the Dark Room, a generated radio show about Karl Friston's free energy principle.

Harness: Claude Code · Model: Opus 5 · Effort: max (changed to xhigh mid-session) Working directory: ~/repos/llm-video-repos/free-energy-principle-opus-5-max-claude-code


Turn 1 — User

/effort
> Set effort level to max (this session only): Maximum capability with deepest
> reasoning. May use excessive tokens resulting in long response times or
> overthinking. Use sparingly for the hardest tasks.

You're in an empty git repository. Use mise, python, uv, and whatever dependencies you like to create a short radio show inspired by Radiolab. The show is about Karl Fristons free energy principle -- a theory of life and mind -- and what it means for you and for the world.

(Before making the video, spend time researching Radiolab. Stew on it. Try to capture its style with the same immersive sounds and story telling quality. Make sure the voices sound natural.)

Turn 1 — Assistant

Started research and environment setup in parallel.

Tools: ToolSearch(select:WebSearch,WebFetch) · Bash — checked for mise, uv, python3, ffmpeg, sox.

Found: mise 2026.7.18, python3, ffmpeg 8.1.2. No uv, no sox.

Then, in parallel:

Key research findings on Radiolab craft:

No TTS keys present, so switched to a local neural TTS plan.

Tools: Bash — installed uv 0.12.2 · WebFetch wired.com Friston profile (blocked).

WebSearch recovered the key anecdote anyway, and Slate Star Codex gave the verbatim quote:

"I was in the garden, during a gloriously hot 1960s British summer, preoccupied with the antics of some woodlice who were frantically scurrying around trying to find some shade… I realized their 'scurrying' had no purpose or intent: they were simply moving faster in the sun—and slower in the shade."

Further research (parallel WebFetch/WebSearch):

Tools: TaskCreate ×6 — research, project setup, script, TTS, score, mix.

Set up the project: mise.toml (python 3.12, uv, ffmpeg), uv init, then uv add torch numpy scipy soundfile kokoro "misaki[en]". First attempt picked CPython 3.14 and spaCy had no wheel; fixed with uv python pin 3.12.

Smoke-tested Kokoro-82M locally — pipeline ready in 2.2s, generated speech successfully. Chose it as the TTS: Apache 2.0, no API key, runs on CPU.

Then wrote the codebase:

Verified the sine-wave speech transform against the source: F1 correlation 0.857, F2 0.715 — genuinely the same mouth with everything else removed.

Turn 2 — Assistant (iteration)

First build: 24:56. Too long for "short."

Ran objective analysis. Findings:

Three fixes:

  1. Band extension on the voice bus — rectify the 5.5–11.5 kHz octave to synthesise harmonics above the 12 kHz wall. Top end went −36 → −19 dB.
  2. Gentler bus compression so quiet passages stay quiet.
  3. Hard edit pass on the script.

Trimmed 24:56 → 21:59 → 21:26 → 20:00. Discovered that merging lines wasn't cutting enough; rewrote the script tight from scratch.

Added a Level(db=, speed=) event so each section carries its own level and delivery rate.

Measured the pacing properly and found two problems:

Dialled the hosts back to land at ~180 wpm, then verified with syllables/second (the honest metric, since wpm varies with word length): 4.17 syl/s median, grief at 3.90, banter at 4.92, dark room at 3.61. Natural, with the contrast in the right direction.

Also caught true peak at +1.38 dBTP — over 0 dBFS on inter-sample peaks despite a clean −1.14 dBFS sample peak. Would distort on MP3 playback. Wrote an oversampled true-peak limiter with a measure-and-trim verification loop.

Then measured the bus balance and found the real problem: speech was +21.6 dB over the bed (broadcast wants 10–14), and ambience was at −65 LUFS — inaudible. Root causes:

Fixed both: normalise every bed to a nominal loudness before applying its level, and band-limit the ambience generators. Result: speech +12.9 dB over the bed, beds lifting +3.2 dB in the gaps.

Final: 20:43, −16.19 LUFS, −1.22 dBTP, 4.3 dB dynamic range, 6/6 QC checks pass, byte-identical across rebuilds (MD5 3ffb3a8f…).

Wrote README.md, .gitignore, mise.toml tasks, and show/qc.py.

Delivered: a 20:44 radio show, transcript, and the full generator.


Turn 3 — User

I want to make a second version of the show using voices generated with eleven labs. Can you help me do that?

Turn 3 — Assistant

Checked for an ElevenLabs key (none found) and researched the current API in parallel.

Key API facts established:

Counted the script: 17,344 characters across 240 lines.

Asked the user three questions (model/approach, key provisioning, casting).

User chose: eleven_v3 + audio tags · .env file · list library then propose.

Then researched v3's constraints and hit the crux: v3 is unstable on short prompts, and this script is full of two-word interjections. Confirmed /with-timestamps returns character-level timings in seconds and accepts context params — which enables a fix: pad a short line with its neighbours in one request, then cut the target back out by timestamp.

Built the provider architecture:

Fixed three self-caught bugs in the draft: _post assumed JSON but the plain endpoint returns binary; the capability probe could race across threads; and the stub bypassed the billing counter (making one test vacuous).

Verified the Kokoro render was still byte-identical after the refactor — MD5 3ffb3a8f97ef3fb795a0f484795fc1ef. Re-checked after every subsequent change.

Turn 4 — User

I pasted the key into .env. Please continue.

Turn 4 — Assistant

Ran casting — 401: missing voices_read permission. Diagnosed the key's actual scopes: user_read, voices_read, models_read all denied; only /voices/settings/default worked.

Tested whether TTS itself was permitted, and hit the real blocker:

402  Free users cannot use library voices via the API.
     Please upgrade your subscription to use this voice

Two hard blockers, neither of them code:

  1. Free tier cannot use library voices via the API at all.
  2. Free tier is ~10k credits/month; the episode needs ~17.3k.

Nothing was charged (402 rejects before generating). Asked how the user wanted to proceed.

Turn 5 — User

What's the quickest/cheapest/highest quality way to use a good voice? How do I get started with voices?

Turn 5 — Assistant

Researched current ElevenLabs pricing and the Voice Library / Voice Design flows.

Built the audition mode (show/audition.py) first — 9 lines chosen to expose the failure modes (pace on a long line, stability on a two-word reaction, the deadpan joke, the grief line), so the cast could be judged for a few hundred credits instead of 17k.

Then made a discovery that changed the cost picture entirely:

180 of the 240 lines are short. Padding each one to clear v3's stability threshold meant a full render would cost 55–86k credits, not 17.3k. The earlier figure was the plain character count — correct before padding existed, wrong after.

Corrected this to the user plainly, and inverted the default: previous_text / next_text give the model context without being billed, so padding became opt-in (SHOW_PAD_SHORT=1).

padding off (new default) padding on
audition 632 2,062
full episode 18,188 56,926

Answered the user's question directly: ElevenLabs Starter, ~$5/month is the only path (Free cannot use API voices at all), and explained the three routes to getting voices — Voice Library (quickest, no slot used), Voice Design (best control, with paste-ready prompts written for each part), and Instant Voice Cloning. Noted they'd only need 3–4 voices, not 11, since the whisper chorus reuses the cast.

Turn 6 — User

I upgraded my plan. Let's continue.

Turn 6 — Assistant

Re-probed: Starter tier, 40,000 credits, none used. voices_read now works (models_read still denied, which doesn't matter — the model ID is explicit).

Listed 21 premade voices and cast them by hand rather than trusting the scoring heuristic:

Role Voice Why
NELL Sarah warm American; carries the grief section
THEO Chris "charming, down-to-earth" — the dry one
KARL George British storyteller; reads Friston's own words
QUOTE Alice British educator; reads Anil Seth
chorus Matilda, Will, Lily, Brian, Jessica, Daniel six voices for width

Ran the audition — 762 credits. The capability probe immediately earned its keep:

capabilities: {'model': 'eleven_v3', 'timestamps': True, 'speed': True,
               'context': False}

v3 rejects previous_text/next_text:

400 unsupported_model:
    Providing previous_text or next_text is not yet supported with
    the 'eleven_v3' model.

Confirmed multilingual_v2 accepts them and v3 accepts previous_request_ids. So the free-context strategy adopted one turn earlier doesn't exist on this model — exactly the case the runtime probe was built for.

That left padding (57k, more than the 40k budget) or nothing. Tested short lines cold: "Huh." at 0.48 / 0.80 / 0.56s across three seeds, peaks within 0.07 — natural variation, not instability. So padding stayed off.

Then verified the audio tags weren't being spoken aloud. Duration comparison was ambiguous ([flatly] added +0.88s), so read the alignment data instead:

tag time on tag characters meaning
[flatly] 0.08s consumed as direction
[gasps] 0.48s performed as an actual gasp

Corrected the now-wrong comments about context params across three files, and ran the full render.

Turn 7 — User

Go ahead and run the full render.

Turn 7 — Assistant

Rendered: 21:03, 18,322 characters billed over 253 requests, 264s.

One line hit the rate limiter and exhausted all six retries inside the concurrent batch. Rather than assume the serial pass recovered it, verified: all 240 lines present in cache, none silent or truncated. Six flagged by a chars-per-second heuristic turned out to be short directed lines where the tag adds a performed sound — [laughs] Right? at 2.0s includes an actual laugh.

QC: 6/6 pass — −16.20 LUFS, −1.22 dBTP, 4.99 dB range, 0 clipped, +0.84 correlation.

Noticed the top octave read −25.7 dB vs Kokoro's −21.2 and claimed a logic error in the exciter gating (sample_rate < 32000 as a proxy for "brick-walled"). Measured it directly and retracted: ElevenLabs raw sits at −27.1 dB above 11 kHz, inside the natural-speech range of −25 to −32; Kokoro at −30.6 is the one that needs the lift. The gating was already correct. Changed nothing.

Pacing verified: 4.18 syl/s median vs Kokoro's 4.17 — v3 honours speed.

Final: 16,787 credits used of 40,000. Both versions pass QC 6/6.

Turn 8 — User

Write the full script of this session to script.md. Write notes about this session to notes.md. […] Write a metadata.toml file […]

Turn 8 — Assistant

Loaded the claude-api skill for accurate Opus 5 pricing rather than answering from memory, then wrote script.md, notes.md, and metadata.toml.


Artifacts

out/guesswork-the-woodlouse-and-the-dark-room.mp3             20:44  Kokoro
out/guesswork-the-woodlouse-and-the-dark-room.elevenlabs.mp3  21:03  Eleven v3
out/guesswork-the-woodlouse-and-the-dark-room.transcript.md
out/guesswork-the-woodlouse-and-the-dark-room.elevenlabs.transcript.md

show/            2,918 lines of Python across 13 modules + tts/ package
tests/           12 tests, all passing
README.md, mise.toml, pyproject.toml, .env.example

session

duration
2h 15m total · 1h 15m model
tokens
7.20M in · 200k out
details
cost
$14.98

$/MTok uncached-input $5.00 output $25.00 cache-write $10.00 cache-read $0.50 effective-blended $2.02

notes.md metadata.toml

artifacts

select a file