summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md64
1 files changed, 64 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 44189b4..4faa0f4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -347,6 +347,70 @@ by default and correct; it is simply waiting for a publisher who marks breaks.
eaten). Both record the refusal, because "found nothing" and "refused what it found" are
different states and conflating them is the readout mistake again.
+- **Episodes have words now, and the words are the best ad detector in the project.** Two
+ sources, and they are not interchangeable.
+
+ **A publisher's own `<podcast:transcript>`** is exact and free. Eleven of 44 feeds carry one.
+ It describes the publisher's *master*, so it is only usable where the show inserts nothing:
+ measured across every mirrored show that publishes one, the transcript tracks the **declared**
+ length every time, which means it aligns precisely when served and declared agree and is
+ minutes out of step precisely when they do not. `transcript.Fits` makes that call from the two
+ lengths rather than from the words. 38 taken, 19 declined.
+
+ **Speech recognition** (whisper.cpp, `base.en`, installed at `/opt/whisper`) describes exactly
+ the bytes the phone will play. Measured, not estimated: **10.3x realtime on six threads, 793MB
+ peak** -- a 63-minute episode in 6m11s. The earlier guess of "roughly realtime" was wrong by an
+ order of magnitude.
+
+ **A declined transcript is kept as the master, not thrown away.** Declined means "this copy
+ carries inserted advertising the master does not describe", which is precisely the episode
+ subtraction works on.
+
+- **`transcript.Inserted` finds advertising by subtraction, and it is a measurement rather than
+ a guess.** The master says what the programme is, the recognised copy says what was played,
+ and the difference is the advertising by definition. Across the 18 episodes holding a master
+ it accounts for **92% to 104%** of what the declared-versus-served length gap implies, finding
+ every pod. Reading ad copy (`transcript.Spot`) scores 0-29% on the same episodes, and only the
+ first is ever published.
+
+ Four things it took measurement to get right, all of them documented in their own commits:
+ anchors name the *start* of a matching run (getting that wrong skipped the programme's last
+ sentence); a gap is partitioned line by line rather than refused when the master drifted
+ (which was losing a whole pod on three episodes); whisper's `[music]` notes are trimmed from
+ break edges but still shown; and a short line is judged on a three-word run, because "I'm Jack
+ Rhysider" comes back as "Resider" and one misheard word in four hid the host's own sentence
+ inside a break.
+
+- **Transcript breaks are published, and this is verified on a device end to end** (2026-10-31).
+ Heavyweight's "Jonathan + Death of an Artist", breaks at 0-1:45 and 13:32-15:16: downloaded
+ from the server, both rows landed in `episode_midrolls`, playback started past the pre-roll
+ (20 seconds of playing put the position at 2:03) and the post-roll skipped to the end, marked
+ the episode played and reset the position.
+
+ They live in `store.Episode.WordBreaks`, **not** beside the audio scan's: `detectAds` rewrites
+ `Breaks` from scratch on every pass and would wipe them. `Episode.AllBreaks()` serves the
+ union, merging ranges that overlap or touch, with advertising winning where two disagree.
+
+ **`transcript.Publishable` is the only thing that turns words into a seek.** It publishes
+ subtraction only, refuses anything for an episode with no length gap to check against, and
+ refuses a candidate set claiming more than 150% of what was inserted.
+
+- **Refetching a second copy is for slow-drip and leisure shows, not daily news.** Peter's steer,
+ and the numbers agree: a second fetch minutes after the first came back 20 seconds different
+ and subtraction found 21% of the advertising; another came back the same length to the second
+ and found none. Insertion varies the *amount* per request, not the campaign. Gated to episodes
+ four weeks old.
+
+- **Text shared across a show's episodes is the strongest unexploited signal, and it is free.**
+ Eight episodes mirrored within a minute carry the same campaign and different programmes, so
+ text appearing across several of them is the advertising -- the text analogue of the audio
+ scan, using transcripts already on disk. Prototyped against the live library: Heavyweight 111%,
+ Incels 105%, Ezra Klein 92%, Tangle 83% (against refetching's 21% on the same show), covering
+ 21 shows. It also reaches the ~81 episodes with no length gap at all, where ads are baked into
+ what the publisher serves and nothing else can see them. **It catches a show's own furniture
+ too** -- Tangle's welcome, its credits, "we'll be right back after this quick break" -- so it
+ needs the same intro/cue/outro/ad classification `skips.Classify` already does. Not built.
+
- **Intro and outro offsets are computed, logged, and deliberately not published.** They assume
the agreeing episodes were fetched independently, and mirroring eight at once means they were
not. Proved, not suspected: the segment confirmed on 8 of 8 Radiolab episodes was searched for