128 lines
3.2 KiB
Go
128 lines
3.2 KiB
Go
package intromarkers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"os/exec"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
)
|
|
|
|
type boundaryRefiner interface {
|
|
RefineChapterEnd(ctx context.Context, candidate Candidate, segment Segment) (Segment, bool, error)
|
|
}
|
|
|
|
type SilenceBoundaryRefiner struct {
|
|
config Config
|
|
}
|
|
|
|
type silenceInterval struct {
|
|
Start float64
|
|
End float64
|
|
}
|
|
|
|
var (
|
|
silenceStartPattern = regexp.MustCompile(`silence_start:\s*([0-9]+(?:\.[0-9]+)?)`)
|
|
silenceEndPattern = regexp.MustCompile(`silence_end:\s*([0-9]+(?:\.[0-9]+)?)`)
|
|
)
|
|
|
|
func NewSilenceBoundaryRefiner(config Config) *SilenceBoundaryRefiner {
|
|
return &SilenceBoundaryRefiner{config: config.normalized()}
|
|
}
|
|
|
|
func (r *SilenceBoundaryRefiner) RefineChapterEnd(ctx context.Context, candidate Candidate, segment Segment) (Segment, bool, error) {
|
|
cfg := r.config.normalized()
|
|
if !cfg.SilenceRefinementEnabled {
|
|
return segment, false, nil
|
|
}
|
|
if candidate.FilePath == "" || segment.End <= segment.Start {
|
|
return segment, false, nil
|
|
}
|
|
|
|
windowStart := math.Max(0, segment.End-cfg.SilenceWindowBeforeSeconds)
|
|
windowEnd := segment.End + cfg.SilenceWindowAfterSeconds
|
|
if candidate.DurationSeconds > 0 {
|
|
windowEnd = math.Min(windowEnd, candidate.DurationSeconds)
|
|
}
|
|
if windowEnd <= windowStart {
|
|
return segment, false, nil
|
|
}
|
|
|
|
args := []string{
|
|
"-hide_banner",
|
|
"-nostdin",
|
|
"-loglevel", "info",
|
|
"-ss", formatSeconds(windowStart),
|
|
"-i", candidate.FilePath,
|
|
"-t", formatSeconds(windowEnd - windowStart),
|
|
"-vn",
|
|
"-sn",
|
|
"-dn",
|
|
"-af", fmt.Sprintf("silencedetect=noise=%ddB:duration=%s", *cfg.SilenceNoiseThresholdDB, formatSeconds(cfg.SilenceMinimumDurationSeconds)),
|
|
"-f", "null",
|
|
"-",
|
|
}
|
|
output, err := exec.CommandContext(ctx, cfg.FFmpegPath, args...).CombinedOutput()
|
|
if err != nil {
|
|
return segment, false, fmt.Errorf("detecting intro boundary silence for file %d: %w", candidate.FileID, err)
|
|
}
|
|
|
|
intervals := parseSilenceDetectOutput(output, windowStart)
|
|
for _, interval := range intervals {
|
|
if interval.Start < segment.End {
|
|
continue
|
|
}
|
|
if interval.Start-segment.End < cfg.SilenceMinimumExtensionSeconds {
|
|
continue
|
|
}
|
|
if interval.Start-segment.End > cfg.SilenceMaximumExtensionSeconds {
|
|
continue
|
|
}
|
|
if interval.Start-segment.Start > 180 {
|
|
continue
|
|
}
|
|
refined := segment
|
|
refined.End = interval.Start
|
|
refined.Confidence = 0.98
|
|
refined.Algorithm = ChapterSilenceAlgorithm
|
|
return refined, true, nil
|
|
}
|
|
|
|
return segment, false, nil
|
|
}
|
|
|
|
func parseSilenceDetectOutput(output []byte, windowStart float64) []silenceInterval {
|
|
matches := silenceStartPattern.FindAllSubmatch(output, -1)
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
intervals := make([]silenceInterval, 0, len(matches))
|
|
for _, match := range matches {
|
|
start, err := strconv.ParseFloat(string(match[1]), 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
intervals = append(intervals, silenceInterval{Start: windowStart + start})
|
|
}
|
|
|
|
endMatches := silenceEndPattern.FindAllSubmatch(output, -1)
|
|
for i, match := range endMatches {
|
|
if i >= len(intervals) {
|
|
break
|
|
}
|
|
end, err := strconv.ParseFloat(string(match[1]), 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
intervals[i].End = windowStart + end
|
|
}
|
|
|
|
sort.Slice(intervals, func(i, j int) bool {
|
|
return intervals[i].Start < intervals[j].Start
|
|
})
|
|
return intervals
|
|
}
|