fixed rating
This commit is contained in:
parent
33657733d0
commit
c6e518105b
@ -52,11 +52,11 @@ func ratingSmoothStep(v float64) float64 {
|
|||||||
return v * v * (3 - 2*v)
|
return v * v * (3 - 2*v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingSoftCap(value, knee float64) float64 {
|
func ratingSaturatingEvidence(value, scale float64) float64 {
|
||||||
if value <= 0 || knee <= 0 {
|
if value <= 0 || scale <= 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return value / (value + knee)
|
return ratingClamp01(1 - math.Exp(-value/scale))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingEffectiveDurationSeconds(seconds float64) float64 {
|
func ratingEffectiveDurationSeconds(seconds float64) float64 {
|
||||||
@ -460,25 +460,25 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
|
|||||||
|
|
||||||
if set.HasPosition {
|
if set.HasPosition {
|
||||||
// Position soll das Hauptkriterium sein.
|
// Position soll das Hauptkriterium sein.
|
||||||
// Kleidung/Objekte dürfen unterstützen, aber nicht das Rating tragen.
|
// Sexuelle Körperteile und explizite Kleidung verstärken die Position.
|
||||||
score =
|
score =
|
||||||
0.82*set.Position +
|
0.82*set.Position +
|
||||||
0.08*set.Body +
|
0.12*set.Body +
|
||||||
0.05*set.Clothing +
|
0.08*set.Clothing +
|
||||||
0.03*set.Object +
|
0.015*set.Object +
|
||||||
0.02*set.Person
|
0.005*set.Person
|
||||||
|
|
||||||
if set.HasBody {
|
if set.HasBody {
|
||||||
score += 0.020
|
score += 0.030
|
||||||
}
|
}
|
||||||
if set.HasClothing {
|
if set.HasClothing {
|
||||||
score += 0.015
|
score += 0.020
|
||||||
}
|
}
|
||||||
if set.HasObject {
|
if set.HasObject {
|
||||||
score += 0.010
|
score += 0.006
|
||||||
}
|
}
|
||||||
if set.HasPerson {
|
if set.HasPerson {
|
||||||
score += 0.005
|
score += 0.002
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standing ohne echten Kontext klar schwächer halten.
|
// Standing ohne echten Kontext klar schwächer halten.
|
||||||
@ -829,6 +829,7 @@ func mergeRatingActivitySegments(
|
|||||||
labels map[string]bool
|
labels map[string]bool
|
||||||
positionWeights map[string]float64
|
positionWeights map[string]float64
|
||||||
positionBounds map[string]segmentBounds
|
positionBounds map[string]segmentBounds
|
||||||
|
positionRanges map[string][]segmentBounds
|
||||||
marker float64
|
marker float64
|
||||||
markerWeight float64
|
markerWeight float64
|
||||||
}
|
}
|
||||||
@ -836,6 +837,7 @@ func mergeRatingActivitySegments(
|
|||||||
var addRatingPositionSignal func(
|
var addRatingPositionSignal func(
|
||||||
weights map[string]float64,
|
weights map[string]float64,
|
||||||
bounds map[string]segmentBounds,
|
bounds map[string]segmentBounds,
|
||||||
|
ranges map[string][]segmentBounds,
|
||||||
label string,
|
label string,
|
||||||
weight float64,
|
weight float64,
|
||||||
start float64,
|
start float64,
|
||||||
@ -845,6 +847,7 @@ func mergeRatingActivitySegments(
|
|||||||
addRatingPositionSignal = func(
|
addRatingPositionSignal = func(
|
||||||
weights map[string]float64,
|
weights map[string]float64,
|
||||||
bounds map[string]segmentBounds,
|
bounds map[string]segmentBounds,
|
||||||
|
ranges map[string][]segmentBounds,
|
||||||
label string,
|
label string,
|
||||||
weight float64,
|
weight float64,
|
||||||
start float64,
|
start float64,
|
||||||
@ -858,7 +861,7 @@ func mergeRatingActivitySegments(
|
|||||||
if strings.HasPrefix(label, "combo:") {
|
if strings.HasPrefix(label, "combo:") {
|
||||||
raw := strings.TrimPrefix(label, "combo:")
|
raw := strings.TrimPrefix(label, "combo:")
|
||||||
for _, part := range strings.Split(raw, "+") {
|
for _, part := range strings.Split(raw, "+") {
|
||||||
addRatingPositionSignal(weights, bounds, part, weight, start, end)
|
addRatingPositionSignal(weights, bounds, ranges, part, weight, start, end)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -891,6 +894,13 @@ func mergeRatingActivitySegments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bounds[normalized] = b
|
bounds[normalized] = b
|
||||||
|
if end > start {
|
||||||
|
ranges[normalized] = append(ranges[normalized], segmentBounds{
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveRatingLabelsForBlock := func(
|
effectiveRatingLabelsForBlock := func(
|
||||||
@ -971,10 +981,12 @@ func mergeRatingActivitySegments(
|
|||||||
|
|
||||||
positionWeights := map[string]float64{}
|
positionWeights := map[string]float64{}
|
||||||
positionBounds := map[string]segmentBounds{}
|
positionBounds := map[string]segmentBounds{}
|
||||||
|
positionRanges := map[string][]segmentBounds{}
|
||||||
|
|
||||||
addRatingPositionSignal(
|
addRatingPositionSignal(
|
||||||
positionWeights,
|
positionWeights,
|
||||||
positionBounds,
|
positionBounds,
|
||||||
|
positionRanges,
|
||||||
s.Label,
|
s.Label,
|
||||||
conf*math.Max(1, dur),
|
conf*math.Max(1, dur),
|
||||||
s.StartSeconds,
|
s.StartSeconds,
|
||||||
@ -992,11 +1004,60 @@ func mergeRatingActivitySegments(
|
|||||||
labels: labels,
|
labels: labels,
|
||||||
positionWeights: positionWeights,
|
positionWeights: positionWeights,
|
||||||
positionBounds: positionBounds,
|
positionBounds: positionBounds,
|
||||||
|
positionRanges: positionRanges,
|
||||||
marker: marker,
|
marker: marker,
|
||||||
markerWeight: markerWeight,
|
markerWeight: markerWeight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coveredDuration := func(ranges []segmentBounds, start, end float64) float64 {
|
||||||
|
if len(ranges) == 0 || end <= start {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
clipped := make([]segmentBounds, 0, len(ranges))
|
||||||
|
for _, r := range ranges {
|
||||||
|
rangeStart := math.Max(start, r.start)
|
||||||
|
rangeEnd := math.Min(end, r.end)
|
||||||
|
if rangeEnd > rangeStart {
|
||||||
|
clipped = append(clipped, segmentBounds{
|
||||||
|
start: rangeStart,
|
||||||
|
end: rangeEnd,
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clipped) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(clipped, func(i, j int) bool {
|
||||||
|
if clipped[i].start != clipped[j].start {
|
||||||
|
return clipped[i].start < clipped[j].start
|
||||||
|
}
|
||||||
|
return clipped[i].end < clipped[j].end
|
||||||
|
})
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
currentStart := clipped[0].start
|
||||||
|
currentEnd := clipped[0].end
|
||||||
|
|
||||||
|
for _, r := range clipped[1:] {
|
||||||
|
if r.start <= currentEnd {
|
||||||
|
if r.end > currentEnd {
|
||||||
|
currentEnd = r.end
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
total += currentEnd - currentStart
|
||||||
|
currentStart = r.start
|
||||||
|
currentEnd = r.end
|
||||||
|
}
|
||||||
|
|
||||||
|
return total + currentEnd - currentStart
|
||||||
|
}
|
||||||
|
|
||||||
finishBlock := func(b activityBlock) (aiSegmentMeta, bool) {
|
finishBlock := func(b activityBlock) (aiSegmentMeta, bool) {
|
||||||
dur := b.end - b.start
|
dur := b.end - b.start
|
||||||
if dur <= 0 {
|
if dur <= 0 {
|
||||||
@ -1037,8 +1098,19 @@ func mergeRatingActivitySegments(
|
|||||||
return aiSegmentMeta{}, false
|
return aiSegmentMeta{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ratingDuration := dur
|
||||||
|
if bestPosition != "" {
|
||||||
|
if evidenceDuration := coveredDuration(
|
||||||
|
b.positionRanges[bestPosition],
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
); evidenceDuration > 0 {
|
||||||
|
ratingDuration = evidenceDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Kurze Segmente dürfen bleiben, wenn sie stark genug sind.
|
// Kurze Segmente dürfen bleiben, wenn sie stark genug sind.
|
||||||
if dur < minDurationSec && sev < 0.72 {
|
if ratingDuration < minDurationSec && sev < 0.72 {
|
||||||
return aiSegmentMeta{}, false
|
return aiSegmentMeta{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1060,7 +1132,7 @@ func mergeRatingActivitySegments(
|
|||||||
Score: ratingClamp01(score),
|
Score: ratingClamp01(score),
|
||||||
StartSeconds: start,
|
StartSeconds: start,
|
||||||
EndSeconds: end,
|
EndSeconds: end,
|
||||||
DurationSeconds: dur,
|
DurationSeconds: ratingDuration,
|
||||||
AutoSelected: true,
|
AutoSelected: true,
|
||||||
Position: segmentPositionFromAnalyzeLabel(label),
|
Position: segmentPositionFromAnalyzeLabel(label),
|
||||||
Tags: segmentTagsFromAnalyzeLabel(label),
|
Tags: segmentTagsFromAnalyzeLabel(label),
|
||||||
@ -1076,13 +1148,15 @@ func mergeRatingActivitySegments(
|
|||||||
n := valid[i]
|
n := valid[i]
|
||||||
|
|
||||||
gap := n.StartSeconds - cur.end
|
gap := n.StartSeconds - cur.end
|
||||||
|
nextLabels := map[string]bool{}
|
||||||
|
addRatingLabelsFromSegment(nextLabels, n.Label)
|
||||||
|
positionConflict := ratingPositionSetsConflict(cur.labels, nextLabels)
|
||||||
|
|
||||||
shouldBridge := gap <= maxSilentGapSec
|
// A short gap may connect supporting signals or the same position, but
|
||||||
|
// must not turn consecutive different positions into one long segment.
|
||||||
if !shouldBridge && gap <= ratingBridgeStrongGapSec {
|
shouldBridge := gap <= maxSilentGapSec && !positionConflict
|
||||||
nextLabels := map[string]bool{}
|
|
||||||
addRatingLabelsFromSegment(nextLabels, n.Label)
|
|
||||||
|
|
||||||
|
if !shouldBridge && !positionConflict && gap <= ratingBridgeStrongGapSec {
|
||||||
curSet := ratingSignalSetFromLabels(cur.labels)
|
curSet := ratingSignalSetFromLabels(cur.labels)
|
||||||
nextSet := ratingSignalSetFromLabels(nextLabels)
|
nextSet := ratingSignalSetFromLabels(nextLabels)
|
||||||
|
|
||||||
@ -1128,6 +1202,7 @@ func mergeRatingActivitySegments(
|
|||||||
addRatingPositionSignal(
|
addRatingPositionSignal(
|
||||||
cur.positionWeights,
|
cur.positionWeights,
|
||||||
cur.positionBounds,
|
cur.positionBounds,
|
||||||
|
cur.positionRanges,
|
||||||
n.Label,
|
n.Label,
|
||||||
conf*math.Max(1, dur),
|
conf*math.Max(1, dur),
|
||||||
n.StartSeconds,
|
n.StartSeconds,
|
||||||
@ -1179,6 +1254,28 @@ func ratingLinearGate(value float64, start float64, full float64) float64 {
|
|||||||
return ratingClamp01((value - start) / (full - start))
|
return ratingClamp01((value - start) / (full - start))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ratingExplicitContextBonus(
|
||||||
|
bodyEffectiveWeighted float64,
|
||||||
|
clothingEffectiveWeighted float64,
|
||||||
|
objectEffectiveWeighted float64,
|
||||||
|
videoMinutes float64,
|
||||||
|
) float64 {
|
||||||
|
if videoMinutes <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyDensity := bodyEffectiveWeighted / videoMinutes
|
||||||
|
clothingDensity := clothingEffectiveWeighted / videoMinutes
|
||||||
|
objectDensity := objectEffectiveWeighted / videoMinutes
|
||||||
|
|
||||||
|
bonus :=
|
||||||
|
0.060*ratingSaturatingEvidence(bodyDensity, 1.8) +
|
||||||
|
0.035*ratingSaturatingEvidence(clothingDensity, 1.5) +
|
||||||
|
0.015*ratingSaturatingEvidence(objectDensity, 2.2)
|
||||||
|
|
||||||
|
return math.Min(bonus, 0.10)
|
||||||
|
}
|
||||||
|
|
||||||
func ratingExcellenceBonus(
|
func ratingExcellenceBonus(
|
||||||
peakQuality float64,
|
peakQuality float64,
|
||||||
positionEffectiveWeighted float64,
|
positionEffectiveWeighted float64,
|
||||||
@ -1237,13 +1334,13 @@ func ratingExcellenceBonus(
|
|||||||
|
|
||||||
func starsFromHighlightScore(score float64) int {
|
func starsFromHighlightScore(score float64) int {
|
||||||
switch {
|
switch {
|
||||||
case score < 24:
|
case score < 22:
|
||||||
return 1
|
return 1
|
||||||
case score < 48:
|
case score < 45:
|
||||||
return 2
|
return 2
|
||||||
case score < 72:
|
case score < 68:
|
||||||
return 3
|
return 3
|
||||||
case score < 90:
|
case score < 85:
|
||||||
return 4
|
return 4
|
||||||
default:
|
default:
|
||||||
return 5
|
return 5
|
||||||
@ -1304,10 +1401,28 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
var positionEffectiveWeighted float64
|
var positionEffectiveWeighted float64
|
||||||
var positionFlagged float64
|
var positionFlagged float64
|
||||||
var peakQuality float64
|
var peakQuality float64
|
||||||
|
var qualityDurationSum float64
|
||||||
|
var qualityDurationWeight float64
|
||||||
|
|
||||||
|
var contextFlagged float64
|
||||||
|
var contextWeighted float64
|
||||||
|
var contextEffectiveWeighted float64
|
||||||
|
var contextPeakQuality float64
|
||||||
|
var contextQualityDurationSum float64
|
||||||
|
var contextQualityDurationWeight float64
|
||||||
|
var contextLongest float64
|
||||||
|
var contextConfSum float64
|
||||||
|
var contextConfWeightSum float64
|
||||||
|
var contextN int
|
||||||
|
var bodyEffectiveWeighted float64
|
||||||
|
var clothingEffectiveWeighted float64
|
||||||
|
var objectEffectiveWeighted float64
|
||||||
|
|
||||||
var longest float64
|
var longest float64
|
||||||
var confSum float64
|
var confSum float64
|
||||||
|
var confWeightSum float64
|
||||||
var n int
|
var n int
|
||||||
|
uniquePositions := map[string]bool{}
|
||||||
|
|
||||||
for _, s := range segments {
|
for _, s := range segments {
|
||||||
segDur := s.DurationSeconds
|
segDur := s.DurationSeconds
|
||||||
@ -1341,9 +1456,27 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
effectiveWeightedDur := effectiveDur * quality
|
effectiveWeightedDur := effectiveDur * quality
|
||||||
|
|
||||||
set := ratingSignalSetFromLabel(s.Label)
|
set := ratingSignalSetFromLabel(s.Label)
|
||||||
|
bodyEffectiveWeighted += effectiveDur * set.Body * confWeight
|
||||||
|
clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight
|
||||||
|
objectEffectiveWeighted += effectiveDur * set.Object * confWeight
|
||||||
|
|
||||||
if !set.HasPosition {
|
if !set.HasPosition {
|
||||||
// Nur echte Positions-Segmente fließen ins Rating ein.
|
contextFlagged += segDur
|
||||||
// Nicht-Positions-Segmente dürfen weder Coverage noch Flagged-Zeit erhöhen.
|
contextWeighted += weightedDur
|
||||||
|
contextEffectiveWeighted += effectiveWeightedDur
|
||||||
|
contextQualityDurationSum += quality * effectiveDur
|
||||||
|
contextQualityDurationWeight += effectiveDur
|
||||||
|
contextConfSum += conf * effectiveDur
|
||||||
|
contextConfWeightSum += effectiveDur
|
||||||
|
contextN++
|
||||||
|
|
||||||
|
if quality > contextPeakQuality {
|
||||||
|
contextPeakQuality = quality
|
||||||
|
}
|
||||||
|
if segDur > contextLongest {
|
||||||
|
contextLongest = segDur
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1352,51 +1485,133 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
|
|
||||||
positionEffectiveWeighted += effectiveWeightedDur
|
positionEffectiveWeighted += effectiveWeightedDur
|
||||||
positionFlagged += segDur
|
positionFlagged += segDur
|
||||||
|
qualityDurationSum += quality * effectiveDur
|
||||||
|
qualityDurationWeight += effectiveDur
|
||||||
|
|
||||||
if quality > peakQuality {
|
if quality > peakQuality {
|
||||||
peakQuality = quality
|
peakQuality = quality
|
||||||
}
|
}
|
||||||
|
|
||||||
confSum += conf
|
confSum += conf * effectiveDur
|
||||||
|
confWeightSum += effectiveDur
|
||||||
n++
|
n++
|
||||||
|
|
||||||
if segDur > longest {
|
if segDur > longest {
|
||||||
longest = segDur
|
longest = segDur
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labels := map[string]bool{}
|
||||||
|
addRatingLabelsFromSegment(labels, s.Label)
|
||||||
|
for position := range ratingPositionSetFromLabels(labels) {
|
||||||
|
uniquePositions[position] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username))
|
if contextN == 0 {
|
||||||
|
appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
contextCoverageRatio := ratingClamp01(contextFlagged / durationSec)
|
||||||
|
contextWeightedCoverageRatio := ratingClamp01(contextWeighted / durationSec)
|
||||||
|
contextSegmentsPerMinute := float64(contextN) / videoMinutes
|
||||||
|
contextAvgConfidence := contextConfSum / contextConfWeightSum
|
||||||
|
contextAvgQuality := contextQualityDurationSum / contextQualityDurationWeight
|
||||||
|
contextDensity := contextEffectiveWeighted / videoMinutes
|
||||||
|
|
||||||
|
qualityNorm := ratingSmoothStep(
|
||||||
|
ratingLinearGate(contextAvgQuality, 0.08, 0.55),
|
||||||
|
)
|
||||||
|
peakNorm := ratingSmoothStep(
|
||||||
|
ratingLinearGate(contextPeakQuality, 0.12, 0.62),
|
||||||
|
)
|
||||||
|
durationNorm := ratingSaturatingEvidence(contextFlagged, 150.0)
|
||||||
|
longestNorm := ratingSaturatingEvidence(contextLongest, 60.0)
|
||||||
|
densityNorm := ratingSaturatingEvidence(contextDensity, 2.5)
|
||||||
|
coverageNorm := ratingSaturatingEvidence(contextWeightedCoverageRatio, 0.08)
|
||||||
|
|
||||||
|
raw :=
|
||||||
|
0.28*qualityNorm +
|
||||||
|
0.12*peakNorm +
|
||||||
|
0.25*durationNorm +
|
||||||
|
0.15*longestNorm +
|
||||||
|
0.15*coverageNorm +
|
||||||
|
0.05*densityNorm
|
||||||
|
|
||||||
|
// Context can produce useful 1-3 star ratings, but never the 4-5 star
|
||||||
|
// range reserved for sustained position detections.
|
||||||
|
if contextFlagged < 20.0 {
|
||||||
|
raw = math.Min(raw, 0.44)
|
||||||
|
}
|
||||||
|
raw = math.Min(raw, 0.64)
|
||||||
|
|
||||||
|
score := ratingRound(ratingClamp01(raw)*100, 1)
|
||||||
|
|
||||||
|
r.Score = score
|
||||||
|
r.Stars = starsFromHighlightScore(score)
|
||||||
|
r.Segments = contextN
|
||||||
|
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
|
||||||
|
r.FlaggedSeconds = ratingRound(contextFlagged, 2)
|
||||||
|
r.WeightedFlaggedSeconds = ratingRound(contextWeighted, 2)
|
||||||
|
r.CoverageRatio = ratingRound(contextCoverageRatio, 4)
|
||||||
|
r.WeightedCoverageRatio = ratingRound(contextWeightedCoverageRatio, 4)
|
||||||
|
r.LongestSegmentSeconds = ratingRound(contextLongest, 2)
|
||||||
|
r.AvgConfidence = ratingRound(contextAvgConfidence, 3)
|
||||||
|
|
||||||
|
appLogf(
|
||||||
|
"✅ %s context rating score=%.1f stars=%d segs=%d flagged=%.1f density=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
||||||
|
ratingLogSubject(username),
|
||||||
|
r.Score,
|
||||||
|
r.Stars,
|
||||||
|
r.Segments,
|
||||||
|
r.FlaggedSeconds,
|
||||||
|
contextDensity,
|
||||||
|
r.WeightedCoverageRatio,
|
||||||
|
r.LongestSegmentSeconds,
|
||||||
|
r.AvgConfidence,
|
||||||
|
)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
coverageRatio := ratingClamp01(totalFlagged / durationSec)
|
coverageRatio := ratingClamp01(totalFlagged / durationSec)
|
||||||
weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec)
|
weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec)
|
||||||
segmentsPerMinute := float64(n) / videoMinutes
|
segmentsPerMinute := float64(n) / videoMinutes
|
||||||
avgConfidence := confSum / float64(n)
|
avgConfidence := confSum / confWeightSum
|
||||||
|
avgPositionQuality := qualityDurationSum / qualityDurationWeight
|
||||||
|
|
||||||
positionDensity := positionEffectiveWeighted / videoMinutes
|
positionDensity := positionEffectiveWeighted / videoMinutes
|
||||||
|
|
||||||
peakNorm := ratingSmoothStep(peakQuality)
|
avgQualityNorm := ratingSmoothStep(
|
||||||
positionDensityNorm := ratingSoftCap(positionDensity, 5.5)
|
ratingLinearGate(avgPositionQuality, 0.45, 0.95),
|
||||||
coverageNorm := ratingSoftCap(weightedCoverageRatio, 0.20)
|
)
|
||||||
longestNorm := ratingSoftCap(longest, 24.0)
|
peakNorm := ratingSmoothStep(
|
||||||
positionDurationNorm := ratingSoftCap(positionFlagged, 45.0)
|
ratingLinearGate(peakQuality, 0.42, 0.95),
|
||||||
confNorm := ratingSmoothStep((avgConfidence - 0.30) / 0.65)
|
)
|
||||||
|
positionDurationNorm := ratingSaturatingEvidence(positionFlagged, 65.0)
|
||||||
|
longestNorm := ratingSaturatingEvidence(longest, 35.0)
|
||||||
|
positionDensityNorm := ratingSaturatingEvidence(positionDensity, 3.2)
|
||||||
|
coverageNorm := ratingSaturatingEvidence(weightedCoverageRatio, 0.075)
|
||||||
|
positionVarietyNorm := ratingLinearGate(float64(len(uniquePositions)), 1, 4)
|
||||||
|
|
||||||
// Positionen dominieren. Kleidung/Objekte fließen durch Combo-Segment-Qualität (peakNorm) ein.
|
// Position quality is the primary signal. Actual position duration and the
|
||||||
// Die Länge der erkannten Positionen wird bewusst stark gewichtet:
|
// longest continuous segment decide whether a detection is brief or strong.
|
||||||
// längstes Positions-Segment (longestNorm) + Gesamtdauer aller Positionen (positionDurationNorm).
|
|
||||||
// Je länger eine Position erkannt wird, desto höher der Score.
|
|
||||||
// Keine Penalties, keine Caps – die Formel bewertet natürlich nach Positions-Intensität.
|
|
||||||
// Summe = 1.00.
|
|
||||||
raw :=
|
raw :=
|
||||||
0.34*positionDensityNorm +
|
0.30*avgQualityNorm +
|
||||||
0.22*peakNorm +
|
0.10*peakNorm +
|
||||||
0.13*coverageNorm +
|
0.26*positionDurationNorm +
|
||||||
0.16*longestNorm +
|
0.16*longestNorm +
|
||||||
0.11*positionDurationNorm +
|
0.10*positionDensityNorm +
|
||||||
0.04*confNorm
|
0.06*coverageNorm +
|
||||||
|
0.02*positionVarietyNorm
|
||||||
|
|
||||||
|
explicitContextBonus := ratingExplicitContextBonus(
|
||||||
|
bodyEffectiveWeighted,
|
||||||
|
clothingEffectiveWeighted,
|
||||||
|
objectEffectiveWeighted,
|
||||||
|
videoMinutes,
|
||||||
|
)
|
||||||
|
|
||||||
// Sehr wenig Material nicht überbewerten.
|
// Sehr wenig Material nicht überbewerten.
|
||||||
if totalFlagged < 5.0 && n <= 1 {
|
if totalFlagged < 5.0 && n <= 1 {
|
||||||
@ -1415,7 +1630,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
n,
|
n,
|
||||||
)
|
)
|
||||||
|
|
||||||
raw = ratingClamp01(raw + excellenceBonus)
|
raw = ratingClamp01(raw + explicitContextBonus + excellenceBonus)
|
||||||
|
|
||||||
score := ratingRound(raw*100, 1)
|
score := ratingRound(raw*100, 1)
|
||||||
|
|
||||||
@ -1431,10 +1646,11 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
r.AvgConfidence = ratingRound(avgConfidence, 3)
|
r.AvgConfidence = ratingRound(avgConfidence, 3)
|
||||||
|
|
||||||
appLogf(
|
appLogf(
|
||||||
"✅ %s rating score=%.1f stars=%d bonus=%.1f segs=%d flagged=%.1f posFlagged=%.1f posDensity=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
"✅ %s rating score=%.1f stars=%d contextBonus=%.1f excellenceBonus=%.1f segs=%d flagged=%.1f posFlagged=%.1f posDensity=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
||||||
ratingLogSubject(username),
|
ratingLogSubject(username),
|
||||||
r.Score,
|
r.Score,
|
||||||
r.Stars,
|
r.Stars,
|
||||||
|
ratingRound(explicitContextBonus*100, 1),
|
||||||
ratingRound(excellenceBonus*100, 1),
|
ratingRound(excellenceBonus*100, 1),
|
||||||
r.Segments,
|
r.Segments,
|
||||||
r.FlaggedSeconds,
|
r.FlaggedSeconds,
|
||||||
|
|||||||
271
backend/rating_test.go
Normal file
271
backend/rating_test.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ratingTestSegment(label string, start, duration, confidence float64) aiSegmentMeta {
|
||||||
|
return aiSegmentMeta{
|
||||||
|
Label: label,
|
||||||
|
StartSeconds: start,
|
||||||
|
EndSeconds: start + duration,
|
||||||
|
DurationSeconds: duration,
|
||||||
|
Score: confidence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingSpansAllStars(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
segments []aiSegmentMeta
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "one weak standing detection",
|
||||||
|
segments: []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:standing", 10, 6, 0.45),
|
||||||
|
},
|
||||||
|
want: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one brief strong position",
|
||||||
|
segments: []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 8, 0.72),
|
||||||
|
},
|
||||||
|
want: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two solid position segments",
|
||||||
|
segments: []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 20, 18, 0.78),
|
||||||
|
ratingTestSegment("position:cowgirl", 120, 18, 0.78),
|
||||||
|
},
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "several long high quality positions",
|
||||||
|
segments: []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 20, 25, 0.82),
|
||||||
|
ratingTestSegment("position:cowgirl", 120, 25, 0.82),
|
||||||
|
ratingTestSegment("position:missionary", 220, 25, 0.82),
|
||||||
|
ratingTestSegment("position:blowjob", 320, 25, 0.82),
|
||||||
|
},
|
||||||
|
want: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exceptional sustained and varied positions",
|
||||||
|
segments: []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 40, 0.90),
|
||||||
|
ratingTestSegment("position:cowgirl", 100, 40, 0.90),
|
||||||
|
ratingTestSegment("position:missionary", 190, 40, 0.90),
|
||||||
|
ratingTestSegment("position:blowjob", 280, 40, 0.90),
|
||||||
|
ratingTestSegment("position:cunnilingus", 370, 40, 0.90),
|
||||||
|
ratingTestSegment("position:reverse_cowgirl", 460, 40, 0.90),
|
||||||
|
},
|
||||||
|
want: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := computeHighlightRating(tt.segments, 600)
|
||||||
|
if got.Stars != tt.want {
|
||||||
|
t.Fatalf("stars = %d, want %d (score %.1f)", got.Stars, tt.want, got.Score)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) {
|
||||||
|
short := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 8, 0.80),
|
||||||
|
}, 600)
|
||||||
|
medium := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 35, 0.80),
|
||||||
|
}, 600)
|
||||||
|
long := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 120, 0.80),
|
||||||
|
}, 600)
|
||||||
|
|
||||||
|
if !(short.Score < medium.Score && medium.Score < long.Score) {
|
||||||
|
t.Fatalf(
|
||||||
|
"position duration should increase score: short=%.1f medium=%.1f long=%.1f",
|
||||||
|
short.Score,
|
||||||
|
medium.Score,
|
||||||
|
long.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
|
||||||
|
clothingOnly := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("clothing:lingerie", 10, 60, 0.90),
|
||||||
|
ratingTestSegment("clothing:lingerie", 120, 60, 0.90),
|
||||||
|
ratingTestSegment("clothing:lingerie", 230, 60, 0.90),
|
||||||
|
}, 600)
|
||||||
|
richContext := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("combo:body:breasts+clothing:lingerie", 10, 45, 0.90),
|
||||||
|
ratingTestSegment("combo:object:vibrator+clothing:lingerie", 120, 60, 0.90),
|
||||||
|
ratingTestSegment("combo:body:penis+object:dildo", 240, 45, 0.90),
|
||||||
|
}, 600)
|
||||||
|
|
||||||
|
if clothingOnly.Stars != 2 {
|
||||||
|
t.Fatalf(
|
||||||
|
"clothing-only stars = %d, want 2 (score %.1f)",
|
||||||
|
clothingOnly.Stars,
|
||||||
|
clothingOnly.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if richContext.Stars != 3 {
|
||||||
|
t.Fatalf(
|
||||||
|
"rich context stars = %d, want 3 (score %.1f)",
|
||||||
|
richContext.Stars,
|
||||||
|
richContext.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if richContext.Stars > 3 {
|
||||||
|
t.Fatalf("context rating must stay below 4 stars: %#v", richContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingPositionsOutrankContext(t *testing.T) {
|
||||||
|
context := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("combo:body:breasts+clothing:lingerie+object:vibrator", 10, 240, 0.95),
|
||||||
|
}, 600)
|
||||||
|
positions := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 40, 0.90),
|
||||||
|
ratingTestSegment("position:cowgirl", 100, 40, 0.90),
|
||||||
|
ratingTestSegment("position:missionary", 190, 40, 0.90),
|
||||||
|
ratingTestSegment("position:blowjob", 280, 40, 0.90),
|
||||||
|
}, 600)
|
||||||
|
|
||||||
|
if positions.Score <= context.Score || positions.Stars <= context.Stars {
|
||||||
|
t.Fatalf(
|
||||||
|
"positions should outrank context: positions=%.1f/%d context=%.1f/%d",
|
||||||
|
positions.Score,
|
||||||
|
positions.Stars,
|
||||||
|
context.Score,
|
||||||
|
context.Stars,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingExplicitSignalsBoostPositions(t *testing.T) {
|
||||||
|
positionSegments := []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 20, 18, 0.78),
|
||||||
|
ratingTestSegment("position:cowgirl", 120, 18, 0.78),
|
||||||
|
}
|
||||||
|
|
||||||
|
base := computeHighlightRating(positionSegments, 600)
|
||||||
|
withObject := computeHighlightRating(append(
|
||||||
|
append([]aiSegmentMeta{}, positionSegments...),
|
||||||
|
ratingTestSegment("object:dildo", 220, 60, 0.90),
|
||||||
|
), 600)
|
||||||
|
withClothing := computeHighlightRating(append(
|
||||||
|
append([]aiSegmentMeta{}, positionSegments...),
|
||||||
|
ratingTestSegment("clothing:lingerie", 220, 60, 0.90),
|
||||||
|
), 600)
|
||||||
|
withBody := computeHighlightRating(append(
|
||||||
|
append([]aiSegmentMeta{}, positionSegments...),
|
||||||
|
ratingTestSegment("body:pussy", 220, 60, 0.90),
|
||||||
|
), 600)
|
||||||
|
|
||||||
|
if !(base.Score < withObject.Score &&
|
||||||
|
withObject.Score < withClothing.Score &&
|
||||||
|
withClothing.Score < withBody.Score) {
|
||||||
|
t.Fatalf(
|
||||||
|
"explicit signal priority is wrong: base=%.1f object=%.1f clothing=%.1f body=%.1f",
|
||||||
|
base.Score,
|
||||||
|
withObject.Score,
|
||||||
|
withClothing.Score,
|
||||||
|
withBody.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingExplicitContextCanElevateStrongPositions(t *testing.T) {
|
||||||
|
positions := []aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 20, 25, 0.82),
|
||||||
|
ratingTestSegment("position:cowgirl", 120, 25, 0.82),
|
||||||
|
ratingTestSegment("position:missionary", 220, 25, 0.82),
|
||||||
|
ratingTestSegment("position:blowjob", 320, 25, 0.82),
|
||||||
|
}
|
||||||
|
positionOnly := computeHighlightRating(positions, 600)
|
||||||
|
withExplicitContext := computeHighlightRating(append(
|
||||||
|
append([]aiSegmentMeta{}, positions...),
|
||||||
|
ratingTestSegment("body:pussy", 400, 90, 0.92),
|
||||||
|
ratingTestSegment("clothing:lingerie", 500, 90, 0.92),
|
||||||
|
), 600)
|
||||||
|
|
||||||
|
if positionOnly.Stars != 4 {
|
||||||
|
t.Fatalf("position-only stars = %d, want 4 (score %.1f)", positionOnly.Stars, positionOnly.Score)
|
||||||
|
}
|
||||||
|
if withExplicitContext.Stars != 5 {
|
||||||
|
t.Fatalf(
|
||||||
|
"positions with explicit context stars = %d, want 5 (score %.1f)",
|
||||||
|
withExplicitContext.Stars,
|
||||||
|
withExplicitContext.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingLongMixedContextRegression(t *testing.T) {
|
||||||
|
segments := []aiSegmentMeta{
|
||||||
|
ratingTestSegment("object:dildo", 24.5, 5, 0.792783260345459),
|
||||||
|
ratingTestSegment("object:dildo", 43.5, 8, 0.923585057258606),
|
||||||
|
ratingTestSegment("clothing:lingerie", 94.5, 21, 0.9069388088058022),
|
||||||
|
ratingTestSegment("clothing:lingerie", 120.5, 33, 0.887133002281189),
|
||||||
|
ratingTestSegment("object:vibrator", 215.5, 6, 0.40453797578811646),
|
||||||
|
ratingTestSegment("combo:body:breasts+clothing:lingerie", 298.5, 8, 0.6898824721574783),
|
||||||
|
ratingTestSegment("object:vibrator", 330.5, 5, 0.6836381554603577),
|
||||||
|
ratingTestSegment("clothing:lingerie", 348.5, 59, 0.9197490215301514),
|
||||||
|
ratingTestSegment("combo:object:vibrator+object:collar+clothing:lingerie", 416.5, 36, 0.8156423893044976),
|
||||||
|
ratingTestSegment("clothing:lingerie", 465.5, 36, 0.8007044196128845),
|
||||||
|
}
|
||||||
|
|
||||||
|
got := computeHighlightRating(segments, 504)
|
||||||
|
if got.Stars != 3 {
|
||||||
|
t.Fatalf("stars = %d, want 3 (score %.1f)", got.Stars, got.Score)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.Marshal(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("rating=%s", encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareAIRatingSegmentsKeepsDifferentPositionsSeparate(t *testing.T) {
|
||||||
|
segments := prepareAIRatingSegments([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 12, 0.85),
|
||||||
|
ratingTestSegment("position:cowgirl", 23, 12, 0.85),
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(segments) != 2 {
|
||||||
|
t.Fatalf("segments = %d, want 2: %#v", len(segments), segments)
|
||||||
|
}
|
||||||
|
if segments[0].Position == segments[1].Position {
|
||||||
|
t.Fatalf("different positions were collapsed: %#v", segments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareAIRatingSegmentsMergesSamePositionAcrossShortGap(t *testing.T) {
|
||||||
|
segments := prepareAIRatingSegments([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 12, 0.85),
|
||||||
|
ratingTestSegment("position:doggy", 23, 12, 0.85),
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(segments) != 1 {
|
||||||
|
t.Fatalf("segments = %d, want 1: %#v", len(segments), segments)
|
||||||
|
}
|
||||||
|
if segments[0].Position != "doggy" {
|
||||||
|
t.Fatalf("position = %q, want doggy", segments[0].Position)
|
||||||
|
}
|
||||||
|
if segments[0].DurationSeconds != 24 {
|
||||||
|
t.Fatalf(
|
||||||
|
"duration = %.1f, want 24 seconds of actual position evidence",
|
||||||
|
segments[0].DurationSeconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user