Spaces:
Sleeping
Sleeping
| title: PoolCoach | |
| emoji: 🎱 | |
| colorFrom: green | |
| colorTo: gray | |
| sdk: docker | |
| app_port: 7860 | |
| # PoolCoach | |
| <!-- Khối YAML trên là metadata BẮT BUỘC của Hugging Face Spaces (SDK docker). | |
| GitHub render nó thành bảng nhỏ, vô hại. Xem Documents/PoolCoach_Deploy_HFSpaces.md §3. --> | |
| App huấn luyện **tư duy điều bi 9-ball**: đặt thế bàn → nhận cú đánh gợi ý | |
| (hướng, lực, điểm chạm bi cái) kèm quỹ đạo mô phỏng → đánh thử và chơi tiếp | |
| chuỗi. Phần lõi nghiên cứu của luận văn thạc sĩ PoolCoach. | |
| Hai phần trong một repo: | |
| 1. **Web app gợi ý cú đánh** (`app/` + `src/poolcoach_rl/recommend/`) — | |
| FastAPI + canvas thuần, chạy trên sim vật lý thật. | |
| 2. **Nghiên cứu RL** (`src/poolcoach_rl/envs/` + `scripts/train_*`, | |
| `models/`) — môi trường Gymnasium + các thí nghiệm PPO/SAC/BC/Q-field, | |
| hồ sơ số liệu của luận văn. | |
| ## Engine gợi ý — ZonePlanner | |
| Một engine duy nhất, tiêu chí chốt 31/07/2026 | |
| (`Documents/PoolCoach_App_ZonePlanner_Design.md` §8, repo poolcoach-docs): | |
| - **Tập ứng viên**: chọn 1-2 lỗ gần bi mục tiêu nhất (loại lỗ có góc cắt | |
| > 70°) × 11 kỹ thuật cơ bản (tâm bi / cu lê / trô / xoáy ± kết hợp nhẹ) | |
| × 9 nấc lực — mô phỏng TỪNG cú bằng `pooltool`, lọc trên kết quả sim. | |
| - **Lọc cứng** (AND, không nới): ăn bi trực tiếp không combo, đường bi mục | |
| tiêu sạch; đường bi cái SAU va chạm không đụng bất kỳ bi nào (chạm băng | |
| thì được); góc cắt sang bi kế tại điểm đáp ≤ 45°; khoảng cách đáp | |
| 0.15–1.50 m; lực nấc 2–10 của thang. | |
| - **Xếp hạng** lexicographic, không trọng số: đường bi cái lăn sau va chạm | |
| NGẮN NHẤT (arc length thật — đường có băng dài hơn đường thẳng) → hoà thì | |
| `|d_đáp − 1 m|` nhỏ nhất → hoà nữa thì thứ tự duyệt tất định. | |
| - **Hết đường là một câu trả lời**: thế bàn không có cú đạt đủ tiêu chí thì | |
| app nói thẳng như vậy — kỹ năng cân bi / kick / safety nằm trong lộ trình, | |
| không có thang nới lấy số bằng mọi giá. | |
| Lịch sử nghiên cứu dẫn tới tiêu chí này (oracle grid search, hybrid Q-field | |
| net, zone V1 raster + ablation của chúng) nằm nguyên vẹn ở nhánh | |
| **`v1-full`** và trong nhật ký nghiên cứu (repo poolcoach-docs). | |
| ## Chạy web app | |
| ```bash | |
| # cài core + app (xem mục Cài đặt) | |
| python -m uvicorn app.main:app --port 8000 | |
| # mở http://localhost:8000 — lần đầu chờ ~40s JIT Numba | |
| ``` | |
| 4 endpoint: `GET /api/table`, `GET /api/health`, `POST /api/recommend`, | |
| `POST /api/trajectory` (quỹ đạo cú thay thế — render lười). Bàn mẫu lấy từ | |
| `app/static/demo_boards.json` — danh sách thế bàn đã quét offline sao cho | |
| engine tự chơi chạy hết chuỗi tới bi 9 (`scripts/gen_demo_boards.py`). | |
| Gate nghiệm thu engine: `scripts/eval_fullrack.py --tables 20` — 0 cú gợi ý | |
| phạm luật, search median < 5s (đo 31/07: median 1.42s, max 4.67s, serial). | |
| ## Thư viện bài tập (F3 MVP) | |
| Tab **"Bài tập"** trên UI: chọn drill → canvas vẽ layout + vòng tròn | |
| `cue_zone` ("vùng đích" của bi cái) → tập trên bàn thật, tự khai Đạt/Trượt | |
| đủ số `reps` → chốt x/N đạt. Nút **"Xem gợi ý"** gọi engine trên đúng thế | |
| drill để so cú của mình với cú máy (engine xếp hạng theo tiêu chí riêng, | |
| *không* nhắm zone của drill — cú máy không đạt goal là chuyện bình thường). | |
| - Drill chuẩn nằm trong **`app/drills.json`** (file trong repo, không phải | |
| bảng DB — Space không có Postgres vẫn demo được F3). API: | |
| `GET /api/drills`, `GET /api/drills/{id}`. | |
| - Lượt tập ghi qua `POST /api/drill-attempts` — best-effort: không DB vẫn | |
| 200 với `persisted: false` (FE hiện "chế độ demo — tiến bộ không được | |
| lưu"); lịch sử `GET /api/drill-attempts` cần DB (503 khi không có). | |
| Có phiên bàn (QR) thì tab Bài tập hiện khu **"Tiến bộ phiên này"** — | |
| pass/fail theo drill của đúng phiên đó. | |
| - Sửa/thêm drill thì chạy lại gate khả thi: | |
| `python scripts/verify_drills.py` — 100% layout hợp lệ, mỗi drill phải có | |
| cú pot sạch và ≥1 cú đạt trọn goal trên grid kỹ thuật × lực của engine | |
| (curation, không phải số đo engine). | |
| ## TV tại bàn (demo) | |
| Trang **`/tv`** cho màn hình đặt cạnh bàn (Android TV box / PC mini cắm HDMI | |
| — chạy được trình duyệt là được): mở là hiện **mã pair 6 số**; trên điện | |
| thoại bấm **📺 Ghép TV**, nhập mã → từ đó mỗi lần bấm **Gợi ý**, cú đánh | |
| được đẩy qua WebSocket (`/display/{id}`) xuống TV: sơ đồ bàn nằm ngang chiếm | |
| màn, quỹ đạo cú tốt nhất + 2 cú thay thế, chip lực/điểm chạm cỡ lớn, góc màn | |
| hình ghi giờ cập nhật. TV **read-only** và tự reconnect khi rớt mạng. | |
| Không cần DB/Redis — registry pair nằm in-memory (restart server là ghép | |
| lại; bảng `displays` trong design chờ có auth/dashboard). Trigger hiện là | |
| **tay** (bấm Gợi ý trên điện thoại) — camera + edge agent của chế độ realtime | |
| (FullVision §5.4) thuộc bàn giao sau. Response `/api/recommend` không đổi | |
| một byte dù có hay không TV ghép (test khoá byte-equal). | |
| ## Chạy dev với DB | |
| App chạy được **không cần DB** (không set `DATABASE_URL` là tầng DB tắt hẳn — | |
| HF Space đang chạy đúng chế độ này). DB chỉ cần cho các tính năng quán: | |
| QR bàn → guest session, log recommend vào bảng `recommendations`, lưu + | |
| xem lịch sử lượt tập drill (bảng `drill_attempts`). | |
| ```bash | |
| # 1. Postgres dev (Docker Desktop phải đang chạy) | |
| docker compose -f docker-compose.dev.yml up -d | |
| # 2. Trỏ app vào DB rồi tạo schema | |
| set DATABASE_URL=postgresql+psycopg://poolcoach:poolcoach@localhost:5432/poolcoach | |
| alembic upgrade head | |
| # 3. Seed tenant "dev" + 1 bàn — idempotent, in token QR + URL để thử | |
| python scripts\seed_db.py | |
| # 4. Chạy app như thường (cùng cửa sổ đã set DATABASE_URL) | |
| python -m uvicorn app.main:app --port 8000 | |
| ``` | |
| **Thử luồng QR end-to-end:** mở URL mà seed in ra (`/?qr=<token>`) trong | |
| trình duyệt — FE tự đổi token lấy guest session TTL 24h, header hiện badge | |
| "Bàn: …". Phiên nằm trong `sessionStorage` (của lượt ngồi bàn — đóng tab là | |
| hết); từ lúc đó mọi `POST /api/recommend` và lượt tập drill gửi kèm | |
| `session_id`, và tab Bài tập có khu "Tiến bộ phiên này" đọc lịch sử của đúng | |
| phiên. Token sai / server không DB → FE chỉ toast một câu rồi chạy chế độ | |
| thường như cũ. | |
| `POST /api/recommend` nhận 3 field optional `scan_id`/`edited`/`session_id` | |
| — chỉ để ghi log (session sống thì row `recommendations` mang `tenant_id`), | |
| response không đổi một byte; DB lỗi hay tắt thì recommend vẫn chạy bình | |
| thường (log là best-effort). Secret ký token QR đặt qua env | |
| `POOLCOACH_SECRET` (không set → dùng secret dev, chỉ để chạy local). | |
| `pytest` **không cần** Docker/Postgres — test DB chạy trên SQLite in-memory. | |
| ## Chạy mode queue (worker tách process) | |
| App chạy được **không cần Redis** (không set `REDIS_URL` là queue tắt hẳn, | |
| `/api/recommend` tính in-process y hệt trước — HF Space đang chạy đúng chế | |
| độ này). Mode queue tách engine recommend ra **worker riêng** qua Redis | |
| (request-reply tự viết ~100 dòng, `app/jobqueue.py` — không rq/celery): | |
| đây là mảnh hạ tầng cho realtime/TV sau này, hiện chỉ có một queue | |
| `pc:jobs:recommend`, facade API vẫn đồng bộ. | |
| ```bash | |
| # 1. Redis dev (Docker Desktop phải đang chạy) | |
| docker compose -f docker-compose.dev.yml up -d redis | |
| # 2. Worker — process riêng, JIT ~40s lúc boot, xong mới nhận job | |
| set REDIS_URL=redis://localhost:6379/0 | |
| python scripts\engine_worker.py | |
| # 3. API cùng REDIS_URL (cửa sổ khác) — /api/recommend đi qua worker | |
| set REDIS_URL=redis://localhost:6379/0 | |
| python -m uvicorn app.main:app --port 8000 | |
| ``` | |
| - Engine hai mode là **một hàm** (`app/engine.py::run_recommend`) — response | |
| bằng nhau từng byte, đã đo trên bàn demo (chỉ `search.elapsed_s` — đồng hồ | |
| — khác nhau giữa hai lần chạy bất kỳ). Overhead queue ~100ms/request. | |
| - `/api/trajectory` giữ in-process có chủ đích (1 sim nhẹ). | |
| - Worker chết/kẹt → request 503 message rõ sau timeout (mặc định 30s, env | |
| `POOLCOACH_QUEUE_TIMEOUT_S`) — **không** âm thầm fallback in-process. | |
| Bật lại worker là request kế chạy tiếp, không cần restart API. | |
| - `GET /api/health` thêm `mode` / `redis_ok` / `worker_alive` (heartbeat | |
| TTL 15s, chỉ bật sau khi worker JIT xong); ba trường cũ giữ nguyên. | |
| - `pytest` **không cần** Docker/Redis — transport test bằng fakeredis. | |
| ## Phần nghiên cứu RL | |
| - `src/poolcoach_rl/envs/` — `SinkOneBallEnv` (stage 1), | |
| `PositionPlayEnv` (stage 2a: reward điều bi qua `_position_q`). | |
| - `scripts/train_sink.py`, `train_bc*.py`, `train_qfield.py`, | |
| `gen_bc_dataset.py`, `relabel_bc_dataset.py`, `eval_position.py`, | |
| `oracle_controllability.py` — chuỗi thí nghiệm PPO/SAC → BC warm-start → | |
| Q-field; bản lưu lệnh tái lập ở `scripts/experiments/` (kèm ngưỡng nghiệm | |
| thu trong comment từng file). | |
| - `models/` (Git LFS) — trong đó `qfield_20260721/qfield.pt` là model | |
| Q-field mà luận văn trích số; app thôi dùng nó từ 31/07/2026. | |
| ## F2 CV — baseline offline (05/08/2026) | |
| Khởi động nhánh CV scan bàn (FullVision §5.2): detect bi từ ảnh + homography | |
| 4 góc → toạ độ bàn hệ pooltool. Hoàn toàn offline — chưa camera, chưa tích | |
| hợp app. Code ở `src/poolcoach_cv/` (không import lẫn với `poolcoach_rl`), | |
| deps riêng `requirements-cv.txt` (môi trường app không cần torch). | |
| **Dataset: pix2pockets** (arXiv [2504.12045](https://arxiv.org/abs/2504.12045), | |
| SCIA 2025) — bản export Roboflow `8-Ball-Pool-3` tác giả ship trong | |
| [repo chính thức](https://github.com/viktorseba/pix2pockets), **License CC BY 4.0** | |
| (ghi trong README.dataset.txt + data.yaml của bản export; | |
| [project Roboflow gốc](https://universe.roboflow.com/bachelorthesis/8-ball-pool-l530o)). | |
| 247 ảnh / 7244 bbox YOLOv5-format, 5 class `Black · Cue · Dot · Solid · Striped` | |
| (ảnh frame giải 8-ball quốc tế, nhiều góc camera). Zip gốc dồn hết vào `train/` | |
| nên script tải tự dựng split 80/20 tất định (seed 20260805): | |
| **train 198 ảnh / 5801 box · val 49 ảnh / 1443 box**. | |
| ```bash | |
| # tải + giải nén + dựng split (idempotent; ~41 MB, không cần API key) | |
| python scripts/cv/fetch_dataset.py | |
| ``` | |
| Dataset nằm ở `datasets/` — bị gitignore, không vào git. | |
| **Hai venv, tách cố ý** (05/08/2026): app/RL dùng `..\poolcoach-env` (torch | |
| CPU), CV dùng `..\poolcoach-cv-env` (torch 2.12.1+cu130, train GPU RTX 3070) — | |
| vì đổi venv chung sang CUDA sẽ lặng lẽ lật `device="auto"` của SB3 sang GPU, | |
| làm số RL tương lai hết so được với số RL cũ đo trên CPU. Dựng venv CV: venv | |
| Python 3.12 mới, cài `torch==2.12.1 torchvision` từ index | |
| `https://download.pytorch.org/whl/cu130` trước, rồi `requirements-cv.txt` | |
| (riêng, kéo ultralytics — cài bằng uv như các tầng khác). | |
| **Train baseline** — YOLO11n fine-tune từ COCO, chạy trên `poolcoach-cv-env`: | |
| ```bash | |
| # launcher: run_cv_train_baseline.bat (epochs 30 — GPU ~1 phút, CPU ~40 phút) | |
| python scripts/cv/train_baseline.py --epochs 30 --device 0 | |
| ``` | |
| Bản đầy đủ (epochs 150 · batch 32 GPU): `run_cv_train_full.bat`. Số baseline | |
| 05/08/2026 (CPU, seed 20260805): **mAP50 0.674 · mAP50-95 0.473** val; | |
| per-class + args trong `..\cv_baseline_20260805\metrics.json`. Số full | |
| 05/08/2026 (GPU, cùng seed/imgsz/split): **mAP50 0.893 · mAP50-95 0.678**, | |
| wall 7.8 phút; per-class + args trong `..\cv_full_20260805\metrics.json`. | |
| **Demo end-to-end mini** — detect → tâm bbox → homography 4 góc (đo tay, | |
| hardcode trong script cho 3 ảnh val) → toạ độ bàn hệ pooltool + overlay: | |
| ```bash | |
| python scripts/cv/demo_scan.py | |
| ``` | |
| Kết quả 05/08: 44/45 điểm bi nằm trong [0,W]×[0,L] (ca trượt duy nhất lệch | |
| 9 mm — bi sát băng, tâm bbox cao hơn điểm chạm nỉ). Overlay ở | |
| `..\cv_baseline_20260805\demo\`. | |
| ## Scan ảnh bàn (F2 MVP, 05/08/2026) | |
| Nút **"📷 Scan ảnh bàn"** trên UI: chọn ảnh chụp bàn → chấm **4 góc mặt nỉ** | |
| theo hướng dẫn từng bước trên overlay (đi vòng quanh bàn, 2 góc đầu là một | |
| băng ngắn) → **Quét** → CV detect bi + homography đổ thế bi lên canvas → | |
| người chơi **kéo-thả sửa vị trí / đổi số** rồi bấm Gợi ý như thường. Đây là | |
| van an toàn FullVision §5.2: CV chỉ *gợi ý* thế bi, người chơi mới là người | |
| chốt. Góc đã chấm lưu `sessionStorage` — ảnh sau chụp cùng khung hình (camera | |
| cố định) thì khỏi chấm lại. KHÔNG camera, KHÔNG realtime — ảnh do người dùng | |
| upload. | |
| Kiến trúc: `POST /api/scan` (multipart ảnh + corners) → queue `pc:jobs:scan` | |
| (cùng transport request-reply của `app/jobqueue.py`) → **CV worker** | |
| (`scripts/cv_worker.py`, process riêng trên venv `..\poolcoach-cv-env` CUDA, | |
| load `best.pt` một lần lúc boot, heartbeat riêng) → YOLO predict (conf vận | |
| hành **0.2993**, argmax F1 any-ball trên val — `scripts/cv/pick_conf.py`) → | |
| tâm bbox → homography (`src/poolcoach_cv/`) → `{scan_id, balls}`. App | |
| **không import torch** — không có REDIS_URL/worker thì `/api/scan` trả 503 | |
| nói thẳng, mọi thứ còn lại của app y nguyên. | |
| ```bash | |
| # 1. Redis (Docker Desktop phải đang chạy) | |
| docker compose -f docker-compose.dev.yml up -d redis | |
| # 2. CV worker — venv CV, load model vài giây rồi mới nhận job | |
| # (launcher: run_cv_worker.bat ở thư mục cha) | |
| set REDIS_URL=redis://localhost:6379/0 | |
| ..\poolcoach-cv-env\Scripts\python.exe scripts\cv_worker.py | |
| # 3. App cùng REDIS_URL (cửa sổ khác; kèm engine worker nếu muốn recommend | |
| # cũng đi mode queue — xem mục "Chạy mode queue") | |
| set REDIS_URL=redis://localhost:6379/0 | |
| python -m uvicorn app.main:app --port 8000 | |
| ``` | |
| Số đo 05/08 (2 ảnh val pix2pockets, GPU RTX 3070): scan end-to-end qua HTTP | |
| **~0.3–0.4 s/ảnh** (job thuần trong worker 40–110 ms); bi trả về nằm trọn | |
| trong bàn, đúng 1 bi cái, class Dot bị loại. `scan_id` + `edited` (người chơi | |
| có sửa sau scan không) đi kèm `POST /api/recommend` chỉ để ghi log — response | |
| recommend không đổi một byte. | |
| Thước deploy (predict-pipeline, conf 0.2993, any-ball, val 49 ảnh): **P 0.9679 | |
| · R 0.9094 · F1 0.9377** (`scripts/cv/eval_deploy_pr.py` — không so ngang AP val-pipeline). | |
| **Số bi — BallID hai tầng theo màu** (`src/poolcoach_cv/ballid.py`, 06/08): | |
| dataset không có nhãn số nên tầng 1 chấm điểm MÀU per-crop không cần train | |
| (chuẩn hoá trắng theo bi cue, hue circular + tỉ lệ pixel trắng cho sọc bi 9), | |
| tầng 2 gán toàn cục greedy với ràng buộc mỗi số ≤ 1 bi; bi không khớp trả | |
| `number=null` và FE rơi về đánh-số-theo-vị-trí cũ. Đây là baseline có chủ | |
| đích để chương CV so "trước/sau classifier" khi có bộ ảnh VN gán nhãn; đo | |
| trên ảnh thật: `scripts/cv/eval_ballid.py` (GT tay đang chờ điền). | |
| **Hạn chế đã biết (chấp nhận ở MVP):** | |
| - **Số bi gán theo màu là gợi ý** — bi quán VN bạc màu/bộ bi không chuẩn sẽ | |
| lẫn (kỳ vọng lẫn {4↔7↔3, 1↔9}); người chơi sửa chip/kéo-thả — đó chính là | |
| vai trò của van an toàn. | |
| - Bi **sát băng lệch ~9 mm** (tâm bbox cao hơn điểm chạm nỉ — đo 05/08). | |
| - Model có thể **bỏ sót bi** (vd ảnh val 103 góc quay xiên: bi cái không | |
| detect được ở mọi conf) — FE giữ bi cái ở chỗ cũ + toast nhắc kéo tay; | |
| không bi cái cũng đồng nghĩa **không chuẩn hoá trắng được** (`wb=false`). | |
| - Bi conf < 0.5 hoặc số CV chưa chắc tô **viền cam đứt** nhắc kiểm; kéo bi | |
| là viền tắt. | |
| - Ảnh quán VN thật **ngoài phân phối** dataset (frame giải 8-ball quốc tế) — | |
| gate chỉ đo trên ảnh val; ảnh thật xấu là chất liệu cho bộ ảnh VN sau. | |
| ## Analyzer clip broadcast (lát cắt dọc, 11/08/2026) | |
| Tab **Analyzer**: upload clip **1 cú trọn** (MP4/MOV ≤ 30s) cắt từ video | |
| broadcast → chấm 4 pocket góc trên frame đầu → server track cue ball + | |
| đo **V0 / hướng φ / va chạm / coverage** (`src/poolcoach_cv/broadcast.py`, | |
| nhấc từ pilot P0) → quỹ đạo vẽ lên table view + bảng số kèm cảnh báo. | |
| Từ 11/08 (BG25) va chạm phân loại **bi/băng/unknown** (`contact`) và có | |
| **baseline spin giải tích thô**: `spin_class` follow/draw/stun/side-L/R + | |
| confidence + evidence — chỉ đọc khi cú có va chạm đủ chứng cứ, không thì | |
| `null` có lý do; đây là mốc mà net BG26 (train synthetic) phải thắng. | |
| Job async qua queue `pc:jobs:analyze`, cùng CV worker với scan; chạy | |
| **local-only** (không deploy — bản quyền frame broadcast). Launcher: | |
| `run_analyzer_local.bat` ở thư mục cha (Redis + worker + app một lệnh). | |
| Từ 13/08 (BG31) mọi toạ độ map được **bù độ cao tâm bi** k=R/(h−R) theo | |
| camera decompose từ homography của chính clip (`poolcoach_cv/camera.py`); | |
| kết quả mang cờ `height_comp` (on/off + h_m, f_px) — 4 góc phải chấm đúng | |
| **mép nose vải**, không chấm miệng lỗ trên mặt gỗ. | |
| Dataset synthetic cho net suy ngược (BG26): `scripts/broadcast/` | |
| `gen_synth_shots.py` sinh 150k+5k cú pooltool làm bẩn theo nhiễu P0 đo thật | |
| (spec tái lập: `scripts/broadcast/synth_spec.json`; data ở `datasets/bb9_synth/`, | |
| ngoài git) — `eval_baseline_synth.py` đo baseline giải tích trên held-out | |
| làm mốc gate BG27. | |
| Net suy ngược **ShotNet** (BG27, P2): `src/poolcoach_cv/shotnet.py` | |
| (transformer + shot token + RoPE timestamp thật) train trên 150k synthetic | |
| (`scripts/broadcast/train_shotnet.py`, checkpoint + spec: `models/shotnet_*`), | |
| chấm cùng harness với baseline (`--predictor shotnet`). Kết quả 12/08: thắng | |
| baseline cả 4 chỉ số trên held-out nhưng trượt bar tuyệt đối P2 — số ở | |
| `scripts/experiments/README.md`. | |
| ## Stack | |
| - [`pooltool`](https://pooltool.readthedocs.io) — mô phỏng billiards có vật lý spin | |
| - [Gymnasium](https://gymnasium.farama.org/) — chuẩn hoá môi trường RL | |
| - [Stable-Baselines3](https://stable-baselines3.readthedocs.io) — thuật toán (PPO / SAC), phần train | |
| - FastAPI + uvicorn — BE; canvas thuần không framework — FE | |
| ## Cài đặt (uv — khuyến nghị) | |
| ```bash | |
| # 0. Cài uv (một lần duy nhất) | |
| pip install uv | |
| # 1. Tạo/kích hoạt venv | |
| uv venv poolcoach-env # hoặc dùng venv sẵn có | |
| poolcoach-env\Scripts\activate # Windows | |
| # source poolcoach-env/bin/activate # Linux/macOS | |
| # 2. CORE: pooltool + gymnasium (~100MB) | |
| uv pip install -r requirements.txt --index-strategy unsafe-best-match | |
| # 3. APP: fastapi + uvicorn (web app) | |
| uv pip install -r requirements-app.txt | |
| # 4. RL (chỉ cần cho train): SB3 + torch (~200MB) | |
| uv pip install -r requirements-rl.txt | |
| # 5. DEV (chạy test): pytest + httpx | |
| uv pip install -r requirements-dev.txt | |
| ``` | |
| > `--index-strategy unsafe-best-match` bắt buộc với uv: panda3d có bản stable | |
| > trên PyPI nhưng bản dev 1.11 chỉ có trên `archive.panda3d.org`, flag này cho | |
| > uv nhìn cả hai kho. Với pip thì không cần. | |
| Kiểm tra: | |
| ```bash | |
| python -c "import pooltool; print(pooltool.__version__)" # -> 0.6.0 | |
| python -m pytest tests -q # contract test, ~5s (không cần vật lý thật) | |
| python examples/pooltool_demo.py # lần ĐẦU chậm (~40s Numba JIT), sau nhanh | |
| ``` | |
| ## Cấu trúc | |
| ``` | |
| poolcoach-rl/ | |
| ├── app/ # FastAPI BE + FE canvas (static/) | |
| ├── src/poolcoach_rl/ | |
| │ ├── envs/ # môi trường RL (hồ sơ nghiên cứu) | |
| │ └── recommend/ # engine ZonePlanner V2 + tầng sim/luật | |
| │ ├── simulate.py # facts vật lý (sim tất định) | |
| │ ├── rules.py # luật 9-ball + Q/EV | |
| │ ├── geometry.py # chọn lỗ khả thi, đường bị chắn | |
| │ ├── zone_v2.py # máy lọc + xếp hạng (design §8) | |
| │ └── core.py # recommend_v2 + validate + describe | |
| ├── scripts/ # eval/train/check + experiments/ (bản lưu) | |
| ├── tests/ # contract test (stub pooltool, ~1s) | |
| └── Dockerfile # HF Spaces (torch CPU + detector + ShotNet) | |
| ``` | |
| ## Deploy | |
| Hugging Face Spaces (Docker SDK) — push `main` lên remote `space` là build. | |
| Chi tiết: `Documents/PoolCoach_Deploy_HFSpaces.md` (repo poolcoach-docs). | |
| Từ **14/08/2026** image chạy được cả **Analyzer** trên CPU, trong **một | |
| tiến trình**: `POOLCOACH_LOCAL_CV=1` bật mode `local` — transport chạy | |
| fakeredis trong tiến trình + một luồng gọi đúng hàm của `scripts/cv_worker.py` | |
| (`app/localcv.py`), còn `/api/recommend` vẫn in-process như bản không queue. | |
| Không Redis, không worker rời, không GPU. | |
| Chạy y hệt bản deploy trên máy mình (giả lập Space): | |
| ``` | |
| D:\Khoa luan\run_app_local.bat # POOLCOACH_LOCAL_CV=1 + device cpu | |
| ``` | |
| Kèm image một **video mẫu đã phân tích sẵn** (`app/demo_analyzer/`, xem | |
| README trong đó): mở app là bảng danh sách cú hiện ngay, không chờ phân | |
| tích — trên CPU một VOD 5 phút mất hàng chục phút, không ai đợi được. | |
| File cần có trong image (đều bị `.gitignore` chặn theo mặc định — force-add | |
| hoặc khai ngoại lệ): `models/cv_full_20260805/best.pt` (detector), | |
| `models/shotnet_20260812_c4b/best.pt` (LFS, đã track), và | |
| `app/demo_analyzer/**` (video mẫu; `.gitignore` có dòng ngoại lệ cho | |
| `*.mp4` ở đúng thư mục này). | |