poolcoach-analyzer / Dockerfile
masterdanh's picture
refactor(analyzer): translate identifiers and comments to English
876aff5
Raw History Blame Contribute Delete
4.31 kB
# PoolCoach Analyzer -- Hugging Face Spaces (Docker SDK) or a VPS.
# HF builds this on their own servers when you push, so the local machine
# needs no Docker.
#
# Three big decisions, each standing on its own reason:
#
# 1. CPU torch, ultralytics and cv2 are IN the image. The Analyzer needs
# them: the table/ball detector, the ShotNet checkpoint, video decoding.
# The CPU build comes from pytorch's own CPU index -- the PyPI wheel is
# the ~2.5 GB CUDA build, useless on a Space with no GPU.
# 2. ONE process, no Redis, no separate worker: `POOLCOACH_LOCAL_CV=1`
# switches on "local" mode (app/jobqueue.py) -- an in-process fakeredis
# transport plus a THREAD calling the very functions of
# scripts/cv_worker.py (app/localcv.py). A free Space is one container,
# and feeding Redis plus two workers in there would be three processes
# and two pooltool JITs on 2 vCPUs.
# 3. Where things are written: HF runs the container as uid 1000, for which
# /code is read-only. Everything that NEEDS TO WRITE has to go to /tmp --
# the numba/matplotlib/ultralytics caches AND the Analyzer result
# directory (POOLCOACH_ANALYZER_DIR). Missing that kills the app at
# runtime, not at build time.
FROM python:3.12-slim
WORKDIR /code
# uid 1000 has to EXIST IN /etc/passwd, not merely "be written somewhere".
# Measured by running the image with `docker run --user 1000:1000`: the CV
# thread died while loading the model with `KeyError: getpwuid(): uid not
# found: 1000` -- libraries (ultralytics/torch) ask for HOME through pwd, and
# with no entry for uid 1000 that raises instead of falling back. The app
# survived (the embedded thread catches its own failure) but the Analyzer was
# silently dead on the Space. `-m` so /home/user exists and belongs to
# uid 1000.
RUN useradd -m -u 1000 user
# ffmpeg : cuts the per-shot clips (cv_worker.cut_clip) and measures the
# duration at upload time with ffprobe (app/main.py). Without
# ffmpeg every shot becomes "ffmpeg could not cut the clip" --
# exactly the main feature broken.
# libgl1 + libglib2.0-0 : opencv-python (the non-headless wheel) needs
# 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
# CPU torch in a SEPARATE step with an --index-url pointing ONLY at pytorch's
# CPU index -- it must not be mixed with PyPI here, or the resolver is free to
# pick the CUDA build. The next step pins these exact two versions
# (requirements-cv-cpu.txt) so ultralytics cannot pull a different torch over
# the top.
RUN uv pip install --system --index-url https://download.pytorch.org/whl/cpu \
torch==2.12.1 torchvision==0.27.1
# dependencies first, to make the layer cache useful (requirements.txt
# already carries --extra-index-url archive.panda3d.org for the panda3d 1.11
# dev build)
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
# ONLY the worker file, not all of scripts/. "local" mode loads it by path --
# see app/localcv.py.
COPY scripts/cv_worker.py ./scripts/cv_worker.py
# Exactly TWO models, by name: the table/ball detector and the ShotNet
# checkpoint the app runs. Nothing else under models/ goes into the 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 runs the container as uid 1000, which cannot write /code: point
# every cache and config AND the Analyzer result directory at /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
# Run as the very uid HF will use, so `docker run` on a laptop behaves like
# the Space instead of discovering a permission error after the push (the
# lesson recorded just above).
USER user
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]