From 9fd30224d6e0e8c71429ef33681b44c5d4b9855b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 10 Aug 2026 13:31:19 +0000 Subject: [PATCH] fix: a moment your athlete is not in is not your athlete's moment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production, with everything else working: seven suggested moments, of which five contained no trace of the child they were supposedly of. He was on screen from 218s; the moments were at 49s, 68s, 158s, 175s and 182s. Every one was a real scramble under the rim and every one was somebody else's kid. Two of the seven signals — `activity_near_goal` and `high_motion` — read the whole scene and never look at the athlete. Between them they carry 0.25 of weight against a 0.35 threshold and a 0.575 denominator floor, which is enough to clear it unaided. So the scorer was answering "was anything happening?" when the question is "was anything happening to *him*?", and the product promise is the second one. A highlight reel of the game is not the thing anyone came here for. Once somebody has said which child is theirs, a window they are not in scores zero. Scene signals still contribute — a scramble the athlete is *in* is exactly what to keep — they simply cannot carry a window alone. With no athlete identified nothing changes: there is no one to be absent, and the denominator floor remains what stops scene motion from carrying a moment. Re-scored against the production project, read-only: before 7 moments, 5 of them with no athlete in frame after 2 moments 248.0-261.0s 0.602 [player_acceleration, high_motion, ball_approaching_player, toward_goal, activity_near_goal] 263.0-272.0s 0.501 [player_ball_proximity] Fewer clips, and all of them him. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/scoring.ts | 20 ++++++ packages/core/src/theirmoments.test.ts | 97 ++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packages/core/src/theirmoments.test.ts diff --git a/packages/core/src/scoring.ts b/packages/core/src/scoring.ts index 79086c1..c43245c 100644 --- a/packages/core/src/scoring.ts +++ b/packages/core/src/scoring.ts @@ -423,6 +423,26 @@ export const scoreWindow = ( plugin: SportPlugin, ts: number, ): WindowScore => { + /** + * Once somebody has said which child is theirs, a moment they are not in is + * not their moment. + * + * Two of the seven signals — `activity_near_goal` and `high_motion` — read the + * whole scene and never look at the athlete at all. Between them they can + * clear the threshold on their own, so a run with an athlete identified for + * 51s of a 300s game returned seven moments of which five contained no trace + * of him: 49s, 68s, 158s, 175s, 182s, against an athlete on screen from 218s. + * Every one was a real scramble near the rim, and every one was somebody + * else's child. That is a highlight reel of the game, which is not the thing + * anyone came here for. + * + * Scene signals still contribute — a scramble the athlete is *in* is exactly + * what to keep — they simply cannot carry a window on their own any more. + */ + if (context.focal !== null && focalAt(context, ts) === null) { + return { ts, score: 0, reasons: [] }; + } + const weights = ruleWeights(plugin); const reasons: string[] = []; let total = 0; diff --git a/packages/core/src/theirmoments.test.ts b/packages/core/src/theirmoments.test.ts new file mode 100644 index 0000000..7c2a183 --- /dev/null +++ b/packages/core/src/theirmoments.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import { getSport } from '@reeleel/sports'; + +import { computeMoments, scoreWindow, buildContext } from './scoring.js'; +import type { ScoringInput, TrackSeries } from './scoring.js'; + +/** + * Whose highlights are these? + * + * `activity_near_goal` and `high_motion` read the whole scene and never look at + * the athlete. Between them they cleared the threshold on their own, so a + * production run with the athlete identified from 218s returned seven moments + * of which five — 49s, 68s, 158s, 175s, 182s — contained no trace of him. They + * were real scrambles near the rim, and every one of them was somebody else's + * child. + */ + +const DURATION = 300; +const plugin = getSport('basketball')!; + +const dense = (from: number, to: number, fn: (ts: number) => { x: number; y: number }) => { + const out = []; + for (let ts = from; ts <= to; ts += 1 / 30) { + out.push({ ts: Number(ts.toFixed(3)), ...fn(ts), w: 60, h: 150, confidence: 0.9 }); + } + return out; +}; + +/** The athlete, on screen only in the second half of the game. */ +const focal: TrackSeries = { + id: 'trk_focal', + className: 'player', + samples: dense(218, 273, (ts) => ({ x: 400 + Math.sin(ts) * 300, y: 500 })), +}; + +/** A crowd that is busy near the rim all game long, athlete or no athlete. */ +const crowd = Array.from({ length: 6 }, (_unused, i) => ({ + id: `trk_crowd_${i}`, + className: 'player', + samples: dense(0, DURATION, (ts) => ({ + x: 1450 + Math.sin(ts * 3 + i) * 90, + y: 250 + Math.cos(ts * 2 + i) * 80, + })), +})) as TrackSeries[]; + +const hoop: TrackSeries = { + id: 'trk_hoop', + className: 'hoop', + samples: dense(0, DURATION, () => ({ x: 1500, y: 200 })), +}; + +const input = (focalTrackIds: string[]): ScoringInput => ({ + durationSeconds: DURATION, + frameWidth: 1920, + frameHeight: 1080, + focalTrackId: focalTrackIds[0] ?? null, + focalTrackIds, + tracks: [focal, ...crowd, hoop], +}); + +describe('a moment the athlete is not in', () => { + const bound = buildContext(input(['trk_focal']), plugin.targetClass); + + it('scores nothing while the athlete is off screen, however busy the court', () => { + // 100s: six players packed under the rim, and no sign of the athlete. + expect(scoreWindow(bound, plugin, 100).score).toBe(0); + expect(scoreWindow(bound, plugin, 100).reasons).toEqual([]); + }); + + it('still scores while the athlete is on screen', () => { + expect(scoreWindow(bound, plugin, 240).score).toBeGreaterThan(0); + }); + + it('suggests nothing outside the athlete’s time on court', () => { + for (const moment of computeMoments(input(['trk_focal']), plugin)) { + // Pre/post roll may reach a little past them, but not to another half. + expect(moment.end).toBeGreaterThan(210); + expect(moment.start).toBeLessThan(280); + } + }); + + it('keeps a scramble the athlete is part of', () => { + // The scene signals are not banned, they just cannot carry a window alone. + const window = scoreWindow(bound, plugin, 240); + expect(window.reasons.length).toBeGreaterThan(0); + }); +}); + +describe('with nobody identified', () => { + it('leaves the old behaviour alone', () => { + // Unchanged: without a focal track there is no one to be absent, and the + // denominator floor is what keeps scene motion from carrying a moment. + const context = buildContext(input([]), plugin.targetClass); + expect(scoreWindow(context, plugin, 100).score).toBeLessThan(plugin.moments.minScore); + }); +});