nsfwapp/backend/rating.go

187 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 "anus_exposed":
return 0.95
case "female_breast_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 24 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
}