diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/detection-findings.md | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/docs/detection-findings.md b/docs/detection-findings.md index 26b31b3..35c4525 100644 --- a/docs/detection-findings.md +++ b/docs/detection-findings.md @@ -569,3 +569,102 @@ detects, with very high confidence, is **the inserted advertising itself** -- be simultaneous fetches share it exactly. Skipping ads is the actual goal, and the declared-versus- served gap is a free, independent check on whether a scan has accounted for all of it. Detecting ads directly may be a better target than detecting intros and hoping ads do not get in the way. + +--- + +## 2026-10-06: following the data -- detecting the advertising instead + +Peter's call, on the evidence of the previous section: stop trying to detect intros and detect +what the server demonstrably finds. + +### Why this is the right way round + +The property that broke intro detection is the property that makes ad detection work. Eight +episodes mirrored within one minute are maximally dependent, because the ad server inserts +whatever campaign is running into all of them identically. For intro detection that destroys the +independence the confirmation threshold needs. For ad detection it *is* the signal. + +And unlike intro detection, this can be checked against a number the detector did not produce. +The feed declares the ad-free length, the file served is longer, and the difference is the +advertising that was inserted. + +### The algorithm, and why the old one could not do this + +The sliding-window search cost one full-length correlation per window position -- 199 seconds for +a 240-second clip, measured. Over a 70-minute episode it is hopeless. + +`detect.BestLags` instead computes the cross-correlation over *every* lag in one FFT pair, which +says where two recordings align. `detect.MatchedRuns` then walks the extent of the agreement at +that alignment directly, window by window, with no transform at all. Sized against measurements +rather than guesses (`fft_bench_test.go`: 2^22 is 1.69s), a 70-minute episode decimated to 250Hz +is three transforms of 2^22 -- about five seconds a pair, so a batch of eight is minutes. + +Silence is explicitly not agreement. Two recordings agree perfectly on it, and counting it would +weld separate advertisements together across the pauses between them. + +### Measured on the real deployment + +Radiolab, eight mirrored episodes: + +``` +Radiolab: ads on 8/8 episode(s) (5 refused as too extensive to be advertising), + accounted 53:29 of 59:39 inserted, took 2m56s +``` + +Per episode, found against what the length gap implies: + +| episode | found | inserted | ratio | +| --- | --- | --- | --- | +| Grass Apes | 6:31 | 6:13 | 105% | +| The Sweetest Thing | 6:31 | 6:13 | 105% | +| 23 Weeks 6 Days | 6:40 | 8:59 | 74% | +| Dolly Parton's America | 6:11 | 6:58 | 89% | +| Nigel and the Real Gannet | 6:54 | 7:07 | 97% | +| The Shoe Thrower | 6:47 | 9:33 | 71% | +| The Holy Shiver | 6:26 | 6:59 | 92% | +| This is Your Brain on Hormones | 7:26 | 7:33 | 98% | + +**Every episode lands between 71% and 105% of an independent measure, and the batch total is +90%.** That is the first time in this project that detection has been checked against external +ground truth and agreed with it. + +### What is published, and what is not + +Ad breaks are published, as ranges, into `episode_midrolls` -- which `MidrollSkip` and +`PlayheadPolicy` have read since the on-device detector existed. Playback needed no changes and +there is no migration. + +**Intro and outro offsets are no longer published.** They are still computed and still logged, +because they will become trustworthy once the agreeing episodes are required to span more than +one fetch occasion. Until then they are known to mis-identify, and the advertising they kept +finding is better served by the scan that measures itself against the length gap. The previously +published ad-derived offsets were cleared from the server's state. + +Two guards decide nothing gets published rather than something wrong: + +- **An episode whose matches cover more than 35% of it publishes nothing**, because two episodes + of a show can legitimately share a great deal -- a re-aired episode, a two-parter with a long + recap -- and skipping that would eat the programme. +- **A single match longer than four minutes is dropped**, being longer than any plausible ad pod. + +Both record a refusal rather than silently leaving an episode empty. An episode that published +nothing because its matches were implausible is a different state from one where nothing +matched, and conflating those two is exactly the mistake the app's skip readout already made. + +### Three data-loss bugs found while deploying this, all fixed + +Found by reading the journal after a restart, not by a test: + +1. **The subscription list lived only in memory.** `SetFeeds` from the API did not persist, and + state was saved only when a whole poll cycle finished -- which for 61 feeds is hours. A + restart in between lost every subscription the phone had sent, and that is the one thing the + server cannot rediscover. Now saved the moment it arrives. +2. **A poll cycle re-applied a stale subscription snapshot at the end**, which would silently + undo a push that arrived during the cycle and then delete the audio of every show the stale + list omitted. Now reconciled against the list as it stands. +3. **Shutdown could not complete.** Detection ignored cancellation, so systemd's default + 90-second stop would SIGKILL the process mid-scan and the save-on-exit never ran. Detection + now checks for cancellation and the unit allows ten minutes to stop. + +State is also saved after every feed now, so a restart costs one feed's progress rather than a +whole cycle's. |
