Files
silo-server/internal/intromarkers/silence.go
T

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
}