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
107
108
109
110
111
112
113
114
115
116
117
118
119
|
// Command listen-detect reports the recurring intro and outro across a show's episode files.
//
// The detection core of the server, usable on its own so a result can be checked against real
// audio rather than only against synthetic fixtures. Point it at several episodes of one show:
//
// listen-detect episode1.mp3 episode2.mp3 episode3.mp3 episode4.mp3
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"time"
"github.com/terst/listen/server/internal/decode"
"github.com/terst/listen/server/internal/skips"
)
func main() {
clipSeconds := flag.Int("clip-seconds", skips.DefaultClipSeconds, "window searched at each end of an episode")
referenceSeconds := flag.Int("reference-seconds", skips.DefaultReferenceSeconds, "length of the segment looked for")
threshold := flag.Int("confirm", skips.DefaultConfirmThreshold, "episodes that must agree (lowering this defeats the safety mechanism)")
coarse := flag.Int("coarse", 0, "decimation for the candidate search; 0 uses the default, 1 searches at full resolution")
flag.Parse()
files := flag.Args()
if len(files) == 0 {
fmt.Fprintln(os.Stderr, "usage: listen-detect [flags] <episode file>...")
flag.PrintDefaults()
os.Exit(2)
}
cfg := skips.DefaultConfig()
cfg.ClipSeconds = *clipSeconds
cfg.ReferenceSeconds = *referenceSeconds
cfg.ConfirmThreshold = *threshold
if *coarse > 0 {
cfg.Tuning.CoarseFactor = *coarse
}
episodes := make([]skips.Episode, 0, len(files))
for _, f := range files {
episodes = append(episodes, skips.Episode{ID: filepath.Base(f), Path: f})
}
decoder := decode.Decoder{}
ctx := context.Background()
fmt.Printf("%d episodes, %ds windows, %ds reference, confirm on %d, coarse factor %d\n\n",
len(episodes), cfg.ClipSeconds, cfg.ReferenceSeconds, cfg.ConfirmThreshold, cfg.Tuning.CoarseFactor)
for _, ep := range episodes {
ms, err := decoder.DurationMs(ctx, ep.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", ep.ID, err)
os.Exit(1)
}
fmt.Printf(" %-44s %s\n", ep.ID, clock(ms))
}
started := time.Now()
result, err := skips.Detect(ctx, decoder, episodes, cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
elapsed := time.Since(started)
fmt.Printf("\nintro: confirmed on %d of %d episodes%s\n",
result.IntroConfirmedOn, len(episodes), verdict(result.IntroCheck))
fmt.Printf("outro: confirmed on %d of %d episodes%s\n",
result.OutroConfirmedOn, len(episodes), verdict(result.OutroCheck))
fmt.Printf("detection took %s\n\n", elapsed.Round(time.Millisecond))
fmt.Printf(" %-44s %-14s %s\n", "episode", "skip intro to", "end early by")
for _, o := range result.Offsets {
fmt.Printf(" %-44s %-14s %s\n", o.EpisodeID, optional(o.IntroMs), optional(o.OutroMs))
}
// Non-zero when a confirmation could not verify itself, so this is usable as a check and
// not only as a report.
if !result.IntroCheck.OK || !result.OutroCheck.OK {
fmt.Fprintln(os.Stderr, "\nat least one confirmation failed its self-check; no offsets published for it")
os.Exit(1)
}
}
// verdict describes whether the located segments agreed with each other -- a different and
// stricter question than the detection threshold, and the one that says the answer is real.
func verdict(r skips.CheckReport) string {
if r.Pairs == 0 {
return ""
}
if r.OK {
return fmt.Sprintf("; self-check passed on %d pairs, worst r=%.4f", r.Pairs, r.WorstScore)
}
return fmt.Sprintf("; SELF-CHECK FAILED: %s vs %s at r=%.4f -- offsets withheld",
r.WorstA, r.WorstB, r.WorstScore)
}
func optional(ms *int64) string {
if ms == nil {
return "-"
}
return clock(*ms)
}
// clock renders a duration to the millisecond. Not to the nearest second: an offset read back
// at second precision is out by up to half a second, which is enough to make the same audio
// look like different audio, and that cost real time the first time this was checked by hand.
func clock(ms int64) string {
total := ms / 1000
if h := total / 3600; h > 0 {
return fmt.Sprintf("%d:%02d:%02d.%03d", h, total/60%60, total%60, ms%1000)
}
return fmt.Sprintf("%d:%02d.%03d", total/60, total%60, ms%1000)
}
|