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

245 lines
6.3 KiB
Go

package intromarkers
import (
"math"
"math/bits"
"sort"
)
type fingerprintInput struct {
Candidate Candidate
Points []uint32
}
func CompareFingerprints(inputs []fingerprintInput, cfg Config) map[int]Segment {
cfg = cfg.normalized()
best := map[int]Segment{}
confirmations := map[int]int{}
for i := 0; i < len(inputs); i++ {
for j := i + 1; j < len(inputs); j++ {
left, right := inputs[i], inputs[j]
if left.Candidate.EpisodeID == "" || left.Candidate.EpisodeID == right.Candidate.EpisodeID {
continue
}
leftSeg, rightSeg, ok := comparePair(left.Points, right.Points, cfg)
if !ok {
continue
}
leftSeg = adjustSegment(leftSeg, left.Candidate)
rightSeg = adjustSegment(rightSeg, right.Candidate)
if validAdjustedSegment(leftSeg) {
recordBest(best, confirmations, left.Candidate.FileID, leftSeg)
}
if validAdjustedSegment(rightSeg) {
recordBest(best, confirmations, right.Candidate.FileID, rightSeg)
}
}
}
for fileID, segment := range best {
confidence := 0.65
if segment.End-segment.Start >= 30 {
confidence += 0.10
}
if confirmations[fileID] >= 2 {
confidence += 0.10
}
if segment.Start == 0 {
confidence += 0.05
}
if confidence > 0.90 {
confidence = 0.90
}
segment.Confidence = confidence
segment.Algorithm = ChromaprintAlgorithm
best[fileID] = segment
}
return best
}
func comparePair(left, right []uint32, cfg Config) (Segment, Segment, bool) {
if len(left) == 0 || len(right) == 0 {
return Segment{}, Segment{}, false
}
shifts := candidateShifts(left, right)
var bestLeft, bestRight Segment
bestDuration := 0.0
for _, shift := range shifts {
leftSeg, rightSeg, ok := comparePairAtShift(left, right, cfg, shift)
if !ok {
continue
}
if duration := leftSeg.End - leftSeg.Start; duration > bestDuration {
bestLeft = leftSeg
bestRight = rightSeg
bestDuration = duration
}
}
if bestDuration == 0 {
return Segment{}, Segment{}, false
}
return bestLeft, bestRight, true
}
func comparePairAtShift(left, right []uint32, cfg Config, shift int) (Segment, Segment, bool) {
type pair struct {
left int
right int
}
var matches []pair
tolerance := candidateShiftSamplePoints()
for i, lp := range left {
center := i + shift
from := max(0, center-tolerance)
to := min(len(right)-1, center+tolerance)
if to < from {
continue
}
for j := from; j <= to; j++ {
if bits.OnesCount32(lp^right[j]) <= 6 {
matches = append(matches, pair{left: i, right: j})
break
}
}
}
if len(matches) == 0 {
return Segment{}, Segment{}, false
}
maxGapPoints := int(math.Ceil(3.5 / DefaultPointHopSeconds))
bestStart := 0
bestEnd := 0
bestRunStart := 0
runStart := 0
for i := 1; i < len(matches); i++ {
leftGap := matches[i].left - matches[i-1].left
rightGap := matches[i].right - matches[i-1].right
if leftGap <= maxGapPoints && rightGap >= -candidateShiftSamplePoints() && rightGap <= maxGapPoints {
continue
}
if matches[i-1].left-matches[runStart].left > bestEnd-bestStart {
bestStart = matches[runStart].left
bestEnd = matches[i-1].left
bestRunStart = runStart
}
runStart = i
}
if matches[len(matches)-1].left-matches[runStart].left > bestEnd-bestStart {
bestStart = matches[runStart].left
bestEnd = matches[len(matches)-1].left
bestRunStart = runStart
}
start := float64(bestStart) * DefaultPointHopSeconds
end := float64(bestEnd+1) * DefaultPointHopSeconds
duration := end - start
if duration < float64(cfg.MinimumIntroDurationSeconds) || duration > float64(cfg.MaximumIntroDurationSeconds) {
return Segment{}, Segment{}, false
}
rightShift := matches[bestRunStart].right - matches[bestRunStart].left
rightStart := float64(max(0, bestStart+rightShift)) * DefaultPointHopSeconds
rightEnd := rightStart + duration
return Segment{Start: start, End: end}, Segment{Start: rightStart, End: rightEnd}, true
}
func candidateShifts(left, right []uint32) []int {
step := candidateShiftSamplePoints()
counts := map[int]int{}
for i := 0; i < len(left); i += step {
for j := 0; j < len(right); j += step {
if bits.OnesCount32(left[i]^right[j]) <= 6 {
counts[j-i]++
}
}
}
type candidate struct {
shift int
count int
}
candidates := []candidate{{shift: 0, count: counts[0]}}
for shift, count := range counts {
if shift == 0 || count < 3 {
continue
}
candidates = append(candidates, candidate{shift: shift, count: count})
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].count != candidates[j].count {
return candidates[i].count > candidates[j].count
}
if absInt(candidates[i].shift) != absInt(candidates[j].shift) {
return absInt(candidates[i].shift) < absInt(candidates[j].shift)
}
return candidates[i].shift < candidates[j].shift
})
limit := min(8, len(candidates))
shifts := make([]int, 0, limit)
seen := map[int]struct{}{}
for _, candidate := range candidates {
if len(shifts) >= limit {
break
}
if _, ok := seen[candidate.shift]; ok {
continue
}
seen[candidate.shift] = struct{}{}
shifts = append(shifts, candidate.shift)
}
return shifts
}
func candidateShiftSamplePoints() int {
return max(1, int(math.Round(1/DefaultPointHopSeconds)))
}
func absInt(v int) int {
if v < 0 {
return -v
}
return v
}
func adjustSegment(segment Segment, candidate Candidate) Segment {
if segment.Start <= 5 {
segment.Start = 0
}
for _, chapter := range candidate.Chapters {
segment.Start = snapBoundary(segment.Start, chapter.StartSeconds)
segment.End = snapBoundary(segment.End, chapter.StartSeconds)
segment.End = snapBoundary(segment.End, chapter.EndSeconds)
}
if segment.Start < 0 {
segment.Start = 0
}
if candidate.DurationSeconds > 0 && segment.End > candidate.DurationSeconds {
segment.End = candidate.DurationSeconds
}
return segment
}
func snapBoundary(value, boundary float64) float64 {
delta := boundary - value
if delta >= -5 && delta <= 2 {
return boundary
}
return value
}
func validAdjustedSegment(segment Segment) bool {
duration := segment.End - segment.Start
return segment.Start >= 0 && segment.End > segment.Start && duration >= 10 && duration <= 180
}
func recordBest(best map[int]Segment, confirmations map[int]int, fileID int, segment Segment) {
confirmations[fileID]++
if current, ok := best[fileID]; !ok || segment.End-segment.Start > current.End-current.Start {
best[fileID] = segment
}
}