diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-26 16:05:19 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-26 16:05:19 -1000 |
| commit | 850479ac3ffbff353c12faadd76f9ae840575136 (patch) | |
| tree | 0cce18ac0e4691828eb8fcd066f10ea9b18aeac6 /CLAUDE.md | |
| parent | 4e37185852ef3d1a2ed6348dd4c513648dafdd5d (diff) | |
Extend Tier 3 fingerprinting to midroll ad breaks
Desktop-prototyped first, same discipline as the intro/outro half: three
Darknet Diaries episodes spanning 2026 and one from 2017 were fetched fresh
and cross-correlated. Beyond the show's fixed ~51s intro (present unchanged
across all nine years), the same coarse-to-fine scan found a second, shorter
segment recurring at a *different* timestamp in every episode's body and
independently present in the 2017 episode too -- a real recurring house
promo, not a coincidence of two fetches landing close in time.
That two-episode, different-position, still-present-nine-years-later shape
is exactly what a midroll break looks like and exactly what IntroOutroDetector
was never built to find: it only ever compares fixed head/tail windows.
Midroll detection means scanning a whole episode body, and a show can have
several distinct breaks at once, not one fixed slot per end.
- MidrollScanner: pure, single-resolution "find every segment two recordings
share, wherever each has it" -- deliberately position-agnostic, built on
the existing AudioCorrelation primitive rather than a new one.
- Decimate: block-averaging downsample, the coarse half of a coarse-to-fine
split. A brute-force native-rate scan across a full episode measured
30-160s per episode *pair* on a desktop CPU in the prototype; MidrollUpdater
runs MidrollScanner once on ~8x-decimated audio to find candidates cheaply,
then re-verifies each at full resolution with a single AudioCorrelation
call -- both for a trustworthy score and to reject anything the decimation
made look like a match but isn't.
- MidrollDetector: IntroOutroDetector's confirm/discard state machine,
generalised from one candidate per show to a list. A candidate an episode
doesn't confirm is discarded outright, same discipline, same reason: the
Darknet Diaries trap applies here too.
- MidrollUpdater: orchestrates discovery and confirmation per show. Discovery
compares the show's single rolling "pending" episode against whatever is
newest; unlike ShowFingerprintEntity's pendingClipPcm, no audio is stored
for it between runs -- a pending *whole episode* is tens of megabytes of
PCM, not the few seconds an intro/outro clip costs, so the file is
re-decoded from disk on demand and the pending slot just moves forward if
it's since been reclaimed by retention. A real bug the tests caught: the
same segment surfaces again every time the pending pointer rolls forward,
which without dedup became a second row for content already tracked.
Fixed by recognising a fresh discovery hit as already-tracked by *where*
it landed in the new episode (MidrollDetector.allMatches), not by
re-correlating extracted clip content, which two different coarse passes
don't reliably extract the same sub-range of.
- Storage: AppDatabase 7->8, a show_midrolls table (one row per candidate,
not per show -- several breaks can be tracked at once) and an
episode_midrolls table (the confirmed ranges detected per episode).
EpisodeEntity gains midrollCheckedAtMs (per-episode, not a show cursor, so
an out-of-order backfilled download still gets checked). SubscriptionEntity
gains midrollPendingEpisodeId.
- Playback: MidrollSkip mirrors AdChapters' shape exactly. PlayheadPolicy
checks it unconditionally, same as intro/outro -- no per-show toggle, the
confirmation threshold is the safety mechanism. PlaybackSources grows a
midrolls() method alongside chapters() rather than threading a new DAO
through PlaybackAdvancer's constructor, which would have pushed it over
detekt's LongParameterList threshold for no real reason.
- Trigger: MidrollWorker runs nightly while charging, not per-download like
FingerprintWorker -- a full-body scan is too expensive to run eagerly, and
unlike a download it never touches the network, so there is no wifi gate
either. Peter's call.
MediaCodecClipSource gains decodeFull, sizing the decode buffer from
durationMs with a margin rather than the fixed 4-minute clip window
decodeHead/decodeTail use.
Coverage 91.70% -> 91.76%. Neither half of Tier 3 has run on a device this
session -- see CLAUDE.md's known-gaps list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sj3CoWEedM56bebByKgNxA
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 29 |
1 files changed, 26 insertions, 3 deletions
@@ -127,9 +127,27 @@ downloaded at once, Facebook videos are supported, the library ranks shows by th and opt-in skipping of breaks the publisher marked `toc: false`. Nothing is guessed from chapter titles. -**Tier 3 skip detection — audio fingerprinting of theme music — is still not built.** Peter -deferred it; the design plan puts it in Phase 3 and wants a desktop prototype against real -episodes before any Android code. +**Tier 3 skip detection — audio fingerprinting — is built, in two parts.** Both were desktop- +prototyped against real episodes before any Android code, per the design plan's own instruction. + +- **Intro/outro** (`IntroOutroDetector`, `AudioCorrelation`, `MediaCodecClipSource`, + `FingerprintUpdater`): one fixed candidate per show per end, needing 3+ independently-fetched + episodes to agree before it is trusted -- the confirmation threshold exists because two episodes + fetched close together can agree on a segment that is a dynamically-inserted ad, not the show's + real intro (found prototyping against Darknet Diaries). Triggered per download via + `FingerprintWorker`. +- **Midroll ad breaks** (`MidrollScanner`, `MidrollDetector`, `MidrollUpdater`): a show can have + several distinct midroll breaks, at a different timestamp in every episode, so this scans a + whole episode body rather than a fixed head/tail window -- `MidrollScanner.findSharedSegments` + finds every segment two episodes share, at whatever position each happens to have it, and + `MidrollUpdater` composes a coarse (decimated) pass with a native-resolution verify so that scan + is fast enough to run on a phone. Prototyping this against Darknet Diaries found a real recurring + segment (not the fixed intro) independently present across episodes nine years apart. Runs + nightly while charging (`MidrollWorker`), not per-download: a full-body scan is too expensive for + that, and unlike a download it never touches the network. No per-show toggle, same as + intro/outro -- the confirmation threshold is the safety mechanism. + +Neither has been verified on a device yet -- see the known-gaps list below. **ID3 `CHAP` frames are dropped, not deferred** — decided 2026-09-26 by measuring Peter's actual 61 subscriptions rather than guessing. Every feed's newest episode was fetched far enough to read @@ -161,6 +179,11 @@ by default and correct; it is simply waiting for a publisher who marks breaks. - **Android Auto is unverified in a real car.** The session protocol is proven by `MediaBrowseTest`; whether Auto's own compatibility checks pass on a head unit is untested, and a sideloaded media app may need Auto's developer mode enabled. +- **Neither half of Tier 3 fingerprinting has run on a device.** The decode/decimate/correlate + pipeline is proven against synthetic fixtures and, separately, against real downloaded episodes + in a throwaway desktop prototype -- never against `MediaCodecClipSource`'s actual + MediaExtractor/MediaCodec decode loop on real hardware, and the `MigrationTest` 7→8 case has + only run on the JVM-backed `MigrationTestHelper`, not an emulator or device. ## Domain notes worth keeping |
