summaryrefslogtreecommitdiff
path: root/server
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
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')
-rw-r--r--server/cmd/second-probe/main.go56
-rw-r--r--server/internal/api/api.go19
-rw-r--r--server/internal/api/transcript_test.go66
3 files changed, 85 insertions, 56 deletions
diff --git a/server/cmd/second-probe/main.go b/server/cmd/second-probe/main.go
deleted file mode 100644
index 1d2a840..0000000
--- a/server/cmd/second-probe/main.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// Command second-probe subtracts a recognised second fetch from the mirrored copy. Throwaway.
-package main
-
-import (
- "flag"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/terst/listen/server/internal/transcribe"
- "github.com/terst/listen/server/internal/transcript"
-)
-
-func main() {
- episode := flag.String("episode", "", "episode id")
- secondFile := flag.String("second", "", "whisper -oj output for the second fetch")
- servedMs := flag.Int64("served", 0, "")
- declaredMs := flag.Int64("declared", 0, "")
- show := flag.Bool("show", false, "")
- flag.Parse()
-
- store := transcript.Store{Dir: "/site/listen.terst.org/data/transcripts"}
- mirrored, err := store.Get(*episode)
- if err != nil {
- panic(err)
- }
- raw, err := os.ReadFile(*secondFile)
- if err != nil {
- panic(err)
- }
- second, err := transcribe.ParseOutput(raw)
- if err != nil {
- panic(err)
- }
- fmt.Printf("mirrored %d cues, second %d cues\n", len(mirrored.Cues), len(second.Cues))
-
- found := transcript.Inserted(second.Cues, mirrored.Cues)
- var total int64
- for _, c := range found {
- total += c.EndMs - c.StartMs
- fmt.Printf(" %s-%s (%s)\n", clock(c.StartMs), clock(c.EndMs), clock(c.EndMs-c.StartMs))
- if *show {
- for _, cue := range mirrored.Cues {
- if cue.StartMs >= c.StartMs && cue.EndMs <= c.EndMs {
- fmt.Printf(" %.88s\n", cue.Text)
- }
- }
- }
- }
- gap := *servedMs - *declaredMs
- fmt.Printf("\n%d pods, %s found of the %s inserted (%d%%)\n",
- len(found), clock(total), clock(gap), total*100/gap)
- _ = filepath.Join
-}
-
-func clock(ms int64) string { return fmt.Sprintf("%d:%02d", ms/1000/60, ms/1000%60) }
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)
+ }
+}