updated
This commit is contained in:
parent
9f3ae43f8a
commit
56b63223f9
@ -11,6 +11,17 @@ import (
|
|||||||
type aiRatingMeta struct {
|
type aiRatingMeta struct {
|
||||||
Score float64 `json:"score"`
|
Score float64 `json:"score"`
|
||||||
ActionScore float64 `json:"actionScore"`
|
ActionScore float64 `json:"actionScore"`
|
||||||
|
BestPosition string `json:"bestPosition,omitempty"`
|
||||||
|
BestPositionRank int `json:"bestPositionRank,omitempty"`
|
||||||
|
BestClothing string `json:"bestClothing,omitempty"`
|
||||||
|
BestClothingRank int `json:"bestClothingRank,omitempty"`
|
||||||
|
PositionScore float64 `json:"positionScore"`
|
||||||
|
ClothingScore float64 `json:"clothingScore"`
|
||||||
|
DurationScore float64 `json:"durationScore"`
|
||||||
|
StrengthScore float64 `json:"strengthScore"`
|
||||||
|
CoverageScore float64 `json:"coverageScore"`
|
||||||
|
ContextScore float64 `json:"contextScore"`
|
||||||
|
VarietyScore float64 `json:"varietyScore"`
|
||||||
Stars int `json:"stars"`
|
Stars int `json:"stars"`
|
||||||
Segments int `json:"segments"`
|
Segments int `json:"segments"`
|
||||||
SegmentsPerMinute float64 `json:"segmentsPerMinute"`
|
SegmentsPerMinute float64 `json:"segmentsPerMinute"`
|
||||||
@ -136,31 +147,46 @@ func isKnownPositionLabel(label string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func positionSeverityWeight(label string) float64 {
|
type explicitRatingRank struct {
|
||||||
|
Rank int
|
||||||
|
Weight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func positionExplicitRatingRank(label string) explicitRatingRank {
|
||||||
label = strings.ToLower(strings.TrimSpace(label))
|
label = strings.ToLower(strings.TrimSpace(label))
|
||||||
|
|
||||||
switch label {
|
switch label {
|
||||||
|
case "cowgirl":
|
||||||
|
return explicitRatingRank{Rank: 1, Weight: 1.00}
|
||||||
|
case "reverse_cowgirl":
|
||||||
|
return explicitRatingRank{Rank: 2, Weight: 0.99}
|
||||||
case "doggy", "doggystyle", "standing_doggy":
|
case "doggy", "doggystyle", "standing_doggy":
|
||||||
return 1.00
|
return explicitRatingRank{Rank: 3, Weight: 0.98}
|
||||||
case "cowgirl", "reverse_cowgirl":
|
|
||||||
return 0.98
|
|
||||||
case "missionary", "prone_bone":
|
|
||||||
return 0.95
|
|
||||||
case "blowjob", "cunnilingus", "69", "facesitting", "boobjob":
|
|
||||||
return 0.94
|
|
||||||
case "toy_play":
|
case "toy_play":
|
||||||
return 0.88
|
return explicitRatingRank{Rank: 4, Weight: 0.96}
|
||||||
case "handjob", "fingering":
|
case "fingering":
|
||||||
return 0.84
|
return explicitRatingRank{Rank: 5, Weight: 0.94}
|
||||||
|
case "missionary":
|
||||||
|
return explicitRatingRank{Rank: 6, Weight: 0.92}
|
||||||
|
case "prone_bone":
|
||||||
|
return explicitRatingRank{Rank: 7, Weight: 0.90}
|
||||||
case "spooning":
|
case "spooning":
|
||||||
return 0.78
|
return explicitRatingRank{Rank: 8, Weight: 0.86}
|
||||||
case "standing":
|
case "standing":
|
||||||
return 0.42
|
return explicitRatingRank{Rank: 9, Weight: 0.56}
|
||||||
|
case "69", "facesitting":
|
||||||
|
return explicitRatingRank{Rank: 10, Weight: 0.54}
|
||||||
|
case "blowjob", "handjob", "cunnilingus", "boobjob":
|
||||||
|
return explicitRatingRank{Rank: 11, Weight: 0.52}
|
||||||
default:
|
default:
|
||||||
return 0.00
|
return explicitRatingRank{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func positionSeverityWeight(label string) float64 {
|
||||||
|
return positionExplicitRatingRank(label).Weight
|
||||||
|
}
|
||||||
|
|
||||||
func bodyPartSeverityWeight(label string) float64 {
|
func bodyPartSeverityWeight(label string) float64 {
|
||||||
label = strings.ToLower(strings.TrimSpace(label))
|
label = strings.ToLower(strings.TrimSpace(label))
|
||||||
|
|
||||||
@ -199,25 +225,39 @@ func objectSeverityWeight(label string) float64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clothingSeverityWeight(label string) float64 {
|
func clothingExplicitRatingRank(label string) explicitRatingRank {
|
||||||
label = strings.ToLower(strings.TrimSpace(label))
|
label = strings.ToLower(strings.TrimSpace(label))
|
||||||
|
|
||||||
switch label {
|
switch label {
|
||||||
case "lingerie":
|
case "lingerie":
|
||||||
return 0.50
|
return explicitRatingRank{Rank: 1, Weight: 0.62}
|
||||||
case "panties", "bra":
|
case "stockings":
|
||||||
return 0.44
|
return explicitRatingRank{Rank: 2, Weight: 0.57}
|
||||||
|
case "skirt":
|
||||||
|
return explicitRatingRank{Rank: 3, Weight: 0.52}
|
||||||
|
case "hotpants":
|
||||||
|
return explicitRatingRank{Rank: 4, Weight: 0.47}
|
||||||
case "bikini":
|
case "bikini":
|
||||||
return 0.30
|
return explicitRatingRank{Rank: 5, Weight: 0.44}
|
||||||
case "stockings", "heels":
|
case "bra":
|
||||||
return 0.28
|
return explicitRatingRank{Rank: 6, Weight: 0.41}
|
||||||
case "skirt", "dress", "hotpants", "croptop":
|
case "panties":
|
||||||
return 0.22
|
return explicitRatingRank{Rank: 7, Weight: 0.36}
|
||||||
|
case "croptop":
|
||||||
|
return explicitRatingRank{Rank: 8, Weight: 0.31}
|
||||||
|
case "heels":
|
||||||
|
return explicitRatingRank{Rank: 9, Weight: 0.26}
|
||||||
|
case "dress":
|
||||||
|
return explicitRatingRank{Rank: 10, Weight: 0.22}
|
||||||
default:
|
default:
|
||||||
return 0.00
|
return explicitRatingRank{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clothingSeverityWeight(label string) float64 {
|
||||||
|
return clothingExplicitRatingRank(label).Weight
|
||||||
|
}
|
||||||
|
|
||||||
func personContextWeight(label string) float64 {
|
func personContextWeight(label string) float64 {
|
||||||
if !isPersonSegmentLabel(label) {
|
if !isPersonSegmentLabel(label) {
|
||||||
return 0
|
return 0
|
||||||
@ -437,26 +477,6 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelParts []string
|
|
||||||
|
|
||||||
if set.HasPosition {
|
|
||||||
labelParts = append(labelParts, "position:x")
|
|
||||||
}
|
|
||||||
if set.HasBody {
|
|
||||||
labelParts = append(labelParts, "body:x")
|
|
||||||
}
|
|
||||||
if set.HasObject {
|
|
||||||
labelParts = append(labelParts, "object:x")
|
|
||||||
}
|
|
||||||
if set.HasClothing {
|
|
||||||
labelParts = append(labelParts, "clothing:x")
|
|
||||||
}
|
|
||||||
if set.HasPerson {
|
|
||||||
labelParts = append(labelParts, "person:x")
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = labelParts
|
|
||||||
|
|
||||||
var score float64
|
var score float64
|
||||||
|
|
||||||
if set.HasPosition {
|
if set.HasPosition {
|
||||||
@ -484,7 +504,9 @@ func contextualSegmentSeverityWeightFromSet(set ratingSignalSet) float64 {
|
|||||||
|
|
||||||
// Standing ohne echten Kontext klar schwächer halten.
|
// Standing ohne echten Kontext klar schwächer halten.
|
||||||
if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing {
|
if set.Position < 0.60 && !set.HasBody && !set.HasObject && !set.HasClothing {
|
||||||
score = math.Min(score, 0.32)
|
// Den Rang innerhalb der schwachen Positionen erhalten, statt
|
||||||
|
// Standing, 69 und orale Positionen auf denselben Wert zu kappen.
|
||||||
|
score = math.Min(score, 0.10+0.40*set.Position)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ratingClamp01(score)
|
return ratingClamp01(score)
|
||||||
@ -655,6 +677,37 @@ func addRatingLabelsFromSegment(labels map[string]bool, label string) {
|
|||||||
labels[normalized] = true
|
labels[normalized] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateBestExplicitRatingRanks(
|
||||||
|
label string,
|
||||||
|
bestPosition *string,
|
||||||
|
bestPositionRank *int,
|
||||||
|
bestClothing *string,
|
||||||
|
bestClothingRank *int,
|
||||||
|
) {
|
||||||
|
labels := map[string]bool{}
|
||||||
|
addRatingLabelsFromSegment(labels, label)
|
||||||
|
|
||||||
|
for normalized := range labels {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(normalized, "position:"):
|
||||||
|
raw := strings.TrimPrefix(normalized, "position:")
|
||||||
|
ranked := positionExplicitRatingRank(raw)
|
||||||
|
if ranked.Rank > 0 && (*bestPositionRank == 0 || ranked.Rank < *bestPositionRank) {
|
||||||
|
*bestPosition = raw
|
||||||
|
*bestPositionRank = ranked.Rank
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.HasPrefix(normalized, "clothing:"):
|
||||||
|
raw := strings.TrimPrefix(normalized, "clothing:")
|
||||||
|
ranked := clothingExplicitRatingRank(raw)
|
||||||
|
if ranked.Rank > 0 && (*bestClothingRank == 0 || ranked.Rank < *bestClothingRank) {
|
||||||
|
*bestClothing = raw
|
||||||
|
*bestClothingRank = ranked.Rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ratingSignalSetFromLabels(labels map[string]bool) ratingSignalSet {
|
func ratingSignalSetFromLabels(labels map[string]bool) ratingSignalSet {
|
||||||
var set ratingSignalSet
|
var set ratingSignalSet
|
||||||
|
|
||||||
@ -1256,119 +1309,103 @@ func ratingLinearGate(value float64, start float64, full float64) float64 {
|
|||||||
return ratingClamp01((value - start) / (full - start))
|
return ratingClamp01((value - start) / (full - start))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingExplicitContextBonus(
|
type ratingTimeRange struct {
|
||||||
bodyEffectiveWeighted float64,
|
Start float64
|
||||||
clothingEffectiveWeighted float64,
|
End float64
|
||||||
objectEffectiveWeighted float64,
|
}
|
||||||
videoMinutes float64,
|
|
||||||
) float64 {
|
func ratingCoveredSeconds(ranges []ratingTimeRange, durationSec float64) float64 {
|
||||||
if videoMinutes <= 0 {
|
if len(ranges) == 0 || durationSec <= 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyDensity := bodyEffectiveWeighted / videoMinutes
|
valid := make([]ratingTimeRange, 0, len(ranges))
|
||||||
clothingDensity := clothingEffectiveWeighted / videoMinutes
|
for _, item := range ranges {
|
||||||
objectDensity := objectEffectiveWeighted / videoMinutes
|
start := math.Max(0, math.Min(item.Start, durationSec))
|
||||||
|
end := math.Max(0, math.Min(item.End, durationSec))
|
||||||
|
if end > start {
|
||||||
|
valid = append(valid, ratingTimeRange{Start: start, End: end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(valid) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
bonus :=
|
sort.SliceStable(valid, func(i, j int) bool {
|
||||||
0.060*ratingSaturatingEvidence(bodyDensity, 1.8) +
|
if valid[i].Start != valid[j].Start {
|
||||||
0.035*ratingSaturatingEvidence(clothingDensity, 1.5) +
|
return valid[i].Start < valid[j].Start
|
||||||
0.015*ratingSaturatingEvidence(objectDensity, 2.2)
|
}
|
||||||
|
return valid[i].End < valid[j].End
|
||||||
|
})
|
||||||
|
|
||||||
return math.Min(bonus, 0.10)
|
total := 0.0
|
||||||
|
current := valid[0]
|
||||||
|
|
||||||
|
for _, item := range valid[1:] {
|
||||||
|
if item.Start <= current.End {
|
||||||
|
if item.End > current.End {
|
||||||
|
current.End = item.End
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
total += current.End - current.Start
|
||||||
|
current = item
|
||||||
|
}
|
||||||
|
|
||||||
|
return total + current.End - current.Start
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingBlendWithAction(base, action, actionWeight float64) float64 {
|
func ratingStrengthComponent(avgConfidence, peakConfidence float64) float64 {
|
||||||
actionWeight = ratingClamp01(actionWeight)
|
avg := ratingSmoothStep(ratingLinearGate(avgConfidence, 0.35, 0.90))
|
||||||
return ratingClamp01((1-actionWeight)*base + actionWeight*action)
|
peak := ratingSmoothStep(ratingLinearGate(peakConfidence, 0.50, 0.95))
|
||||||
|
return ratingClamp01(0.75*avg + 0.25*peak)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingPositionActionScore(
|
func ratingDurationComponent(totalSeconds, longestSeconds float64) float64 {
|
||||||
durationNorm float64,
|
total := ratingSaturatingEvidence(totalSeconds, 55.0)
|
||||||
longestNorm float64,
|
longest := ratingSaturatingEvidence(longestSeconds, 30.0)
|
||||||
densityNorm float64,
|
return ratingClamp01(0.62*total + 0.38*longest)
|
||||||
coverageNorm float64,
|
|
||||||
peakNorm float64,
|
|
||||||
) float64 {
|
|
||||||
return ratingClamp01(
|
|
||||||
0.38*durationNorm +
|
|
||||||
0.24*longestNorm +
|
|
||||||
0.18*densityNorm +
|
|
||||||
0.12*coverageNorm +
|
|
||||||
0.08*peakNorm,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingContextActionScore(
|
func ratingCoverageComponent(coveredSeconds, durationSec float64) float64 {
|
||||||
durationNorm float64,
|
if coveredSeconds <= 0 || durationSec <= 0 {
|
||||||
longestNorm float64,
|
return 0
|
||||||
coverageNorm float64,
|
}
|
||||||
densityNorm float64,
|
return ratingSaturatingEvidence(ratingClamp01(coveredSeconds/durationSec), 0.12)
|
||||||
peakNorm float64,
|
|
||||||
) float64 {
|
|
||||||
return ratingClamp01(
|
|
||||||
0.40*durationNorm +
|
|
||||||
0.25*longestNorm +
|
|
||||||
0.20*coverageNorm +
|
|
||||||
0.10*densityNorm +
|
|
||||||
0.05*peakNorm,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ratingExcellenceBonus(
|
func ratingActionComponent(duration, coverage, strength float64) float64 {
|
||||||
peakQuality float64,
|
return ratingClamp01(0.50*duration + 0.30*coverage + 0.20*strength)
|
||||||
positionEffectiveWeighted float64,
|
}
|
||||||
positionDensity float64,
|
|
||||||
weightedCoverageRatio float64,
|
func ratingApplyPositionEvidenceCaps(
|
||||||
totalFlagged float64,
|
raw float64,
|
||||||
longest float64,
|
positionSeconds float64,
|
||||||
|
longestSeconds float64,
|
||||||
avgConfidence float64,
|
avgConfidence float64,
|
||||||
segmentsPerMinute float64,
|
positionScore float64,
|
||||||
n int,
|
|
||||||
) float64 {
|
) float64 {
|
||||||
// Kein pauschaler Score-Shift:
|
switch {
|
||||||
// Der Bonus darf nur greifen, wenn wirklich mehrere starke Signale vorhanden sind.
|
case positionSeconds < 5:
|
||||||
if n < 2 {
|
raw = math.Min(raw, 0.21)
|
||||||
return 0
|
case positionSeconds < 12:
|
||||||
}
|
raw = math.Min(raw, 0.44)
|
||||||
if positionEffectiveWeighted <= 0 {
|
case positionSeconds < 30:
|
||||||
return 0
|
raw = math.Min(raw, 0.67)
|
||||||
}
|
|
||||||
if totalFlagged < 12.0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if peakQuality < 0.68 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if avgConfidence < 0.42 {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
peakGate := ratingLinearGate(peakQuality, 0.68, 0.90)
|
if positionSeconds < 75 ||
|
||||||
positionGate := ratingLinearGate(positionDensity, 2.20, 7.00)
|
longestSeconds < 18 ||
|
||||||
coverageGate := ratingLinearGate(weightedCoverageRatio, 0.045, 0.160)
|
avgConfidence < 0.55 ||
|
||||||
durationGate := ratingLinearGate(totalFlagged, 12.0, 90.0)
|
positionScore < 0.65 {
|
||||||
longestGate := ratingLinearGate(longest, 8.0, 45.0)
|
raw = math.Min(raw, 0.84)
|
||||||
confGate := ratingLinearGate(avgConfidence, 0.42, 0.78)
|
}
|
||||||
frequencyGate := ratingLinearGate(segmentsPerMinute, 0.25, 1.00)
|
if avgConfidence < 0.50 && positionScore < 0.65 {
|
||||||
|
raw = math.Min(raw, 0.21)
|
||||||
bonus :=
|
|
||||||
0.030*peakGate +
|
|
||||||
0.022*positionGate +
|
|
||||||
0.016*coverageGate +
|
|
||||||
0.012*durationGate +
|
|
||||||
0.012*longestGate +
|
|
||||||
0.010*confGate +
|
|
||||||
0.006*frequencyGate
|
|
||||||
|
|
||||||
// Maximal +8.5 Scorepunkte.
|
|
||||||
// Dadurch werden starke 72-79 Scores 5-Sterne-fähig,
|
|
||||||
// aber mittelmäßige Scores springen nicht einfach pauschal hoch.
|
|
||||||
if bonus > 0.085 {
|
|
||||||
bonus = 0.085
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bonus
|
return ratingClamp01(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func starsFromHighlightScore(score float64) int {
|
func starsFromHighlightScore(score float64) int {
|
||||||
@ -1434,34 +1471,39 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
|
|
||||||
videoMinutes := math.Max(durationSec/60.0, 0.25)
|
videoMinutes := math.Max(durationSec/60.0, 0.25)
|
||||||
|
|
||||||
var totalFlagged float64
|
var positionRanges []ratingTimeRange
|
||||||
var totalWeighted float64
|
var positionWeighted float64
|
||||||
|
var positionExplicitSum float64
|
||||||
|
var positionEvidenceWeight float64
|
||||||
|
var peakPositionExplicitness float64
|
||||||
|
var peakPositionConfidence float64
|
||||||
|
|
||||||
var positionEffectiveWeighted float64
|
var contextRanges []ratingTimeRange
|
||||||
var positionFlagged float64
|
|
||||||
var peakQuality float64
|
|
||||||
var qualityDurationSum float64
|
|
||||||
var qualityDurationWeight float64
|
|
||||||
|
|
||||||
var contextFlagged float64
|
|
||||||
var contextWeighted float64
|
var contextWeighted float64
|
||||||
var contextEffectiveWeighted float64
|
var contextExplicitSum float64
|
||||||
var contextPeakQuality float64
|
var contextEvidenceWeight float64
|
||||||
var contextQualityDurationSum float64
|
var peakContextExplicitness float64
|
||||||
var contextQualityDurationWeight float64
|
var peakContextConfidence float64
|
||||||
var contextLongest float64
|
var contextLongest float64
|
||||||
var contextConfSum float64
|
var contextConfSum float64
|
||||||
var contextConfWeightSum float64
|
var contextConfWeightSum float64
|
||||||
var contextN int
|
var contextN int
|
||||||
|
|
||||||
var bodyEffectiveWeighted float64
|
var bodyEffectiveWeighted float64
|
||||||
var clothingEffectiveWeighted float64
|
var clothingEffectiveWeighted float64
|
||||||
var objectEffectiveWeighted float64
|
var objectEffectiveWeighted float64
|
||||||
|
var clothingExplicitSum float64
|
||||||
|
var clothingEvidenceWeight float64
|
||||||
|
|
||||||
var longest float64
|
var longest float64
|
||||||
var confSum float64
|
var confSum float64
|
||||||
var confWeightSum float64
|
var confWeightSum float64
|
||||||
var n int
|
var n int
|
||||||
uniquePositions := map[string]bool{}
|
uniquePositions := map[string]bool{}
|
||||||
|
bestPosition := ""
|
||||||
|
bestPositionRank := 0
|
||||||
|
bestClothing := ""
|
||||||
|
bestClothingRank := 0
|
||||||
|
|
||||||
for _, s := range segments {
|
for _, s := range segments {
|
||||||
segDur := s.DurationSeconds
|
segDur := s.DurationSeconds
|
||||||
@ -1492,25 +1534,41 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
|
|
||||||
effectiveDur := ratingEffectiveDurationSeconds(segDur)
|
effectiveDur := ratingEffectiveDurationSeconds(segDur)
|
||||||
weightedDur := segDur * quality
|
weightedDur := segDur * quality
|
||||||
effectiveWeightedDur := effectiveDur * quality
|
segmentRange := ratingTimeRange{
|
||||||
|
Start: s.StartSeconds,
|
||||||
|
End: s.StartSeconds + segDur,
|
||||||
|
}
|
||||||
|
|
||||||
set := ratingSignalSetFromLabel(s.Label)
|
set := ratingSignalSetFromLabel(s.Label)
|
||||||
|
updateBestExplicitRatingRanks(
|
||||||
|
s.Label,
|
||||||
|
&bestPosition,
|
||||||
|
&bestPositionRank,
|
||||||
|
&bestClothing,
|
||||||
|
&bestClothingRank,
|
||||||
|
)
|
||||||
bodyEffectiveWeighted += effectiveDur * set.Body * confWeight
|
bodyEffectiveWeighted += effectiveDur * set.Body * confWeight
|
||||||
clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight
|
clothingEffectiveWeighted += effectiveDur * set.Clothing * confWeight
|
||||||
objectEffectiveWeighted += effectiveDur * set.Object * confWeight
|
objectEffectiveWeighted += effectiveDur * set.Object * confWeight
|
||||||
|
if set.HasClothing {
|
||||||
|
clothingExplicitSum += (set.Clothing / clothingExplicitRatingRank("lingerie").Weight) * effectiveDur
|
||||||
|
clothingEvidenceWeight += effectiveDur
|
||||||
|
}
|
||||||
|
|
||||||
if !set.HasPosition {
|
if !set.HasPosition {
|
||||||
contextFlagged += segDur
|
contextRanges = append(contextRanges, segmentRange)
|
||||||
contextWeighted += weightedDur
|
contextWeighted += weightedDur
|
||||||
contextEffectiveWeighted += effectiveWeightedDur
|
contextExplicitSum += sev * effectiveDur
|
||||||
contextQualityDurationSum += quality * effectiveDur
|
contextEvidenceWeight += effectiveDur
|
||||||
contextQualityDurationWeight += effectiveDur
|
|
||||||
contextConfSum += conf * effectiveDur
|
contextConfSum += conf * effectiveDur
|
||||||
contextConfWeightSum += effectiveDur
|
contextConfWeightSum += effectiveDur
|
||||||
contextN++
|
contextN++
|
||||||
|
|
||||||
if quality > contextPeakQuality {
|
if sev > peakContextExplicitness {
|
||||||
contextPeakQuality = quality
|
peakContextExplicitness = sev
|
||||||
|
}
|
||||||
|
if conf > peakContextConfidence {
|
||||||
|
peakContextConfidence = conf
|
||||||
}
|
}
|
||||||
if segDur > contextLongest {
|
if segDur > contextLongest {
|
||||||
contextLongest = segDur
|
contextLongest = segDur
|
||||||
@ -1519,16 +1577,16 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
totalFlagged += segDur
|
positionRanges = append(positionRanges, segmentRange)
|
||||||
totalWeighted += weightedDur
|
positionWeighted += weightedDur
|
||||||
|
positionExplicitSum += set.Position * effectiveDur
|
||||||
|
positionEvidenceWeight += effectiveDur
|
||||||
|
|
||||||
positionEffectiveWeighted += effectiveWeightedDur
|
if set.Position > peakPositionExplicitness {
|
||||||
positionFlagged += segDur
|
peakPositionExplicitness = set.Position
|
||||||
qualityDurationSum += quality * effectiveDur
|
}
|
||||||
qualityDurationWeight += effectiveDur
|
if conf > peakPositionConfidence {
|
||||||
|
peakPositionConfidence = conf
|
||||||
if quality > peakQuality {
|
|
||||||
peakQuality = quality
|
|
||||||
}
|
}
|
||||||
|
|
||||||
confSum += conf * effectiveDur
|
confSum += conf * effectiveDur
|
||||||
@ -1546,58 +1604,71 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clothingScore := 0.0
|
||||||
|
if clothingEvidenceWeight > 0 {
|
||||||
|
avgClothingExplicitness := ratingClamp01(clothingExplicitSum / clothingEvidenceWeight)
|
||||||
|
clothingDensity := clothingEffectiveWeighted / videoMinutes
|
||||||
|
clothingScore = ratingClamp01(
|
||||||
|
0.70*avgClothingExplicitness +
|
||||||
|
0.30*ratingSaturatingEvidence(clothingDensity, 1.8),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyContext := ratingSaturatingEvidence(bodyEffectiveWeighted/videoMinutes, 1.8)
|
||||||
|
objectContext := ratingSaturatingEvidence(objectEffectiveWeighted/videoMinutes, 2.2)
|
||||||
|
contextScore := ratingClamp01(0.78*bodyContext + 0.22*objectContext)
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
if contextN == 0 {
|
if contextN == 0 {
|
||||||
appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username))
|
appLogf("⚠️ %s rating result is zero because all segments were skipped", ratingLogSubject(username))
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextFlagged := ratingCoveredSeconds(contextRanges, durationSec)
|
||||||
contextCoverageRatio := ratingClamp01(contextFlagged / durationSec)
|
contextCoverageRatio := ratingClamp01(contextFlagged / durationSec)
|
||||||
contextWeightedCoverageRatio := ratingClamp01(contextWeighted / durationSec)
|
contextWeightedCoverageRatio := ratingClamp01(contextWeighted / durationSec)
|
||||||
contextSegmentsPerMinute := float64(contextN) / videoMinutes
|
contextSegmentsPerMinute := float64(contextN) / videoMinutes
|
||||||
contextAvgConfidence := contextConfSum / contextConfWeightSum
|
contextAvgConfidence := contextConfSum / contextConfWeightSum
|
||||||
contextAvgQuality := contextQualityDurationSum / contextQualityDurationWeight
|
avgContextExplicitness := contextExplicitSum / contextEvidenceWeight
|
||||||
contextDensity := contextEffectiveWeighted / videoMinutes
|
contextQuality := ratingClamp01(
|
||||||
|
(0.75*avgContextExplicitness + 0.25*peakContextExplicitness) / 0.72,
|
||||||
qualityNorm := ratingSmoothStep(
|
|
||||||
ratingLinearGate(contextAvgQuality, 0.08, 0.55),
|
|
||||||
)
|
)
|
||||||
peakNorm := ratingSmoothStep(
|
durationScore := ratingDurationComponent(contextFlagged, contextLongest)
|
||||||
ratingLinearGate(contextPeakQuality, 0.12, 0.62),
|
strengthScore := ratingStrengthComponent(contextAvgConfidence, peakContextConfidence)
|
||||||
)
|
coverageScore := ratingCoverageComponent(contextFlagged, durationSec)
|
||||||
durationNorm := ratingSaturatingEvidence(contextFlagged, 150.0)
|
|
||||||
longestNorm := ratingSaturatingEvidence(contextLongest, 60.0)
|
|
||||||
densityNorm := ratingSaturatingEvidence(contextDensity, 2.5)
|
|
||||||
coverageNorm := ratingSaturatingEvidence(contextWeightedCoverageRatio, 0.08)
|
|
||||||
|
|
||||||
raw :=
|
raw :=
|
||||||
0.28*qualityNorm +
|
0.30*contextQuality +
|
||||||
0.12*peakNorm +
|
0.25*durationScore +
|
||||||
0.25*durationNorm +
|
0.15*strengthScore +
|
||||||
0.15*longestNorm +
|
0.12*coverageScore +
|
||||||
0.15*coverageNorm +
|
0.12*clothingScore +
|
||||||
0.05*densityNorm
|
0.06*contextScore
|
||||||
|
|
||||||
actionRaw := ratingContextActionScore(
|
actionRaw := ratingActionComponent(durationScore, coverageScore, strengthScore)
|
||||||
durationNorm,
|
|
||||||
longestNorm,
|
|
||||||
coverageNorm,
|
|
||||||
densityNorm,
|
|
||||||
peakNorm,
|
|
||||||
)
|
|
||||||
raw = ratingBlendWithAction(raw, actionRaw, 0.35)
|
|
||||||
|
|
||||||
// Context can produce useful 1-3 star ratings, but never the 4-5 star
|
|
||||||
// range reserved for sustained position detections.
|
|
||||||
if contextFlagged < 20.0 {
|
if contextFlagged < 20.0 {
|
||||||
raw = math.Min(raw, 0.44)
|
raw = math.Min(raw, 0.44)
|
||||||
}
|
}
|
||||||
raw = math.Min(raw, 0.64)
|
semanticCeiling := 0.52 + 0.12*math.Max(
|
||||||
|
contextQuality,
|
||||||
|
math.Max(clothingScore, contextScore),
|
||||||
|
)
|
||||||
|
raw = math.Min(raw, semanticCeiling)
|
||||||
|
|
||||||
score := ratingRound(ratingClamp01(raw)*100, 1)
|
score := ratingRound(ratingClamp01(raw)*100, 1)
|
||||||
|
|
||||||
r.Score = score
|
r.Score = score
|
||||||
r.ActionScore = ratingRound(actionRaw*100, 1)
|
r.ActionScore = ratingRound(actionRaw*100, 1)
|
||||||
|
r.BestPosition = bestPosition
|
||||||
|
r.BestPositionRank = bestPositionRank
|
||||||
|
r.BestClothing = bestClothing
|
||||||
|
r.BestClothingRank = bestClothingRank
|
||||||
|
r.ClothingScore = ratingRound(clothingScore*100, 1)
|
||||||
|
r.DurationScore = ratingRound(durationScore*100, 1)
|
||||||
|
r.StrengthScore = ratingRound(strengthScore*100, 1)
|
||||||
|
r.CoverageScore = ratingRound(coverageScore*100, 1)
|
||||||
|
r.ContextScore = ratingRound(contextScore*100, 1)
|
||||||
r.Stars = starsFromHighlightScore(score)
|
r.Stars = starsFromHighlightScore(score)
|
||||||
r.Segments = contextN
|
r.Segments = contextN
|
||||||
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
|
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
|
||||||
@ -1609,13 +1680,13 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
r.AvgConfidence = ratingRound(contextAvgConfidence, 3)
|
r.AvgConfidence = ratingRound(contextAvgConfidence, 3)
|
||||||
|
|
||||||
appLogf(
|
appLogf(
|
||||||
"✅ %s context rating score=%.1f stars=%d segs=%d flagged=%.1f density=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
"rating %s context score=%.1f stars=%d segs=%d flagged=%.1f quality=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
||||||
ratingLogSubject(username),
|
ratingLogSubject(username),
|
||||||
r.Score,
|
r.Score,
|
||||||
r.Stars,
|
r.Stars,
|
||||||
r.Segments,
|
r.Segments,
|
||||||
r.FlaggedSeconds,
|
r.FlaggedSeconds,
|
||||||
contextDensity,
|
contextQuality,
|
||||||
r.WeightedCoverageRatio,
|
r.WeightedCoverageRatio,
|
||||||
r.LongestSegmentSeconds,
|
r.LongestSegmentSeconds,
|
||||||
r.AvgConfidence,
|
r.AvgConfidence,
|
||||||
@ -1624,97 +1695,76 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
coverageRatio := ratingClamp01(totalFlagged / durationSec)
|
positionFlagged := ratingCoveredSeconds(positionRanges, durationSec)
|
||||||
weightedCoverageRatio := ratingClamp01(totalWeighted / durationSec)
|
coverageRatio := ratingClamp01(positionFlagged / durationSec)
|
||||||
|
weightedCoverageRatio := ratingClamp01(positionWeighted / durationSec)
|
||||||
segmentsPerMinute := float64(n) / videoMinutes
|
segmentsPerMinute := float64(n) / videoMinutes
|
||||||
avgConfidence := confSum / confWeightSum
|
avgConfidence := confSum / confWeightSum
|
||||||
avgPositionQuality := qualityDurationSum / qualityDurationWeight
|
avgPositionExplicitness := positionExplicitSum / positionEvidenceWeight
|
||||||
|
positionScore := ratingClamp01(
|
||||||
positionDensity := positionEffectiveWeighted / videoMinutes
|
0.75*avgPositionExplicitness + 0.25*peakPositionExplicitness,
|
||||||
|
|
||||||
avgQualityNorm := ratingSmoothStep(
|
|
||||||
ratingLinearGate(avgPositionQuality, 0.45, 0.95),
|
|
||||||
)
|
)
|
||||||
peakNorm := ratingSmoothStep(
|
durationScore := ratingDurationComponent(positionFlagged, longest)
|
||||||
ratingLinearGate(peakQuality, 0.42, 0.95),
|
strengthScore := ratingStrengthComponent(avgConfidence, peakPositionConfidence)
|
||||||
)
|
coverageScore := ratingCoverageComponent(positionFlagged, durationSec)
|
||||||
positionDurationNorm := ratingSaturatingEvidence(positionFlagged, 65.0)
|
varietyScore := ratingLinearGate(float64(len(uniquePositions)), 1, 4)
|
||||||
longestNorm := ratingSaturatingEvidence(longest, 35.0)
|
|
||||||
positionDensityNorm := ratingSaturatingEvidence(positionDensity, 3.2)
|
|
||||||
coverageNorm := ratingSaturatingEvidence(weightedCoverageRatio, 0.075)
|
|
||||||
positionVarietyNorm := ratingLinearGate(float64(len(uniquePositions)), 1, 4)
|
|
||||||
|
|
||||||
// Position quality is the primary signal. Actual position duration and the
|
|
||||||
// longest continuous segment decide whether a detection is brief or strong.
|
|
||||||
raw :=
|
raw :=
|
||||||
0.30*avgQualityNorm +
|
0.36*positionScore +
|
||||||
0.10*peakNorm +
|
0.27*durationScore +
|
||||||
0.26*positionDurationNorm +
|
0.14*strengthScore +
|
||||||
0.16*longestNorm +
|
0.10*coverageScore +
|
||||||
0.10*positionDensityNorm +
|
0.06*contextScore +
|
||||||
0.06*coverageNorm +
|
0.04*clothingScore +
|
||||||
0.02*positionVarietyNorm
|
0.03*varietyScore
|
||||||
|
|
||||||
actionRaw := ratingPositionActionScore(
|
actionRaw := ratingActionComponent(durationScore, coverageScore, strengthScore)
|
||||||
positionDurationNorm,
|
|
||||||
longestNorm,
|
|
||||||
positionDensityNorm,
|
|
||||||
coverageNorm,
|
|
||||||
peakNorm,
|
|
||||||
)
|
|
||||||
raw = ratingBlendWithAction(raw, actionRaw, 0.30)
|
|
||||||
|
|
||||||
explicitContextBonus := ratingExplicitContextBonus(
|
// Short or weak detections cannot reach high ratings on rank alone.
|
||||||
bodyEffectiveWeighted,
|
raw = ratingApplyPositionEvidenceCaps(
|
||||||
clothingEffectiveWeighted,
|
raw,
|
||||||
objectEffectiveWeighted,
|
positionFlagged,
|
||||||
videoMinutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sehr wenig Material nicht überbewerten.
|
|
||||||
if totalFlagged < 5.0 && n <= 1 {
|
|
||||||
raw = math.Min(raw, 0.44)
|
|
||||||
}
|
|
||||||
|
|
||||||
excellenceBonus := ratingExcellenceBonus(
|
|
||||||
peakQuality,
|
|
||||||
positionEffectiveWeighted,
|
|
||||||
positionDensity,
|
|
||||||
weightedCoverageRatio,
|
|
||||||
totalFlagged,
|
|
||||||
longest,
|
longest,
|
||||||
avgConfidence,
|
avgConfidence,
|
||||||
segmentsPerMinute,
|
positionScore,
|
||||||
n,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
raw = ratingClamp01(raw + explicitContextBonus + excellenceBonus)
|
|
||||||
|
|
||||||
score := ratingRound(raw*100, 1)
|
score := ratingRound(raw*100, 1)
|
||||||
|
|
||||||
r.Score = score
|
r.Score = score
|
||||||
r.ActionScore = ratingRound(actionRaw*100, 1)
|
r.ActionScore = ratingRound(actionRaw*100, 1)
|
||||||
|
r.BestPosition = bestPosition
|
||||||
|
r.BestPositionRank = bestPositionRank
|
||||||
|
r.BestClothing = bestClothing
|
||||||
|
r.BestClothingRank = bestClothingRank
|
||||||
|
r.PositionScore = ratingRound(positionScore*100, 1)
|
||||||
|
r.ClothingScore = ratingRound(clothingScore*100, 1)
|
||||||
|
r.DurationScore = ratingRound(durationScore*100, 1)
|
||||||
|
r.StrengthScore = ratingRound(strengthScore*100, 1)
|
||||||
|
r.CoverageScore = ratingRound(coverageScore*100, 1)
|
||||||
|
r.ContextScore = ratingRound(contextScore*100, 1)
|
||||||
|
r.VarietyScore = ratingRound(varietyScore*100, 1)
|
||||||
r.Stars = starsFromHighlightScore(score)
|
r.Stars = starsFromHighlightScore(score)
|
||||||
r.Segments = n
|
r.Segments = n
|
||||||
r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2)
|
r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2)
|
||||||
r.FlaggedSeconds = ratingRound(totalFlagged, 2)
|
r.FlaggedSeconds = ratingRound(positionFlagged, 2)
|
||||||
r.WeightedFlaggedSeconds = ratingRound(totalWeighted, 2)
|
r.WeightedFlaggedSeconds = ratingRound(positionWeighted, 2)
|
||||||
r.CoverageRatio = ratingRound(coverageRatio, 4)
|
r.CoverageRatio = ratingRound(coverageRatio, 4)
|
||||||
r.WeightedCoverageRatio = ratingRound(weightedCoverageRatio, 4)
|
r.WeightedCoverageRatio = ratingRound(weightedCoverageRatio, 4)
|
||||||
r.LongestSegmentSeconds = ratingRound(longest, 2)
|
r.LongestSegmentSeconds = ratingRound(longest, 2)
|
||||||
r.AvgConfidence = ratingRound(avgConfidence, 3)
|
r.AvgConfidence = ratingRound(avgConfidence, 3)
|
||||||
|
|
||||||
appLogf(
|
appLogf(
|
||||||
"✅ %s rating score=%.1f stars=%d contextBonus=%.1f excellenceBonus=%.1f segs=%d flagged=%.1f posFlagged=%.1f posDensity=%.2f coverage=%.4f longest=%.1f avgConf=%.3f",
|
"rating %s score=%.1f stars=%d position=%.1f clothing=%.1f segs=%d flagged=%.1f duration=%.1f strength=%.1f coverage=%.4f longest=%.1f avgConf=%.3f",
|
||||||
ratingLogSubject(username),
|
ratingLogSubject(username),
|
||||||
r.Score,
|
r.Score,
|
||||||
r.Stars,
|
r.Stars,
|
||||||
ratingRound(explicitContextBonus*100, 1),
|
r.PositionScore,
|
||||||
ratingRound(excellenceBonus*100, 1),
|
r.ClothingScore,
|
||||||
r.Segments,
|
r.Segments,
|
||||||
r.FlaggedSeconds,
|
r.FlaggedSeconds,
|
||||||
ratingRound(positionFlagged, 1),
|
r.DurationScore,
|
||||||
positionDensity,
|
r.StrengthScore,
|
||||||
r.WeightedCoverageRatio,
|
r.WeightedCoverageRatio,
|
||||||
r.LongestSegmentSeconds,
|
r.LongestSegmentSeconds,
|
||||||
r.AvgConfidence,
|
r.AvgConfidence,
|
||||||
|
|||||||
@ -46,22 +46,22 @@ func TestComputeHighlightRatingSpansAllStars(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "several long high quality positions",
|
name: "several long high quality positions",
|
||||||
segments: []aiSegmentMeta{
|
segments: []aiSegmentMeta{
|
||||||
ratingTestSegment("position:doggy", 20, 25, 0.82),
|
ratingTestSegment("position:cowgirl", 20, 25, 0.82),
|
||||||
ratingTestSegment("position:cowgirl", 120, 25, 0.82),
|
ratingTestSegment("position:reverse_cowgirl", 120, 25, 0.82),
|
||||||
ratingTestSegment("position:missionary", 220, 25, 0.82),
|
ratingTestSegment("position:doggy", 220, 25, 0.82),
|
||||||
ratingTestSegment("position:blowjob", 320, 25, 0.82),
|
ratingTestSegment("position:toy_play", 320, 25, 0.82),
|
||||||
},
|
},
|
||||||
want: 4,
|
want: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "exceptional sustained and varied positions",
|
name: "exceptional sustained and varied positions",
|
||||||
segments: []aiSegmentMeta{
|
segments: []aiSegmentMeta{
|
||||||
ratingTestSegment("position:doggy", 10, 40, 0.90),
|
ratingTestSegment("position:cowgirl", 10, 40, 0.90),
|
||||||
ratingTestSegment("position:cowgirl", 100, 40, 0.90),
|
ratingTestSegment("position:reverse_cowgirl", 100, 40, 0.90),
|
||||||
ratingTestSegment("position:missionary", 190, 40, 0.90),
|
ratingTestSegment("position:doggy", 190, 40, 0.90),
|
||||||
ratingTestSegment("position:blowjob", 280, 40, 0.90),
|
ratingTestSegment("position:toy_play", 280, 40, 0.90),
|
||||||
ratingTestSegment("position:cunnilingus", 370, 40, 0.90),
|
ratingTestSegment("position:fingering", 370, 40, 0.90),
|
||||||
ratingTestSegment("position:reverse_cowgirl", 460, 40, 0.90),
|
ratingTestSegment("position:missionary", 460, 40, 0.90),
|
||||||
},
|
},
|
||||||
want: 5,
|
want: 5,
|
||||||
},
|
},
|
||||||
@ -77,6 +77,141 @@ func TestComputeHighlightRatingSpansAllStars(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExplicitPositionRanking(t *testing.T) {
|
||||||
|
labels := []string{
|
||||||
|
"cowgirl",
|
||||||
|
"reverse_cowgirl",
|
||||||
|
"doggy",
|
||||||
|
"toy_play",
|
||||||
|
"fingering",
|
||||||
|
"missionary",
|
||||||
|
"prone_bone",
|
||||||
|
"spooning",
|
||||||
|
"standing",
|
||||||
|
"69",
|
||||||
|
"blowjob",
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWeight := 2.0
|
||||||
|
for index, label := range labels {
|
||||||
|
ranked := positionExplicitRatingRank(label)
|
||||||
|
wantRank := index + 1
|
||||||
|
|
||||||
|
if ranked.Rank != wantRank {
|
||||||
|
t.Fatalf("%s rank = %d, want %d", label, ranked.Rank, wantRank)
|
||||||
|
}
|
||||||
|
if ranked.Weight <= 0 || ranked.Weight >= previousWeight {
|
||||||
|
t.Fatalf(
|
||||||
|
"%s weight = %.2f, want a positive value below %.2f",
|
||||||
|
label,
|
||||||
|
ranked.Weight,
|
||||||
|
previousWeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWeight = ranked.Weight
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range []string{"doggystyle", "standing_doggy"} {
|
||||||
|
if ranked := positionExplicitRatingRank(alias); ranked.Rank != 3 {
|
||||||
|
t.Fatalf("%s rank = %d, want alias rank 3", alias, ranked.Rank)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, grouped := range []string{"facesitting", "handjob", "cunnilingus", "boobjob"} {
|
||||||
|
if ranked := positionExplicitRatingRank(grouped); ranked.Rank != positionExplicitRatingRank("69").Rank &&
|
||||||
|
ranked.Rank != positionExplicitRatingRank("blowjob").Rank {
|
||||||
|
t.Fatalf("%s has unexpected grouped rank %d", grouped, ranked.Rank)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplicitClothingRanking(t *testing.T) {
|
||||||
|
labels := []string{
|
||||||
|
"lingerie",
|
||||||
|
"stockings",
|
||||||
|
"skirt",
|
||||||
|
"hotpants",
|
||||||
|
"bikini",
|
||||||
|
"bra",
|
||||||
|
"panties",
|
||||||
|
"croptop",
|
||||||
|
"heels",
|
||||||
|
"dress",
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWeight := 2.0
|
||||||
|
for index, label := range labels {
|
||||||
|
ranked := clothingExplicitRatingRank(label)
|
||||||
|
wantRank := index + 1
|
||||||
|
|
||||||
|
if ranked.Rank != wantRank {
|
||||||
|
t.Fatalf("%s rank = %d, want %d", label, ranked.Rank, wantRank)
|
||||||
|
}
|
||||||
|
if ranked.Weight <= 0 || ranked.Weight >= previousWeight {
|
||||||
|
t.Fatalf(
|
||||||
|
"%s weight = %.2f, want a positive value below %.2f",
|
||||||
|
label,
|
||||||
|
ranked.Weight,
|
||||||
|
previousWeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
previousWeight = ranked.Weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingReportsBestExplicitRanks(t *testing.T) {
|
||||||
|
got := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("combo:position:standing+clothing:dress", 10, 20, 0.90),
|
||||||
|
ratingTestSegment("combo:position:cowgirl+clothing:panties", 80, 20, 0.90),
|
||||||
|
ratingTestSegment("combo:position:doggy+clothing:lingerie", 150, 20, 0.90),
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
if got.BestPosition != "cowgirl" || got.BestPositionRank != 1 {
|
||||||
|
t.Fatalf(
|
||||||
|
"best position = %q rank %d, want cowgirl rank 1",
|
||||||
|
got.BestPosition,
|
||||||
|
got.BestPositionRank,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if got.BestClothing != "lingerie" || got.BestClothingRank != 1 {
|
||||||
|
t.Fatalf(
|
||||||
|
"best clothing = %q rank %d, want lingerie rank 1",
|
||||||
|
got.BestClothing,
|
||||||
|
got.BestClothingRank,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingFollowsExplicitRank(t *testing.T) {
|
||||||
|
positionScore := func(label string) float64 {
|
||||||
|
return computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:"+label, 10, 30, 0.85),
|
||||||
|
}, 300).Score
|
||||||
|
}
|
||||||
|
clothingScore := func(label string) float64 {
|
||||||
|
return computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("clothing:"+label, 10, 90, 0.90),
|
||||||
|
}, 300).Score
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(positionScore("cowgirl") > positionScore("doggy") &&
|
||||||
|
positionScore("doggy") > positionScore("toy_play") &&
|
||||||
|
positionScore("toy_play") > positionScore("missionary") &&
|
||||||
|
positionScore("missionary") > positionScore("standing") &&
|
||||||
|
positionScore("standing") > positionScore("blowjob")) {
|
||||||
|
t.Fatal("position score does not follow the explicit ranking")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(clothingScore("lingerie") > clothingScore("stockings") &&
|
||||||
|
clothingScore("stockings") > clothingScore("hotpants") &&
|
||||||
|
clothingScore("hotpants") > clothingScore("bikini") &&
|
||||||
|
clothingScore("bikini") > clothingScore("panties") &&
|
||||||
|
clothingScore("panties") > clothingScore("dress")) {
|
||||||
|
t.Fatal("clothing score does not follow the explicit ranking")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) {
|
func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) {
|
||||||
short := computeHighlightRating([]aiSegmentMeta{
|
short := computeHighlightRating([]aiSegmentMeta{
|
||||||
ratingTestSegment("position:doggy", 10, 8, 0.80),
|
ratingTestSegment("position:doggy", 10, 8, 0.80),
|
||||||
@ -122,6 +257,67 @@ func TestComputeHighlightRatingActionPrefersSustainedActivity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingDoesNotDoubleCountOverlaps(t *testing.T) {
|
||||||
|
got := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:cowgirl", 0, 30, 0.90),
|
||||||
|
ratingTestSegment("position:doggy", 10, 30, 0.90),
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
if got.FlaggedSeconds != 40 {
|
||||||
|
t.Fatalf("flagged seconds = %.1f, want 40.0", got.FlaggedSeconds)
|
||||||
|
}
|
||||||
|
if got.CoverageRatio != 0.4 {
|
||||||
|
t.Fatalf("coverage ratio = %.4f, want 0.4000", got.CoverageRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingReportsIndependentComponents(t *testing.T) {
|
||||||
|
got := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("combo:position:cowgirl+body:pussy+clothing:lingerie", 10, 40, 0.90),
|
||||||
|
ratingTestSegment("combo:position:doggy+object:dildo+clothing:stockings", 100, 40, 0.90),
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
components := map[string]float64{
|
||||||
|
"position": got.PositionScore,
|
||||||
|
"clothing": got.ClothingScore,
|
||||||
|
"duration": got.DurationScore,
|
||||||
|
"strength": got.StrengthScore,
|
||||||
|
"coverage": got.CoverageScore,
|
||||||
|
"context": got.ContextScore,
|
||||||
|
"variety": got.VarietyScore,
|
||||||
|
"action": got.ActionScore,
|
||||||
|
}
|
||||||
|
for name, value := range components {
|
||||||
|
if value <= 0 || value > 100 {
|
||||||
|
t.Fatalf("%s component = %.1f, want value in (0, 100]", name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeHighlightRatingStrengthFollowsConfidence(t *testing.T) {
|
||||||
|
weak := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 45, 0.45),
|
||||||
|
}, 300)
|
||||||
|
strong := computeHighlightRating([]aiSegmentMeta{
|
||||||
|
ratingTestSegment("position:doggy", 10, 45, 0.90),
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
if strong.StrengthScore <= weak.StrengthScore {
|
||||||
|
t.Fatalf(
|
||||||
|
"strength should follow confidence: strong=%.1f weak=%.1f",
|
||||||
|
strong.StrengthScore,
|
||||||
|
weak.StrengthScore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if strong.Score <= weak.Score {
|
||||||
|
t.Fatalf(
|
||||||
|
"rating should reward stronger evidence: strong=%.1f weak=%.1f",
|
||||||
|
strong.Score,
|
||||||
|
weak.Score,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
|
func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
|
||||||
clothingOnly := computeHighlightRating([]aiSegmentMeta{
|
clothingOnly := computeHighlightRating([]aiSegmentMeta{
|
||||||
ratingTestSegment("clothing:lingerie", 10, 60, 0.90),
|
ratingTestSegment("clothing:lingerie", 10, 60, 0.90),
|
||||||
@ -134,9 +330,9 @@ func TestComputeHighlightRatingRatesContextWithoutPosition(t *testing.T) {
|
|||||||
ratingTestSegment("combo:body:penis+object:dildo", 240, 45, 0.90),
|
ratingTestSegment("combo:body:penis+object:dildo", 240, 45, 0.90),
|
||||||
}, 600)
|
}, 600)
|
||||||
|
|
||||||
if clothingOnly.Stars != 2 {
|
if clothingOnly.Stars != 3 {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"clothing-only stars = %d, want 2 (score %.1f)",
|
"clothing-only stars = %d, want 3 (score %.1f)",
|
||||||
clothingOnly.Stars,
|
clothingOnly.Stars,
|
||||||
clothingOnly.Score,
|
clothingOnly.Score,
|
||||||
)
|
)
|
||||||
@ -210,10 +406,10 @@ func TestComputeHighlightRatingExplicitSignalsBoostPositions(t *testing.T) {
|
|||||||
|
|
||||||
func TestComputeHighlightRatingExplicitContextCanElevateStrongPositions(t *testing.T) {
|
func TestComputeHighlightRatingExplicitContextCanElevateStrongPositions(t *testing.T) {
|
||||||
positions := []aiSegmentMeta{
|
positions := []aiSegmentMeta{
|
||||||
ratingTestSegment("position:doggy", 20, 25, 0.82),
|
ratingTestSegment("position:cowgirl", 20, 25, 0.82),
|
||||||
ratingTestSegment("position:cowgirl", 120, 25, 0.82),
|
ratingTestSegment("position:reverse_cowgirl", 120, 25, 0.82),
|
||||||
ratingTestSegment("position:missionary", 220, 25, 0.82),
|
ratingTestSegment("position:doggy", 220, 25, 0.82),
|
||||||
ratingTestSegment("position:blowjob", 320, 25, 0.82),
|
ratingTestSegment("position:toy_play", 320, 25, 0.82),
|
||||||
}
|
}
|
||||||
positionOnly := computeHighlightRating(positions, 600)
|
positionOnly := computeHighlightRating(positions, 600)
|
||||||
withExplicitContext := computeHighlightRating(append(
|
withExplicitContext := computeHighlightRating(append(
|
||||||
|
|||||||
@ -1481,11 +1481,8 @@ func trainingImportVideoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ab hier existiert das erste echte extrahierte Bild.
|
// Nach jeder erfolgreichen Extraktion das aktuell verarbeitete Frame zeigen.
|
||||||
// Dieses bleibt als Overlay-Background für Analyse/Speichern/weitere Frames.
|
previewURL = "/api/training/frame?id=" + url.QueryEscape(id)
|
||||||
if previewURL == "" {
|
|
||||||
previewURL = "/api/training/frame?id=" + url.QueryEscape(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
trainingPublishAnalysisStepWithPreview(
|
trainingPublishAnalysisStepWithPreview(
|
||||||
requestID,
|
requestID,
|
||||||
|
|||||||
@ -138,6 +138,17 @@ export const RATING_VALUE_LABELS: Record<string, string> = {
|
|||||||
stars: 'Sterne',
|
stars: 'Sterne',
|
||||||
score: 'Score',
|
score: 'Score',
|
||||||
actionScore: 'Action',
|
actionScore: 'Action',
|
||||||
|
bestPosition: 'Top-Position',
|
||||||
|
bestPositionRank: 'Positionsrang',
|
||||||
|
bestClothing: 'Top-Kleidung',
|
||||||
|
bestClothingRank: 'Kleidungsrang',
|
||||||
|
positionScore: 'Position',
|
||||||
|
clothingScore: 'Kleidung',
|
||||||
|
durationScore: 'Dauer',
|
||||||
|
strengthScore: 'Stärke',
|
||||||
|
coverageScore: 'Abdeckung',
|
||||||
|
contextScore: 'Kontext',
|
||||||
|
varietyScore: 'Vielfalt',
|
||||||
confidence: 'Konfidenz',
|
confidence: 'Konfidenz',
|
||||||
probability: 'Wahrscheinlichkeit',
|
probability: 'Wahrscheinlichkeit',
|
||||||
nsfw: 'NSFW',
|
nsfw: 'NSFW',
|
||||||
|
|||||||
@ -767,13 +767,13 @@ function TrainingStageOverlay(props: {
|
|||||||
}) {
|
}) {
|
||||||
const progress = clampPercent(props.progress ?? 0)
|
const progress = clampPercent(props.progress ?? 0)
|
||||||
const isTraining = props.mode === 'training'
|
const isTraining = props.mode === 'training'
|
||||||
const hasBackground = !isTraining && Boolean(props.backgroundUrl)
|
const hasBackground = Boolean(props.backgroundUrl)
|
||||||
const visible = props.visible ?? true
|
const visible = props.visible ?? true
|
||||||
|
|
||||||
const [backgroundVisible, setBackgroundVisible] = useState(false)
|
const [backgroundVisible, setBackgroundVisible] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.backgroundUrl || isTraining) {
|
if (!props.backgroundUrl) {
|
||||||
setBackgroundVisible(false)
|
setBackgroundVisible(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -785,7 +785,7 @@ function TrainingStageOverlay(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => window.cancelAnimationFrame(frame)
|
return () => window.cancelAnimationFrame(frame)
|
||||||
}, [props.backgroundUrl, isTraining])
|
}, [props.backgroundUrl])
|
||||||
|
|
||||||
const title = isTraining ? 'Training läuft…' : 'Analyse läuft…'
|
const title = isTraining ? 'Training läuft…' : 'Analyse läuft…'
|
||||||
const fallbackText = isTraining
|
const fallbackText = isTraining
|
||||||
@ -5685,9 +5685,9 @@ export default function TrainingTab(props: {
|
|||||||
progress={stageOverlayProgress}
|
progress={stageOverlayProgress}
|
||||||
visible={stageOverlayIsVisible}
|
visible={stageOverlayIsVisible}
|
||||||
backgroundUrl={
|
backgroundUrl={
|
||||||
stageOverlayMode === 'analysis'
|
stageOverlayMode === 'training'
|
||||||
? loadingPreviewBackgroundUrl
|
? imageSrc
|
||||||
: undefined
|
: loadingPreviewBackgroundUrl
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user