From 4e37185852ef3d1a2ed6348dd4c513648dafdd5d Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 26 Sep 2026 12:57:26 -1000 Subject: Tier 3 skip detection: audio fingerprinting, prototyped and built Closes the last deferred Phase 2 item. Desktop-prototyped first, per the design plan's own instruction, against three real shows (see the prototype findings recorded in memory): NPR Up First (a clean ~6s intro sting, r 0.96-0.998 across every pair tested), Tangle (a long 60-90s open/sponsor read, equally reliable), and Darknet Diaries (no real intro -- a segment that matched between two temporally-close episodes turned out to be a dynamically-inserted ad, confirmed by checking a 2018 episode that matched the same "intro" despite the multi-year gap). That last finding drove the actual design: IntroOutroDetector requires 3+ independently-fetched episodes to agree before trusting a candidate and auto-applying it, which is exactly what the Darknet Diaries ad would not have survived. The confirmation threshold is the safety mechanism, not just caution for its own sake -- and it's now a parameter, not a hardcoded assumption, after coverage review found the "still tentative" branch was unreachable at the original threshold. - AudioCorrelation: FFT-based normalized cross-correlation, a Kotlin port of the validated prototype. Tested against numpy/scipy-derived ground truth fixtures, not just self-consistency -- the prototype's own normalization bug (global mean instead of each window's local mean, producing a bogus 0.98 "match" plain correlation put at -0.02) is exactly what that guards against being ported here unnoticed. - IntroOutroDetector: the pure state machine (waiting/tentative/confirmed/ discard-on-mismatch), mirroring DripSchedule's shape. - MediaCodecClipSource: the one Android-only piece (MediaExtractor/ MediaCodec decode to mono 8kHz PCM), on the framework-adapter coverage exclusion list for the same reason PlayerConnection is. - Storage: AppDatabase 6->7, a new show_fingerprints table (one row per show per intro/outro, tracking the current candidate clip and how many episodes have confirmed it) and two new nullable EpisodeEntity columns. Tier 2's manual per-show offset remains the fallback -- PlaybackProgress itself is unchanged; DetectedSkip.kt is the entire integration surface. - Trigger: EpisodeDownloader gained an onDownloaded hook (every download route ends up there, manual or queued) firing a new one-shot FingerprintWorker per episode. Coverage improved (90.13% -> 91.70%): the new pure logic is fully tested, and closing two real gaps the coverage review surfaced (an untested first-two-episodes-disagree path, and ShowFingerprintEntity's hand-written equals/hashCode, needed because a ByteArray property breaks the generated one) accounted for more than the new Android-glue lines cost. Not yet verified on a device -- no phone connected this session. The MigrationTest 6->7 case and the actual decode/match pipeline both need a real run before this is trusted with real playback. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Sj3CoWEedM56bebByKgNxA --- app/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'app/build.gradle.kts') diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f0ae29f..f76f892 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -284,6 +284,12 @@ val coverageExclusions = listOf( "**/data/Graph*", "**/data/AppGraph*", "**/ListenApp*", + + // MediaExtractor/MediaCodec decode loop -- no JVM equivalent, same bar as PlayerConnection. + // AudioCorrelation, IntroOutroDetector, PcmCodec and FingerprintUpdater all stay measured: + // this is the one class between them that actually touches the Android media stack. + "**/fingerprint/MediaCodecClipSource*", + "**/work/FingerprintWorker*", ) tasks.register("jacocoTestReport") { -- cgit v1.2.3