summaryrefslogtreecommitdiff
path: root/server/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-10-30 08:25:35 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-10-30 08:25:35 +0000
commitb78b846189124ede389a25aee2a3b45c98b528ec (patch)
tree073fcfed176f2fe213250bba0e3aaff8a29b33dc /server/internal
parentb9f9ece078a597a820b9425323481232c3fa12e8 (diff)
Tell the phone what reading the words concluded
Three numbers on the line that already says an episode has a transcript: how many stretches of advertising were found, how long they come to, and how. The method travels with them because subtraction and ad-copy reading are not close in quality -- 92-104% of the length gap against 0-29% -- and a figure on its own would invite reading the two alike. An episode that turned up nothing says nothing rather than zero, which is the same three-state care the rest of this takes. Still nothing published as a skip. This is so the evidence can be looked at from the app rather than from my logs, which is what the decision to publish should rest on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'server/internal')
-rw-r--r--server/internal/api/api.go19
-rw-r--r--server/internal/api/transcript_test.go66
2 files changed, 85 insertions, 0 deletions
diff --git a/server/internal/api/api.go b/server/internal/api/api.go
index d2ad841..0d18c0f 100644
--- a/server/internal/api/api.go
+++ b/server/internal/api/api.go
@@ -97,6 +97,16 @@ type transcriptNote struct {
// server keys them by its own id, and handing over a URL is what keeps that from having
// to change. Empty when there is nothing to fetch.
URL string `json:"url,omitempty"`
+
+ // Ads and AdMs are what reading the words concluded about the advertising in this episode:
+ // how many stretches and how long they come to. Candidates, not skips -- nothing in the
+ // playback path reads them.
+ Ads int `json:"ads,omitempty"`
+ AdMs int64 `json:"adMs,omitempty"`
+ // Method is how they were found: "difference" where a second description of the programme
+ // was available to subtract, or the kinds of ad copy that were read otherwise. The two are
+ // not close in quality and the screen should not present them as if they were.
+ Method string `json:"method,omitempty"`
}
type breakRange struct {
@@ -233,6 +243,15 @@ func (s *Server) transcriptNoteFor(e *store.Episode) *transcriptNote {
if e.HasTranscript() {
note.URL = "/api/transcript?episode=" + url.QueryEscape(e.ID) +
"&token=" + url.QueryEscape(s.opts.Token)
+ if held, err := (transcript.Store{Dir: s.opts.TranscriptDir}).Get(e.ID); err == nil {
+ note.Ads = len(held.Ads)
+ for _, ad := range held.Ads {
+ note.AdMs += ad.EndMs - ad.StartMs
+ if note.Method == "" && len(ad.Signals) > 0 {
+ note.Method = ad.Signals[0]
+ }
+ }
+ }
}
return note
}
diff --git a/server/internal/api/transcript_test.go b/server/internal/api/transcript_test.go
index 651225d..6d0ad7f 100644
--- a/server/internal/api/transcript_test.go
+++ b/server/internal/api/transcript_test.go
@@ -235,3 +235,69 @@ func TestTheTranscriptRouteNeedsTheToken(t *testing.T) {
t.Errorf("status %d, want 401", rec.Code)
}
}
+
+/*
+What reading the words concluded about the advertising, on the episode response.
+
+The phone needs it per episode and it is three numbers, so it travels with the line that says
+there is a transcript rather than waiting for somebody to open one. The method travels with it
+because subtraction and ad-copy reading are not close in quality -- 92-104% of the length gap
+against 0-29% -- and a screen that showed only a figure would invite reading them alike.
+*/
+func TestTheResponseSaysWhatTheWordsFound(t *testing.T) {
+ s, st, _, audioDir := newServer(t)
+ e := seedTranscribedEpisode(t, s, st, audioDir)
+
+ held, err := (transcript.Store{Dir: s.opts.TranscriptDir}).Get(e.ID)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ held.Ads = []transcript.Candidate{
+ {StartMs: 1_000, EndMs: 61_000, Signals: []string{transcript.SignalDifference}},
+ {StartMs: 100_000, EndMs: 130_000, Signals: []string{transcript.SignalDifference}},
+ }
+ if err := (transcript.Store{Dir: s.opts.TranscriptDir}).Put(held); err != nil {
+ t.Fatalf("Put: %v", err)
+ }
+
+ rec := get(t, s, "/api/episode?feed=https://f&guid=g1")
+ var got struct {
+ Transcript *struct {
+ Ads int `json:"ads"`
+ AdMs int64 `json:"adMs"`
+ Method string `json:"method"`
+ } `json:"transcript"`
+ }
+ if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if got.Transcript == nil {
+ t.Fatal("no transcript line at all")
+ }
+ if got.Transcript.Ads != 2 || got.Transcript.AdMs != 90_000 {
+ t.Errorf("got %+v, want 2 breaks totalling 90s", *got.Transcript)
+ }
+ if got.Transcript.Method != transcript.SignalDifference {
+ t.Errorf("method = %q, want %q", got.Transcript.Method, transcript.SignalDifference)
+ }
+}
+
+// An episode whose words turned up no advertising says nothing rather than zero.
+func TestAnEpisodeWithNoAdvertisingInItsWordsSaysNothing(t *testing.T) {
+ s, st, _, audioDir := newServer(t)
+ seedTranscribedEpisode(t, s, st, audioDir)
+
+ rec := get(t, s, "/api/episode?feed=https://f&guid=g1")
+ var got struct {
+ Transcript *struct {
+ Ads int `json:"ads"`
+ AdMs int64 `json:"adMs"`
+ } `json:"transcript"`
+ }
+ if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+ if got.Transcript.Ads != 0 || got.Transcript.AdMs != 0 {
+ t.Errorf("got %+v, want nothing claimed", *got.Transcript)
+ }
+}