Spaces:
Sleeping
Sleeping
Commit History
-
fix(analyzer): static overlay boxes end when the ball departs
balls_init is a photograph of the first frame, not a standing claim: on shot_05 the dashed box for the 8 sat on its empty starting spot for 217 of 243 frames with the ball's own tail leading away. It now stops the frame the entry's track leaves the seed by more than BALL_DEDUP_M -- shot_05 217 -> 1, shot_02's ball 5 235 -> 28 (last drawn frame 35, t 1.167). Entries with no track are untouched.58e7bd2 -
fix(fe): suppress cushion-snap when a ball sits at the apex
A track turns round off a rail or off a BALL and near a cushion the two look alike: shot_02's cue turns 152 degrees 10.3 cm off the far rail with the 5 sitting 6.9 cm away. collisions calls that kink and shot_05's real bounce `unknown` alike, so the veto reads the layout in both places the picture holds it. shot_02 1 snap -> 0 (50 canvas pixels, all in the removed segment); shot_05 canvas byte-identical.1e61cc4 -
fix(analyzer): clamp cue position across long track gaps
A line drawn between the ends of a 0.9 s hole is not a measurement: on shot_07 it swept within CUE_EXCL_M of six still balls in turn (the 2 at 4.1 mm), switching their detections off. Each half of a hole over LOST_MAX_S now answers with its own end; shorter holes still interpolate. The two detections that stole ball 8's track sit 3.4 mm from the end and stay excluded. 271 passed / 27 skipped.ff7db2d -
feat(fe): dashed cushion-snap for near-rail turnarounds
A ball pressed against a cushion is half hidden by it, so its bbox centre reads ~6.5 cm short and the drawn line turns round in mid-air -- shot_05 apexes at y 2.4464 with the rail at 2.54. The measurement cannot move (the fix is full boxes on the ND1 line), so the DRAWING closes the gap: an apex within CUSHION_SNAP_M 0.12 of its nearest rail that turns at least 90 degrees gets a dashed segment out to that rail, the same language the pocket-bound stretch uses. JSON unchanged. Accepted on the DOM: shot_05 ends on the rail pixel row, shot_01 and shot_03 grow nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>4f0b4f3 -
fix(analyzer): exclude cue-coincident detections from ball tracks
The detector classifies the cue as a Solid now and then, and an object track whose ball has just gone into a pocket adopts it: on shot_02 that ran ball 5 backwards out of the far corner pocket to t=1.233 and cost a real pot flag. Candidates in EITHER band within CUE_EXCL_M 0.06 of the interpolated cue position are struck off; track_cue is untouched. Measured: ball 5 now ends t=1.200 at (1.244, 2.458), 0.086 m from the (1.27, 2.54) pocket at 1.01 m/s heading +9 deg -- pocketed_guess True. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>20b78c7 -
fix(analyzer): persistent overlay boxes for static balls_init entries
Ball 5 of shot_02 reads 0.208 against an operating point of 0.2993, so it never entered frames_dets and had no box on any frame, while balls_init (the 1280 first-frame pass) reads it at 0.688 and the metric table draws it. A balls_init entry with no high detection within BALL_DEDUP_M now draws itself: a thin DASHED square carrying the number balls_init already has, BallID not run again. Measured on the probe clip: ball 5 boxed on 235 of 243 frames, no ball drawn twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>fbf1204 -
fix(analyzer): draw only on-table detections in the overlay
The overlay drew every recorded box while analyze_clip has always dropped the ones projecting off the cloth, so the imgsz 960 pass put rectangles on the wall behind the far rail -- 2172 of 3476 ball boxes on shot_02, reaching table y 4.48 and x -0.42. _on_table_dets applies the same class filter and the same IN_TOL_M gate, in the same height-compensated frame, and none survives into the drawing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>b202b21 -
fix(analyzer): anchor rack_guess to the rack spots
The first demo regeneration fired the flag on shot_07, the tail of a break: eight balls strung along the foot rail, five inside RACK_R_M and the cue 0.75 m away, so every other condition held. A rack is built ON THE SPOT, so require the cluster centre within RACK_SPOT_R 0.25 m of one. Measured on that bundle: shot_09 0.067 m, shot_10 0.083 m, shot_07 0.570 m. 5 tests on the real layouts; pytest 2x green on both venvs.a40be3b -
feat(fe): draw ghost rack when rack_guess is on
The break frame comes back with four or five of nine balls -- the rest are physically hidden behind them at this camera angle. The JSON keeps measurements plus one flag, so the missing balls belong to the drawing: a dashed, faint diamond under the balls that were really seen. Gate: 3 contract tests; toDataURL diff shows all 9 ghosts plus the caption and leaves every measured ball pixel untouched.3d87b03 -
fix(analyzer): overlay BallID honours ball-set profile
The overlay called identify_balls with the standard palette while balls_init used the job's profile, so a Dynaspheres set (5 purple, not orange) would be numbered one way in the video and another on the metric table -- one statement to the viewer, two answers. Read the profile once per job and hand it to both. Gate: 2 tests, both layers pass the same table; pytest 2x green, 2 venvs.5b4ac58 -
feat(analyzer): rack_guess display flag for occluded break racks
Down the table the rack is a diamond seen edge on, so balls hide behind balls and 9 of 9 is unreachable -- at 1280 the shot_09 cluster even returns ten boxes for nine balls. Flag it instead of inventing points: 3+ balls inside 0.15 m of one centre with the cue 0.5 m away. Gate: on for shot_09 (4 of 9 seen) and shot_10 (5 of 9), off for shot_02/03/05; 9 unit tests; pytest 2x green on both venvs.2bb6493 -
feat(analyzer): track-gated low-conf recovery for live tracks
Detect at DET_CONF_FLOOR 0.05 and read the answer in two bands: above op_conf nothing changes anywhere, below it a detection may only extend a track already alive, inside that track's gate, when no high candidate is in it. IN_TOL_M gates the low band too -- the probe found boxes past the far cushion projecting 1.7 m off the cloth. Gate: 13 unit tests over both trackers; pytest 2x green on both venvs.562bec5 -
feat(analyzer): hybrid detect imgsz 960 with 1280 first-frame pass
Balls at the far rail are ~22 px wide and imgsz 640 shrinks them to 7, which is why shot_02 never had ball 5. 960 for every frame costs 2.1x, and the one frame balls_init is built from is read at 1280, where that ball reads 0.688 instead of 0.208. Segmentation stays at 640. Gate: shot_02 balls_init has a ball 0.0158 m from (1.145, 2.468) at conf 0.688 >= OP_CONF 0.2993; pytest 2x green on both venvs.9694f55 -
feat(analyzer): load ball-set colour profile for BallID
The tournament uses Dynaspheres Titan (the 5 is purple, not orange), so a correctly detected ball still gets the wrong number. load_profile reads a JSON of palette / white_range / the ball-8 thresholds; the method's own constants stay unreachable from it. With POOLCOACH_BALLID_PROFILE unset nothing moves: no key in the job JSON, the default palette, and the 14 demo shots still serialise without one. pytest green 3x in both venvs.d5faecf
-
feat(fe): extrapolate pocket-bound balls into the pocket on replay
Measuring stops where the detector gave up, so a pocket-bound ball stalled on the cloth. The replay now rolls it the last stretch at the speed it was last seen doing, and the table view draws that piece DASHED so measured and inferred never look alike. Drawing only -- nothing is written back. Gate: shot_03 replay ends 0 cm from the pocket against 23.75 cm short before; pytest 213 app / 227 cv.36d3768 -
feat(api): expose the guessed pocket in ball tracks
The pocket-bound guess is useless to the drawing without the pocket it guessed, so the result schema carries it: an optional pair of coordinates, absent when there is no guess and absent from every result stored before it existed. It only ADDS -- no number above it moves. Gate: pytest 210 app-env / 224 cv-env, and a track built without a pocket still validates and dumps no phantom key.16d608d -
fix(cv): flag pocket-bound balls that vanish mid-flight
The detector loses a ball still flying at the far corners -- 23.7 cm short on shot_03 -- so the near-pocket rule never fired and the ball stalled mid-cloth on the replay. Guess the pocket from the end chord as well (early end, still moving, aimed, close) and name it in pocket_xy. Gate: pytest 208 app-env / 222 cv-env; each of the four conditions proven load-bearing by relaxing its own constant alone.e940fef -
fix(cv): cap the association gate and close lost ball tracks
The gate widened past the 2.84 m table diagonal after 0.253 s out of sight, so 6/19 demo tracks ended within 3 cm of ANOTHER ball's start and a pocketed ball's trail dragged across the cloth. Cap the gate at GATE_CAP_M 0.5 and close a track missing over LOST_MAX_S 0.5 s. Object-ball tracks only; track_cue untouched. Gate: pytest 197 passed / 14 skipped (app), 211 passed (cv), twice.bff9861 -
feat(fe): draw and replay object-ball tracks on the table view
The table view showed only the cue moving while the JSON already carried every ball. Tracks are resampled onto one even time base before animateShot, which interpolates by index -- untouched, so the collisions still land where they landed. A numberless ball is grey, not a second white cue. Gate: pytest 194/208 green; verified live (paths at 0.6 alpha, potted fade, no ghost ball: 145 vs 279 px).b0d16f3 -
feat(cv): draw object-ball trails in the shot overlay
Only the cue left a tail, so the video showed the target balls teleporting between boxes. Each ball_track now draws its own tail in the same TRAIL_N window, coloured through init_idx -> balls_init -> number, under the cue tail so the cue stays the main character. Gate: pytest 188/202 green, 5 new cases on the pure half.2bcd966 -
feat(cv): flag likely pocketed balls in ball tracks
The replay needs to know which balls to fade out. Two conditions, both required, so a ball simply lost behind a player is not read as potted: the track ends within POT_NEAR_M of a pocket AND stops more than POT_GONE_S before the clip ends. Display only -- no metric reads it. Gate: pytest 183/197 green, 11 new cases.0d6965c -
feat(api): expose ball_tracks in the analyze result schema
The per-shot JSON gains the object-ball paths. Default empty so every result stored before tracking existed still validates and the demo bundle does not break; nothing above it changes meaning. Gate: pytest 172/186 green, 3 new schema contract tests.6903d96 -
feat(cv): track object balls across frames in analyze_clip
The non-cue detections were already collected and thrown away after the still-ball clustering; keeping them per frame gives every object ball a track for free, with no extra detection pass. Deterministic greedy NN with a gate that widens over a miss, seeded from balls_init and suppressed below MOVE_MIN_M. Gate: pytest 169/183 green, 2 runs of the tracker on one input byte-identical.bfd4175
-
test(analyzer): cover the shotnet inference path
The path the CV worker runs had no test: checkpoint load, predict shapes and ranges, and the track preprocessing at its edges. It also pins CPU determinism, which the rebaselined demo bundle now depends on. 7 tests: 174 pass in the cv venv, 14 skipped in the app venv (no torch).ece9af6 -
chore(repo): normalize line endings with text=auto
The tree mixed CRLF and LF with only *.bat declared, so an English-only edit produced a 3704-line diff and every commit needed its EOL restored by hand. Content is untouched: git diff --ignore-cr-at-eol shows .gitattributes alone. pytest 160+7skip (app) / 167 (cv), 3 runs each.2cfc662
-
fix(fe): reword hero captions in viewer-facing words
A dot at the top right captioned a=-0.20 reads backwards to anyone watching the ball, so the caption now says "0.20 right, 0.27 up" through one contactWords the quickbar shares. The convention is unchanged and the signed pair stays in the numbers table, which also takes the checkpoint name the viewer-facing label no longer carries. Gate: pytest 160 passed + 7 skipped, 2 runs; 167 collected, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>da71d35 -
fix(fe): unflex an-note and flatten drawer background
A second `.an-note` block overrode only the type, so `display: flex` from the illustrated-note rule leaked into every note and laid prose out as flex items -- the "hard to read" Danh saw. The flex layout is now its own class, the two prose notes are two sentences each, and the drawer drops its lighter ground so the panel reads as one surface. Gate: pytest 160 passed + 7 skipped, 2 runs; every plain note is block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>2bdb082 -
feat(fe): simplify shot list to spin and power columns
Eight debug columns asked a player to read V0, phi, a/b and RMSE; the list now shows what spin and how hard, and those numbers stay one click away on the detail page under Technical details. Contract test rekeyed to the five columns per the 24/08 design decision, not loosened: the old headers are now asserted ABSENT. Gate: pytest 160 passed + 7 skipped, 2 runs; demo 14 rows checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>dded2ea -
test(analyzer): port pure-CV test suites from poolcoach-rl
The nine poolcoach_cv modules came over at the port with no tests of their own; this restores the safety net for the ones that need no heavy deps. Eight suites ported and translated: homography, segmentation, autocorner, ballid, resim, broadcast analytics, height compensation, overlay. Gate: 68 -> 167 tests (160 passed, 7 cv2-gated skipped on the app venv and run by hand in the CV venv, all green). No src/ or app/ code was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>7d9c5f3 -
refactor(fe): translate front-end UI strings to English
Second half of the one-language decision: the server already speaks English, so the page had to follow or the app would read as a mix. Gate: pytest 68 passed; 0 Vietnamese lines left outside demo_analyzer; id/class multiset and tag sequence of index.html identical to HEAD (109 attributes); list and detail pages read clean, no console errors. Also carries the CLAUDE.md language rule and the new `fe` scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>fae8ae3 -
refactor(analyzer): translate server-generated strings to English
The app is going one language; Vietnamese error and warning text in an otherwise English code base made every reader switch twice. Gate: pytest 68 passed; AST identical after blanking every string on all 7 modules, cv_worker only by the _CONF_LABEL_VI -> _CONF_LABEL rename (the VI suffix stops meaning anything once the labels are English). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>fe8d0e2 -
fix(tests): wait for per-shot status before cleanup in analyzer test
wait_manifest returned as soon as the manifest file appeared, which is the FIRST thing the fake worker writes -- the per-shot set_analyze_status calls come after, so a test deleting or overwriting one of those keys raced the worker. Joining the worker thread makes the wait structural instead of a timing bet, and asserts every queued shot has its status. pytest: 68 passed x3, and 25/25 on the two affected tests under CPU load.6c32b63 -
refactor(analyzer): translate identifiers and comments to English
Comments, docstrings, identifiers and test names are now English, including the Dockerfile and .dockerignore reasoning. Strings the user reads stay Vietnamese, because the product's UI language is Vietnamese: HTTP error details, per-shot reasons, warnings and the CSV headers. Semantics are unchanged -- an AST comparison with string constants blanked is identical for every module except three renamed locals; pytest 68 passed.876aff5 -
feat(analyzer): port analyzer pipeline from poolcoach-rl
Traced the import graph from the Analyzer routes: app store/transport/ embedded worker, src/poolcoach_cv end to end, cv_worker, the multi-shot contract test, the packaged demo video and the Hugging Face deploy set. The suggestion engine, drills, QR/TV and the image-scan job stay behind, so their routes, schemas, tables and front-end sections are gone with them; /api/table now reads pooltool directly (src/poolcoach_sim/table.py).da25347 -
chore(repo): scaffold poolcoach-app with layered requirements
Fresh repository for the Analyzer, split off from poolcoach-rl. Git LFS tracks *.pt/*.mp4/*.png from this first commit because Hugging Face (Xet) rejects any binary that never went through LFS, even in an old commit -- the previous repository had to push an orphan snapshot branch instead. Requirements are rebuilt from the import graph of the code being ported, not copied: core / app / cv / cv-cpu / dev.27b06b1