Releases: profullstack/reeleel
Release list
v0.3.0 — your athlete, and only your athlete
The release that follows your athlete through the whole game, and stops suggesting moments they were never in.
Your athlete, past the fragment you pointed at
Identifying a child only ever labelled them where you happened to look. On a real game that left an athlete known for 31.7s of 300s across six fragments — all inside the one 32-second window originally clicked. Every signal that follows the athlete was dark for the other 90%, and the moments that survived were scene-wide ones with nothing to do with them.
A jersey is the one thing about a child a detector can see that stays the same all afternoon. The CV worker now takes an appearance pass: a coarse HSV histogram of the torso, read from the 540p proxy in one pass.
Colour is a veto, not an identifier, and that is the whole design. Measured against the production game three ways:
| rule | result |
|---|---|
| colour only (≥ 0.55) | 661 of 1152 tracks — 2306s of "athlete" in a 300s video |
| continuity only | 56 tracks, 120.3s |
| both | 14 tracks, 51.2s (up from 6 tracks, 31.7s) |
Teammates wear the same shirt, so colour alone selects a team — and the children it wrongly volunteers are exactly the ones standing next to yours. Continuity alone links whoever happens to be nearby. So the identity claim rests on continuity — a fragment beginning where and when another ended, within 2s and a distance a child could actually run — with colour able only to rule a link out.
Nothing is assigned automatically. Matches are pre-selected in the picker with the evidence behind each one — the gap, the distance, the colour agreement — and a human confirms. The cost of a confident wrong answer is another family's child in your highlight reel.
A moment your athlete is not in is not your athlete's moment
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 carried 0.25 of weight against a 0.35 threshold, enough to clear it unaided. The scorer was answering "was anything happening?" when the question is "was anything happening to him?"
Once you have said which child is yours, 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.
before 7 moments, 5 of them with no athlete in frame
after 2 moments, both of them him
Four reasons a five-minute game suggested nothing
1,415 tracks, a rim seen for 28s, a ball for 23s, and every line the user saw reading plausibly. Together they said the footage was dull. The footage was fine — the athlete every focal signal depends on was bound to a ten-frame fragment lasting 0.3s of 300s, and nothing on screen said so.
- Absent is not idle. Focal signals returned 0 rather than null when the athlete had no position, keeping 0.35 of weight in every denominator and reporting a ceiling of 1.000 that was arithmetically unreachable.
- Say how much of the game the athlete is actually on screen for. A binding covering under 5% of the footage now names itself as the likely reason, instead of "athlete identified: yes" implying otherwise.
- Re-identification could only shrink an athlete. Matching took the single best new track per old track, so the 0.3s binding survived two re-detections intact.
- Do not detect from a proxy smaller than the detection size. The 540p editing proxy was being upscaled — identical inference cost for strictly less picture. Same preset, same game: 145,975 detections across 3,948 tracks from the source, against 67,985 across 1,415 from the proxy. The ball goes first.
An announcer over the reel
Per-moment titles with AI-written lines, spoken by ElevenLabs and mixed over the game. One request writes every line at once, so the reel reads as a single broadcast rather than four disconnected sentences.
It never claims a basket, a steal, or a score, because nothing in this system knows any of those — a line about a shot that missed is worse than silence to someone who was in the gym. Everything degrades rather than fails: no key means templates, a refusal means templates, a line that will not synthesise is skipped. Speech is cached by content hash, so re-rendering after changing one clip does not re-pay for the rest.
A front door
reeleel.com redirected straight to /login. A site whose only public page is a password prompt tells a visitor nothing about what it is. There is now a landing page, with the palette sampled from the mascot rather than invented, and no webfont — nothing on the page makes a network request.
It states plainly what the product cannot do: it cannot know whether a shot went in, read a scoreboard, or tell you who won. That is the most useful thing on the page.
Fixes worth naming
- Every clip rendered pure black.
-sswas an output option, so FFmpeg filtered from 0:00 and handed the graph frames stamped past the fade-out's end. Measured YAVG 16 — pure black — against 123 with the fade removed. Clips rendered before this fix are still black on disk and need regenerating. - The review page could not be played. The box canvas covered the play button: "i can't click play i just have cross hairs". Identifying is now an explicit mode, and the canvas is transparent to the pointer.
- A real logo at the size it is displayed. The mark was generated at 96px and shown at 148, so the browser was upscaling it. The placeholder teal square that was standing in for the mascot on a live site is deleted outright.
- The footer no longer claims your footage stays on your own server. True only when self-hosting, and the landing page is read by people who are not.
Known limits
- The COCO model cannot see a hoop. A basketball-specific model is on the server, selected by a global environment variable rather than a per-project choice.
- Nothing extracts audio, so the
audio_spikesignal has never fired. - Every deploy kills a running analysis.
- Stitching raises coverage from 31.7s to 51.2s of a 300s game — better, not solved. A child who leaves frame entirely and returns in different light is still two athletes as far as the colour veto is concerned.
v0.2.0 — see what the detector sees
The release that makes the system inspectable, and fixes what inspecting it revealed.
You can see what the detector sees
A review surface plays the whole game with every stored track drawn over it. It renders the same data scoring reads — not a re-run, not an approximation — so what it shows is the truth about what will be scored.
This exists because "it hasn't really detected anybody" and "still sub-par moments" were unanswerable from logs. They turned out to be the same bug.
Identifying your athlete by pointing at them
The picker offered the longest tracks. On footage that fragments into 3,652 of them, the longest belong to a coach, the referee, and whoever stood still longest — so it presented a page of strangers with no way to say not those, him. Reported as "just random kids", which is exactly what it was. The wrong track got bound, every score after that followed the wrong child, and the automatic re-bind faithfully preserved the mistake across re-detections.
Now: scrub to a point where you know your athlete is on screen and click the box around them. That binds and re-scores without re-running detection. A click resolves to the smallest box containing the point, so a child standing in front of a wide crowd box selects the child.
Boxes also appear on each suggested moment's player, with the athlete drawn distinctly from everyone else, and a sentence saying what is on screen: "9 person(s) tracked at 142.3s" rather than a track count.
The reel looks edited
- Fades at both ends of every clip, picture and sound. The audio half matters more: a crowd at full volume stopping dead mid-syllable reads as a corrupt file rather than a cut. Clamped to a third of the clip so a short moment does not become mostly fade.
- Background music, uploaded per project and mixed under the game audio at 0.18 — the crowd, the squeaking shoes and a parent shouting are most of why a clip is worth keeping. It loops, ends with the footage rather than running on, and fades out instead of being cut off mid-bar. Level and fade length are adjustable at export.
Both filter graphs were run against real FFmpeg on the production container rather than only asserted as strings: a filter_complex that is subtly wrong does not fail loudly, it silently drops what you asked for.
Also in this release
- Uploaded music lives in a project directory deliberately excluded from the derived-file sweep, so
project cleancannot throw away something you provided. - Version strings in the CLI and the API health payload track the release; the sport plugin and CV worker versions deliberately do not, because they describe contracts that did not change.
Known limits, unchanged
- The COCO model cannot see a hoop. A basketball-specific model is on the server and selected by a global environment variable, which is not yet a per-project choice.
- Nothing extracts audio, so the
audio_spikesignal has never fired. - Every deploy kills a running analysis. The loss is visible; preventing it needs detection to outlive the web process.
- Tiling roughly doubles track fragmentation, which makes identification harder — which is why pointing at your athlete now matters more than the picker.
v0.1.0 — suggested moments you can actually watch
First tagged release. The app went from "a 200MB upload dies at 70% with no error" to "suggested moments you can watch, with the detector's boxes drawn on them".
Uploads
Streaming multipart parsing, so peak memory is one chunk rather than the file. Resumable across dropped connections and server restarts, with a real progress bar and full CRUD. A stall is distinguished from a slow connection and says which it was.
The original failure was two things at once: Node's 300s requestTimeout destroying any upload slower than ~700kB/s, and a response written before the body finished, which reached the browser as a bare ERR_HTTP2_PROTOCOL_ERROR.
Detection
It never worked. Every preset asked for an inference size the model could not accept — 512, 768, 1280 against a graph with a fixed [1,3,416,416] input — so detection failed on every run since the first commit. The model now has the final say on its own input size.
- Tiled inference for objects too small to survive being squeezed into that input. Measured on real footage: ball confidence 0.54 full-frame against 0.89 from a tile, and frames the full-frame pass missed entirely coming back at 0.88. End to end, 3 ball tracks over 40 seconds became 14. Opt-in as the
thoroughpreset, because it costs 5 inferences per frame. - Sport-specific models are usable at all: a model may declare its own classes, its own head format (YOLOv8 as well as YOLOX), and its own pixel convention. All three fail silently when wrong — feeding raw 0-255 pixels to a model expecting 0-1 produced 700 "basketball" detections at 1.00 confidence in a frame of an empty gym.
- Thread pool sized from the cgroup, not the host. Oversubscribing was measured at 56ms/frame against 194ms/frame.
- Tracks are replaced, not appended. A project analysed six times held six overlapping copies of every track; 9,961 stored tracks collapsed to 2,790 distinct ones.
Scoring
- An athlete can be bound to a track.
updateAthletehad always accepted afocalTrackIdand no surface ever passed one, so the three signals carrying most of the weight were dark on every run ever made — capping every window at 0.087 against a 0.35 threshold. - An athlete is more than one track. Tracking splits a child into fragments; binding one followed 24 seconds of a five-minute game. Selecting every fragment took a real game from 0 suggested moments to 5.
- The athlete survives re-detection. Track ids do not, but positions do, so bindings are re-matched by box overlap instead of asking the user to identify their child a fourth time.
- Signals that cannot be measured no longer divide the score. No hoop in frame and no audio track is not evidence of a dull moment, and charging every window for it held genuine possessions below the threshold.
Answering "why did this produce nothing"
A zero-moment run now reports what was seen, how long the longest track was, whether an athlete was identified, which signals had data, the best window score, and — the number that separates three unrelated failures — the highest reachable score. When the threshold was unreachable it says so, rather than implying the footage was dull.
Watching the results
- Each suggested moment has a player that plays exactly its span, streamed with Range support so seeking does not mean downloading everything before it.
- Detection boxes drawn over that player: the focal athlete bright and labelled, ball, hoop and referee in their own colours, everyone else quiet. With a sentence saying the same thing in words.
- Rendering is a job, so it reports progress and failures where the person who asked is looking, instead of writing them to the server's stderr and nowhere else.
- Analysis has a live SSE log, stop/replay/remove, and a footage picker.
Infrastructure
- A service worker that pinned browsers to stale JS.
watchPatternsthat let merged fixes sit undeployed.- Jobs interrupted by a restart are failed with a reason instead of claiming to run for ever.
- CI runs 462 tests on every push, including the detector against real weights — which had never run once.
Known limits
- The shipped COCO model cannot see a hoop; a sport-specific model is required, and selecting one is currently a global environment variable rather than a per-project choice.
- Nothing extracts audio, so the
audio_spikesignal has never fired. - Every deploy kills any running analysis. That loss is now visible; preventing it needs detection to outlive the web process.
- Tiling roughly doubles player track fragmentation.