1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
|
# Telling a show's own audio from the advertising inserted into it
2026-10-08. Design for review; nothing here is built.
## What is wrong now
Three defects, all in the ad scan, all visible in the first full corpus (42 shows, 322 mirrored
episodes).
**D1 -- it finds about two thirds of the advertising.** Median agreement with the declared-versus-
served length gap is 67%, and 47 of 108 checkable episodes come in under half. The scan can only
see advertising that two simultaneously-mirrored episodes *share*; an advertisement served into
one episode and not another is invisible to it. Search Engine is the extreme: 8:23 of insertion
on one episode, of which we skip 0:49.
**D2 -- it reports a show's own audio as advertising.** 138 of 322 mirrored episodes have a served
length equal to their declared length, which means no advertising was inserted into them at all.
The scan publishes "ad breaks" on 111 of those, 74 minutes in total. What it has actually found is
the show's opening theme, its break cue and its closing credits. Worth skipping -- that is what
intelligent skipping was asked for -- but it is not advertising, and the app says "Ads inserted".
**D3 -- intro and outro offsets are computed and withheld.** They are found by eight episodes
agreeing, and eight episodes mirrored inside one minute are not independent evidence: that is how
an advertisement came to be confirmed as an intro on 8 of 8 Radiolab episodes. Nothing has changed
that, so the offsets stay unpublished.
## The observation that connects them
A show's own recurring audio is **permanent**. The opening theme is in this week's batch and will
be in next month's. Inserted advertising is **transient**: three consecutive fetches of one
enclosure came back byte-identical to each other and 82KB different from the copy mirrored days
earlier, so the ad load is fixed within a session and rotates over days.
So persistence across separate fetch occasions is both the thing that distinguishes furniture from
advertising *and* the independence test D3 has been waiting for.
Peter's observation about theme music is the same structure seen from the listener's side. Tangle,
Jon Stewart and Search Engine each play a short cue at every break, and the scan already finds it
-- it is the short, high-partner-count block that the partner-count filter would have discarded.
Search Engine's is 8.0s at `13:08`, `35:28` and `44:51`, found in 5 to 13 other episodes;
templating it and scanning episodes it was not taken from locates it at r = 0.96, 0.92, 0.85.
**It marks a break's entry, not its extent.** Scanning down to r = 0.6 returns one isolated hit
per slot in ad-laden and ad-free copies alike. There is no closing cue, so a cue says a break
starts here and nothing about how long it runs -- and the length is what a skip needs.
## Three approaches
### 1. Read it off what is already computed (recommended)
No stored audio, no new state.
- **D2, exactly and for free.** Served length equal to declared means nothing was inserted,
therefore everything found is furniture. That classification is available on every mirrored
episode -- all 322 have a usable declared duration -- and it settles all 111 mislabelled ones.
- **Cues, from their signature within one batch.** A cue is a short block (<= ~15s) of near-
identical duration occurring several times per episode across most of the batch. Search Engine:
8.0s, three or four per episode. Jon Stewart: ~5s, four per episode. Both already in the data.
- **D1, partially.** Treat a cue as a local prior: in the window after a cue an advertisement is
certainly present, so accept weaker cross-episode evidence there than mid-programme. This
attacks the under-reporting where we know to look, rather than lowering the threshold globally
-- which the disproven filter experiment shows is indiscriminate.
- **D3: not addressed.**
### 2. A per-show furniture library
Everything in approach 1, plus a stored set of the show's own clips.
Per show, up to 8 clips, each holding the audio at the discovery rate (250Hz -- 8s is 2000
samples, 16KB), the number of separate fetch occasions it has been seen on, where it sits
(opening / mid-episode / closing), and when it was last seen. Clips live as files beside the
mirrored audio; `state.json` keeps the metadata and a reference. 42 shows at 8 clips is about
5.4MB.
Each pass scans every episode for every known clip -- one cross-correlation per clip per episode,
2000 samples over 750,000, which benchmarks around 0.33s, so roughly 20-40s per show against the
6-10 minutes a pass already costs. Candidates come from the ad scan's short high-partner blocks,
and a candidate is **promoted only when it reappears on a later pass whose batch differs** -- a
genuinely separate fetch occasion.
- **D3 addressed.** A clip confirmed on two separate occasions and consistently at the opening
*is* the intro, on independent evidence. The withheld offsets become publishable.
- **D2 more precisely.** For the 154 episodes that do carry advertising, this separates furniture
from ads *inside* them, which approach 1 cannot.
- Cost: new persistent state, a promotion rule, and the eviction policy.
### 3. Metadata-only persistence
Store each candidate's shape across cycles -- duration, position, partner count -- but not its
audio. Cheap, and useless: it can confirm that a block of some shape recurs, but cannot locate it
in a new episode, which is the entire purpose. Rejected.
## Recommendation
**Approach 1 now. Approach 2 only if the intro and outro offsets are wanted back.**
Approach 1 fixes the one defect that is provably wrong today (D2), attacks the largest one (D1),
needs no storage and no new state, and can be validated immediately. Approach 2 is the only route
to D3, but its value arrives a fetch occasion later and it carries the one genuinely awkward risk
below.
## How it gets judged
The declared-versus-served length gap stays the judge, because the detector does not produce it.
- **D2 is binary.** The 138 zero-gap episodes must report zero *advertising* while still reporting
furniture. Any advertising reported on them is a failure.
- **D1 moves or it does not.** Median found-versus-inserted must rise from 67%, while the
over-150% group -- 9 of 108 episodes today -- does not grow.
- **A new guard: the gap as a budget.** Where a feed declares a length, never publish more
skipping than the gap implies. `MaxSkipFraction` (0.35) and `MaxBreakMs` (240s on the matches)
stay as they are.
## Risks
**A long campaign could be promoted as furniture** (approach 2 only). A sponsor running for weeks
survives two fetch occasions and looks permanent. Bounded by the eviction cap and visible in the
gap accounting, but real, and the honest answer is that persistence is a good discriminator rather
than a perfect one.
**The cue prior could over-extend a break.** Bounded by the gap budget and the fraction guard.
**Cue identification could misfire on musically repetitive shows.** Sleepy History reuses long
ambient beds. Mitigated by requiring a cue to appear at *several distinct positions within a
single episode* -- which a break cue does and an opening theme does not -- at a consistent
duration.
## Question for Peter
Furniture and advertising both get skipped by default, and the readout names which is which --
"Show intro", "Break cue", "Credits", "Ads inserted" -- rather than calling all of it advertising?
That is the proposed default on the grounds that skipping the intro is what was originally asked
for. The alternative is to skip only inserted advertising and merely mark the furniture.
## Phases
1. Zero-gap classification. Stops reporting advertising where none was inserted. Safe: changes
what the readout *says*, not what gets skipped.
2. Cue identification within a batch, reported but not acted on, so the signature can be checked
against real shows before it moves a playhead.
3. The cue prior, validated against the length gap.
4. Approach 2's library, if D3 is wanted.
---
## Decisions, 2026-10-08
**Approach 2, built in the phase order above.** Approach 2 is a superset of approach 1, so the
sequencing recommendation stands: the ad-detection improvements land first and the library last.
**Furniture skipping is a setting; advertising is not.** Advertising is always skipped. The show's
own intro, break cue and credits are skipped according to a single preference, default on --
default on because that is what happens today, so defaulting it off would be an unrequested
behaviour change.
### What this adds to the design
**Breaks carry a kind.** `ad`, `intro`, `cue`, `outro`. The server decides it; the phone stores it
alongside the range and filters on it at playback.
**The kind is decided by a rule that works before the library has anything in it**, which matters
because a clip cannot be promoted until a second fetch occasion has passed:
1. If the episode's served length equals its declared length, nothing was inserted, so every
break is furniture. Position decides which: opening, closing, or `cue` in between.
2. Otherwise the episode does carry advertising, so a break is `ad` unless it matches a promoted
furniture clip.
So day one gives correct labels for the 138 zero-gap episodes and treats everything else as
advertising -- exactly today's behaviour, which is the safe default -- and the labels get finer as
the library fills.
**The cue stays skippable even with furniture skipping off.** A cue sits immediately before an
advertisement, so hearing it is a useful signal that something was cut. It is furniture, it obeys
the setting, and nothing special is needed.
### Where each part lives
- Server: `kind` on `store.Break`, decided in `skips.DetectAds` plus the zero-gap rule, carried in
the API's break objects.
- Phone: a `kind` column on `episode_midrolls` (Room schema 13), a boolean in the existing
preferences, and a filter in `MidrollSkip` and `PlayheadPolicy`.
- Readout: the episode screen names each break by kind and shows whether it will be skipped, so
the setting's effect is visible rather than inferred.
### Open, deliberately
How the library's clips are chosen for eviction when a show exceeds eight, beyond
least-recently-seen. Not worth settling before there is a real library to watch fill.
|