From d7da85860bde910a182df2fc65ce284750272a78 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 30 Oct 2026 08:37:15 +0000 Subject: Publish the advertising the words measured, and only that Subtraction has earned it. Eighteen episodes, 92% to 104% of what the declared-versus-served length gap implies, every pod found, edges pulled back off the programme, and the one class of mistake that mattered -- a break opening on the host's own sentence -- traced and fixed. **Only subtraction is published.** Reading the ad copy measured 0%, 0%, 15% and 29% on the same kind of episode, and on a show that bakes its reads in it found 2:13 where the lengths can see nothing at all. That stays a hint beside the words, dimmed on the transcript screen, and never becomes a seek. **The length gap is the guard as well as the measure.** It comes from the feed and the file rather than from any detector, so it is the one number able to contradict one. Nothing is published for an episode with no gap -- there would be nothing to check it against -- and a candidate set claiming half again as much as was inserted is refused with its reason, because it has found something other than advertising. They land in their own field rather than beside the audio scan's, which rewrites its list from scratch on every detection pass and would wipe them. The API serves the union, merging ranges that overlap or touch: playback seeks to a break's end, so two overlapping breaks would seek out of the first and straight into the second, and two touching ones would resume for no time at all before skipping again. Advertising wins where merged ranges disagree about what they are -- the phone skips advertising unconditionally and a show's own audio only by preference, and being wrong the other way means sitting through an advertisement the server had already found. Nothing about playback changes. They arrive in the field the phone has read since the on-device detector existed. **Refetching is now gated on age**, which is Peter's steer and what the numbers say. A second fetch made minutes after the first came back twenty seconds different and found 21% of the advertising; another came back the same length to the second and found none. Insertion varies the amount per request, not the campaign. So it is for shows whose episodes are still worth hearing four weeks later -- a slow-drip archive, a leisure show -- and not for daily news, which is pruned long before a campaign rotates. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B --- server/cmd/listend/main.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'server/cmd') diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go index 8620d0d..6a76f08 100644 --- a/server/cmd/listend/main.go +++ b/server/cmd/listend/main.go @@ -574,6 +574,7 @@ func (w *worker) rederiveAds() { log.Printf("re-deriving %q: %v", e.Title, err) continue } + w.publishWordBreaks(e, found) changed++ } } @@ -594,6 +595,29 @@ func sameCandidates(left, right []transcript.Candidate) bool { return true } +/* +publishWordBreaks turns what the words found into stretches the phone will actually seek past. + +The only place anything read out of a transcript reaches playback. Everything else in this +pipeline produces candidates to look at; transcript.Publishable decides which of them are fit to +act on, and refuses the rest with a reason rather than silently dropping them. + +Kept in their own field so the ad scan, which rewrites its own list from scratch on every +detection pass, cannot wipe them -- and so that which detector found a break stays answerable. +*/ +func (w *worker) publishWordBreaks(e *store.Episode, found []transcript.Candidate) { + fit, why := transcript.Publishable(found, e.DurationMs, e.DeclaredDurationMs) + e.WordAdsRefused = why + e.WordBreaks = nil + for _, c := range fit { + e.WordBreaks = append(e.WordBreaks, store.Break{ + StartMs: c.StartMs, + EndMs: c.EndMs, + Kind: string(skips.KindAd), + }) + } +} + /* adsIn works out which stretches of a recognised episode are advertising. @@ -781,6 +805,7 @@ func (w *worker) refetchOne( if err := w.transcripts.Put(held); err != nil { return nil, second.DurationMs, err } + w.publishWordBreaks(e, held.Ads) return held.Ads, second.DurationMs, nil } @@ -791,10 +816,11 @@ Newest first: an episode about to be played is worth the work before one from th catalogue. */ func (w *worker) pendingRefetch() []*store.Episode { + now := time.Now().Unix() var pending []*store.Episode for _, show := range w.state.Shows() { for _, e := range show.Episodes { - if !e.NeedsRefetch() { + if !e.NeedsRefetch(now) { continue } // Without a length gap nothing was inserted, so a second fetch would be the same -- cgit v1.2.3