// backend\rating.go package main import ( "math" "strings" ) type aiRatingMeta struct { Score float64 `json:"score"` Stars int `json:"stars"` Segments int `json:"segments"` SegmentsPerMinute float64 `json:"segmentsPerMinute"` FlaggedSeconds float64 `json:"flaggedSeconds"` WeightedFlaggedSeconds float64 `json:"weightedFlaggedSeconds"` CoverageRatio float64 `json:"coverageRatio"` WeightedCoverageRatio float64 `json:"weightedCoverageRatio"` LongestSegmentSeconds float64 `json:"longestSegmentSeconds"` AvgConfidence float64 `json:"avgConfidence"` } func ratingClamp01(v float64) float64 { if v < 0 { return 0 } if v > 1 { return 1 } return v } func ratingRound(v float64, places int) float64 { p := math.Pow(10, float64(places)) return math.Round(v*p) / p } func ratingSmoothStep(v float64) float64 { v = ratingClamp01(v) return v * v * (3 - 2*v) } func ratingSoftCap(value, knee float64) float64 { if value <= 0 || knee <= 0 { return 0 } return value / (value + knee) } func ratingEffectiveDurationSeconds(seconds float64) float64 { if seconds <= 0 { return 0 } // Lange Segmente zählen weiter, aber mit abnehmendem Zusatznutzen. // Dadurch kippen falsche oder zu breite Merges nicht sofort auf 5 Sterne. const kneeSeconds = 24.0 return kneeSeconds * math.Log1p(seconds/kneeSeconds) } func nsfwSegmentSeverityWeight(label string) float64 { switch strings.ToLower(strings.TrimSpace(label)) { case "female_genitalia_exposed": return 1.00 case "female_breast_exposed": return 0.95 case "anus_exposed": return 0.85 case "male_genitalia_exposed": return 0.80 case "buttocks_exposed": return 0.65 default: return 0.50 } } func ratingConfidenceWeight(conf float64) float64 { conf = ratingClamp01(conf) // Unterhalb ~0.30 soll Confidence kaum boosten. // Oberhalb ~0.95 ist praktisch gesättigt. n := ratingSmoothStep((conf - 0.30) / 0.65) return 0.60 + 0.40*n } func starsFromNSFWScore(score float64) int { switch { case score < 15: return 1 case score < 35: return 2 case score < 58: return 3 case score < 78: return 4 default: return 5 } } func computeNSFWRating(segments []aiSegmentMeta, durationSec float64) *aiRatingMeta { r := &aiRatingMeta{ Score: 0, Stars: 1, } if durationSec <= 0 || len(segments) == 0 { return r } videoMinutes := math.Max(durationSec/60.0, 0.25) var totalFlagged float64 var totalWeighted float64 var totalEffectiveWeighted float64 var longest float64 var confSum float64 var n int for _, s := range segments { segDur := s.DurationSeconds if segDur <= 0 { segDur = s.EndSeconds - s.StartSeconds } if segDur <= 0 { continue } sev := nsfwSegmentSeverityWeight(s.Label) conf := ratingClamp01(s.Score) weight := sev * ratingConfidenceWeight(conf) totalFlagged += segDur totalWeighted += segDur * weight totalEffectiveWeighted += ratingEffectiveDurationSeconds(segDur) * weight confSum += conf n++ if segDur > longest { longest = segDur } } if n == 0 { return r } coverageRatio := ratingClamp01(totalFlagged / durationSec) weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec) segmentsPerMinute := float64(n) / videoMinutes avgConfidence := confSum / float64(n) effectiveWeightedSecondsPerMinute := totalEffectiveWeighted / videoMinutes // Weichere Normalisierung statt harter Sättigung. // Das reduziert 1/5-Ausreißer und verteilt mehr Fälle auf 2–4 Sterne. densityNorm := ratingSoftCap(effectiveWeightedSecondsPerMinute, 8.0) coverageNorm := ratingSoftCap(weightedCoverageRatio, 0.22) frequencyNorm := ratingSoftCap(segmentsPerMinute, 1.25) longestNorm := ratingSoftCap(longest, 25.0) confNorm := ratingSmoothStep((avgConfidence - 0.35) / 0.60) raw := 0.38*densityNorm + 0.30*coverageNorm + 0.12*frequencyNorm + 0.10*longestNorm + 0.10*confNorm score := ratingRound(ratingClamp01(raw)*100, 1) r.Score = score r.Stars = starsFromNSFWScore(score) r.Segments = n r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2) r.FlaggedSeconds = ratingRound(totalFlagged, 2) r.WeightedFlaggedSeconds = ratingRound(totalWeighted, 2) r.CoverageRatio = ratingRound(coverageRatio, 4) r.WeightedCoverageRatio = ratingRound(weightedCoverageRatio, 4) r.LongestSegmentSeconds = ratingRound(longest, 2) r.AvgConfidence = ratingRound(avgConfidence, 3) return r }