Spaces:
Running
Running
Commit History
-
Merge pull request #14 from congdanh71298/chore/workspace-cleanup Running
Danh NguyenUnverified signaturedocs(experiments): note archived launchers run at recorded commit only5000a34 -
docs(experiments): note archived launchers run at recorded commit only
Saved copies pin internal APIs at their commit; running them on a newer tree can fail or mislead. Decision approved 2026-08-28.44fa8c8
-
Merge pull request #13 from congdanh71298/fix/analyzer-farzone-polish-r4
Danh NguyenUnverified signaturefix(analyzer): polish far-zone rules and ship the r4 demo04ee11f -
chore(analyzer): archive the polish-r4 launchers
The two runs that produced numbers this round: the real-frame check of the static-box departure (shot_05's 8 goes from 217 boxes on an empty spot to 1) and the r4 demo regeneration itself. Both copies are byte-identical to the launchers, CRLF on both sides so a checkout does not break that.2829057 -
chore(data): regenerate CPU demo with polish-r4 rules
The shipped set now carries shot_02's pot (11 flags, none lost) and drops shot_07's false track -- 54 of its 56 points survive the cue exclusion and peak 1.03 cm from the seed against MOVE_MIN_M 0.06, so no ball that moved lost a track. Every leaf outside ball_tracks reproduces on all 14 against both ad534bb8 and r3; inside it only shot_07, the one shot with a cue hole over LOST_MAX_S (+1 point).ba200bf -
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 -
Merge pull request #12 from congdanh71298/fix/analyzer-farzone-eyeball-polish
Danh NguyenUnverified signaturefix(analyzer): polish the far-zone round after the eyeball review69cc9f9 -
chore(analyzer): archive the eyeball-review polish launchers
Both have run, so both get their byte-for-byte copy. The gate check measured the three real-frame numbers of steps 1-3; the r3 driver regenerated the demo and came back RED on one line -- shot_07's init_idx 5 track disappears, a BRIEF-listed stop condition -- so no bundle enters the repo and `ad534bb8` stays the packaged set. Every other gate held, including 0 leaves differing outside `ball_tracks` across all fourteen shots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>2b1d64e -
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 -
Merge pull request #11 from congdanh71298/feat/analyzer-farzone-detect-hybrid
Danh NguyenUnverified signaturefeat(analyzer): hybrid far-zone detect, recovery lane and rack_guess000a154 -
chore(data): regenerate CPU demo with hybrid far-zone detect
The detector changed, so the shipped demo had to come from the pipeline that now exists. First regeneration whose gate is not "nothing may change": boundaries 14/14, no pocket flag lost and the one new flag only on shot_10 (10 total, eye-checked), suspect flagged 0/14, ball 5 present at conf 0.688, shot_05 coverage 1.000, rack_guess on 09+10 only. Reproduces the first run leaf for leaf outside timings and rack_guess.46857ab -
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 -
chore(analyzer): archive the far-zone demo regeneration driver
The run happened and its gate came back RED, so the bundle was not shipped and step 6 has no data commit -- but the driver and the thresholds it measured belong in git either way. Also drops two stray blank lines above rack_cluster. Red: rack_guess also fires on shot_07, pocketed_guess gains a tenth flag on shot_10, shot_09 v0 moves -41.7%. Numbers in the HANDOFF.2e493d6 -
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 -
docs(repo): widen the heredoc backslash trap to markdown files
The trap bit a third time, this time on a Windows path inside HANDOFF.md: a bare CR split one table row in two, and read_text() hides it because universal newlines fold it away. Record the wider scope plus the read_bytes() scan that actually sees it.c8e78ea -
Merge pull request #10 from congdanh71298/feat/analyzer-farzone-probe-ballid-profile
Danh NguyenUnverified signaturefeat(analyzer): probe far-zone detection and load a BallID colour profiled1f00b4 -
feat(dataset): add ND1 frame harvest script for archive VODs
The real repair for 20 px far balls and for a rack seen down its own axis is a fine-tuned detector, and that starts with frames whose provenance is recorded. One frame every 2 s, decoded sequentially so the PTS in the manifest belongs to the image beside it, plus the source SHA-256. First run on rack_a1: 160 frames against a duration of 320.0 s, 65311 archive files identical before and after. New commit scope "dataset" (approved in the BRIEF of 28/08): research data that never enters a repository.3ce4659 -
feat(analyzer): add BallID colour-profile calibration tool
Dynaspheres publishes no colour table, so the palette is read off the footage: click cue then 1..9, measured through ballid._features with the same crop shrink and cue white balance the scorer uses. Every measured vector is turned into a cost vector and checked against score_balls, so the tool cannot silently drift from the module. Mechanism run on shot_02 frame 0 of the demo VOD: 6 balls, purple ball at hue 253 deg where the standard palette has orange at 27, JSON read back by load_profile with no warning.45730a1 -
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 -
chore(analyzer): add far-zone detection probe (640/960/1280/band)
Design doc 7 section 3.1: measure, do not choose. Ball 5 of shot_02 is invisible at 640 (no box at all, conf floor 0.05), 0.208 at 960, 0.266 on the native far-band pass, 0.688 at 1280 -- only 1280 clears OP_CONF 0.2993. Cue at the far cushion: 5/16 frames at 640, 16/16 everywhere else. Cost per frame 56.4 / 118.4 / 212.7 / 117.0 ms = 1.0 / 2.1 / 3.8 / 2.07x. No configuration meets all three clauses; the numbers go to Cowork.9e5b26f
-
Merge pull request #9 from congdanh71298/fix/analyzer-pocket-bound-guess
Danh NguyenUnverified signaturefix(cv): flag and draw balls lost in mid-flight at a pocket796bb86 -
chore(data): regenerate the CPU demo bundle with pocket-bound flags
The shipped flags came from the near-pocket rule alone. Same VOD and corners; this round leaves the tracker alone, so the comparison walked into ball_tracks too: 0/84 headline cells, 0 other pre-existing leaves down to every track point, 0 shot boundaries, all 14 overlays byte for byte identical. pocketed_guess 7 -> 9 (shot_03 and shot_14); tracks, points, steal-ends and longest hole all unchanged.b33d4a2 -
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 -
Merge pull request #8 from congdanh71298/fix/analyzer-ball-track-gate-cap
Danh NguyenUnverified signaturefix(cv): cap the association gate and close lost ball tracks70332b9 -
docs(repo): allow the data commit scope
The packaged demo bundle is neither code nor documentation, and the regeneration commits had to name it. Adds `data` to the scope list alongside repo/analyzer/tests/fe.9949c2a -
chore(data): regenerate the CPU demo bundle with capped ball tracks
The shipped tracks came from the uncapped gate. Same VOD and corners; only ball_tracks was allowed to move, and nothing else did: 0/84 headline cells, 0 other pre-existing leaves, 0 shot boundaries. Tracks 19 -> 17, pocketed_guess 1 -> 7, ends within 3 cm of another ball 6 -> 3, longest hole in a track 5.267s -> 0.467s.e837cee -
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 -
Merge pull request #7 from congdanh71298/feat/analyzer-multiball-tracking
Danh NguyenUnverified signaturefeat(analyzer): track and replay every object ball, not just the cue10ef6fe -
chore(data): regenerate the CPU demo bundle with ball tracks
The shipped demo still showed only the cue moving. Same VOD and corners as the 25/08 bundle; object-ball tracking may only ADD, so the gate was that nothing else moved: 0/84 headline cells, 0 leaves over a FULL comparison of every pre-existing key, 0 shot boundaries. Adds 19 ball_tracks over 14 shots, 1 flagged pocketed.0abf5e0 -
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
-
Merge pull request #6 from congdanh71298/docs/repo-experiments-readme-launchers-path
Danh NguyenUnverified signaturedocs(repo): point the experiments README at launchers/2b68974 -
docs(repo): point the experiments README at launchers/
The intro still described the pre-25/08 convention of keeping hand-run launchers at the workspace root. Last place in the app repo saying so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>1a8b8c8 -
Merge pull request #5 from congdanh71298/docs/repo-launchers-directory-convention
Danh NguyenUnverified signaturedocs(repo): pin hand-run launchers to the launchers/ directory2ad807a -
docs(repo): pin hand-run launchers to the launchers/ directory
Convention settled 25/08/2026: hand-run launchers are collected in D:\poolcoach\launchers\ instead of being scattered at the workspace root. Only the app repo note was still stale; the docs repo side was already updated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>94712a0 -
Merge pull request #4 from congdanh71298/feat/analyzer-cpu-demo-baseline
Danh NguyenUnverified signaturefeat(analyzer): rebaseline the demo bundle on the cpu pipeline090788c