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)
}
func ratingSoftCap(value, knee float64) float64 {
if value <= 0 || knee <= 0 {
func ratingSaturatingEvidence(value, scale float64) float64 {
if value <= 0 || scale <= 0 {
return 0
}
return value / (value + knee)
return ratingClamp01(1 - math.Exp(-value/scale))
}
func ratingEffectiveDurationSeconds(seconds float64) float64 {
@ -460,25 +460,25 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
if set.HasPosition {
// 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 =
0.82*set.Position +
0.08*set.Body +
0.05*set.Clothing +
0.03*set.Object +
0.02*set.Person
0.12*set.Body +
0.08*set.Clothing +
0.015*set.Object +
0.005*set.Person
if set.HasBody {
score += 0.020
score += 0.030
}
if set.HasClothing {
score += 0.015
score += 0.020
}
if set.HasObject {
score += 0.010
score += 0.006
}
if set.HasPerson {
score += 0.005
score += 0.002
}
// Standing ohne echten Kontext klar schwächer halten.
@ -829,6 +829,7 @@ func mergeRatingActivitySegments(
labels map[string]bool
positionWeights map[string]float64
positionBounds map[string]segmentBounds
positionRanges map[string][]segmentBounds
marker float64
markerWeight float64
}
@ -836,6 +837,7 @@ func mergeRatingActivitySegments(
var addRatingPositionSignal func(
weights map[string]float64,
bounds map[string]segmentBounds,
ranges map[string][]segmentBounds,
label string,
weight float64,
start float64,
@ -845,6 +847,7 @@ func mergeRatingActivitySegments(
addRatingPositionSignal = func(
weights map[string]float64,
bounds map[string]segmentBounds,
ranges map[string][]segmentBounds,
label string,
weight float64,
start float64,
@ -858,7 +861,7 @@ func mergeRatingActivitySegments(
if strings.HasPrefix(label, "combo:") {
raw := strings.TrimPrefix(label, "combo:")
for _, part := range strings.Split(raw, "+") {
addRatingPositionSignal(weights, bounds, part, weight, start, end)
addRatingPositionSignal(weights, bounds, ranges, part, weight, start, end)
}
return
}
@ -891,6 +894,13 @@ func mergeRatingActivitySegments(
}
bounds[normalized] = b
if end > start {
ranges[normalized] = append(ranges[normalized], segmentBounds{
start: start,
end: end,
ok: true,
})
}
}
effectiveRatingLabelsForBlock := func(
@ -971,10 +981,12 @@ func mergeRatingActivitySegments(
positionWeights := map[string]float64{}
positionBounds := map[string]segmentBounds{}
positionRanges := map[string][]segmentBounds{}
addRatingPositionSignal(
positionWeights,
positionBounds,
positionRanges,
s.Label,
conf*math.Max(1, dur),
s.StartSeconds,
@ -992,11 +1004,60 @@ func mergeRatingActivitySegments(
labels: labels,
positionWeights: positionWeights,
positionBounds: positionBounds,
positionRanges: positionRanges,
marker: marker,
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) {
dur := b.end - b.start
if dur <= 0 {
@ -1037,8 +1098,19 @@ func mergeRatingActivitySegments(
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.
if dur < minDurationSec && sev < 0.72 {
if ratingDuration < minDurationSec && sev < 0.72 {
return aiSegmentMeta{}, false
}
@ -1060,7 +1132,7 @@ func mergeRatingActivitySegments(
Score: ratingClamp01(score),
StartSeconds: start,
EndSeconds: end,
DurationSeconds: dur,
DurationSeconds: ratingDuration,
AutoSelected: true,
Position: segmentPositionFromAnalyzeLabel(label),
Tags: segmentTagsFromAnalyzeLabel(label),
@ -1076,13 +1148,15 @@ func mergeRatingActivitySegments(
n := valid[i]
gap := n.StartSeconds - cur.end
shouldBridge := gap <= maxSilentGapSec
if !shouldBridge && gap <= ratingBridgeStrongGapSec {
nextLabels := map[string]bool{}
addRatingLabelsFromSegment(nextLabels, n.Label)
positionConflict := ratingPositionSetsConflict(cur.labels, nextLabels)
// A short gap may connect supporting signals or the same position, but
// must not turn consecutive different positions into one long segment.
shouldBridge := gap <= maxSilentGapSec && !positionConflict
if !shouldBridge && !positionConflict && gap <= ratingBridgeStrongGapSec {
curSet := ratingSignalSetFromLabels(cur.labels)
nextSet := ratingSignalSetFromLabels(nextLabels)
@ -1128,6 +1202,7 @@ func mergeRatingActivitySegments(
addRatingPositionSignal(
cur.positionWeights,
cur.positionBounds,
cur.positionRanges,
n.Label,
conf*math.Max(1, dur),
n.StartSeconds,
@ -1179,6 +1254,28 @@ func ratingLinearGate(value float64, start float64, full float64) float64 {
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(
peakQuality float64,
positionEffectiveWeighted float64,
@ -1237,13 +1334,13 @@ func ratingExcellenceBonus(
func starsFromHighlightScore(score float64) int {
switch {
case score < 24:
case score < 22:
return 1
case score < 48:
case score < 45:
return 2
case score < 72:
case score < 68:
return 3
case score < 90:
case score < 85:
return 4
default:
return 5
@ -1304,10 +1401,28 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
var positionEffectiveWeighted float64
var positionFlagged 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 confSum float64
var confWeightSum float64
var n int
uniquePositions := map[string]bool{}
for _, s := range segments {
segDur := s.DurationSeconds
@ -1341,9 +1456,27 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
effectiveWeightedDur := effectiveDur * quality
set := ratingSignalSetFromLabel(s.Label)
bodyEffectiveWeighted += effectiveDur * set.Body * confWeight
clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight
objectEffectiveWeighted += effectiveDur * set.Object * confWeight
if !set.HasPosition {
// Nur echte Positions-Segmente fließen ins Rating ein.
// Nicht-Positions-Segmente dürfen weder Coverage noch Flagged-Zeit erhöhen.
contextFlagged += segDur
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
}
@ -1352,51 +1485,133 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
positionEffectiveWeighted += effectiveWeightedDur
positionFlagged += segDur
qualityDurationSum += quality * effectiveDur
qualityDurationWeight += effectiveDur
if quality > peakQuality {
peakQuality = quality
}
confSum += conf
confSum += conf * effectiveDur
confWeightSum += effectiveDur
n++
if segDur > longest {
longest = segDur
}
labels := map[string]bool{}
addRatingLabelsFromSegment(labels, s.Label)
for position := range ratingPositionSetFromLabels(labels) {
uniquePositions[position] = true
}
}
if n == 0 {
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
}
coverageRatio := ratingClamp01(totalFlagged / durationSec)
weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec)
segmentsPerMinute := float64(n) / videoMinutes
avgConfidence := confSum / float64(n)
avgConfidence := confSum / confWeightSum
avgPositionQuality := qualityDurationSum / qualityDurationWeight
positionDensity := positionEffectiveWeighted / videoMinutes
peakNorm := ratingSmoothStep(peakQuality)
positionDensityNorm := ratingSoftCap(positionDensity, 5.5)
coverageNorm := ratingSoftCap(weightedCoverageRatio, 0.20)
longestNorm := ratingSoftCap(longest, 24.0)
positionDurationNorm := ratingSoftCap(positionFlagged, 45.0)
confNorm := ratingSmoothStep((avgConfidence - 0.30) / 0.65)
avgQualityNorm := ratingSmoothStep(
ratingLinearGate(avgPositionQuality, 0.45, 0.95),
)
peakNorm := ratingSmoothStep(
ratingLinearGate(peakQuality, 0.42, 0.95),
)
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.
// Die Länge der erkannten Positionen wird bewusst stark gewichtet:
// 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.
// Position quality is the primary signal. Actual position duration and the
// longest continuous segment decide whether a detection is brief or strong.
raw :=
0.34*positionDensityNorm +
0.22*peakNorm +
0.13*coverageNorm +
0.30*avgQualityNorm +
0.10*peakNorm +
0.26*positionDurationNorm +
0.16*longestNorm +
0.11*positionDurationNorm +
0.04*confNorm
0.10*positionDensityNorm +
0.06*coverageNorm +
0.02*positionVarietyNorm
explicitContextBonus := ratingExplicitContextBonus(
bodyEffectiveWeighted,
clothingEffectiveWeighted,
objectEffectiveWeighted,
videoMinutes,
)
// Sehr wenig Material nicht überbewerten.
if totalFlagged < 5.0 && n <= 1 {
@ -1415,7 +1630,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
n,
)
raw = ratingClamp01(raw + excellenceBonus)
raw = ratingClamp01(raw + explicitContextBonus + excellenceBonus)
score := ratingRound(raw*100, 1)
@ -1431,10 +1646,11 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
r.AvgConfidence = ratingRound(avgConfidence, 3)
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),
r.Score,
r.Stars,
ratingRound(explicitContextBonus*100, 1),
ratingRound(excellenceBonus*100, 1),
r.Segments,
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,
)
}
}