Four root causes behind zero moments, measured against your production data - #17
Merged
Merged
Conversation
…n data Diagnosed by pulling the real project database off the prod volume and running the actual scorer over 9961 tracks from a five-minute game. Every number below is from that data, not a fixture. Detection appended tracks instead of replacing them, so each re-analysis layered another copy over the last: 9961 stored tracks collapse to 2790 distinct ones, and three copies of the same 33.334s/364-point track sat in the top ten. clearTracks now replaces a video's tracks, and because focal_track_id is a bare column with no foreign key it also clears the binding and warns that the athlete needs re-identifying. buildContext took the first ball track and ignored the rest. A detector that only glimpses the ball emits it as many short fragments — 56 of them, totalling 10.1 seconds of 300, longest 0.4s — so the two signals carrying 0.45 of the weight read one arbitrary fragment. All fragments are now considered, and when several are live the one nearest the athlete wins. Those signals also returned 0 when no ball was visible, which kept their weight in the denominator while contributing nothing to the numerator. An unseen ball is unmeasured, not stationary; they now return null. Worth 0.251 to 0.305 on the real data. The tracker splits one child into many tracks, so the picker showed Sam as several people and binding one followed 23.9 seconds of a 300-second game. An athlete can now be identified across several fragments — tracks.athlete_id already existed and nothing wrote it — and scoring stitches them into one series, keeping the more confident observation where fragments overlap and refusing to interpolate across a gap, since a straight line through a hole puts a child on the court while they sat on the bench. The cameraman still interpolates freely; the cap applies only to the focal athlete. Measured end to end on Sam's game: Sam = 1 track (as prod is today) best 0.305 0 moments Sam stitched from 3 tracks best 0.419 3 moments Sam stitched from 12 tracks best 0.435 5 moments What this does not fix: the shipped detector cannot see a basketball. At 1920x1080 downscaled to a 540p proxy and then to the model's fixed 416x416 input, the ball is about six pixels across, below YOLOX-Tiny's finest stride of eight. Ball proximity will stay mostly dark until detection runs at higher resolution or on tiles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ralyodio
force-pushed
the
fix-root-causes
branch
from
August 10, 2026 05:28
b4344a9 to
7628c42
Compare
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You said stop working around it. So I pulled the real project database off the prod volume and ran the actual scorer over your 9961 tracks. Every number here is from Sam's game, not a fixture.
Four root causes
1. Tracks accumulate. Detection appended instead of replacing, so each re-analysis layered another copy over the last.
clearTracksnow replaces a video's tracks. Becausefocal_track_idis a bare column with no foreign key, it also clears the binding and warns you to re-identify — otherwise scoring keeps an anchor pointed at a deleted row.2. Only the first ball track was ever read.
tracks.find(t => t.className === 'ball'). Your detector emits the ball as 56 fragments totalling 10.1 seconds of 300, longest 0.4s — so the signals carrying 0.45 of the weight read one arbitrary fragment. All fragments now count; when several are live, the one nearest the athlete wins.3. An unseen ball scored zero instead of nothing. That kept 0.45 of weight in the denominator contributing nothing to the numerator, for 97% of the game. Absence of evidence isn't evidence of a dull moment. Worth 0.251 → 0.305 on your data.
4. Sam is not one track. The tracker splits a child into fragments; the picker showed him as several people; binding one followed 23.9 seconds of a 300-second game. You spotted this — "there's 2-3 photos of him".
An athlete can now be identified across several fragments.
tracks.athlete_idalready existed and nothing wrote it. Scoring stitches them into one series, keeps the more confident observation where fragments overlap, and refuses to interpolate across a gap — a straight line through a hole puts a child on the court while he sat on the bench. The virtual cameraman still interpolates freely; the cap is focal-only.Measured on your game
Coverage 23.9s → 93s → 99s. Caveat worth stating: I selected those extra fragments by duration, not by verifying they are Sam. The mechanism is proven; the exact scores depend on which crops are actually him.
What this does not fix
The detector cannot see a basketball. 1920×1080 → 540p proxy → the model's fixed 416×416 input makes the ball roughly six pixels, below YOLOX-Tiny's finest stride of eight. It is under the model's resolution floor, which is why 10 seconds of 300.
That caps this footage at 0.435 against a 0.35 threshold, on player acceleration and scene motion alone — moments clear by 0.07. Thin. The durable fix is detection at higher effective resolution: a 640-input model, or tiling each frame into quarters at 416 (≈4× inference, ~8 min for a 5-minute clip). I did not guess at that here because it needs validating against your footage, not asserted.
Also unused: your video has an audio stream and nothing extracts it, so
audio_spike(weight 0.1) has been permanently dark.Verification
423 tests pass (14 new), lint and typecheck clean. The prod numbers come from
explainScoringandcomputeMomentsrun over the exported tracks.One correction made along the way: my first version capped interpolation globally and broke the virtual cameraman, which legitimately bridges sparse keyframes. The cap is now opt-in and applies only to the focal athlete.