summaryrefslogtreecommitdiff
path: root/server/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'server/cmd')
-rw-r--r--server/cmd/listend/main.go83
1 files changed, 75 insertions, 8 deletions
diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go
index 1b746f1..d93325d 100644
--- a/server/cmd/listend/main.go
+++ b/server/cmd/listend/main.go
@@ -376,12 +376,18 @@ func (w *worker) pollOne(ctx context.Context, feedURL string) {
// every poll would be work done for ever to reach the same answer -- and the refusal is itself
// the record of which episodes speech recognition would have to do.
func (w *worker) harvestTranscripts(ctx context.Context, show *store.Show, candidates []*store.Episode) {
- taken, declined, spotted := 0, 0, 0
+ taken, declined, spotted, masters := 0, 0, 0, 0
for _, e := range candidates {
if ctx.Err() != nil {
return
}
- if !e.Mirrored() || e.TranscriptExamined() || e.TranscriptURL == "" {
+ // A declined episode is examined and has no words, but its publisher's transcript is
+ // still the ad-free master and is worth fetching -- it is what Inserted subtracts.
+ // Episodes declined before masters were kept arrive here once.
+ if !e.Mirrored() || e.TranscriptURL == "" {
+ continue
+ }
+ if e.TranscriptExamined() && !e.NeedsMaster() {
continue
}
result, err := w.harvester.Harvest(ctx, transcript.Offer{
@@ -396,6 +402,15 @@ func (w *worker) harvestTranscripts(ctx context.Context, show *store.Show, candi
log.Printf("%s: transcript for %q: %v", show.Title, e.Title, err)
continue
}
+ if result.Master > 0 {
+ e.MasterCues = result.Master
+ e.MasterNote = ""
+ masters++
+ } else if e.NeedsMaster() {
+ // Offered, fetched, and not usable as a master either. Recorded so it is not
+ // fetched again on every poll for ever.
+ e.MasterNote = result.Note
+ }
switch {
case result.Stored:
e.TranscriptSource = string(transcript.SourcePublished)
@@ -410,11 +425,12 @@ func (w *worker) harvestTranscripts(ctx context.Context, show *store.Show, candi
declined++
}
}
- if taken > 0 || declined > 0 {
+ if taken > 0 || declined > 0 || masters > 0 {
// The ad count is reported, not acted on. These are candidates read out of the words,
// and nothing publishes them as ranges to skip -- see transcript.Spot.
- log.Printf("%s: transcripts -- took %d, declined %d, %d ad candidate(s) in the words",
- show.Title, taken, declined, spotted)
+ log.Printf("%s: transcripts -- took %d, declined %d (%d kept as ad-free masters), "+
+ "%d ad candidate(s) in the words",
+ show.Title, taken, declined, masters, spotted)
}
}
@@ -473,7 +489,7 @@ func (w *worker) transcribeSome(ctx context.Context) {
Source: transcript.SourceASR,
LanguageCode: result.LanguageCode,
Cues: result.Cues,
- Ads: transcript.Spot(result.Cues),
+ Ads: w.adsIn(e, result.Cues),
}
if err := w.transcripts.Put(stored); err != nil {
log.Printf("transcribe %q: %v", e.Title, err)
@@ -499,8 +515,9 @@ func (w *worker) transcribeSome(ctx context.Context) {
done++
breaker.Success()
failing = nil
- log.Printf("transcribed %q: %d lines, %d ad candidate(s), took %s",
- e.Title, len(stored.Cues), len(stored.Ads), time.Since(started).Round(time.Second))
+ log.Printf("transcribed %q: %d lines, %s, took %s",
+ e.Title, len(stored.Cues), adAccounting(e, stored.Ads),
+ time.Since(started).Round(time.Second))
w.save()
}
@@ -510,6 +527,31 @@ func (w *worker) transcribeSome(ctx context.Context) {
}
}
+/*
+adsIn works out which stretches of a recognised episode are advertising.
+
+By subtraction where the publisher's ad-free master is held, and by reading the ad copy where it
+is not. They are not close in quality and the choice is not a preference. Measured on Darknet
+Diaries' "Conti": the feed declares 59:17 and the file served runs 63:33, so 4:15 of advertising
+was inserted -- subtraction found 4:20 of it in three pods, each starting on the first word of
+an ad read and ending on the last, while reading the copy found 0:16. 102% against 6%.
+
+Subtraction wins because it is a measurement. The master says what the programme is, the
+recognised copy says what was played, and the difference is the advertising by definition --
+there is no phrase list to be wrong about, and no show whose hosts happen to talk like an
+advertisement.
+*/
+func (w *worker) adsIn(e *store.Episode, cues []transcript.Cue) []transcript.Candidate {
+ if e.MasterCues > 0 {
+ if master, err := w.transcripts.Master(e.ID); err == nil {
+ if found := transcript.Inserted(master.Cues, cues); len(found) > 0 {
+ return found
+ }
+ }
+ }
+ return transcript.Spot(cues)
+}
+
// giveBackAttempts unmarks episodes that failed for a reason that was nothing to do with them,
// so they are tried again rather than hidden for ever.
func giveBackAttempts(episodes []*store.Episode) {
@@ -518,6 +560,31 @@ func giveBackAttempts(episodes []*store.Episode) {
}
}
+/*
+adAccounting describes what was found against what the lengths say is there.
+
+The declared-versus-served gap comes from the feed and the file rather than from any detector,
+which makes it the one number able to contradict one. Every other readout in this project that
+reported only what it found turned out to be hiding something.
+*/
+func adAccounting(e *store.Episode, found []transcript.Candidate) string {
+ var total int64
+ for _, c := range found {
+ total += c.EndMs - c.StartMs
+ }
+ how := "read from the ad copy"
+ if e.MasterCues > 0 {
+ how = "by subtracting the publisher's master"
+ }
+ gap := e.DurationMs - e.DeclaredDurationMs
+ if gap <= 0 {
+ return fmt.Sprintf("%d ad candidate(s) %s, %s; nothing was inserted into this copy",
+ len(found), how, clock(total))
+ }
+ return fmt.Sprintf("%d ad candidate(s) %s, %s of the %s inserted (%d%%)",
+ len(found), how, clock(total), clock(gap), total*100/gap)
+}
+
// pendingRecognition is every mirrored episode still without words, newest first.
func (w *worker) pendingRecognition() []*store.Episode {
var pending []*store.Episode