Give scoring something to anchor on: focal athlete, per-sport target class, thread cap - #14
Merged
Conversation
… nothing Three findings from a real basketball run that produced 1287 tracks and zero suggested moments. Without an athlete to follow, a moment is arithmetically impossible. Three of the seven scoring signals need a focal track and together carry 0.6 of the 1.15 total weight. With no focal track the only signal that can fire is high_motion at 0.1, so the best any window can score is 0.087 against a threshold of 0.35. The app said "Nothing suggested yet — run analysis", ran analysis for a minute, and completed with nothing — never mentioning that it could not have succeeded. It now says so before the work starts, not after. Scoring looked for a track named `goal` whatever the sport. Basketball tracks a `hoop`, hockey a `net`, football an `end_zone`, so for those sports both target signals were permanently dark even with a model that saw the thing perfectly. Soccer worked by luck of naming. Sports now declare the class that is their scoring target, derived from the noun where the two agree and stated explicitly where they do not. A sport with no trackable target resolves to null rather than to a name nothing will match. The thread cap from #10 was tuned on a four-core laptop and production turned out to have twenty-four: worker: threads: using 4 (cgroup-aware 24, visible cores 48) Those two numbers disagreeing is what #10 existed to reveal, and it was right to pin the pool — onnxruntime would have sized from 48. But four is now the limit rather than the protection, so the cap moves to eight, which is where a model this small stops benefiting rather than the size of the machine it was measured on. REELEEL_CV_THREADS overrides it without a deploy, so the ceiling can be found without another release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 10, 2026
ralyodio
added a commit
that referenced
this pull request
Aug 10, 2026
"we should use team color and names not just names — that would help with detection (ie: #14 in white team not #14 on black team)." Correct, and the column was already there: `jersey_color` has been on the athlete row since the first migration, and nothing has ever written it or shown it. So the picker offered a name — the one attribute a detector cannot help you match against — while the two things a parent actually points with, the number and the shirt, went unrecorded. Both teams have a 14 and on a school court they are regularly on screen together. Identity is now collected where the user is already looking at the child, in the picker itself, rather than in a separate "Add an athlete" form they would have to find first: name, number, shirt colour, team, all optional. Quick-identify no longer produces "My athlete" when the user told us who it was. Everywhere an athlete is named now reads "Fred #14 in white (Triton)", colour before team because colour is the part visible in the footage. The appearance matcher already separates the teams — a shirt signature is how it refuses the black team's 14 — so this makes the thing it keys on visible to the person judging its suggestions, rather than adding a second mechanism. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
that referenced
this pull request
Aug 11, 2026
…ody else (#50) Three faults from one 61-minute basketball run: some clips followed #14 on the black team instead of white, clips ended while #14 in white still had the ball, and twenty seconds of cheerleaders reached the end of the reel. **It followed the other team's 14.** `jersey_color` has been on the athlete row since the first migration and nothing but a label has ever read it, so the matcher was told a shirt and then went looking without one. What colour it did use was agreement with a running average, re-derived after every acceptance so it can follow the athlete's own lighting down the court — which also means it can be walked, one admissible step at a time, toward a shirt the athlete never wore. The walk got easier when the link gap went from two seconds to six (#49). Worse, the tracker can hand over an identity before re-identification ever sees it. Association is overlap plus a constant-velocity guess, and neither can tell one child from another; `maxAge` of 30 frames lets a track survive a second of absence. Measured on the shipped tracker: a player at x=900 who leaves at frame 30, followed at frame 45 by an opponent arriving at x=905, produces *one* track spanning both children. Nothing downstream can undo that — re-identification vets one track against another, so a track that is already two children has no seam left to find, and its signature is a blend that matches neither. So: detections now carry a torso histogram, measured in the decoded frame where the pixels and the box are both to hand, and a track that has *missed* a frame must recognise the shirt before it may claim whoever is standing there now. Consecutive frames still associate on geometry alone, where geometry is cheap and reliable. Refusing a join ends the track and starts another, which is the recoverable failure: a fragment is something the colour-vetted stitcher can put back together, and a spliced identity is not. And the declared shirt is now an absolute veto in the stitcher — white against black measures 0.28 where a shirt against itself is close to 1.0, and an absolute bar cannot be walked. Colours the panel offers are guarded against the bins the matcher has, because a suggestion it cannot bin is a promise the panel's own copy makes and nothing keeps. An unrecognised word still leaves matching exactly as permissive as it was, rather than rejecting every child in the game. **The clip ended mid-play.** A window the athlete is not in scores zero, which is right for *starting* a moment and wrong for ending one: the run flushes on the first sub-threshold window, so the clip stops when the tracker loses the child — a fact about the tracker, not about the play. On a drive reproducing the report (athlete tracked 20.0s-30.0s, ball in his hands to 40.0s) the scorer returned 17.0s-31.0s, cutting as he went up. A moment now follows the ball its athlete was holding for up to six more seconds, and when that overruns the sport's maximum the length comes out of the run-up rather than the finish: 22.0s-37.0s. **Cheerleaders.** On screen is not involved, and the presence gate only asks the former, so an athlete standing on the sideline let the two signals that never look at him carry the window: measured at 0.417 against a 0.35 threshold, reasons `activity_near_goal, high_motion`. Scene signals may now amplify a moment and never originate one — capped at the athlete's own contribution, so a scramble he is in still scores full marks while one he is watching scores nothing. With nobody identified there is nothing to measure against and behaviour is untouched. The twenty seconds came from somewhere else: `maxDurationSeconds` was applied per run and then discarded, and `mergeOverlapping` fuses any two moments whose pre/post roll touches without re-checking. Basketball's 15-second cap produced a 29-second clip out of two legal 15-second ones. The cap is now re-applied after merging, centred on the peak. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
that referenced
this pull request
Aug 12, 2026
* fix: follow #14 in white, and say so when nobody is bound to him Reported as "it's still not tracking #14 white — he'll have the ball and it fades out to a new clip". Read off the 61-minute game in production, and it is not a tracking failure at all: nothing in that run was ever looking at him. **Nobody was bound to him.** The focal athlete is "Fred, #14 in white", `focal_track_id` is null, and not one of 22,955 tracks carries his athlete id — the eight clicks that marked him are sitting on eight other athlete rows named "My athlete". Naming a child and pointing at one are different acts and only the second gives the scorer something to follow, so scoring fell through to the two signals that never look at the athlete: all 38 moments came back `activity_near_goal` + `high_motion`, each one a single window that crossed the threshold plus its 3s/2s roll — which is the five-second clip that fades out. They were then stamped with Fred's athlete id, which is the one claim the whole reel rests on and it was not true. Nothing said so. Every line of the scoring diagnosis is gated on `momentsGenerated === 0`, because that was the shape of failure this job knew about. An athlete who has been named but never pointed at produces the other shape — a full set of plausible moments about a busy gym, and "done" in green. So: `generateMoments` reports the athlete nothing was bound to, the job logs it whatever the count, and moments no signal of his contributed to no longer carry his name. **And the camera followed a stranger.** `buildClipFilter` read `athlete.focal_track_id` — one fragment of a child the tracker breaks into dozens, where scoring has stitched the whole set since the appearance matcher landed — and when that was null it fell through to `tracks.find(className === 'player')`, the first player in the list. On the export that is indistinguishable from tracking that keeps losing your kid. It now follows every fragment he was marked in, and with nobody bound it keeps the full frame rather than inventing a 55% centre crop. **Then the clip length, which is the rest of the report.** Basketball's cap was 15 seconds. A youth possession with no shot clock — inbound, walk it up, run the set, shoot — routinely runs past twenty, so the cap fired on nearly every real possession, and `capDuration` centred what it kept on the peak: it cut the finish, seconds after `flush` had explicitly moved the start forward to protect it. The cap is now the FIBA shot clock, and what it trims is the run-up, walking the start forward only as far as the peak. The tail that follows a play past the athlete also stopped on the first ball fragment to end. A basketball is about six pixels across once a 1080p frame becomes a 416x416 tensor and the detector emitted 4,067 ball fragments on this game, so "the ball track ended" is a fact about the detector. The play is over when we can see neither the ball nor the child. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: judge "is my child in this" per video, not per project Found while checking whether the production project's stray athlete rows could be folded back into the focal athlete. They cannot usefully be: all fifteen of his marked fragments are on output2.mp4, a 91-second clip, and none are on the 61-minute game the reel is made from. Which is a case the previous commit gets wrong. It asked `tracksForAthlete`, which spans the project, so an athlete marked on any footage counts as bound on all of it: the game scores on the scene signals exactly as if nobody had been identified, the moments carry his name anyway, and the warning that exists to catch precisely this stays quiet. Merging those rows would have silenced the one message that explains the reel. A track belongs to the video it was detected in, so boundness does too. Scoring now passes only the fragments belonging to the video being scored, attributes a moment only when it had them, and the warning names the footage he is missing from rather than implying he was never picked at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Three findings from your basketball run — 1,287 tracks, 34,089 detections, zero suggested moments.
1. Without an athlete to follow, a moment is arithmetically impossible
Three of the seven scoring signals need a focal track, and together they carry 0.6 of the 1.15 total weight. With no focal track the only signal that can fire is
high_motionat 0.1.Not "unlikely" — impossible. The app said "Nothing suggested yet — run analysis", ran analysis for a minute, and completed with nothing, never mentioning it could not have succeeded. It now says so before the work starts.
2. Scoring looked for
goalwhatever the sportBasketball tracks a
hoop, hockey anet, football anend_zone. For those sports both target signals were permanently dark even with a model that saw the thing perfectly. Soccer worked by luck of naming.Sports now declare their target class, derived from the noun where the two agree and stated explicitly where they don't:
A sport with no trackable target resolves to
nullrather than a name nothing will ever match.3. The thread cap was tuned on the wrong machine
Those two numbers disagreeing is exactly what #10 existed to reveal, and pinning the pool was right — onnxruntime would have sized from 48 on a 24-core allowance. But 4 is now the limit rather than the protection. The cap moves to 8, which is where a model this small stops benefiting rather than the size of the laptop it was measured on.
REELEEL_CV_THREADSoverrides it without a deploy, so the real ceiling can be found on the box that has 24 cores instead of guessed on one with 4.Verification
393 tests pass (7 new), eslint and typecheck clean. The ceiling arithmetic above was computed by running the real plugin weights through the real scoring code, not derived by hand — after a first attempt that got it wrong by counting
user_marker, which has a weight but no implementation and so never reaches the denominator.What to expect
Mark an athlete, re-run on
fast. Detection already works; this gives scoring the anchor it needs. If it still finds nothing, the log will now say whether tracks existed and the threshold is simply too high for this footage — which is a tuning conversation, not a bug hunt.