Spaces:
Sleeping
Sleeping
| # 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"] | |