diff options
Diffstat (limited to 'server/cmd')
| -rw-r--r-- | server/cmd/listend/main.go | 87 |
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 |
