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)
|
||||
}
|
||||
|
||||
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
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