Skip to content

Give scoring something to anchor on: focal athlete, per-sport target class, thread cap - #14

Merged
ralyodio merged 1 commit into
masterfrom
scoring-anchors
Aug 9, 2026
Merged

Give scoring something to anchor on: focal athlete, per-sport target class, thread cap#14
ralyodio merged 1 commit into
masterfrom
scoring-anchors

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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_motion at 0.1.

scenario best possible score threshold
no athlete marked 0.087 0.35
with a focal athlete 0.609 0.35

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 goal whatever the sport

Basketball tracks a hoop, hockey a net, football an end_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:

baseball    base        football    end_zone
basketball  hoop        hockey      net
soccer      goal        volleyball  net

A sport with no trackable target resolves to null rather than a name nothing will ever match.

3. The thread cap was tuned on the wrong machine

worker: threads: using 4 (cgroup-aware 24, visible cores 48)

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_THREADS overrides 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.

… 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>
@ralyodio
ralyodio merged commit bbc16fe into master Aug 9, 2026
4 checks passed
@ralyodio
ralyodio deleted the scoring-anchors branch August 9, 2026 23:46
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant