This commit is contained in:
Linrador 2026-06-14 22:27:43 +02:00
parent 9f3ae43f8a
commit 56b63223f9
5 changed files with 543 additions and 289 deletions

View File

@ -11,6 +11,17 @@ import (
type aiRatingMeta struct { type aiRatingMeta struct {
Score float64 `json:"score"` Score float64 `json:"score"`
ActionScore float64 `json:"actionScore"` ActionScore float64 `json:"actionScore"`
BestPosition string `json:"bestPosition,omitempty"`
BestPositionRank int `json:"bestPositionRank,omitempty"`
BestClothing string `json:"bestClothing,omitempty"`
BestClothingRank int `json:"bestClothingRank,omitempty"`
PositionScore float64 `json:"positionScore"`
ClothingScore float64 `json:"clothingScore"`
DurationScore float64 `json:"durationScore"`
StrengthScore float64 `json:"strengthScore"`
CoverageScore float64 `json:"coverageScore"`
ContextScore float64 `json:"contextScore"`
VarietyScore float64 `json:"varietyScore"`
Stars int `json:"stars"` Stars int `json:"stars"`
Segments int `json:"segments"` Segments int `json:"segments"`
SegmentsPerMinute float64 `json:"segmentsPerMinute"` SegmentsPerMinute float64 `json:"segmentsPerMinute"`
@ -136,31 +147,46 @@ func isKnownPositionLabel(label string) bool {
} }
} }
func positionSeverityWeight(label string) float64 { type explicitRatingRank struct {
Rank int
Weight float64
}
func positionExplicitRatingRank(label string) explicitRatingRank {
label = strings.ToLower(strings.TrimSpace(label)) label = strings.ToLower(strings.TrimSpace(label))
switch label { switch label {
case "cowgirl":
return explicitRatingRank{Rank: 1, Weight: 1.00}
case "reverse_cowgirl":
return explicitRatingRank{Rank: 2, Weight: 0.99}
case "doggy", "doggystyle", "standing_doggy": case "doggy", "doggystyle", "standing_doggy":
return 1.00 return explicitRatingRank{Rank: 3, Weight: 0.98}
case "cowgirl", "reverse_cowgirl":
return 0.98
case "missionary", "prone_bone":
return 0.95
case "blowjob", "cunnilingus", "69", "facesitting", "boobjob":
return 0.94
case "toy_play": case "toy_play":
return 0.88 return explicitRatingRank{Rank: 4, Weight: 0.96}
case "handjob", "fingering": case "fingering":
return 0.84 return explicitRatingRank{Rank: 5, Weight: 0.94}
case "missionary":
return explicitRatingRank{Rank: 6, Weight: 0.92}
case "prone_bone":
return explicitRatingRank{Rank: 7, Weight: 0.90}
case "spooning": case "spooning":
return 0.78 return explicitRatingRank{Rank: 8, Weight: 0.86}
case "standing": case "standing":
return 0.42 return explicitRatingRank{Rank: 9, Weight: 0.56}
case "69", "facesitting":
return explicitRatingRank{Rank: 10, Weight: 0.54}
case "blowjob", "handjob", "cunnilingus", "boobjob":
return explicitRatingRank{Rank: 11, Weight: 0.52}
default: default:
return 0.00 return explicitRatingRank{}
} }
} }
func positionSeverityWeight(label string) float64 {
return positionExplicitRatingRank(label).Weight
}
func bodyPartSeverityWeight(label string) float64 { func bodyPartSeverityWeight(label string) float64 {
label = strings.ToLower(strings.TrimSpace(label)) label = strings.ToLower(strings.TrimSpace(label))
@ -199,25 +225,39 @@ func objectSeverityWeight(label string) float64 {
} }
} }
func clothingSeverityWeight(label string) float64 { func clothingExplicitRatingRank(label string) explicitRatingRank {
label = strings.ToLower(strings.TrimSpace(label)) label = strings.ToLower(strings.TrimSpace(label))
switch label { switch label {
case "lingerie": case "lingerie":
return 0.50 return explicitRatingRank{Rank: 1, Weight: 0.62}
case "panties", "bra": case "stockings":
return 0.44 return explicitRatingRank{Rank: 2, Weight: 0.57}
case "skirt":
return explicitRatingRank{Rank: 3, Weight: 0.52}
case "hotpants":
return explicitRatingRank{Rank: 4, Weight: 0.47}
case "bikini": case "bikini":
return 0.30 return explicitRatingRank{Rank: 5, Weight: 0.44}
case "stockings", "heels": case "bra":
return 0.28 return explicitRatingRank{Rank: 6, Weight: 0.41}
case "skirt", "dress", "hotpants", "croptop": case "panties":
return 0.22 return explicitRatingRank{Rank: 7, Weight: 0.36}
case "croptop":
return explicitRatingRank{Rank: 8, Weight: 0.31}
case "heels":
return explicitRatingRank{Rank: 9, Weight: 0.26}
case "dress":
return explicitRatingRank{Rank: 10, Weight: 0.22}
default: default:
return 0.00 return explicitRatingRank{}
} }
} }
func clothingSeverityWeight(label string) float64 {
return clothingExplicitRatingRank(label).Weight
}
func personContextWeight(label string) float64 { func personContextWeight(label string) float64 {
if !isPersonSegmentLabel(label) { if !isPersonSegmentLabel(label) {
return 0 return 0
@ -437,26 +477,6 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
return 0 return 0
} }
var labelParts []string
if set.HasPosition {
labelParts = append(labelParts, "position:x")
}
if set.HasBody {
labelParts = append(labelParts, "body:x")
}
if set.HasObject {
labelParts = append(labelParts, "object:x")
}
if set.HasClothing {
labelParts = append(labelParts, "clothing:x")
}
if set.HasPerson {
labelParts = append(labelParts, "person:x")
}
_ = labelParts
var score float64 var score float64
if set.HasPosition { if set.HasPosition {
@ -484,7 +504,9 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
// Standing ohne echten Kontext klar schwächer halten. // Standing ohne echten Kontext klar schwächer halten.
if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing { if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing {
score = math.Min(score, 0.32) // Den Rang innerhalb der schwachen Positionen erhalten, statt
// Standing, 69 und orale Positionen auf denselben Wert zu kappen.
score = math.Min(score, 0.10+0.40*set.Position)
} }
return ratingClamp01(score) return ratingClamp01(score)
@ -655,6 +677,37 @@ func addRatingLabelsFromSegment(labels map[string]bool, label string) {
labels[normalized] = true labels[normalized] = true
} }
func updateBestExplicitRatingRanks(
label string,
bestPosition *string,
bestPositionRank *int,
bestClothing *string,
bestClothingRank *int,
) {
labels := map[string]bool{}
addRatingLabelsFromSegment(labels, label)
for normalized := range labels {
switch {
case strings.HasPrefix(normalized, "position:"):
raw := strings.TrimPrefix(normalized, "position:")
ranked := positionExplicitRatingRank(raw)
if ranked.Rank > 0 && (*bestPositionRank == 0 || ranked.Rank < *bestPositionRank) {
*bestPosition = raw
*bestPositionRank = ranked.Rank
}
case strings.HasPrefix(normalized, "clothing:"):
raw := strings.TrimPrefix(normalized, "clothing:")
ranked := clothingExplicitRatingRank(raw)
if ranked.Rank > 0 && (*bestClothingRank == 0 || ranked.Rank < *bestClothingRank) {
*bestClothing = raw
*bestClothingRank = ranked.Rank
}
}
}
}
func ratingSignalSetFromLabels(labels map[string]bool) ratingSignalSet { func ratingSignalSetFromLabels(labels map[string]bool) ratingSignalSet {
var set ratingSignalSet var set ratingSignalSet
@ -1256,119 +1309,103 @@ func ratingLinearGate(value float64, start float64, full float64) float64 {
return ratingClamp01((value - start) / (full - start)) return ratingClamp01((value - start) / (full - start))
} }
func ratingExplicitContextBonus( type ratingTimeRange struct {
bodyEffectiveWeighted float64, Start float64
clothingEffectiveWeighted float64, End float64
objectEffectiveWeighted float64, }
videoMinutes float64,
) float64 { func ratingCoveredSeconds(ranges []ratingTimeRange, durationSec float64) float64 {
if videoMinutes <= 0 { if len(ranges) == 0 || durationSec <= 0 {
return 0 return 0
} }
bodyDensity := bodyEffectiveWeighted / videoMinutes valid := make([]ratingTimeRange, 0, len(ranges))
clothingDensity := clothingEffectiveWeighted / videoMinutes for _, item := range ranges {
objectDensity := objectEffectiveWeighted / videoMinutes start := math.Max(0, math.Min(item.Start, durationSec))
end := math.Max(0, math.Min(item.End, durationSec))
if end > start {
valid = append(valid, ratingTimeRange{Start: start, End: end})
}
}
if len(valid) == 0 {
return 0
}
bonus := sort.SliceStable(valid, func(i, j int) bool {
0.060*ratingSaturatingEvidence(bodyDensity, 1.8) + if valid[i].Start != valid[j].Start {
0.035*ratingSaturatingEvidence(clothingDensity, 1.5) + return valid[i].Start < valid[j].Start
0.015*ratingSaturatingEvidence(objectDensity, 2.2) }
return valid[i].End < valid[j].End
})
return math.Min(bonus, 0.10) total := 0.0
current := valid[0]
for _, item := range valid[1:] {
if item.Start <= current.End {
if item.End > current.End {
current.End = item.End
}
continue
}
total += current.End - current.Start
current = item
}
return total + current.End - current.Start
} }
func ratingBlendWithAction(base, action, actionWeight float64) float64 { func ratingStrengthComponent(avgConfidence, peakConfidence float64) float64 {
actionWeight = ratingClamp01(actionWeight) avg := ratingSmoothStep(ratingLinearGate(avgConfidence, 0.35, 0.90))
return ratingClamp01((1-actionWeight)*base + actionWeight*action) peak := ratingSmoothStep(ratingLinearGate(peakConfidence, 0.50, 0.95))
return ratingClamp01(0.75*avg + 0.25*peak)
} }
func ratingPositionActionScore( func ratingDurationComponent(totalSeconds, longestSeconds float64) float64 {
durationNorm float64, total := ratingSaturatingEvidence(totalSeconds, 55.0)
longestNorm float64, longest := ratingSaturatingEvidence(longestSeconds, 30.0)
densityNorm float64, return ratingClamp01(0.62*total + 0.38*longest)
coverageNorm float64,
peakNorm float64,
) float64 {
return ratingClamp01(
0.38*durationNorm +
0.24*longestNorm +
0.18*densityNorm +
0.12*coverageNorm +
0.08*peakNorm,
)
} }
func ratingContextActionScore( func ratingCoverageComponent(coveredSeconds, durationSec float64) float64 {
durationNorm float64, if coveredSeconds <= 0 || durationSec <= 0 {
longestNorm float64, return 0
coverageNorm float64, }
densityNorm float64, return ratingSaturatingEvidence(ratingClamp01(coveredSeconds/durationSec), 0.12)
peakNorm float64,
) float64 {
return ratingClamp01(
0.40*durationNorm +
0.25*longestNorm +
0.20*coverageNorm +
0.10*densityNorm +
0.05*peakNorm,
)
} }
func ratingExcellenceBonus( func ratingActionComponent(duration, coverage, strength float64) float64 {
peakQuality float64, return ratingClamp01(0.50*duration + 0.30*coverage + 0.20*strength)
positionEffectiveWeighted float64, }
positionDensity float64,
weightedCoverageRatio float64, func ratingApplyPositionEvidenceCaps(
totalFlagged float64, raw float64,
longest float64, positionSeconds float64,
longestSeconds float64,
avgConfidence float64, avgConfidence float64,
segmentsPerMinute float64, positionScore float64,
n int,
) float64 { ) float64 {
// Kein pauschaler Score-Shift: switch {
// Der Bonus darf nur greifen, wenn wirklich mehrere starke Signale vorhanden sind. case positionSeconds < 5:
if n < 2 { raw = math.Min(raw, 0.21)
return 0 case positionSeconds < 12:
} raw = math.Min(raw, 0.44)
if positionEffectiveWeighted <= 0 { case positionSeconds < 30:
return 0 raw = math.Min(raw, 0.67)
}
if totalFlagged < 12.0 {
return 0
}
if peakQuality < 0.68 {
return 0
}
if avgConfidence < 0.42 {
return 0
} }
peakGate := ratingLinearGate(peakQuality, 0.68, 0.90) if positionSeconds < 75 ||
positionGate := ratingLinearGate(positionDensity, 2.20, 7.00) longestSeconds < 18 ||
coverageGate := ratingLinearGate(weightedCoverageRatio, 0.045, 0.160) avgConfidence < 0.55 ||
durationGate := ratingLinearGate(totalFlagged, 12.0, 90.0) positionScore < 0.65 {
longestGate := ratingLinearGate(longest, 8.0, 45.0) raw = math.Min(raw, 0.84)
confGate := ratingLinearGate(avgConfidence, 0.42, 0.78) }
frequencyGate := ratingLinearGate(segmentsPerMinute, 0.25, 1.00) if avgConfidence < 0.50 && positionScore < 0.65 {
raw = math.Min(raw, 0.21)
bonus :=
0.030*peakGate +
0.022*positionGate +
0.016*coverageGate +
0.012*durationGate +
0.012*longestGate +
0.010*confGate +
0.006*frequencyGate
// Maximal +8.5 Scorepunkte.
// Dadurch werden starke 72-79 Scores 5-Sterne-fähig,
// aber mittelmäßige Scores springen nicht einfach pauschal hoch.
if bonus > 0.085 {
bonus = 0.085
} }
return bonus return ratingClamp01(raw)
} }
func starsFromHighlightScore(score float64) int { func starsFromHighlightScore(score float64) int {
@ -1434,34 +1471,39 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
videoMinutes := math.Max(durationSec/60.0, 0.25) videoMinutes := math.Max(durationSec/60.0, 0.25)
var totalFlagged float64 var positionRanges []ratingTimeRange
var totalWeighted float64 var positionWeighted float64
var positionExplicitSum float64
var positionEvidenceWeight float64
var peakPositionExplicitness float64
var peakPositionConfidence float64
var positionEffectiveWeighted float64 var contextRanges []ratingTimeRange
var positionFlagged float64
var peakQuality float64
var qualityDurationSum float64
var qualityDurationWeight float64
var contextFlagged float64
var contextWeighted float64 var contextWeighted float64
var contextEffectiveWeighted float64 var contextExplicitSum float64
var contextPeakQuality float64 var contextEvidenceWeight float64
var contextQualityDurationSum float64 var peakContextExplicitness float64
var contextQualityDurationWeight float64 var peakContextConfidence float64
var contextLongest float64 var contextLongest float64
var contextConfSum float64 var contextConfSum float64
var contextConfWeightSum float64 var contextConfWeightSum float64
var contextN int var contextN int
var bodyEffectiveWeighted float64 var bodyEffectiveWeighted float64
var clothingEffectiveWeighted float64 var clothingEffectiveWeighted float64
var objectEffectiveWeighted float64 var objectEffectiveWeighted float64
var clothingExplicitSum float64
var clothingEvidenceWeight float64
var longest float64 var longest float64
var confSum float64 var confSum float64
var confWeightSum float64 var confWeightSum float64
var n int var n int
uniquePositions := map[string]bool{} uniquePositions := map[string]bool{}
bestPosition := ""
bestPositionRank := 0
bestClothing := ""
bestClothingRank := 0
for _, s := range segments { for _, s := range segments {
segDur := s.DurationSeconds segDur := s.DurationSeconds
@ -1492,25 +1534,41 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
effectiveDur := ratingEffectiveDurationSeconds(segDur) effectiveDur := ratingEffectiveDurationSeconds(segDur)
weightedDur := segDur * quality weightedDur := segDur * quality
effectiveWeightedDur := effectiveDur * quality segmentRange := ratingTimeRange{
Start: s.StartSeconds,
End: s.StartSeconds + segDur,
}
set := ratingSignalSetFromLabel(s.Label) set := ratingSignalSetFromLabel(s.Label)
updateBestExplicitRatingRanks(
s.Label,
&bestPosition,
&bestPositionRank,
&bestClothing,
&bestClothingRank,
)
bodyEffectiveWeighted += effectiveDur * set.Body * confWeight bodyEffectiveWeighted += effectiveDur * set.Body * confWeight
clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight
objectEffectiveWeighted += effectiveDur * set.Object * confWeight objectEffectiveWeighted += effectiveDur * set.Object * confWeight
if set.HasClothing {
clothingExplicitSum += (set.Clothing / clothingExplicitRatingRank("lingerie").Weight) * effectiveDur
clothingEvidenceWeight += effectiveDur
}
if !set.HasPosition { if !set.HasPosition {
contextFlagged += segDur contextRanges = append(contextRanges, segmentRange)
contextWeighted += weightedDur contextWeighted += weightedDur
contextEffectiveWeighted += effectiveWeightedDur contextExplicitSum += sev * effectiveDur
contextQualityDurationSum += quality * effectiveDur contextEvidenceWeight += effectiveDur
contextQualityDurationWeight += effectiveDur
contextConfSum += conf * effectiveDur contextConfSum += conf * effectiveDur
contextConfWeightSum += effectiveDur contextConfWeightSum += effectiveDur
contextN++ contextN++
if quality > contextPeakQuality { if sev > peakContextExplicitness {
contextPeakQuality = quality peakContextExplicitness = sev
}
if conf > peakContextConfidence {
peakContextConfidence = conf
} }
if segDur > contextLongest { if segDur > contextLongest {
contextLongest = segDur contextLongest = segDur
@ -1519,16 +1577,16 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
continue continue
} }
totalFlagged += segDur positionRanges = append(positionRanges, segmentRange)
totalWeighted += weightedDur positionWeighted += weightedDur
positionExplicitSum += set.Position * effectiveDur
positionEvidenceWeight += effectiveDur
positionEffectiveWeighted += effectiveWeightedDur if set.Position > peakPositionExplicitness {
positionFlagged += segDur peakPositionExplicitness = set.Position
qualityDurationSum += quality * effectiveDur }
qualityDurationWeight += effectiveDur if conf > peakPositionConfidence {
peakPositionConfidence = conf
if quality > peakQuality {
peakQuality = quality
} }
confSum += conf * effectiveDur confSum += conf * effectiveDur
@ -1546,58 +1604,71 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
} }
} }
clothingScore := 0.0
if clothingEvidenceWeight > 0 {
avgClothingExplicitness := ratingClamp01(clothingExplicitSum / clothingEvidenceWeight)
clothingDensity := clothingEffectiveWeighted / videoMinutes
clothingScore = ratingClamp01(
0.70*avgClothingExplicitness +
0.30*ratingSaturatingEvidence(clothingDensity, 1.8),
)
}
bodyContext := ratingSaturatingEvidence(bodyEffectiveWeighted/videoMinutes, 1.8)
objectContext := ratingSaturatingEvidence(objectEffectiveWeighted/videoMinutes, 2.2)
contextScore := ratingClamp01(0.78*bodyContext + 0.22*objectContext)
if n == 0 { if n == 0 {
if contextN == 0 { if contextN == 0 {
appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username)) appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username))
return r return r
} }
contextFlagged := ratingCoveredSeconds(contextRanges, durationSec)
contextCoverageRatio := ratingClamp01(contextFlagged / durationSec) contextCoverageRatio := ratingClamp01(contextFlagged / durationSec)
contextWeightedCoverageRatio := ratingClamp01(contextWeighted / durationSec) contextWeightedCoverageRatio := ratingClamp01(contextWeighted / durationSec)
contextSegmentsPerMinute := float64(contextN) / videoMinutes contextSegmentsPerMinute := float64(contextN) / videoMinutes
contextAvgConfidence := contextConfSum / contextConfWeightSum contextAvgConfidence := contextConfSum / contextConfWeightSum
contextAvgQuality := contextQualityDurationSum / contextQualityDurationWeight avgContextExplicitness := contextExplicitSum / contextEvidenceWeight
contextDensity := contextEffectiveWeighted / videoMinutes contextQuality := ratingClamp01(
(0.75*avgContextExplicitness + 0.25*peakContextExplicitness) / 0.72,
qualityNorm := ratingSmoothStep(
ratingLinearGate(contextAvgQuality, 0.08, 0.55),
) )
peakNorm := ratingSmoothStep( durationScore := ratingDurationComponent(contextFlagged, contextLongest)
ratingLinearGate(contextPeakQuality, 0.12, 0.62), strengthScore := ratingStrengthComponent(contextAvgConfidence, peakContextConfidence)
) coverageScore := ratingCoverageComponent(contextFlagged, durationSec)
durationNorm := ratingSaturatingEvidence(contextFlagged, 150.0)
longestNorm := ratingSaturatingEvidence(contextLongest, 60.0)
densityNorm := ratingSaturatingEvidence(contextDensity, 2.5)
coverageNorm := ratingSaturatingEvidence(contextWeightedCoverageRatio, 0.08)
raw := raw :=
0.28*qualityNorm + 0.30*contextQuality +
0.12*peakNorm + 0.25*durationScore +
0.25*durationNorm + 0.15*strengthScore +
0.15*longestNorm + 0.12*coverageScore +
0.15*coverageNorm + 0.12*clothingScore +
0.05*densityNorm 0.06*contextScore
actionRaw := ratingContextActionScore( actionRaw := ratingActionComponent(durationScore, coverageScore, strengthScore)
durationNorm,
longestNorm,
coverageNorm,
densityNorm,
peakNorm,
)
raw = ratingBlendWithAction(raw, actionRaw, 0.35)
// Context can produce useful 1-3 star ratings, but never the 4-5 star
// range reserved for sustained position detections.
if contextFlagged < 20.0 { if contextFlagged < 20.0 {
raw = math.Min(raw, 0.44) raw = math.Min(raw, 0.44)
} }
raw = math.Min(raw, 0.64) semanticCeiling := 0.52 + 0.12*math.Max(
contextQuality,
math.Max(clothingScore, contextScore),
)
raw = math.Min(raw, semanticCeiling)
score := ratingRound(ratingClamp01(raw)*100, 1) score := ratingRound(ratingClamp01(raw)*100, 1)
r.Score = score r.Score = score
r.ActionScore = ratingRound(actionRaw*100, 1) r.ActionScore = ratingRound(actionRaw*100, 1)
r.BestPosition = bestPosition
r.BestPositionRank = bestPositionRank
r.BestClothing = bestClothing
r.BestClothingRank = bestClothingRank
r.ClothingScore = ratingRound(clothingScore*100, 1)
r.DurationScore = ratingRound(durationScore*100, 1)
r.StrengthScore = ratingRound(strengthScore*100, 1)
r.CoverageScore = ratingRound(coverageScore*100, 1)
r.ContextScore = ratingRound(contextScore*100, 1)
r.Stars = starsFromHighlightScore(score) r.Stars = starsFromHighlightScore(score)
r.Segments = contextN r.Segments = contextN
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2) r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
@ -1609,13 +1680,13 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
r.AvgConfidence = ratingRound(contextAvgConfidence, 3) r.AvgConfidence = ratingRound(contextAvgConfidence, 3)
appLogf( appLogf(
"✅ %s context rating score=%.1f stars=%d segs=%d flagged=%.1f density=%.2f coverage=%.4f longest=%.1f avgConf=%.3f", "rating %s context score=%.1f stars=%d segs=%d flagged=%.1f quality=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
ratingLogSubject(username), ratingLogSubject(username),
r.Score, r.Score,
r.Stars, r.Stars,
r.Segments, r.Segments,
r.FlaggedSeconds, r.FlaggedSeconds,
contextDensity, contextQuality,
r.WeightedCoverageRatio, r.WeightedCoverageRatio,
r.LongestSegmentSeconds, r.LongestSegmentSeconds,
r.AvgConfidence, r.AvgConfidence,
@ -1624,97 +1695,76 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
return r return r
} }
coverageRatio := ratingClamp01(totalFlagged / durationSec) positionFlagged := ratingCoveredSeconds(positionRanges, durationSec)
weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec) coverageRatio := ratingClamp01(positionFlagged / durationSec)
weightedCoverageRatio := ratingClamp01(positionWeighted / durationSec)
segmentsPerMinute := float64(n) / videoMinutes segmentsPerMinute := float64(n) / videoMinutes
avgConfidence := confSum / confWeightSum avgConfidence := confSum / confWeightSum
avgPositionQuality := qualityDurationSum / qualityDurationWeight avgPositionExplicitness := positionExplicitSum / positionEvidenceWeight
positionScore := ratingClamp01(
positionDensity := positionEffectiveWeighted / videoMinutes 0.75*avgPositionExplicitness + 0.25*peakPositionExplicitness,
avgQualityNorm := ratingSmoothStep(
ratingLinearGate(avgPositionQuality, 0.45, 0.95),
) )
peakNorm := ratingSmoothStep( durationScore := ratingDurationComponent(positionFlagged, longest)
ratingLinearGate(peakQuality, 0.42, 0.95), strengthScore := ratingStrengthComponent(avgConfidence, peakPositionConfidence)
) coverageScore := ratingCoverageComponent(positionFlagged, durationSec)
positionDurationNorm := ratingSaturatingEvidence(positionFlagged, 65.0) varietyScore := ratingLinearGate(float64(len(uniquePositions)), 1, 4)
longestNorm := ratingSaturatingEvidence(longest, 35.0)
positionDensityNorm := ratingSaturatingEvidence(positionDensity, 3.2)
coverageNorm := ratingSaturatingEvidence(weightedCoverageRatio, 0.075)
positionVarietyNorm := ratingLinearGate(float64(len(uniquePositions)), 1, 4)
// Position quality is the primary signal. Actual position duration and the
// longest continuous segment decide whether a detection is brief or strong.
raw := raw :=
0.30*avgQualityNorm + 0.36*positionScore +
0.10*peakNorm + 0.27*durationScore +
0.26*positionDurationNorm + 0.14*strengthScore +
0.16*longestNorm + 0.10*coverageScore +
0.10*positionDensityNorm + 0.06*contextScore +
0.06*coverageNorm + 0.04*clothingScore +
0.02*positionVarietyNorm 0.03*varietyScore
actionRaw := ratingPositionActionScore( actionRaw := ratingActionComponent(durationScore, coverageScore, strengthScore)
positionDurationNorm,
longestNorm,
positionDensityNorm,
coverageNorm,
peakNorm,
)
raw = ratingBlendWithAction(raw, actionRaw, 0.30)
explicitContextBonus := ratingExplicitContextBonus( // Short or weak detections cannot reach high ratings on rank alone.
bodyEffectiveWeighted, raw = ratingApplyPositionEvidenceCaps(
clothingEffectiveWeighted, raw,
objectEffectiveWeighted, positionFlagged,
videoMinutes,
)
// Sehr wenig Material nicht überbewerten.
if totalFlagged < 5.0 && n <= 1 {
raw = math.Min(raw, 0.44)
}
excellenceBonus := ratingExcellenceBonus(
peakQuality,
positionEffectiveWeighted,
positionDensity,
weightedCoverageRatio,
totalFlagged,
longest, longest,
avgConfidence, avgConfidence,
segmentsPerMinute, positionScore,
n,
) )
raw = ratingClamp01(raw + explicitContextBonus + excellenceBonus)
score := ratingRound(raw*100, 1) score := ratingRound(raw*100, 1)
r.Score = score r.Score = score
r.ActionScore = ratingRound(actionRaw*100, 1) r.ActionScore = ratingRound(actionRaw*100, 1)
r.BestPosition = bestPosition
r.BestPositionRank = bestPositionRank
r.BestClothing = bestClothing
r.BestClothingRank = bestClothingRank
r.PositionScore = ratingRound(positionScore*100, 1)
r.ClothingScore = ratingRound(clothingScore*100, 1)
r.DurationScore = ratingRound(durationScore*100, 1)
r.StrengthScore = ratingRound(strengthScore*100, 1)
r.CoverageScore = ratingRound(coverageScore*100, 1)
r.ContextScore = ratingRound(contextScore*100, 1)
r.VarietyScore = ratingRound(varietyScore*100, 1)
r.Stars = starsFromHighlightScore(score) r.Stars = starsFromHighlightScore(score)
r.Segments = n r.Segments = n
r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2) r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2)
r.FlaggedSeconds = ratingRound(totalFlagged, 2) r.FlaggedSeconds = ratingRound(positionFlagged, 2)
r.WeightedFlaggedSeconds = ratingRound(totalWeighted, 2) r.WeightedFlaggedSeconds = ratingRound(positionWeighted, 2)
r.CoverageRatio = ratingRound(coverageRatio, 4) r.CoverageRatio = ratingRound(coverageRatio, 4)
r.WeightedCoverageRatio = ratingRound(weightedCoverageRatio, 4) r.WeightedCoverageRatio = ratingRound(weightedCoverageRatio, 4)
r.LongestSegmentSeconds = ratingRound(longest, 2) r.LongestSegmentSeconds = ratingRound(longest, 2)
r.AvgConfidence = ratingRound(avgConfidence, 3) r.AvgConfidence = ratingRound(avgConfidence, 3)
appLogf( appLogf(
"✅ %s rating score=%.1f stars=%d contextBonus=%.1f excellenceBonus=%.1f segs=%d flagged=%.1f posFlagged=%.1f posDensity=%.2f coverage=%.4f longest=%.1f avgConf=%.3f", "rating %s score=%.1f stars=%d position=%.1f clothing=%.1f segs=%d flagged=%.1f duration=%.1f strength=%.1f coverage=%.4f longest=%.1f avgConf=%.3f",
ratingLogSubject(username), ratingLogSubject(username),
r.Score, r.Score,
r.Stars, r.Stars,
ratingRound(explicitContextBonus*100, 1), r.PositionScore,
ratingRound(excellenceBonus*100, 1), r.ClothingScore,
r.Segments, r.Segments,
r.FlaggedSeconds, r.FlaggedSeconds,
ratingRound(positionFlagged, 1), r.DurationScore,
positionDensity, r.StrengthScore,
r.WeightedCoverageRatio, r.WeightedCoverageRatio,
r.LongestSegmentSeconds, r.LongestSegmentSeconds,
r.AvgConfidence, r.AvgConfidence,

View File

@ -46,22 +46,22 @@ func TestComputeHighlightRatingSpansAllStars(t *testing.T) {
{ {
name: "several long high quality positions", name: "several long high quality positions",
segments: []aiSegmentMeta{ segments: []aiSegmentMeta{
ratingTestSegment("position:doggy", 20, 25, 0.82), ratingTestSegment("position:cowgirl", 20, 25, 0.82),
ratingTestSegment("position:cowgirl", 120, 25, 0.82), ratingTestSegment("position:reverse_cowgirl", 120, 25, 0.82),
ratingTestSegment("position:missionary", 220, 25, 0.82), ratingTestSegment("position:doggy", 220, 25, 0.82),
ratingTestSegment("position:blowjob", 320, 25, 0.82), ratingTestSegment("position:toy_play", 320, 25, 0.82),
}, },
want: 4, want: 4,
}, },
{ {
name: "exceptional sustained and varied positions", name: "exceptional sustained and varied positions",
segments: []aiSegmentMeta{ segments: []aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 40, 0.90), ratingTestSegment("position:cowgirl", 10, 40, 0.90),
ratingTestSegment("position:cowgirl", 100, 40, 0.90), ratingTestSegment("position:reverse_cowgirl", 100, 40, 0.90),
ratingTestSegment("position:missionary", 190, 40, 0.90), ratingTestSegment("position:doggy", 190, 40, 0.90),
ratingTestSegment("position:blowjob", 280, 40, 0.90), ratingTestSegment("position:toy_play", 280, 40, 0.90),
ratingTestSegment("position:cunnilingus", 370, 40, 0.90), ratingTestSegment("position:fingering", 370, 40, 0.90),
ratingTestSegment("position:reverse_cowgirl", 460, 40, 0.90), ratingTestSegment("position:missionary", 460, 40, 0.90),
}, },
want: 5, want: 5,
}, },
@ -77,6 +77,141 @@ func TestComputeHighlightRatingSpansAllStars(t *testing.T) {
} }
} }
func TestExplicitPositionRanking(t *testing.T) {
labels := []string{
"cowgirl",
"reverse_cowgirl",
"doggy",
"toy_play",
"fingering",
"missionary",
"prone_bone",
"spooning",
"standing",
"69",
"blowjob",
}
previousWeight := 2.0
for index, label := range labels {
ranked := positionExplicitRatingRank(label)
wantRank := index + 1
if ranked.Rank != wantRank {
t.Fatalf("%s rank = %d, want %d", label, ranked.Rank, wantRank)
}
if ranked.Weight <= 0 || ranked.Weight >= previousWeight {
t.Fatalf(
"%s weight = %.2f, want a positive value below %.2f",
label,
ranked.Weight,
previousWeight,
)
}
previousWeight = ranked.Weight
}
for _, alias := range []string{"doggystyle", "standing_doggy"} {
if ranked := positionExplicitRatingRank(alias); ranked.Rank != 3 {
t.Fatalf("%s rank = %d, want alias rank 3", alias, ranked.Rank)
}
}
for _, grouped := range []string{"facesitting", "handjob", "cunnilingus", "boobjob"} {
if ranked := positionExplicitRatingRank(grouped); ranked.Rank != positionExplicitRatingRank("69").Rank &&
ranked.Rank != positionExplicitRatingRank("blowjob").Rank {
t.Fatalf("%s has unexpected grouped rank %d", grouped, ranked.Rank)
}
}
}
func TestExplicitClothingRanking(t *testing.T) {
labels := []string{
"lingerie",
"stockings",
"skirt",
"hotpants",
"bikini",
"bra",
"panties",
"croptop",
"heels",
"dress",
}
previousWeight := 2.0
for index, label := range labels {
ranked := clothingExplicitRatingRank(label)
wantRank := index + 1
if ranked.Rank != wantRank {
t.Fatalf("%s rank = %d, want %d", label, ranked.Rank, wantRank)
}
if ranked.Weight <= 0 || ranked.Weight >= previousWeight {
t.Fatalf(
"%s weight = %.2f, want a positive value below %.2f",
label,
ranked.Weight,
previousWeight,
)
}
previousWeight = ranked.Weight
}
}
func TestComputeHighlightRatingReportsBestExplicitRanks(t *testing.T) {
got := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("combo:position:standing+clothing:dress", 10, 20, 0.90),
ratingTestSegment("combo:position:cowgirl+clothing:panties", 80, 20, 0.90),
ratingTestSegment("combo:position:doggy+clothing:lingerie", 150, 20, 0.90),
}, 300)
if got.BestPosition != "cowgirl" || got.BestPositionRank != 1 {
t.Fatalf(
"best position = %q rank %d, want cowgirl rank 1",
got.BestPosition,
got.BestPositionRank,
)
}
if got.BestClothing != "lingerie" || got.BestClothingRank != 1 {
t.Fatalf(
"best clothing = %q rank %d, want lingerie rank 1",
got.BestClothing,
got.BestClothingRank,
)
}
}
func TestComputeHighlightRatingFollowsExplicitRank(t *testing.T) {
positionScore := func(label string) float64 {
return computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:"+label, 10, 30, 0.85),
}, 300).Score
}
clothingScore := func(label string) float64 {
return computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("clothing:"+label, 10, 90, 0.90),
}, 300).Score
}
if !(positionScore("cowgirl") > positionScore("doggy") &&
positionScore("doggy") > positionScore("toy_play") &&
positionScore("toy_play") > positionScore("missionary") &&
positionScore("missionary") > positionScore("standing") &&
positionScore("standing") > positionScore("blowjob")) {
t.Fatal("position score does not follow the explicit ranking")
}
if !(clothingScore("lingerie") > clothingScore("stockings") &&
clothingScore("stockings") > clothingScore("hotpants") &&
clothingScore("hotpants") > clothingScore("bikini") &&
clothingScore("bikini") > clothingScore("panties") &&
clothingScore("panties") > clothingScore("dress")) {
t.Fatal("clothing score does not follow the explicit ranking")
}
}
func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) { func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) {
short := computeHighlightRating([]aiSegmentMeta{ short := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 8, 0.80), ratingTestSegment("position:doggy", 10, 8, 0.80),
@ -122,6 +257,67 @@ func TestComputeHighlightRatingActionPrefersSustainedActivity(t *testing.T) {
} }
} }
func TestComputeHighlightRatingDoesNotDoubleCountOverlaps(t *testing.T) {
got := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:cowgirl", 0, 30, 0.90),
ratingTestSegment("position:doggy", 10, 30, 0.90),
}, 100)
if got.FlaggedSeconds != 40 {
t.Fatalf("flagged seconds = %.1f, want 40.0", got.FlaggedSeconds)
}
if got.CoverageRatio != 0.4 {
t.Fatalf("coverage ratio = %.4f, want 0.4000", got.CoverageRatio)
}
}
func TestComputeHighlightRatingReportsIndependentComponents(t *testing.T) {
got := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("combo:position:cowgirl+body:pussy+clothing:lingerie", 10, 40, 0.90),
ratingTestSegment("combo:position:doggy+object:dildo+clothing:stockings", 100, 40, 0.90),
}, 300)
components := map[string]float64{
"position": got.PositionScore,
"clothing": got.ClothingScore,
"duration": got.DurationScore,
"strength": got.StrengthScore,
"coverage": got.CoverageScore,
"context": got.ContextScore,
"variety": got.VarietyScore,
"action": got.ActionScore,
}
for name, value := range components {
if value <= 0 || value > 100 {
t.Fatalf("%s component = %.1f, want value in (0, 100]", name, value)
}
}
}
func TestComputeHighlightRatingStrengthFollowsConfidence(t *testing.T) {
weak := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 45, 0.45),
}, 300)
strong := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 45, 0.90),
}, 300)
if strong.StrengthScore <= weak.StrengthScore {
t.Fatalf(
"strength should follow confidence: strong=%.1f weak=%.1f",
strong.StrengthScore,
weak.StrengthScore,
)
}
if strong.Score <= weak.Score {
t.Fatalf(
"rating should reward stronger evidence: strong=%.1f weak=%.1f",
strong.Score,
weak.Score,
)
}
}
func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) { func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
clothingOnly := computeHighlightRating([]aiSegmentMeta{ clothingOnly := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("clothing:lingerie", 10, 60, 0.90), ratingTestSegment("clothing:lingerie", 10, 60, 0.90),
@ -134,9 +330,9 @@ func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
ratingTestSegment("combo:body:penis+object:dildo", 240, 45, 0.90), ratingTestSegment("combo:body:penis+object:dildo", 240, 45, 0.90),
}, 600) }, 600)
if clothingOnly.Stars != 2 { if clothingOnly.Stars != 3 {
t.Fatalf( t.Fatalf(
"clothing-only stars = %d, want 2 (score %.1f)", "clothing-only stars = %d, want 3 (score %.1f)",
clothingOnly.Stars, clothingOnly.Stars,
clothingOnly.Score, clothingOnly.Score,
) )
@ -210,10 +406,10 @@ func TestComputeHighlightRatingExplicitSignalsBoostPositions(t *testing.T) {
func TestComputeHighlightRatingExplicitContextCanElevateStrongPositions(t *testing.T) { func TestComputeHighlightRatingExplicitContextCanElevateStrongPositions(t *testing.T) {
positions := []aiSegmentMeta{ positions := []aiSegmentMeta{
ratingTestSegment("position:doggy", 20, 25, 0.82), ratingTestSegment("position:cowgirl", 20, 25, 0.82),
ratingTestSegment("position:cowgirl", 120, 25, 0.82), ratingTestSegment("position:reverse_cowgirl", 120, 25, 0.82),
ratingTestSegment("position:missionary", 220, 25, 0.82), ratingTestSegment("position:doggy", 220, 25, 0.82),
ratingTestSegment("position:blowjob", 320, 25, 0.82), ratingTestSegment("position:toy_play", 320, 25, 0.82),
} }
positionOnly := computeHighlightRating(positions, 600) positionOnly := computeHighlightRating(positions, 600)
withExplicitContext := computeHighlightRating(append( withExplicitContext := computeHighlightRating(append(

View File

@ -1481,11 +1481,8 @@ func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
continue continue
} }
// Ab hier existiert das erste echte extrahierte Bild. // Nach jeder erfolgreichen Extraktion das aktuell verarbeitete Frame zeigen.
// Dieses bleibt als Overlay-Background für Analyse/Speichern/weitere Frames. previewURL = "/api/training/frame?id=" + url.QueryEscape(id)
if previewURL == "" {
previewURL = "/api/training/frame?id=" + url.QueryEscape(id)
}
trainingPublishAnalysisStepWithPreview( trainingPublishAnalysisStepWithPreview(
requestID, requestID,

View File

@ -138,6 +138,17 @@ export const RATING_VALUE_LABELS: Record<string, string> = {
stars: 'Sterne', stars: 'Sterne',
score: 'Score', score: 'Score',
actionScore: 'Action', actionScore: 'Action',
bestPosition: 'Top-Position',
bestPositionRank: 'Positionsrang',
bestClothing: 'Top-Kleidung',
bestClothingRank: 'Kleidungsrang',
positionScore: 'Position',
clothingScore: 'Kleidung',
durationScore: 'Dauer',
strengthScore: 'Stärke',
coverageScore: 'Abdeckung',
contextScore: 'Kontext',
varietyScore: 'Vielfalt',
confidence: 'Konfidenz', confidence: 'Konfidenz',
probability: 'Wahrscheinlichkeit', probability: 'Wahrscheinlichkeit',
nsfw: 'NSFW', nsfw: 'NSFW',

View File

@ -767,13 +767,13 @@ function TrainingStageOverlay(props: {
}) { }) {
const progress = clampPercent(props.progress ?? 0) const progress = clampPercent(props.progress ?? 0)
const isTraining = props.mode === 'training' const isTraining = props.mode === 'training'
const hasBackground = !isTraining && Boolean(props.backgroundUrl) const hasBackground = Boolean(props.backgroundUrl)
const visible = props.visible ?? true const visible = props.visible ?? true
const [backgroundVisible, setBackgroundVisible] = useState(false) const [backgroundVisible, setBackgroundVisible] = useState(false)
useEffect(() => { useEffect(() => {
if (!props.backgroundUrl || isTraining) { if (!props.backgroundUrl) {
setBackgroundVisible(false) setBackgroundVisible(false)
return return
} }
@ -785,7 +785,7 @@ function TrainingStageOverlay(props: {
}) })
return () => window.cancelAnimationFrame(frame) return () => window.cancelAnimationFrame(frame)
}, [props.backgroundUrl, isTraining]) }, [props.backgroundUrl])
const title = isTraining ? 'Training läuft…' : 'Analyse läuft…' const title = isTraining ? 'Training läuft…' : 'Analyse läuft…'
const fallbackText = isTraining const fallbackText = isTraining
@ -5685,9 +5685,9 @@ export default function TrainingTab(props: {
progress={stageOverlayProgress} progress={stageOverlayProgress}
visible={stageOverlayIsVisible} visible={stageOverlayIsVisible}
backgroundUrl={ backgroundUrl={
stageOverlayMode === 'analysis' stageOverlayMode === 'training'
? loadingPreviewBackgroundUrl ? imageSrc
: undefined : loadingPreviewBackgroundUrl
} }
/> />
) : null} ) : null}