diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/detection-findings.md | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/docs/detection-findings.md b/docs/detection-findings.md new file mode 100644 index 0000000..1eac20a --- /dev/null +++ b/docs/detection-findings.md @@ -0,0 +1,124 @@ +# Tier 3 detection: what is known, and what the next desktop session should measure + +Written 2026-09-29, after getting both halves of the fingerprinting onto a device for the first +time. The code is built, tested and running. **It has not yet confirmed an intro, an outro or a +midroll for a real show**, and the reason is an open question rather than a known bug. This is +the brief for answering it. + +The prototype is the right tool for this and the only one: it can hold a whole corpus in memory, +plot correlations, and sweep parameters in seconds. Do not tune the constants from the app. + +--- + +## What is settled + +**The pipeline runs on hardware.** `MediaCodecClipSource` decodes, `FingerprintWorker` completes +SUCCESS per download, the re-decode path for the pending episode is exercised. This was never +true before 2026-09-28 and two real defects were hiding behind it (below). + +**The confirmation threshold earns its place.** Three independently-fetched episodes must agree +before a candidate is trusted. The original prototype found why: two Darknet Diaries episodes +fetched minutes apart agreed on a segment that turned out to be a rotating inserted ad, not the +show's intro. Everything below makes that trap *more* likely, not less. Do not lower it to make +something confirm. + +**Storage limits are real and were learned the hard way.** Android reads every Room row through +a CursorWindow that refuses anything over 2MB *on the way out* — the insert succeeds and the +next read throws. A 240-second clip at 8kHz 16-bit is 3.84MB. Intro/outro pending clips are no +longer stored at all (the episode id is, and the audio is re-decoded); midroll candidate clips +live in files. `StoredClip` owns the limit and `StoredClipSizeTest` pins it. + +--- + +## The finding that changes the problem + +**Dynamic ad insertion is not a detail; it is the environment.** + +Radiolab declares Grass Apes at `00:22:05`. Measured on two separate fetches of the same +episode: **29:34 once, 27:39 another time.** The ads are stitched in per request, so: + +- the served file's length is not a property of the episode, +- the ad *content* differs between two downloads of the same episode, +- and anything at a fixed offset in one file is at a different offset in the next. + +`insertedAdMs` (declared vs measured) now exposes this per episode in the app. It is free, needs +no audio analysis, and is the obvious cross-check on the detector: **whatever the midroll scan +finds should add up to roughly that much.** If it finds 30 seconds where the gap says seven +minutes, the scan is missing breaks; if it finds more, it is matching content. + +This reframes the intro/outro question. The head window is 240 seconds, which was chosen to +comfortably contain an intro. With a variable-length pre-roll in front of it, a show's real +intro sits at a *different offset in every file* — which the scan does handle, since it searches +the window rather than a fixed position — but the window now has to contain pre-roll **plus** +intro, and the pre-roll is the thing most likely to produce a false match between two +close-together fetches. + +--- + +## The specific open question + +Three consecutive Up First downloads left both ends at "Nothing found yet". + +`FingerprintUpdater.DEFAULT_REFERENCE_SECONDS` is **8**. The original prototype measured Up +First's intro sting at about **6 seconds**. If that is right, every 8-second window being +compared also contains ~2 seconds of differing speech, which would plausibly hold correlation +under `MIN_SCORE` (0.6) for every pair. That is the leading hypothesis and it is untested. + +The competing hypothesis is that pre-roll variability is defeating it — that there is no stable +8-second stretch anywhere in the head window because the ads differ per fetch and the intro's +position moves with them. + +These are distinguishable by measurement, and the measurements are cheap. + +--- + +## Experiments, in order + +1. **Quantify pre-roll variability.** Fetch the *same* episode three times, some hours apart. + Diff the first 240 seconds. How much differs, and where does the common content start? This + also tells you whether `insertedAdMs` varies for one episode, which it did for Grass Apes. + +2. **Locate and measure a real intro.** Across several Up First episodes, find the sting: where + does it start (after how much pre-roll) and how long is it actually? The 6-second figure is + from an earlier note and should be confirmed, not inherited. + +3. **Sweep the two constants against a real corpus.** `DEFAULT_REFERENCE_SECONDS` at 4, 6, 8 and + `MIN_SCORE` around 0.6. Report what confirms and — more importantly — what *falsely* confirms. + A setting that finds Up First's intro but also learns a rotating ad is worse than the current + state, which at least fails safe. + +4. **Check 240 seconds is still enough.** Pre-roll plus intro, on the worst show you can find. + If it is marginal, the window is a cheaper thing to raise than the thresholds — but note it + costs decode time per download, and the clip must never be persisted (see the CursorWindow + note above). + +5. **Validate midroll totals against `insertedAdMs`.** Take a show with a known declared/measured + gap, run the midroll scan across several episodes, and compare the total detected break + duration against the gap. This is the best available ground truth and it costs nothing. + +6. **Re-check the Darknet Diaries trap under the new understanding.** The 2017 episode that + shared an "intro" with 2026 episodes was the original evidence for the threshold. Given ad + insertion varies per *request*, re-fetch that pair now and see whether the false match still + reproduces — it may have been a stable house promo rather than a rotating ad, which would + change what the threshold is actually protecting against. + +--- + +## Do not + +- **Do not change `DEFAULT_REFERENCE_SECONDS`, `MIN_SCORE` or `CONFIRM_THRESHOLD` from the app.** + They are tuned numbers and the app cannot show you what a change costs in false positives. +- **Do not persist a decoded clip in a database row.** See the CursorWindow note. +- **Do not read `EpisodeEntity.durationMs` when you mean the real length.** It is the feed's + claim, it is rewritten on every refresh, and on these shows it is minutes short. Use + `playbackDurationMs`. +- **Do not lower the confirmation threshold to make something confirm.** Failing to skip is a + non-event; skipping real content is the failure that loses trust in the feature. + +--- + +## Environment note + +YouTube extraction and resolution **cannot be exercised from tungsten** — every watch URL gets a +302 to a bot-check page. Anything touching YouTube has to be verified on the phone. Podcast +feeds, Room migrations, playback and the fingerprint pipeline are all fine on the emulator. |
