// 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] ...") 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) }