fixed rating

This commit is contained in:
Linrador 2026-06-13 23:03:53 +02:00
parent 33657733d0
commit c6e518105b
2 changed files with 535 additions and 48 deletions

View File

@ -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
View 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,
)
}
}