# `scripts/experiments/` -- archived launchers **These are not launchers.** The launchers you run by hand live in `D:\poolcoach\launchers\` (outside git), not at the workspace root. This directory keeps a **byte-for-byte copy** of the `.bat` files that genuinely produced measurements, so they cannot disappear along with that untracked directory. When a launcher is only a wrapper around a driver script, the driver is copied next to it under the same name (`.bat` + `.py`) -- half a launcher reproduces nothing. Why it is worth the space: these files carry what `git log` cannot -- the *reason* a run happened and the *acceptance thresholds*, written in the comment header at the time, not reconstructed afterwards. Copies are never edited after the fact, which is why the ones inherited from the previous repository still reference `D:\Khoa luan\` paths and are written in unaccented Vietnamese: that is what was run. A copy runs only at the commit recorded in it -- the internal APIs it calls may have moved since, so pointing one at a newer tree can fail outright or, worse, return numbers that no longer mean what the header says. | file | date | what it ran | |---|---|---| | `run_analyzer_local.bat` | 11/08/2026 | one command bringing up Redis + CV worker + app in queue mode for the Analyzer demo | | `run_analyzer_a1_vod.bat` | 13/08/2026 | first full-VOD acceptance run of the multi-shot Analyzer (gate A1.2): 320 s of a rack -> 14 shots | | `run_analyzer_a2_vod.bat` | 13/08/2026 | A2 acceptance on the same clip: overlay videos, resim RMSE, the not-a-shot flag, CSV export | | `run_analyzer_a2b_vod.bat` | 14/08/2026 | A2b gate 4: ball numbers from BallID carried into `balls_init`; the run that produced the FIRST packaged demo video (`c9fda71c`, GPU) | | `run_analyzer_cpu_rebaseline.bat` + `.py` | 25/08/2026 | rebaseline of the packaged demo on the CPU pipeline (`4fa4e53a`), same VOD and corners: 14/14 shots equal to column C of the 24/08 three-way check | | `run_analyzer_multiball_demo.bat` + `.py` | 27/08/2026 | regeneration of the packaged demo once object-ball tracking existed (`6ee68508`), same VOD and corners. The driver RUNS its own gate, because the gate is the reason the run happened: 0/84 headline cells, 0 pre-existing leaves and 0 shot boundaries may differ from `4fa4e53a` -- tracking is only allowed to ADD `ball_tracks`. All three came back 0 | | `run_analyzer_multiball_gatecap_demo.bat` + `.py` | 27/08/2026 | regeneration after the association gate got a ceiling and lost tracks got closed (`b7c3669e`), same VOD and corners. Same driver as the line above with the baseline moved to `6ee68508` and `ball_tracks` MEASURED instead of compared -- changing it is what the run was for. 0/84 cells, 0 other pre-existing leaves, 0 boundaries; tracks 19 -> 17, `pocketed_guess` 1 -> 7, ends within 3 cm of another ball 6 -> 3 | | `run_analyzer_multiball_pocketbound_demo.bat` + `.py` | 28/08/2026 | regeneration after the pocketed guess learned to read the end chord, so a ball lost in MID-FLIGHT at a pocket is flagged too (`a1cd87e8`), same VOD and corners. Same driver as the line above with the baseline moved to `b7c3669e` and the comparison walking INTO `ball_tracks` -- this round does not touch the tracker, so every track point had to come back identical and only `pocketed_guess` could move. 0/84 cells, 0 other pre-existing leaves (points included), 0 boundaries, all 14 overlays byte-identical; `pocketed_guess` 7 -> 9, every other track number unchanged | | `harvest_nd1_rack_a1.bat` | 28/08/2026 | the first ND1 harvest (design doc 7 section 3.5): 160 frames of `rack_5245_5805.mp4`, one every 2 s, into `D:\poolcoach\datasets\nd1_frames\` outside both repositories. No `.py` copy next to it, unlike the rows above: its driver is `scripts/harvest_frames.py`, which is tracked. 160 frames against a duration of 320.0 s, a manifest carrying the source SHA-256 and every frame's PTS, and 65311 archive files identical before and after | | `probe_farzone_det.bat` + `.py` | 28/08/2026 | the far-zone detection probe of design doc 7 section 3.1 -- MEASURES four detector configurations (640 / 960 / 1280 full frame, plus a native-resolution second pass over the far half of the table) on the frames that failed in `a1cd87e8`, and chooses nothing. It measures nothing until it has proved it is looking at the same image the demo did: full640 re-run at the operating confidence rebuilds shot_02's `balls_init` entry for entry, ball 5 missing included. Result: ball 5 is invisible at 640 (no detection at all, even at conf 0.05), 0.208 at 960, 0.266 on the band pass, 0.688 at 1280 -- so only 1280 clears `OP_CONF` 0.2993, and it costs 3.8x the 640 baseline while 960 and the band cost 2.1x. No configuration satisfies all three clauses of the section 3.1 rule; the numbers went to Cowork rather than to a decision | | `check_farzone_gates.bat` + `.py` | 28/08/2026 | the three acceptance numbers of hand-off 2 (design doc 7 section 5) that only exist in the footage, run over the probe clips against the SHIPPED analyze front end: ball 5 of shot_02 in `balls_init` at or above `OP_CONF`, the hole in shot_05's cue track through [2.30, 2.80], and `rack_guess` on shot_09 + shot_10 only. It measures and reports; it tunes nothing. Re-run after each step that could move those numbers. Step 1 (hybrid detect): ball 5 present at conf 0.688, 0.0158 m from the measured centroid, where the packaged demo had no ball there at all. Step 2 (recovery lane): the cue is detected on all 22 frames of the window, the 0.067 s "hole" left is a duplicate frame of the 25fps upconvert, and the peak stays at y 2.4464 -- 3.6 mm short of the 2.45 the section 5 gate asks for, and the recovery lane cannot help because no frame there is missing. Step 3 (rack flag): on for shot_09 (4 of 9 seen, centre 0.634, 0.568) and shot_10 (5 of 9), off for shot_02, shot_03 and shot_05 | | `run_analyzer_farzone_demo.bat` + `.py` | 28/08/2026 | regeneration of the packaged demo on the far-zone pipeline (hybrid detect 960 + 1280 first frame, the low-confidence recovery lane, `rack_guess`), same VOD and corners. The FIRST round whose gate is split hard/soft: the detector changed, so the metrics are allowed to move and the old "0 differing cells" reflex would be wrong. **The gate came back RED and the bundle was NOT shipped** -- `a1cd87e8` is still the packaged set. Green: 14/14 shot boundaries identical, `suspect_not_shot.flagged` 0/14 differing, schema 14/14, shot_02 has ball 5 at conf 0.688, `rack_guess` on for shot_09 + shot_10. Red, all three of them BRIEF-listed stop conditions: `rack_guess` also fires on shot_07 (5 balls bunched at the foot rail after a break, not a diamond); `pocketed_guess` gains a tenth flag on shot_10 (a track that now dies 3.0 cm from the near right pocket at t = 2.233, gone 5.03 s before the clip ends); the analytic v0 of shot_09 moves -41.7% (0.991 -> 0.578, on a shot the valve already flags as not-a-shot). Plus shot_05's cue peak at y 2.4464, 3.6 mm under the 2.45 the gate asks for, with coverage now 1.000 and the 0.5 s hole gone. The run is in `D:\poolcoach\out_farzone_demo_20260828\`; the numbers went to Cowork, not to a constant | | `check_overlay_polish.bat` + `.py` | 28/08/2026 | the real-frame half of the hand-off 4 gates (design doc 7 section 6, steps 1-3), run over the probe clips against the SHIPPED analyze front end. The unit tests pin the pure functions; this pins what they do to the footage Danh looked at, and it renders each clip's overlay TWICE -- as the code stands and with both new drawing rules off -- so the two can be compared frame by frame. Result: **2172 of 3476 recorded ball boxes on shot_02 project OFF the cloth** (table y reaching 4.48, x down to -0.42), all of them gone from the drawing; ball 5 of shot_02 boxed on 235 of 243 frames where it previously had no box on any, and no ball drawn twice; the cue exclusion strikes off 4 high + 7 low candidates on shot_02 and 18 high on shot_05, and turns ball 5's track from "ends t=1.233 at (1.068, 2.372), 0.262 m from the pocket, heading -140 degrees" into "ends t=1.200 at (1.244, 2.458), 0.086 m from it at 1.01 m/s heading +9 degrees" -- `pocketed_guess` False -> True. shot_05's single track comes back identical, which is the control | | `run_analyzer_farzone_demo_r2.bat` + `.py` | 28/08/2026 | the SAME regeneration re-run after Cowork ruled on those three reds and Danh checked the new flag by eye -- the run that produced the packaged set `ad534bb8`. The first driver is left untouched next to it: it is the record of the run whose numbers produced the rulings. Same two inputs, gate rewritten in five places (no flag may be LOST rather than none may change, with a new flag allowed only on shot_10; coverage 1.000 + `max_gap_s` <= 0.1 instead of a peak-y threshold that was reading bbox bias; `suspect_not_shot.flagged` instead of the reason sentences; the v0 rule waived for a shot the valve calls not-a-shot in both bundles; `rack_guess` under the rack-spot anchor). It also ADDS a reproducibility check against the first run, because the BRIEF makes losing reproducibility a stop condition. Result: every hard gate green -- 14/14 boundaries, 10 pocket flags with none lost, `flagged` 0/14 differing, ball 5 at conf 0.688, shot_05 coverage 1.000 with a 0.067 s longest hole, `rack_guess` on for 09 + 10 only, schema 14/14 -- and **0 leaves differing against the first run** outside the timings and the one `rack_guess` the tightening was for | | `run_analyzer_farzone_demo_r3.bat` + `.py` | 28/08/2026 | regeneration after the four eyeball-review fixes of design doc 7 section 6 (overlay filtered to the cloth, boxes for `balls_init` entries no detection covers, the cue exclusion on object tracks, and the front-end cushion snap). Only the third of the four may move a JSON number, so the gate is unusual: EVERY leaf outside `ball_tracks` has to reproduce, which is what "the cue metrics do not move" means measured rather than asserted. **The gate came back RED on one line and the bundle was NOT shipped** -- `ad534bb8` is still the packaged set. Green: 14/14 boundaries, no pot flag lost, the one new flag exactly where it was allowed (shot_02, ball 5, the (1.27, 2.54) pocket that Danh watched it drop into), `flagged` 0/14 differing, `rack_guess` identical, schema 14/14, and **0 leaves differing outside `ball_tracks`** across all fourteen shots. Red: shot_07's `init_idx` 5 track DISAPPEARS (56 points -> gone), a BRIEF-listed stop condition. Measured, that track was false to begin with -- ball 8 sits at (0.5274, 0.0390) for 2.1 s moving 1.5 mm in total, then jumps 13.4 cm at t = 2.500 onto ball 2's position, and it is only that jump that carries it past `MOVE_MIN_M`. The exclusion strikes the two jumped points because the cue is measured at (0.3957, 0.0673) at t = 2.567, 3.4 mm from them. Whether deleting a false track counts as a track dying is Cowork's call, not the driver's. Total across 14 shots: 3 detections struck off, 0 gained | | `check_static_box_departure.bat` + `.py` | 28/08/2026 | the real-frame half of hand-off 5 step 3 (design doc 7 section 7), over the same probe clips. Counts, per `balls_init` entry, the frames that carry a dashed box with the departure clock OFF and ON, and renders each shot twice so the count can be checked against the picture. Result: shot_05's 8 goes from **217 boxes on an empty spot to 1** (its track leaves the seed at t 0.867, frame 26, and the tail was already leading away from the box); shot_02's ball 5 from 235 to 28, last drawn at frame 35 (t 1.167), the ball being in the pocket after that; and the entries with NO track -- the case the rule exists to protect -- are untouched at 12 and 17 frames. The PNG pair at the boundary frame is identical on both sides, which is what "it cuts at the right frame, not earlier" looks like | | `run_analyzer_farzone_demo_r4.bat` + `.py` | 28/08/2026 | regeneration after the four corrections of design doc 7 section 7 (the cue position clamped across long holes, the front-end snap vetoed when a ball sits on the apex, the static overlay box ended when its ball departs) -- the run that produced the packaged set `e61b1e15`. Two baselines, because there are two questions: the SHIPPED bundle answers what the eight fixes of hand-offs 4 and 5 did, and the r3 run answers whether anything moved that was not asked to. The track-death gate is REWRITTEN here and nowhere else: a track may disappear, but only if the points that SURVIVE the cue exclusion no longer show a ball that moved, measured with the shipped `cue_at` against the new cue track. **Every hard gate green on both baselines.** 14/14 boundaries; 11 pot flags, none lost, the one new flag exactly where Danh watched the pot (shot_02, the (1.27, 2.54) pocket); `flagged` 0/14 differing; `rack_guess` identical; schema 14/14; **0 leaves differing outside `ball_tracks`** against BOTH baselines. The death: shot_07 `init_idx` 5, 56 points gone, 54 surviving the exclusion and peaking **1.03 cm** from the seed against `MOVE_MIN_M` 0.06 -- never a ball that moved. Against r3 the whole run differs in ONE place, shot_07's `init_idx` 0 gaining a point (52 -> 53), and shot_07 is the only shot whose cue track has a hole over `LOST_MAX_S` -- which the driver reads off r3 rather than naming. Segmentation 791 s, total 1174 s |