summaryrefslogtreecommitdiff
path: root/server/internal/transcript/fits.go
blob: de4d74cec39a5495f2b0f7c435a0d9d5e33e1b72 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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)
}