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 /docs | |
| 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 'docs')
0 files changed, 0 insertions, 0 deletions
