summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/internal/api/api.go4
-rw-r--r--server/internal/store/store.go27
-rw-r--r--server/internal/store/store_test.go38
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)
+ }
+}