sentiment-scope / Dockerfile
melkholy's picture
Ship hardening wave: off-loop CSV inference, model_ids cap/dedupe, real detector Hub ids, comment fixes
019b6f4 verified
Raw
History Blame Contribute Delete
5.01 kB
# Single-image deployment for Hugging Face Spaces (free CPU tier).
#
# Why this exists next to docker-compose.yml: a Space is exactly ONE
# container, so the compose topology (nginx + backend) cannot run there.
# Instead FastAPI serves the built SPA itself via StaticFiles (see the
# STATIC_DIR mount in backend/app/main.py). Same app, three serving
# topologies β€” Vite proxy in dev, nginx in compose, FastAPI static here β€”
# and the frontend never changes because it only calls relative /api paths.
# ---- Stage 1: build the static SPA bundle -----------------------------------
FROM node:24-alpine AS frontend-build
WORKDIR /build
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# ---- Stage 2: FastAPI serving both the SPA and the API -----------------------
FROM python:3.13-slim
# Spaces runs the container as uid 1000, NOT root β€” /root is unwritable
# there. Create the same uid at build time so everything (pip installs,
# the HF cache, the app) lives in a home this user owns.
RUN useradd -m -u 1000 user
USER user
ENV HOME=/home/user \
PATH=/home/user/.local/bin:$PATH \
HF_HOME=/home/user/.cache/hf
WORKDIR /home/user/app
# CPU-only torch wheel: ~10x smaller than the default CUDA build, and the
# free Space has no GPU anyway. requirements-docker.txt excludes torch so
# we install it once from the CPU index, then the rest normally.
COPY --chown=user backend/requirements-docker.txt .
RUN pip install --no-cache-dir torch==2.12.1 --index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r requirements-docker.txt
# Bake the model weights INTO the image. A free Space has ephemeral disk and
# sleeps after ~48h idle, so anything downloaded at runtime is re-downloaded
# on every cold start before the health check can pass. Baked image layers, by
# contrast, survive restarts.
#
# Sentiment models bake into the HF cache via their real Hub names (which double
# as the registry's fallback source id). Both ENABLED_MODELS sentiment entries
# are baked: the Compare tab loads distilbert on first use.
RUN python -c "\
from transformers import AutoModelForSequenceClassification, AutoTokenizer; \
names = ['cardiffnlp/twitter-roberta-base-sentiment-latest', \
'distilbert/distilbert-base-uncased-finetuned-sst-2-english']; \
[(AutoTokenizer.from_pretrained(n), AutoModelForSequenceClassification.from_pretrained(n)) for n in names]"
# Detectors are baked differently β€” into local model directories, not the HF
# cache. Each registry detector sets a local_path, and resolve_model_source()
# checks that on-disk dir FIRST (falling back to the Hub name only if it is
# absent), so runtime resolves detectors from local weights. snapshot_download
# each real detector repo into the exact path resolve_model_source() computes at
# runtime (models/ sibling of the app dir). The registry names are the real Hub
# repos too (the same ids used as the snapshot_download source), so the Hub-name
# fallback would resolve as well β€” exactly like the sentiment models above.
#
# All THREE detectors are baked, not lazy-loaded: the AI Detector tab's default
# action runs /api/ai-detect/compare with no model_ids, which scores every
# detector at once β€” so a first click needs all three regardless, and on
# ephemeral disk any un-baked weight would re-download on every cold start.
# ~3GB of DeBERTa-v3-large + two RoBERTa checkpoints; the free Space's 16GB RAM
# fits all five models (~3.4GB resident). *.bin is skipped because every repo
# ships model.safetensors β€” this drops redundant pytorch_model.bin/training_args.bin.
RUN python -c "\
from huggingface_hub import snapshot_download; \
repos = {'desklib/ai-text-detector-v1.01': 'desklib-ai-text-detector-v1.01', \
'fakespot-ai/roberta-base-ai-text-detection-v1': 'fakespot-roberta-base-ai-text-detection-v1', \
'Oxidane/tmr-ai-text-detector': 'oxidane-tmr-ai-text-detector'}; \
[snapshot_download(repo, local_dir=f'/home/user/models/{d}', ignore_patterns=['*.bin']) for repo, d in repos.items()]"
COPY --chown=user backend/app ./app
COPY --chown=user --from=frontend-build /build/dist ./static
# STATIC_DIR turns on the FastAPI StaticFiles mount; PUBLIC_DEPLOY arms the
# slowapi rate limiter; ENABLED_MODELS is the registry allowlist. All five baked
# models are enabled β€” the two sentiment models plus all three AI detectors β€” so
# both the Compare and AI Detector tabs work live; any other registry model
# (finbert, xlm-twitter) still 403s. HF_HUB_OFFLINE is set only now β€” AFTER the
# bake steps β€” so runtime never touches the network: startup either finds the
# baked weights (sentiment in the HF cache, detectors in local dirs) or fails loudly.
ENV STATIC_DIR=/home/user/app/static \
PUBLIC_DEPLOY=1 \
ENABLED_MODELS=twitter-roberta,distilbert-sst2,desklib-ai-detector,fakespot-ai-detector,oxidane-ai-detector \
HF_HUB_OFFLINE=1
EXPOSE 7860
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]