updated rating
This commit is contained in:
parent
c6e518105b
commit
9f3ae43f8a
@ -107,6 +107,7 @@ type aiSegmentMeta struct {
|
||||
EndSeconds float64 `json:"endSeconds"`
|
||||
DurationSeconds float64 `json:"durationSeconds"`
|
||||
Score float64 `json:"score,omitempty"`
|
||||
RatingIntensity float64 `json:"ratingIntensity,omitempty"`
|
||||
AutoSelected bool `json:"autoSelected,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
type aiRatingMeta struct {
|
||||
Score float64 `json:"score"`
|
||||
ActionScore float64 `json:"actionScore"`
|
||||
Stars int `json:"stars"`
|
||||
Segments int `json:"segments"`
|
||||
SegmentsPerMinute float64 `json:"segmentsPerMinute"`
|
||||
@ -1130,6 +1131,7 @@ func mergeRatingActivitySegments(
|
||||
return aiSegmentMeta{
|
||||
Label: label,
|
||||
Score: ratingClamp01(score),
|
||||
RatingIntensity: ratingClamp01(sev * ratingConfidenceWeight(score)),
|
||||
StartSeconds: start,
|
||||
EndSeconds: end,
|
||||
DurationSeconds: ratingDuration,
|
||||
@ -1276,6 +1278,43 @@ func ratingExplicitContextBonus(
|
||||
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(
|
||||
peakQuality float64,
|
||||
positionEffectiveWeighted float64,
|
||||
@ -1539,6 +1578,15 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
||||
0.15*coverageNorm +
|
||||
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
|
||||
// range reserved for sustained position detections.
|
||||
if contextFlagged < 20.0 {
|
||||
@ -1549,6 +1597,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
||||
score := ratingRound(ratingClamp01(raw)*100, 1)
|
||||
|
||||
r.Score = score
|
||||
r.ActionScore = ratingRound(actionRaw*100, 1)
|
||||
r.Stars = starsFromHighlightScore(score)
|
||||
r.Segments = contextN
|
||||
r.SegmentsPerMinute = ratingRound(contextSegmentsPerMinute, 2)
|
||||
@ -1606,6 +1655,15 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
||||
0.06*coverageNorm +
|
||||
0.02*positionVarietyNorm
|
||||
|
||||
actionRaw := ratingPositionActionScore(
|
||||
positionDurationNorm,
|
||||
longestNorm,
|
||||
positionDensityNorm,
|
||||
coverageNorm,
|
||||
peakNorm,
|
||||
)
|
||||
raw = ratingBlendWithAction(raw, actionRaw, 0.30)
|
||||
|
||||
explicitContextBonus := ratingExplicitContextBonus(
|
||||
bodyEffectiveWeighted,
|
||||
clothingEffectiveWeighted,
|
||||
@ -1635,6 +1693,7 @@ func computeHighlightRatingWithUsername(segments []aiSegmentMeta, durationSec fl
|
||||
score := ratingRound(raw*100, 1)
|
||||
|
||||
r.Score = score
|
||||
r.ActionScore = ratingRound(actionRaw*100, 1)
|
||||
r.Stars = starsFromHighlightScore(score)
|
||||
r.Segments = n
|
||||
r.SegmentsPerMinute = ratingRound(segmentsPerMinute, 2)
|
||||
|
||||
@ -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) {
|
||||
clothingOnly := computeHighlightRating([]aiSegmentMeta{
|
||||
ratingTestSegment("clothing:lingerie", 10, 60, 0.90),
|
||||
@ -268,4 +292,19 @@ func TestPrepareAIRatingSegmentsMergesSamePositionAcrossShortGap(t *testing.T) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +137,7 @@ export const AI_LABEL_FALLBACKS: Record<string, string> = {
|
||||
export const RATING_VALUE_LABELS: Record<string, string> = {
|
||||
stars: 'Sterne',
|
||||
score: 'Score',
|
||||
actionScore: 'Action',
|
||||
confidence: 'Konfidenz',
|
||||
probability: 'Wahrscheinlichkeit',
|
||||
nsfw: 'NSFW',
|
||||
@ -452,4 +453,4 @@ export function isDerivedFilterTag(tag: unknown): boolean {
|
||||
'kurz',
|
||||
'lang',
|
||||
].includes(key)
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,18 @@ function readScore(value: unknown): number {
|
||||
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 = {
|
||||
label: string
|
||||
scoreWeight: number
|
||||
@ -115,16 +127,34 @@ function weightedLabelFromValue(value: unknown): WeightedDetectedLabel | null {
|
||||
if (group === 'position') {
|
||||
return {
|
||||
label: prettyAiLabel(normalized),
|
||||
scoreWeight: 1.0,
|
||||
priority: 2,
|
||||
scoreWeight: 0.82,
|
||||
priority: 4,
|
||||
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') {
|
||||
return {
|
||||
label: prettyAiLabel(normalized),
|
||||
scoreWeight: 0.58,
|
||||
scoreWeight: 0.28,
|
||||
priority: 1,
|
||||
source: 'clothing',
|
||||
}
|
||||
@ -237,56 +267,45 @@ export function buildVideoHeatmapSegments(
|
||||
? (ai?.segments ?? ai?.Segments)
|
||||
: []
|
||||
|
||||
for (const item of segments) {
|
||||
if (!isPlainObject(item)) continue
|
||||
const appendItems = (items: unknown[]) => {
|
||||
for (const item of items) {
|
||||
if (!isPlainObject(item)) continue
|
||||
|
||||
const detected = readWeightedLabel(item)
|
||||
if (!detected) continue
|
||||
const detected = readWeightedLabel(item)
|
||||
if (!detected) continue
|
||||
|
||||
const rawScore = readScore(
|
||||
item.score ?? item.Score ?? item.confidence
|
||||
)
|
||||
const ratingIntensity = readOptionalScore(
|
||||
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,
|
||||
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,
|
||||
intensity,
|
||||
detected.source
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const hits = Array.isArray(ai?.hits ?? ai?.Hits)
|
||||
? (ai?.hits ?? ai?.Hits)
|
||||
: []
|
||||
appendItems(segments)
|
||||
|
||||
for (const item of hits) {
|
||||
if (!isPlainObject(item)) continue
|
||||
|
||||
const detected = readWeightedLabel(item)
|
||||
if (!detected) continue
|
||||
|
||||
const rawScore = readScore(
|
||||
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
|
||||
)
|
||||
// Raw hits are the source of the condensed segments and would otherwise
|
||||
// paint the same activity twice. Keep them only for legacy metadata.
|
||||
if (out.length === 0) {
|
||||
const hits = Array.isArray(ai?.hits ?? ai?.Hits)
|
||||
? (ai?.hits ?? ai?.Hits)
|
||||
: []
|
||||
appendItems(hits)
|
||||
}
|
||||
|
||||
return mergeHeatmapSegments(out, durationSec)
|
||||
@ -326,4 +345,4 @@ function mergeHeatmapSegments(
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user