poolcoach / README.md
masterdanh's picture
deploy: snapshot for HF Space
78738de
|
Raw
History Blame Contribute Delete
22.7 kB
metadata
title: PoolCoach
emoji: 🎱
colorFrom: green
colorTo: gray
sdk: docker
app_port: 7860

PoolCoach

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

# 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).

# 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ộ.

# 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, SCIA 2025) — bản export Roboflow 8-Ball-Pool-3 tác giả ship trong repo chính thức, License CC BY 4.0 (ghi trong README.dataset.txt + data.yaml của bản export; project Roboflow gốc). 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.

# 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:

# 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:

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.

# 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 — mô phỏng billiards có vật lý spin
  • Gymnasium — chuẩn hoá môi trường RL
  • Stable-Baselines3 — 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ị)

# 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:

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).