diff --git a/backend/rating.go b/backend/rating.go index b0014af..779839b 100644 --- a/backend/rating.go +++ b/backend/rating.go @@ -874,6 +874,12 @@ func mergeRatingActivitySegments( return valid[i].Label < valid[j].Label }) + type segmentBounds struct { + start float64 + end float64 + ok bool + } + type activityBlock struct { start float64 end float64 @@ -881,13 +887,28 @@ func mergeRatingActivitySegments( scoreWeight float64 labels map[string]bool positionWeights map[string]float64 + positionBounds map[string]segmentBounds marker float64 markerWeight float64 } - var addRatingPositionWeight func(weights map[string]float64, label string, weight float64) + var addRatingPositionSignal func( + weights map[string]float64, + bounds map[string]segmentBounds, + label string, + weight float64, + start float64, + end float64, + ) - addRatingPositionWeight = func(weights map[string]float64, label string, weight float64) { + addRatingPositionSignal = func( + weights map[string]float64, + bounds map[string]segmentBounds, + label string, + weight float64, + start float64, + end float64, + ) { label = strings.ToLower(strings.TrimSpace(label)) if label == "" { return @@ -896,18 +917,45 @@ func mergeRatingActivitySegments( if strings.HasPrefix(label, "combo:") { raw := strings.TrimPrefix(label, "combo:") for _, part := range strings.Split(raw, "+") { - addRatingPositionWeight(weights, part, weight) + addRatingPositionSignal(weights, bounds, part, weight, start, end) } return } normalized := normalizeRatingSignalLabel(label) - if strings.HasPrefix(normalized, "position:") { - weights[normalized] += weight + if !strings.HasPrefix(normalized, "position:") { + return } + + if end < start { + end = start + } + + weights[normalized] += weight + + b := bounds[normalized] + if !b.ok { + b = segmentBounds{ + start: start, + end: end, + ok: true, + } + } else { + if start < b.start { + b.start = start + } + if end > b.end { + b.end = end + } + } + + bounds[normalized] = b } - effectiveRatingLabelsForBlock := func(labels map[string]bool, positionWeights map[string]float64) map[string]bool { + effectiveRatingLabelsForBlock := func( + labels map[string]bool, + positionWeights map[string]float64, + ) (map[string]bool, string) { out := map[string]bool{} bestPosition := "" @@ -940,7 +988,7 @@ func mergeRatingActivitySegments( out[bestPosition] = true } - return out + return out, bestPosition } segmentMarker := func(s aiSegmentMeta) float64 { @@ -981,7 +1029,16 @@ func mergeRatingActivitySegments( } positionWeights := map[string]float64{} - addRatingPositionWeight(positionWeights, s.Label, conf*math.Max(1, dur)) + positionBounds := map[string]segmentBounds{} + + addRatingPositionSignal( + positionWeights, + positionBounds, + s.Label, + conf*math.Max(1, dur), + s.StartSeconds, + s.EndSeconds, + ) marker := segmentMarker(s) markerWeight := segmentMarkerWeight(s) @@ -993,6 +1050,7 @@ func mergeRatingActivitySegments( scoreWeight: dur, labels: labels, positionWeights: positionWeights, + positionBounds: positionBounds, marker: marker, markerWeight: markerWeight, } @@ -1004,7 +1062,7 @@ func mergeRatingActivitySegments( return aiSegmentMeta{}, false } - effectiveLabels := effectiveRatingLabelsForBlock(b.labels, b.positionWeights) + effectiveLabels, bestPosition := effectiveRatingLabelsForBlock(b.labels, b.positionWeights) label := buildRatingComboLabel(effectiveLabels) if label == "" { return aiSegmentMeta{}, false @@ -1012,6 +1070,24 @@ func mergeRatingActivitySegments( sev := ratingSegmentSeverity(label) + start := b.start + + // Wenn der finale Block eine Position trägt, soll die Anzeige/der Klick + // beim ersten echten Auftreten dieser Position starten — nicht schon bei + // früheren Body-/Object-Signalen aus demselben Aktivitätsblock. + if bestPosition != "" { + if bounds, ok := b.positionBounds[bestPosition]; ok && bounds.ok { + if bounds.start > start { + start = bounds.start + } + } + } + + dur = b.end - start + if dur <= 0 { + return aiSegmentMeta{}, false + } + // Kurze Segmente dürfen bleiben, wenn sie stark genug sind. if dur < minDurationSec && sev < 0.72 { return aiSegmentMeta{}, false @@ -1028,7 +1104,7 @@ func mergeRatingActivitySegments( return aiSegmentMeta{ Label: label, Score: ratingClamp01(score), - StartSeconds: b.start, + StartSeconds: start, EndSeconds: b.end, DurationSeconds: dur, AutoSelected: true, @@ -1095,7 +1171,14 @@ func mergeRatingActivitySegments( cur.scoreSum += conf * dur cur.scoreWeight += dur - addRatingPositionWeight(cur.positionWeights, n.Label, conf*math.Max(1, dur)) + addRatingPositionSignal( + cur.positionWeights, + cur.positionBounds, + n.Label, + conf*math.Max(1, dur), + n.StartSeconds, + n.EndSeconds, + ) } nextMarkerWeight := segmentMarkerWeight(n) diff --git a/frontend/src/components/ui/RatingOverlay.tsx b/frontend/src/components/ui/RatingOverlay.tsx index 959c0a8..ea29f07 100644 --- a/frontend/src/components/ui/RatingOverlay.tsx +++ b/frontend/src/components/ui/RatingOverlay.tsx @@ -518,7 +518,7 @@ function readMetaDurationSeconds(metaRaw: unknown): number { return Number.isFinite(n) && n > 0 ? n : 0 } -const RATING_SEGMENT_OPEN_PREROLL_SECONDS = 3 +const RATING_SEGMENT_OPEN_PREROLL_SECONDS = 0 function segmentAnchorTime( obj: Record, @@ -603,10 +603,41 @@ function segmentPreviewTime( return undefined } +function segmentStartTime( + obj: Record, + durationSeconds: number +): number | undefined { + return firstSegmentTimeNumber(obj, [ + 'openAtSec', + 'openSeconds', + 'seekSeconds', + 'seekSec', + 'jumpSeconds', + 'jumpSec', + 'startSec', + 'startSeconds', + 'start_s', + 'start', + 'from', + 'startMs', + 'startRatio', + 'startPercent', + 'startPct', + ], durationSeconds) +} + function segmentOpenTime( obj: Record, durationSeconds: number ): number | undefined { + // Klick soll auf den Segment-Anfang springen, nicht auf den Preview-/Peak-Marker. + const start = segmentStartTime(obj, durationSeconds) + + if (typeof start === 'number') { + return Math.max(0, start - RATING_SEGMENT_OPEN_PREROLL_SECONDS) + } + + // Fallback für alte Daten ohne startSeconds. const anchor = segmentAnchorTime(obj, durationSeconds) ?? segmentPreviewTime(obj, durationSeconds)