updated rating

This commit is contained in:
Linrador 2026-06-13 23:22:15 +02:00
parent c6e518105b
commit 9f3ae43f8a
5 changed files with 161 additions and 42 deletions

View File

@ -107,6 +107,7 @@ type aiSegmentMeta struct {
EndSeconds float64 `json:"endSeconds"` EndSeconds float64 `json:"endSeconds"`
DurationSeconds float64 `json:"durationSeconds"` DurationSeconds float64 `json:"durationSeconds"`
Score float64 `json:"score,omitempty"` Score float64 `json:"score,omitempty"`
RatingIntensity float64 `json:"ratingIntensity,omitempty"`
AutoSelected bool `json:"autoSelected,omitempty"` AutoSelected bool `json:"autoSelected,omitempty"`
Position string `json:"position,omitempty"` Position string `json:"position,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`

View File

@ -10,6 +10,7 @@ import (
type aiRatingMeta struct { type aiRatingMeta struct {
Score float64 `json:"score"` Score float64 `json:"score"`
ActionScore float64 `json:"actionScore"`
Stars int `json:"stars"` Stars int `json:"stars"`
Segments int `json:"segments"` Segments int `json:"segments"`
SegmentsPerMinute float64 `json:"segmentsPerMinute"` SegmentsPerMinute float64 `json:"segmentsPerMinute"`
@ -1130,6 +1131,7 @@ func mergeRatingActivitySegments(
return aiSegmentMeta{ return aiSegmentMeta{
Label: label, Label: label,
Score: ratingClamp01(score), Score: ratingClamp01(score),
RatingIntensity: ratingClamp01(sev * ratingConfidenceWeight(score)),
StartSeconds: start, StartSeconds: start,
EndSeconds: end, EndSeconds: end,
DurationSeconds: ratingDuration, DurationSeconds: ratingDuration,
@ -1276,6 +1278,43 @@ func ratingExplicitContextBonus(
return math.Min(bonus, 0.10) return math.Min(bonus, 0.10)
} }
func ratingBlendWithAction(base, action, actionWeight float64) float64 {
actionWeight = ratingClamp01(actionWeight)
return ratingClamp01((1-actionWeight)*base + actionWeight*action)
}
func ratingPositionActionScore(
durationNorm float64,
longestNorm float64,
densityNorm float64,
coverageNorm float64,
peakNorm float64,
) float64 {
return ratingClamp01(
0.38*durationNorm +
0.24*longestNorm +
0.18*densityNorm +
0.12*coverageNorm +
0.08*peakNorm,
)
}
func ratingContextActionScore(
durationNorm float64,
longestNorm float64,
coverageNorm float64,
densityNorm float64,
peakNorm float64,
) float64 {
return ratingClamp01(
0.40*durationNorm +
0.25*longestNorm +
0.20*coverageNorm +
0.10*densityNorm +
0.05*peakNorm,
)
}
func ratingExcellenceBonus( func ratingExcellenceBonus(
peakQuality float64, peakQuality float64,
positionEffectiveWeighted float64, positionEffectiveWeighted float64,
@ -1539,6 +1578,15 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
0.15*coverageNorm + 0.15*coverageNorm +
0.05*densityNorm 0.05*densityNorm
actionRaw := ratingContextActionScore(
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 // Context can produce useful 1-3 star ratings, but never the 4-5 star
// range reserved for sustained position detections. // range reserved for sustained position detections.
if contextFlagged < 20.0 { if contextFlagged < 20.0 {
@ -1549,6 +1597,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
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.Stars = starsFromHighlightScore(score) r.Stars = starsFromHighlightScore(score)
r.Segments = contextN r.Segments = contextN
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2) r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
@ -1606,6 +1655,15 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
0.06*coverageNorm + 0.06*coverageNorm +
0.02*positionVarietyNorm 0.02*positionVarietyNorm
actionRaw := ratingPositionActionScore(
positionDurationNorm,
longestNorm,
positionDensityNorm,
coverageNorm,
peakNorm,
)
raw = ratingBlendWithAction(raw, actionRaw, 0.30)
explicitContextBonus := ratingExplicitContextBonus( explicitContextBonus := ratingExplicitContextBonus(
bodyEffectiveWeighted, bodyEffectiveWeighted,
clothingEffectiveWeighted, clothingEffectiveWeighted,
@ -1635,6 +1693,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
score := ratingRound(raw*100, 1) score := ratingRound(raw*100, 1)
r.Score = score r.Score = score
r.ActionScore = ratingRound(actionRaw*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)

View File

@ -98,6 +98,30 @@ func TestComputeHighlightRatingRewardsPositionDuration(t *testing.T) {
} }
} }
func TestComputeHighlightRatingActionPrefersSustainedActivity(t *testing.T) {
briefPeak := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 8, 0.95),
}, 600)
sustained := computeHighlightRating([]aiSegmentMeta{
ratingTestSegment("position:doggy", 10, 45, 0.55),
}, 600)
if sustained.ActionScore <= briefPeak.ActionScore {
t.Fatalf(
"sustained action should outrank a brief peak: sustained=%.1f brief=%.1f",
sustained.ActionScore,
briefPeak.ActionScore,
)
}
if sustained.Score <= briefPeak.Score {
t.Fatalf(
"rating should follow sustained action: sustained=%.1f brief=%.1f",
sustained.Score,
briefPeak.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),
@ -268,4 +292,19 @@ func TestPrepareAIRatingSegmentsMergesSamePositionAcrossShortGap(t *testing.T) {
segments[0].DurationSeconds, segments[0].DurationSeconds,
) )
} }
if segments[0].RatingIntensity <= 0 {
t.Fatalf("rating intensity = %.3f, want a positive heatcurve value", segments[0].RatingIntensity)
}
wantIntensity := ratingClamp01(
ratingSegmentSeverity(segments[0].Label) *
ratingConfidenceWeight(segments[0].Score),
)
if diff := segments[0].RatingIntensity - wantIntensity; diff < -0.0001 || diff > 0.0001 {
t.Fatalf(
"rating intensity = %.4f, want %.4f",
segments[0].RatingIntensity,
wantIntensity,
)
}
} }

View File

@ -137,6 +137,7 @@ export const AI_LABEL_FALLBACKS: Record<string, string> = {
export const RATING_VALUE_LABELS: Record<string, string> = { export const RATING_VALUE_LABELS: Record<string, string> = {
stars: 'Sterne', stars: 'Sterne',
score: 'Score', score: 'Score',
actionScore: 'Action',
confidence: 'Konfidenz', confidence: 'Konfidenz',
probability: 'Wahrscheinlichkeit', probability: 'Wahrscheinlichkeit',
nsfw: 'NSFW', nsfw: 'NSFW',
@ -452,4 +453,4 @@ export function isDerivedFilterTag(tag: unknown): boolean {
'kurz', 'kurz',
'lang', 'lang',
].includes(key) ].includes(key)
} }

View File

@ -77,6 +77,18 @@ function readScore(value: unknown): number {
return 0.35 return 0.35
} }
function readOptionalScore(value: unknown): number | null {
if (value == null || value === '') return null
const n = Number(value)
if (!Number.isFinite(n)) return null
if (n >= 0 && n <= 1) return clamp(n, 0, 1)
if (n > 1 && n <= 100) return clamp(n / 100, 0, 1)
return null
}
type WeightedDetectedLabel = { type WeightedDetectedLabel = {
label: string label: string
scoreWeight: number scoreWeight: number
@ -115,16 +127,34 @@ function weightedLabelFromValue(value: unknown): WeightedDetectedLabel | null {
if (group === 'position') { if (group === 'position') {
return { return {
label: prettyAiLabel(normalized), label: prettyAiLabel(normalized),
scoreWeight: 1.0, scoreWeight: 0.82,
priority: 2, priority: 4,
source: 'position', source: 'position',
} }
} }
if (group === 'body') {
return {
label: prettyAiLabel(normalized),
scoreWeight: 0.50,
priority: 3,
source: 'other',
}
}
if (group === 'object') {
return {
label: prettyAiLabel(normalized),
scoreWeight: 0.15,
priority: 2,
source: 'other',
}
}
if (group === 'clothing') { if (group === 'clothing') {
return { return {
label: prettyAiLabel(normalized), label: prettyAiLabel(normalized),
scoreWeight: 0.58, scoreWeight: 0.28,
priority: 1, priority: 1,
source: 'clothing', source: 'clothing',
} }
@ -237,56 +267,45 @@ export function buildVideoHeatmapSegments(
? (ai?.segments ?? ai?.Segments) ? (ai?.segments ?? ai?.Segments)
: [] : []
for (const item of segments) { const appendItems = (items: unknown[]) => {
if (!isPlainObject(item)) continue for (const item of items) {
if (!isPlainObject(item)) continue
const detected = readWeightedLabel(item) const detected = readWeightedLabel(item)
if (!detected) continue if (!detected) continue
const rawScore = readScore( const ratingIntensity = readOptionalScore(
item.score ?? item.Score ?? item.confidence item.ratingIntensity ?? item.RatingIntensity
) )
const rawScore = readScore(
item.score ?? item.Score ?? item.confidence
)
const intensity =
ratingIntensity ??
clamp(rawScore * detected.scoreWeight, 0, 1)
const weightedScore = clamp(rawScore * detected.scoreWeight, 0, 1) addSegment(
addSegment(
out, out,
item.startSeconds ?? item.StartSeconds ?? item.start ?? item.Start, item.startSeconds ?? item.StartSeconds ?? item.start ?? item.Start,
item.endSeconds ?? item.EndSeconds ?? item.end ?? item.End, item.endSeconds ?? item.EndSeconds ?? item.end ?? item.End,
item.time ?? item.Time ?? item.at ?? item.timestamp, item.time ?? item.Time ?? item.at ?? item.timestamp,
durationSec, durationSec,
detected.label, detected.label,
weightedScore, intensity,
detected.source detected.source
) )
}
} }
const hits = Array.isArray(ai?.hits ?? ai?.Hits) appendItems(segments)
? (ai?.hits ?? ai?.Hits)
: []
for (const item of hits) { // Raw hits are the source of the condensed segments and would otherwise
if (!isPlainObject(item)) continue // paint the same activity twice. Keep them only for legacy metadata.
if (out.length === 0) {
const detected = readWeightedLabel(item) const hits = Array.isArray(ai?.hits ?? ai?.Hits)
if (!detected) continue ? (ai?.hits ?? ai?.Hits)
: []
const rawScore = readScore( appendItems(hits)
item.score ?? item.Score ?? item.confidence
)
const weightedScore = clamp(rawScore * detected.scoreWeight, 0, 1)
addSegment(
out,
item.startSeconds ?? item.StartSeconds ?? item.start ?? item.Start,
item.endSeconds ?? item.EndSeconds ?? item.end ?? item.End,
item.time ?? item.Time ?? item.at ?? item.timestamp,
durationSec,
detected.label,
weightedScore,
detected.source
)
} }
return mergeHeatmapSegments(out, durationSec) return mergeHeatmapSegments(out, durationSec)
@ -326,4 +345,4 @@ function mergeHeatmapSegments(
} }
return out return out
} }