summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-09-26 16:05:19 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-09-26 16:05:19 -1000
commit850479ac3ffbff353c12faadd76f9ae840575136 (patch)
tree0cce18ac0e4691828eb8fcd066f10ea9b18aeac6 /CLAUDE.md
parent4e37185852ef3d1a2ed6348dd4c513648dafdd5d (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.md29
1 files changed, 26 insertions, 3 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index ed4373d..a1a57d5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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