File size: 5,024 Bytes
1c0d153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""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())