Spaces:
Sleeping
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:
- 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. - 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ớipersisted: false(FE hiện "chế độ demo — tiến bộ không được lưu"); lịch sửGET /api/drill-attemptscầ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/trajectorygiữ 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/healththêmmode/redis_ok/worker_alive(heartbeat TTL 15s, chỉ bật sau khi worker JIT xong); ba trường cũ giữ nguyên.pytestkhô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.ptlà 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-matchbắt buộc với uv: panda3d có bản stable trên PyPI nhưng bản dev 1.11 chỉ có trênarchive.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).