"""Driver of the CPU rebaseline run (25/08/2026) -- called by ``run_analyzer_cpu_rebaseline.bat``, which owns the environment. It drives the SHIPPED pipeline, not a copy of it: the app is started in "local" mode (``POOLCOACH_LOCAL_CV=1``), so the very functions of ``scripts/cv_worker.py`` run on a thread of this process, reached through the public routes (``POST /api/analyzer/videos`` -> ``GET /api/analyzer/shots``). No probe re-implements segmentation or analysis, so the numbers cannot drift away from what the Space serves. Waits on the RIGHT condition, never on a sleep: first the CV worker heartbeat (the upload route fast-fails without it), then video status ``done`` AND every shot settled (``done`` or ``error``) -- a manifest on disk appears long before the last shot is analyzed. """ from __future__ import annotations import json import os import sys import time from pathlib import Path APP_ROOT = Path(os.environ.get("POOLCOACH_APP_ROOT", r"D:\poolcoach\poolcoach-app")) VOD = Path(os.environ["POOLCOACH_REBASELINE_VOD"]) # The 4 corner pockets of run A2b (14/08) and of run C (24/08), in the order # the front-end asks for them. Same VOD + same corners = the only two inputs # the pipeline takes. CORNERS = "[[527.5,869.7],[1380.1,864.0],[1232.3,278.5],[673.0,272.8]]" WORKER_BOOT_TIMEOUT_S = 900.0 # YOLO + ShotNet + the pooltool Numba JIT ANALYSIS_TIMEOUT_S = 4 * 3600.0 # a 5.3 minute rack on 2 CPU cores POLL_S = 10.0 for _p in (APP_ROOT / "src", APP_ROOT): if str(_p) not in sys.path: sys.path.insert(0, str(_p)) def main() -> int: from fastapi.testclient import TestClient from app.main import app out_dir = Path(os.environ["POOLCOACH_ANALYZER_DIR"]) print(f"[rebaseline] app root : {APP_ROOT}") print(f"[rebaseline] vod : {VOD} " f"({VOD.stat().st_size / 1e6:.1f} MB)") print(f"[rebaseline] result dir: {out_dir}", flush=True) t_boot = time.time() with TestClient(app) as client: while True: health = client.get("/api/health").json() if health.get("cv_worker_alive"): break if time.time() - t_boot > WORKER_BOOT_TIMEOUT_S: print(f"[rebaseline] FAILED: no CV worker heartbeat after " f"{WORKER_BOOT_TIMEOUT_S:.0f}s -- health={health}") return 2 time.sleep(2.0) print(f"[rebaseline] CV worker alive after " f"{time.time() - t_boot:.1f}s (mode={health.get('mode')})", flush=True) with open(VOD, "rb") as fh: resp = client.post( "/api/analyzer/videos", files={"video": (VOD.name, fh, "video/mp4")}, data={"corners": CORNERS}) if resp.status_code != 200: print(f"[rebaseline] FAILED: upload returned " f"{resp.status_code}: {resp.text[:400]}") return 3 video_id = resp.json()["id"] print(f"[rebaseline] video_id : {video_id}", flush=True) t_run = time.time() last = "" while True: body = client.get("/api/analyzer/shots", params={"video": video_id}).json() video = body.get("video", {}) shots = body.get("shots", []) settled = [s for s in shots if s.get("status") in ("done", "error")] line = (f"{video.get('status')} " f"{len(settled)}/{len(shots)} settled " f"(done={body.get('n_done')})") if line != last: print(f"[rebaseline] +{time.time() - t_run:7.1f}s {line}", flush=True) last = line if video.get("status") == "error": print(f"[rebaseline] FAILED: video status error: " f"{video.get('message')}") return 4 if (video.get("status") == "done" and shots and len(settled) == len(shots)): break if time.time() - t_run > ANALYSIS_TIMEOUT_S: print(f"[rebaseline] FAILED: still not finished after " f"{ANALYSIS_TIMEOUT_S:.0f}s") return 5 time.sleep(POLL_S) elapsed = time.time() - t_run print(f"[rebaseline] DONE in {elapsed:.1f}s -- " f"{len(shots)} shots, {body.get('n_done')} analyzed", flush=True) summary = {"video_id": video_id, "vod": str(VOD), "corners": json.loads(CORNERS), "elapsed_s": round(elapsed, 1), "n_shots": len(shots), "n_done": body.get("n_done"), "result_dir": str(out_dir / video_id)} (out_dir / "rebaseline_run.json").write_text( json.dumps(summary, indent=1), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())