# 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"]