summaryrefslogtreecommitdiff
path: root/server/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'server/cmd')
-rw-r--r--server/cmd/listend/main.go87
1 files changed, 86 insertions, 1 deletions
diff --git a/server/cmd/listend/main.go b/server/cmd/listend/main.go
index a98e01f..6cbfb65 100644
--- a/server/cmd/listend/main.go
+++ b/server/cmd/listend/main.go
@@ -40,6 +40,10 @@ import (
"github.com/terst/listen/server/internal/transcript"
)
+// minRecurringCorpus is how many of a show's episodes must have words before what they share
+// means anything. Three: the target and the two others a stretch has to turn up in.
+const minRecurringCorpus = 3
+
func main() {
var (
addr = flag.String("addr", "127.0.0.1:38090", "address to listen on")
@@ -378,6 +382,7 @@ func (w *worker) pollOne(ctx context.Context, feedURL string) {
log.Printf("%s: mirrored %d new episode(s)", channel.Title, fetched)
}
w.harvestTranscripts(ctx, show, kept)
+ w.findRecurring(show)
w.detect(ctx, show, kept)
}
@@ -569,7 +574,13 @@ func (w *worker) rederiveAds() {
// Publishing is decided every time, even when the candidates have not moved: what
// is fit to act on is a separate judgement from what was found, and it changes on
// its own. Only the file is left alone when nothing moved.
- w.publishWordBreaks(e, found)
+ //
+ // Only for episodes this pass owns. An episode with no master has its breaks from
+ // findRecurring, and re-deriving subtraction for it would clear them -- it has
+ // nothing to subtract.
+ if e.MasterCues > 0 {
+ w.publishWordBreaks(e, found)
+ }
if sameCandidates(found, held.Ads) {
continue
}
@@ -607,6 +618,80 @@ func sameCandidates(left, right []transcript.Candidate) bool {
}
/*
+findRecurring works out what each of a show's episodes says every week.
+
+Per show rather than per episode, because the comparison is against the show's other episodes.
+Eight of them are mirrored within a minute of each other, so they carry one advertising campaign
+and eight different programmes -- the text analogue of what makes the audio scan work, using
+transcripts already on disk and costing no download and no recognition.
+
+Published as the show's own audio rather than as advertising. What recurs is the sponsor reads
+and the furniture together, and nothing measurable separates them: ad copy turns up in 5% of the
+advertising and 4% of the furniture, and length and position overlap throughout. The listener's
+own setting governs a show's own audio, so claiming the weaker of the two is the safe mistake.
+
+Skipped for an episode that already has measured advertising. Subtraction knows which stretches
+are inserted; this only knows that they repeat, and the better answer should not be overwritten
+by the vaguer one.
+*/
+func (w *worker) findRecurring(show *store.Show) {
+ type held struct {
+ episode *store.Episode
+ cues []transcript.Cue
+ }
+ var corpus []held
+ for _, e := range show.Episodes {
+ if !e.Mirrored() || !e.HasTranscript() {
+ continue
+ }
+ stored, err := w.transcripts.Get(e.ID)
+ if err != nil {
+ continue
+ }
+ corpus = append(corpus, held{e, stored.Cues})
+ }
+ if len(corpus) < minRecurringCorpus {
+ return
+ }
+
+ published, refused := 0, 0
+ for i, target := range corpus {
+ if target.episode.MasterCues > 0 {
+ continue
+ }
+ others := make([][]transcript.Cue, 0, len(corpus)-1)
+ for j, other := range corpus {
+ if i != j {
+ others = append(others, other.cues)
+ }
+ }
+ found := transcript.Recurring(target.cues, others)
+ fit, why := transcript.PublishableRecurring(found, target.episode.DurationMs)
+ target.episode.WordAdsRefused = why
+ target.episode.WordBreaks = nil
+ for _, c := range fit {
+ target.episode.WordBreaks = append(target.episode.WordBreaks, store.Break{
+ StartMs: c.StartMs,
+ EndMs: c.EndMs,
+ // What recurs is the show's own recurring audio, which is what this kind
+ // means. The listener decides whether to skip it.
+ Kind: string(skips.KindCue),
+ })
+ }
+ if len(fit) > 0 {
+ published++
+ }
+ if why != "" {
+ refused++
+ }
+ }
+ if published > 0 || refused > 0 {
+ log.Printf("%s: recurring text -- %d episode(s) publish it, %d refused, from a corpus of %d",
+ show.Title, published, refused, len(corpus))
+ }
+}
+
+/*
publishWordBreaks turns what the words found into stretches the phone will actually seek past.
The only place anything read out of a transcript reaches playback. Everything else in this