What we did right
Measured the artifact, not the notes
The README asserted one narrator. The file disagreed. A pitch fingerprint on identical text matched macOS system speech to within 0.3 Hz — that settled it in one measurement.
Documentation cannot detect its own defect.
Found a signal that can't be argued with
Pitch statistics overlapped enough to debate. Container format did not: the API returns 24 kHz mono, the bad clips were 48 kHz stereo. That became the load-bearing check.
Prefer a check with no judgement call in it.
Build stops instead of substituting
The replacement TTS step has no fallback path. If the provider is unreachable the build halts rather than quietly reaching for a local voice.
The original bug was a silent fallback nobody could see.
Tested the gate against known-bad input
Ran the new checks against the defective audio to confirm they actually stop the build. A gate never fired in anger is a guess.
Re-ran QC instead of copying last verdict
That is the only reason a 3.2 s dry CTA tail was found — the audio beds were shorter than the new runtime and the previous session's passing result would have hidden it.
Byte-verified every deploy
md5 of the local deliverable against the fetched remote, every time. Also caught edge propagation returning an HTML fallback for ~30 s on fresh paths.
Corrected overstatements on the record
Claimed all seven clips were the wrong voice when only one was proven. Corrected it in chat and in the commit message rather than letting the stronger claim stand.
Killed a blanket re-run after one image
Spotted a generate-all script overwriting already-QC'd plates and stopped it before it reached them. The output directory was gitignored — there was no restore path.
What we did wrong
Answered from the README
Reported "nothing regressed" on a spot that had genuinely lost its voice, because I trusted the project's own build notes instead of probing the file.
→ G6. A defect report is answered by measuring the artifact.
Applied "sheet is the law" to the mascot only
The bot held up across 14 shots because it was passed as a model sheet image. The two women were passed as prose — and rendered as different people, in one case a different art style.
→ G1. Prose never carries identity.
Dropped a check during a rewrite
Restructuring the voice gate for multi-speaker quietly removed the content check. A clip then spoke its own stage direction aloud and passed origin and pitch.
→ G3. Origin and pitch cannot see wrong words.
Fixed a bug in one script, not its sibling
Non-deterministic generators silently replace already-rendered assets. I added
--only= to the voice script, then hit the identical bug in the stills script
beside it an hour later.
→ G2. Fixing a class of bug means grepping for the pattern.
Said a capability wasn't provisioned
Told the Architect ElevenLabs wasn't available. It was — I checked env vars and grepped memory but never listed the secrets directory. That wrong answer shaped a decision.
→ Credentials. List the directory before reporting absence.
Built photoreal humans beside a cartoon mascot
Two full cuts rejected on sight. A generated "real" face lands in the uncanny valley and collides with stylised IP.
→ G0. Cartoon is the default when a mascot is in frame.
Used pitch spread as a provenance proxy
It failed 6 of 14 correct clips, because the reads were deliberately weary and deadpan. The test was measuring performance and calling it origin.
→ G3. Flat delivery legitimately scores narrow.
Nearly signed off on a node count
Counting audio elements in the HTML proves the effects are wired, not that they are audible. Only a level measurement on the rendered file proves that.
→ G5. Verify in the finished file.
Two avoidable tooling wounds
A ps aux | grep waiter matched its own command line and deadlocked forever.
A text-index code splice matched a comment and cut a constants block out of a working file.
→ Tooling notes.
Trusted a helper that quietly did nothing
~/bin/save exits at "Nothing to commit" before pushing or archiving.
On already-committed work, Tier 3 silently does neither.
→ Verify the side effect, not the exit code.
The line
Brief
- Scope: internal or commercial? It changes what is defensible.
- Style: mascot in frame → house cartoon, not photoreal.
- Name every protected element and exclude it in every prompt.
- Raise IP concerns once, take the answer, log the decision.
Identity — sheet is the law, for the whole cast
- Model sheet per recurring character: 3 views, expressions, signature pose.
- Publish the sheets; pass them on every plate, for every character present.
- Prose describes staging only. Wardrobe enumerated per shot.
Generators
- Every generator takes
--only=. Never blanket re-run to add one asset. - If the output dir is gitignored, a blanket run is destructive.
Voice — three independent gates
- Origin: container format matches what the API returns. Deterministic; load-bearing.
- Content: transcribe every clip; must start on the line, no direction tells.
- Cast: group by role, not voice — one voice can play two characters.
- No fallback path. Prompt as
DIRECTION (do not speak this)/LINE:. - Emotion via tags; low stability lets a read swing. Never floor pitch spread.
Composition
- Re-run lint / validate / inspect every build. Never carry a verdict forward.
- Beds cover the full runtime. Reused plates get distinct filenames. One root composition.
- Persistent overlays sit on a contrast chip.
Mix
- Measure the rendered file: level at the effect vs the adjacent silent beat.
alimiterneedslevel=disabledor it re-normalises to 0 dB.
Visual QC
- Contact sheet, then open individual frames. Look, don't infer.
- Identity across shots · wardrobe · mascot on-model · on-screen text vs VO · legibility over art.
- WCAG checkers compare declared CSS colours and cannot see pixels behind text.
Deploy
- md5 local vs fetched remote. Identical or it did not ship.
- Expect ~30 s edge lag on fresh paths; retry with a cache-buster before calling failure.
Record
- Docket → recap → session log → dropbox → D1. Receipts first: md5s, output, measured numbers.
Traps worth remembering
| Trap | Tell | Fix |
|---|---|---|
| Silent local-voice fallback | Container format differs from the API's native output | No fallback path; build stops |
| Clip narrates its own direction | Clip runs much longer than its line (9.25 s for 5.61 s) | Labelled DIRECTION/LINE + content gate |
| Free vs billed API key | 429 partway through a multi-clip run | Billed key lives in the secrets dir — check it first |
| Blanket generator re-run | Assets change under an already-rendered cut | --only= on every generator |
| Two root compositions | Duplicated audio on playback | Variants outside root |
| Self-matching process waiter | Waits forever, no output | Never grep for a string your own command contains |
Piping a long job through tail | No progress until exit | Track by file mtimes |
| Save helper no-op | "Nothing to commit" then silence | Push and archive manually |
Iron Noodle