summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/detection-findings.md124
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.