# PoolCoach — Hugging Face Spaces (Docker SDK) / VPS. # HF tự build trên server của họ khi push — máy local KHÔNG cần Docker. # Chi tiết: Documents/PoolCoach_Deploy_HFSpaces.md (repo poolcoach-docs). # # 14/08/2026 (BRIEF việc B): image này giờ chạy **Analyzer** (video → danh # sách cú), không chỉ app gợi ý như bản 23/07. Ba thay đổi lớn, mỗi cái có # lý do đứng riêng: # # 1. torch CPU + ultralytics + cv2 QUAY LẠI image (bản 23/07 đã gỡ vì engine # ZonePlanner V2 không nạp net). Analyzer thì cần: detector bàn/bi, ShotNet # c4b, decode video. Bản CPU lấy từ index pytorch riêng — bản PyPI là CUDA # ~2.5GB cho một Space không có GPU. # 2. MỘT tiến trình, không Redis, không worker rời: `POOLCOACH_LOCAL_CV=1` # bật mode "local" (app/jobqueue.py) — transport fakeredis trong tiến # trình + một LUỒNG gọi đúng hàm của scripts/cv_worker.py (app/localcv.py). # Space free là một container; nuôi Redis + 2 worker ở đó là ba tiến # trình và hai lần JIT pooltool trên 2 vCPU. # 3. Ghi ở đâu: HF chạy container bằng uid 1000, /code là read-only với nó. # Mọi thứ CẦN GHI phải ra /tmp — cache numba/mpl/ultralytics VÀ thư mục # kết quả Analyzer (POOLCOACH_ANALYZER_DIR). Thiếu là app chết lúc # runtime chứ không phải lúc build. FROM python:3.12-slim WORKDIR /code # uid 1000 phải CÓ TRONG /etc/passwd, không chỉ "được ghi ở đâu đó". # Đo được 14/08 khi chạy thử image bằng `docker run --user 1000:1000`: # luồng CV chết ngay lúc nạp model với `KeyError: getpwuid(): uid not # found: 1000` — thư viện (ultralytics/torch) hỏi HOME qua pwd, mà uid 1000 # không có entry thì ném KeyError chứ không fallback. App vẫn sống (luồng # nhúng tự bắt lỗi) nhưng tab Analyzer chết câm trên Space. `-m` để # /home/user tồn tại và thuộc uid 1000. RUN useradd -m -u 1000 user # ffmpeg : cắt clip per cú (cv_worker.cut_clip) + ffprobe đo thời lượng # lúc upload (app/main.py). Thiếu ffmpeg thì mọi cú thành # "ffmpeg không cắt được clip" — hỏng đúng tính năng chính. # libgl1 + libglib2.0-0 : opencv-python (bản không headless) cần libGL.so.1 RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg libgl1 libglib2.0-0 \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv # torch CPU ở bước RIÊNG với --index-url CHỈ trỏ index CPU của pytorch — # không được trộn với PyPI ở bước này, nếu không resolver có quyền chọn bản # CUDA. Bước sau pin lại đúng hai phiên bản này (requirements-cv-cpu.txt) để # ultralytics không kéo torch khác đè lên. RUN uv pip install --system --index-url https://download.pytorch.org/whl/cpu \ torch==2.12.1 torchvision==0.27.1 # deps trước để tận dụng layer cache (requirements.txt đã chứa # --extra-index-url archive.panda3d.org cho panda3d 1.11 dev) COPY requirements.txt requirements-app.txt requirements-cv-cpu.txt ./ RUN uv pip install --system --index-strategy unsafe-best-match \ -r requirements.txt -r requirements-app.txt -r requirements-cv-cpu.txt COPY src ./src COPY app ./app # CHỈ file worker, không cả scripts/ (train/eval/diag không thuộc image). # Mode "local" nạp nó bằng đường dẫn — xem app/localcv.py. COPY scripts/cv_worker.py ./scripts/cv_worker.py # Đúng HAI model, đích danh: detector bàn/bi + ShotNet c4b đang chạy app. # Cả models/ còn lại (mấy chục run RL/CV cũ) KHÔNG vào image. COPY models/cv_full_20260805/best.pt ./models/cv_full_20260805/best.pt COPY models/shotnet_20260812_c4b/best.pt ./models/shotnet_20260812_c4b/best.pt # HF Spaces chạy container bằng uid 1000 (không ghi được /code): # trỏ mọi cache/config VÀ thư mục kết quả Analyzer về /tmp. ENV HOME=/tmp \ NUMBA_CACHE_DIR=/tmp/numba \ MPLCONFIGDIR=/tmp/mpl \ YOLO_CONFIG_DIR=/tmp \ PYTHONUNBUFFERED=1 \ POOLCOACH_LOCAL_CV=1 \ POOLCOACH_CV_DEVICE=cpu \ POOLCOACH_ANALYZER_DIR=/tmp/analyzer EXPOSE 7860 # Chạy bằng chính uid HF sẽ dùng — để `docker run` trên máy giống Space, # không phải phát hiện lỗi quyền sau khi đã push (bài học ngay ở trên). USER user CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]