diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-10-31 07:03:03 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-10-31 07:03:03 +0000 |
| commit | cbcd04cfb3c11ae0a171f4fce1954214baeff074 (patch) | |
| tree | b8ba3d1eb823cf2d532ac07591fed918d655e8d5 /server | |
| parent | 43a78fba9c46704fed7db3fb5f6b138db2fb0820 (diff) | |
A break says which detector found it, and the episode screen says so
Three detectors now cut audio out of episodes and the app said nothing about
which. The listener judging a skip wants to know what judged it, and they are
not equally sure of themselves: one compares waveforms across a batch of
episodes, the other reads what was said and checks itself against the declared
length. A break both found is marked as both, because agreement by entirely
different means is the strongest thing either can offer.
"Found by the audio and the words", under the numbers and quieter than them --
it explains them rather than competing with them.
Schema 19. Existing rows default to the audio scan because that is where every
break in the database came from; the words could not produce one until this
week. "Unknown" would have been honest about the column and useless about the
rows. A server that says nothing gets the same default, for the same reason.
The coverage gate caught the new field's default never being exercised, which
was fair: every call site passes it, and the case it exists for is an episode
with nothing cut and nothing to account for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'server')
| -rw-r--r-- | server/internal/api/api.go | 4 | ||||
| -rw-r--r-- | server/internal/store/store.go | 27 | ||||
| -rw-r--r-- | server/internal/store/store_test.go | 38 |
3 files changed, 67 insertions, 2 deletions
diff --git a/server/internal/api/api.go b/server/internal/api/api.go index 367a1c6..2db0f91 100644 --- a/server/internal/api/api.go +++ b/server/internal/api/api.go @@ -117,6 +117,9 @@ type breakRange struct { // when a break is tapped: it is the only evidence a listener can weigh before deciding // whether the detector got this one right. Partners int `json:"partners,omitempty"` + // Origin is which detector found it: "audio", "words", or both. Same reasoning as + // Partners -- a listener judging a skip wants to know what decided it. + Origin string `json:"origin,omitempty"` } // Handler returns the routed, authenticated handler. @@ -215,6 +218,7 @@ func (s *Server) episode(w http.ResponseWriter, r *http.Request) { EndMs: br.EndMs, Kind: br.Kind, Partners: br.Partners, + Origin: br.Origin, }) } writeJSON(w, http.StatusOK, episodeResponse{ diff --git a/server/internal/store/store.go b/server/internal/store/store.go index b86474d..931cdd3 100644 --- a/server/internal/store/store.go +++ b/server/internal/store/store.go @@ -174,8 +174,20 @@ type Break struct { // Kind is "ad" for inserted advertising, or "intro", "cue" or "outro" for the show's own // audio. The phone skips advertising always and furniture by preference. Kind string `json:"kind,omitempty"` + // Origin is which detector found it: [FromAudio] or [FromWords]. The listener judging a + // skip wants to know why it happened, and the two are not equally sure of themselves -- + // one compares waveforms across a batch, the other reads what was said. + Origin string `json:"origin,omitempty"` } +// Where a break came from. +const ( + // FromAudio is the cross-correlation scan over several episodes' waveforms. + FromAudio = "audio" + // FromWords is subtraction or recurrence over the episodes' transcripts. + FromWords = "words" +) + // AllBreaks is every stretch to skip, from both detectors, as one ordered list. // // Overlapping and touching ranges are merged, because playback seeks to a break's end: two @@ -187,8 +199,14 @@ type Break struct { // other way means sitting through an advertisement the server had already found. func (e *Episode) AllBreaks() []Break { all := make([]Break, 0, len(e.Breaks)+len(e.WordBreaks)) - all = append(all, e.Breaks...) - all = append(all, e.WordBreaks...) + for _, br := range e.Breaks { + br.Origin = FromAudio + all = append(all, br) + } + for _, br := range e.WordBreaks { + br.Origin = FromWords + all = append(all, br) + } sort.Slice(all, func(i, j int) bool { return all[i].StartMs < all[j].StartMs }) var merged []Break @@ -203,6 +221,11 @@ func (e *Episode) AllBreaks() []Break { if br.Partners > merged[last].Partners { merged[last].Partners = br.Partners } + // Both detectors found it, which is worth saying: they agree by entirely + // different means. + if br.Origin != merged[last].Origin { + merged[last].Origin = FromAudio + "+" + FromWords + } continue } merged = append(merged, br) diff --git a/server/internal/store/store_test.go b/server/internal/store/store_test.go index c4eafdd..f0c111b 100644 --- a/server/internal/store/store_test.go +++ b/server/internal/store/store_test.go @@ -602,3 +602,41 @@ func TestAnEpisodeOfUnknownAgeIsNotRefetched(t *testing.T) { t.Error("would refetch an episode of unknown age") } } + +/* +A break says which detector found it. + +The listener judging a skip wants to know why it happened, and the two detectors are not equally +sure of themselves: one compares waveforms across a batch of episodes, the other reads what was +said. A break both found is worth marking as such -- they agree by entirely different means. +*/ + +func TestABreakSaysWhereItCameFrom(t *testing.T) { + e := Episode{ + Breaks: []Break{{StartMs: 60_000, EndMs: 90_000, Kind: "ad"}}, + WordBreaks: []Break{{StartMs: 600_000, EndMs: 660_000, Kind: "ad"}}, + } + + got := e.AllBreaks() + if got[0].Origin != FromAudio { + t.Errorf("first break says %q, want %q", got[0].Origin, FromAudio) + } + if got[1].Origin != FromWords { + t.Errorf("second break says %q, want %q", got[1].Origin, FromWords) + } +} + +func TestABreakBothDetectorsFoundSaysSo(t *testing.T) { + e := Episode{ + Breaks: []Break{{StartMs: 60_000, EndMs: 90_000, Kind: "ad"}}, + WordBreaks: []Break{{StartMs: 80_000, EndMs: 120_000, Kind: "ad"}}, + } + + got := e.AllBreaks() + if len(got) != 1 { + t.Fatalf("got %+v, want one merged break", got) + } + if got[0].Origin != FromAudio+"+"+FromWords { + t.Errorf("origin = %q, want both named", got[0].Origin) + } +} |
