187 lines
4.4 KiB
Go
187 lines
4.4 KiB
Go
// 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
|
||
}
|