package transcript import ( "fmt" "github.com/terst/listen/server/internal/advertising" ) const ( // insertionMs is how far a served file may exceed its declared length before the excess is // read as inserted advertising rather than as the feed being approximate. One advertising // slot, the same judgement skips.Classify makes and for the same reason. insertionMs = advertising.UnitMs // tailMs is how much of a file may follow the last spoken word before the silence has to be // explained. Closing music runs to fifteen seconds in this corpus (LOW's "Part 5: Bane" // ends 15.2s before its file does); the shortest advertisement does not fit in sixty. tailMs = 60_000 ) // Verdict is whether a published transcript may be used for a particular mirrored file, and if // not, why not. // // Why, not just no. A transcript refused because the file carries inserted advertising is a // different thing from one refused because it describes another recording: the first is an // episode that needs speech recognition, and the second is a publisher's mistake. type Verdict struct { OK bool Why string Reason Reason } // Reason says which kind of refusal this is, so a caller can act on it. Read rather than // matched against the wording of Why, which is for people. type Reason string const ( // ReasonInserted: the file carries advertising the publisher's master does not describe. // The transcript is still the master, and subtracting it from a recognised reading of the // served file is what locates that advertising. See Inserted. ReasonInserted Reason = "inserted" // ReasonOtherRecording: the transcript describes something that is not this file at all, // so it is no use for anything. ReasonOtherRecording Reason = "other-recording" // ReasonEmpty: there were no cues to judge. ReasonEmpty Reason = "empty" ) // Fits reports whether a transcript ending at lastCueEndMs describes a mirrored file of // servedMs, given the declaredMs the feed claims. // // The decision is made on the lengths rather than on the words because the lengths are // independent of the transcript: the feed declares the ad-free programme's length and the file // measures what was served, so their difference is the advertising inserted into this copy. // A publisher's transcript always describes the ad-free master, so it aligns exactly when that // difference is nothing. func Fits(lastCueEndMs, servedMs, declaredMs int64) Verdict { if lastCueEndMs <= 0 { return Verdict{Why: "the transcript has no cues", Reason: ReasonEmpty} } if lastCueEndMs > servedMs { return Verdict{Why: fmt.Sprintf( "the transcript runs %s past the end of the file, so it describes another recording", clock(lastCueEndMs-servedMs)), Reason: ReasonOtherRecording} } if declaredMs > 0 { if gap := servedMs - declaredMs; gap > insertionMs { return Verdict{Why: fmt.Sprintf( "this copy carries %s of inserted advertising the transcript does not describe", clock(gap)), Reason: ReasonInserted} } return Verdict{OK: true} } // No declared length to compare against, so the transcript answers for its own gap. if gap := servedMs - lastCueEndMs; gap > tailMs { return Verdict{Why: fmt.Sprintf( "the transcript stops %s before the file does, and the feed declares no length", clock(gap)), Reason: ReasonInserted} } return Verdict{OK: true} } // HasInsertedAdvertising reports whether this copy is longer than the feed says the programme // is, by more than the feed could be approximate about. // // The one fact about an episode that comes from neither a detector nor a transcript. An episode // without it has nothing inserted to find, so there is no point fetching a second copy to // compare against -- the two would be the same recording. func HasInsertedAdvertising(servedMs, declaredMs int64) bool { return declaredMs > 0 && servedMs-declaredMs > insertionMs } // LastCueEndMs is where a transcript stops describing the audio. func LastCueEndMs(cues []Cue) int64 { if len(cues) == 0 { return 0 } return cues[len(cues)-1].EndMs } func clock(ms int64) string { s := ms / 1000 return fmt.Sprintf("%d:%02d", s/60, s%60) }