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
104
105
106
|
package skips
import (
"github.com/terst/listen/server/internal/detect"
)
// CheckReport is the outcome of verifying a confirmation against itself.
type CheckReport struct {
OK bool
Pairs int
// WorstA, WorstB and WorstScore identify the least convincing pair, which is what a
// failure needs to say to be actionable.
WorstA string
WorstB string
WorstScore float64
}
// slopSamples is how far a reported position may be out before the check calls it a mismatch.
//
// Half a second. A genuine position can differ from a reported one by rounding -- reading an
// offset that was displayed to the nearest second is the first thing that produced a false
// mismatch here -- while a segment that is actually different audio is wrong by far more than
// this. Without the tolerance the check cries wolf; with much more of it, a real error hides.
const slopSamples = 4000
// SelfCheck verifies a confirmation by correlating the located segments against each other
// rather than against the reference they were found with.
//
// This asks a different question from the detection threshold, which is why it catches things
// the threshold cannot. "Each of these scored above 0.6 against one reference" does not imply
// "these are all the same audio": correlation is not transitive, and two segments can each
// clear a threshold against a reference while being unrelated to one another. Publishing skip
// offsets derived from such a set would cut different audio out of different episodes, which
// is precisely the failure a skip feature must never have.
//
// Cheap enough to run every time: a handful of correlations over eight-second clips.
func SelfCheck(found *detect.Recurring, clips []detect.Clip, minScore float64) CheckReport {
if found == nil || len(found.Occurrences) < 2 {
return CheckReport{OK: true}
}
byID := make(map[string][]float64, len(clips))
for _, c := range clips {
byID[c.EpisodeID] = c.Samples
}
segments := make(map[string][]float64, len(found.Occurrences))
for _, o := range found.Occurrences {
samples, ok := byID[o.EpisodeID]
if !ok {
continue
}
// Widened by the tolerance at both ends, so a position that is slightly out still has
// the whole segment inside the window being compared.
from := o.Position - slopSamples
if from < 0 {
from = 0
}
to := o.Position + len(found.Reference) + slopSamples
if to > len(samples) {
to = len(samples)
}
if to-from > 0 {
segments[o.EpisodeID] = samples[from:to]
}
}
report := CheckReport{OK: true, WorstScore: 1}
ids := make([]string, 0, len(segments))
for _, o := range found.Occurrences {
if _, ok := segments[o.EpisodeID]; ok {
ids = append(ids, o.EpisodeID)
}
}
for i := 0; i < len(ids); i++ {
for j := i + 1; j < len(ids); j++ {
// The query is the reference-length core of one segment; the target is the other
// segment with its tolerance either side, so the search absorbs the slop.
query := core(segments[ids[i]], len(found.Reference))
target := segments[ids[j]]
score := 0.0
if m := detect.BestMatch(query, target); m != nil {
score = m.Score
}
report.Pairs++
if score < report.WorstScore {
report.WorstScore, report.WorstA, report.WorstB = score, ids[i], ids[j]
}
if score < minScore {
report.OK = false
}
}
}
return report
}
// core takes the middle referenceLen samples of a widened segment, so the query is the segment
// itself rather than the segment plus the tolerance around it.
func core(segment []float64, referenceLen int) []float64 {
if len(segment) <= referenceLen {
return segment
}
start := (len(segment) - referenceLen) / 2
return segment[start : start+referenceLen]
}
|