Spaces:
Sleeping
Sleeping
File size: 11,852 Bytes
a3953cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | ###############################################################################
# Dockerfile β HF Spaces (Docker SDK, CPU-only, port 7860)
#
# Two-stage build:
# Stage A β node:22-slim β compile React/Vite frontend (VITE_API_BASE="")
# Stage B β python:3.11-slim β runtime with all ML models baked in
#
# HF Spaces rules enforced:
# β’ Non-root user uid=1000 (useradd -m -u 1000 user)
# β’ All writable runtime dirs under /tmp (STORAGE_DIR, JOBS_DB_DIR)
# β’ Model cache baked at build time under /home/user (readable by uid 1000)
# β’ Port 7860
#
# Layer-size note
# ---------------
# Model downloads and COPY instructions run AFTER "USER user" so files are
# created owned by uid 1000 directly. The only root-owned step after mkdir is
# facexlib (must write to site-packages). This eliminates the old
# "RUN chown -R 1000:1000 /home/user" layer that duplicated ~1.5 GB of model
# weights via Docker's copy-on-write mechanism.
###############################################################################
###############################################################################
# Stage A β Build the React / Vite frontend
###############################################################################
FROM node:22-slim AS frontend-build
WORKDIR /build/frontend
# Install npm deps (cached until package*.json changes)
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
# Build with VITE_API_BASE="" so every fetch() in the compiled JS uses a
# relative path (same-origin), not hardcoded http://localhost:8000.
COPY frontend/ .
RUN VITE_API_BASE="" npm run build
# Output lives at /build/frontend/dist
###############################################################################
# Stage B β Python 3.11 runtime
###############################################################################
FROM python:3.11-slim
# ββ OS dependencies βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# libgl1 + libglib2.0-0 : opencv-python requirement
# libsm6 libxext6 libxrender1 : X11 stubs required by cv2 on headless Linux
# wget ca-certificates : download model weights in subsequent RUN steps
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libgl1 \
libglib2.0-0 \
libsm6 \
libxext6 \
libxrender1 \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# ββ Non-root user (HF Spaces hard requirement) ββββββββββββββββββββββββββββββββ
RUN useradd -m -u 1000 user
# ββ Python packages βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Step 1: CPU-only torch + torchvision from the dedicated wheel index.
# Versions pinned to match the local dev environment (torch 2.8.0, tv 0.23.0).
# Installing from pytorch.org/whl/cpu guarantees no CUDA binary is pulled.
# Step 2: rest of requirements.txt. pip finds torch>=2.2.0 already satisfied
# by the CPU wheel above and skips it; all other deps resolve normally.
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir \
"torch==2.8.0" "torchvision==0.23.0" \
--index-url https://download.pytorch.org/whl/cpu \
&& pip install --no-cache-dir -r /tmp/requirements.txt
# ββ facexlib weights (must run as root β writes to site-packages) βββββββββββββ
# URLs verified against facexlib 0.3.0 source:
# recognition/__init__.py : recognition_arcface_ir_se50.pth β v0.1.0
# __init__.py : detection_Resnet50_Final.pth β v0.1.0
# __init__.py : parsing_parsenet.pth β v0.2.2
#
# FaceRestoreHelper.__init__ triggers the download of detection + parsing models
# automatically via load_file_from_url. ArcFace is called separately because
# facerestore_engine.py loads it independently (raises FileNotFoundError if absent).
RUN python -c "\
import torch; \
from facexlib.utils.face_restoration_helper import FaceRestoreHelper; \
from facexlib.utils.misc import load_file_from_url; \
import facexlib; \
from pathlib import Path; \
weights_dir = str(Path(facexlib.__file__).parent / 'weights'); \
print('facexlib weights dir:', weights_dir); \
print('Pre-downloading RetinaFace (detection_Resnet50_Final.pth, 104 MB) and ParseNet (parsing_parsenet.pth, 81 MB)...'); \
FaceRestoreHelper(upscale_factor=1, face_size=512, crop_ratio=(1, 1), det_model='retinaface_resnet50', save_ext='png', use_parse=True, device=torch.device('cpu')); \
print('RetinaFace + ParseNet ready.'); \
print('Downloading ArcFace IR-SE50 (recognition_arcface_ir_se50.pth, 167 MB)...'); \
load_file_from_url(url='https://github.com/xinntao/facexlib/releases/download/v0.1.0/recognition_arcface_ir_se50.pth', model_dir='facexlib/weights', progress=True, save_dir=weights_dir); \
print('All facexlib weights downloaded.')"
# ββ Pre-create model/cache dirs and chown while they are EMPTY (cheap) ββββββββ
# chown on empty directories has zero copy-on-write cost.
# All subsequent writes (model downloads, COPY) happen as uid 1000 directly.
RUN mkdir -p \
/home/user/app/ml_models/birefnet \
/home/user/app/backend/ml_models/codeformer \
/home/user/.cache/huggingface \
/home/user/.paddleocr \
&& chown -R 1000:1000 \
/home/user/app \
/home/user/.cache \
/home/user/.paddleocr
# ββ Switch to uid 1000 β all subsequent RUN steps write as the target user ββββ
USER user
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# MODEL DOWNLOADS (all happen at build time β zero network I/O at startup)
# Running as uid 1000 so files are created with correct ownership directly β
# no chown layer needed at the end.
# Layer order: stable downloads first so code changes don't invalidate them.
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ββ RealESRGAN_x4plus.pth (64 MB) βββββββββββββββββββββββββββββββββββββββββββ
# Official release: https://github.com/xinntao/Real-ESRGAN/releases/tag/v0.1.0
# Destination matches UPSCALE_MODEL_PATH = BASE_DIR / "ml_models" / "RealESRGAN_x4plus.pth"
RUN wget --tries=3 --no-verbose --show-progress \
-O /home/user/app/ml_models/RealESRGAN_x4plus.pth \
"https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"
# ββ codeformer.pth (360 MB) βββββββββββββββββββββββββββββββββββββββββββββββββ
# Official release: https://github.com/sczhou/CodeFormer/releases/tag/v0.1.0
# Destination matches CODEFORMER_MODEL_PATH = backend/ml_models/codeformer/codeformer.pth
RUN wget --tries=3 --no-verbose --show-progress \
-O /home/user/app/backend/ml_models/codeformer/codeformer.pth \
"https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth"
# ββ BiRefNet (~424 MB, HuggingFace Hub) βββββββββββββββββββββββββββββββββββββ
# Uses snapshot_download (no model instantiation) so it is reliable in a
# headless build environment. The cache_dir matches BIREFNET_MODEL_CACHE in
# config.py (BASE_DIR / "ml_models" / "birefnet"). trust_remote_code=True is
# NOT needed here β snapshot_download just copies files. At runtime,
# from_pretrained(..., trust_remote_code=True) reads the same cache.
RUN python -c "\
from huggingface_hub import snapshot_download; \
print('Downloading BiRefNet from HuggingFace Hub (~424 MB)...'); \
snapshot_download(repo_id='ZhengPeng7/BiRefNet', cache_dir='/home/user/app/ml_models/birefnet'); \
print('BiRefNet download complete.')"
# ββ PaddleOCR models (~200 MB, text detection/recognition/orientation) βββββββ
# HOME is set to /home/user so models land in /home/user/.paddleocr, which is
# exactly where the runtime process will look for them.
# Same PaddleOCR constructor args as ocr_service.py: use_textline_orientation,
# lang="en", enable_mkldnn=False (safe on Linux CPU, avoids OneDNN issues).
RUN HOME=/home/user python -c "\
from paddleocr import PaddleOCR; \
print('Pre-warming PaddleOCR (first-run model download ~200 MB)...'); \
PaddleOCR(use_textline_orientation=True, lang='en', enable_mkldnn=False); \
print('PaddleOCR pre-warm complete.')"
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# SOURCE & COMMITTED WEIGHTS (change more often than model downloads β last)
# --chown=user:user ensures uid 1000 owns all copied files without a separate
# chown layer (avoids copy-on-write duplication of large files).
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ββ Backend source (.dockerignore excludes codeformer.pth β already downloaded)
COPY --chown=user:user backend/ /home/user/app/backend/
# ββ lama.pth (197 MB) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# No reliable public download URL exists for the big-lama TorchScript export.
# This file MUST be committed to the repository via Git LFS.
# Register with: git lfs track "ml_models/lama.pth"
# Destination matches MODEL_PATH = BASE_DIR / "ml_models" / "lama.pth"
COPY --chown=user:user ml_models/lama.pth /home/user/app/ml_models/lama.pth
# ββ Built React app from Stage A βββββββββββββββββββββββββββββββββββββββββββββ
# Destination matches the path computed in main.py:
# Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
# = /home/user/app/frontend/dist (when __file__ is backend/app/main.py)
COPY --chown=user:user --from=frontend-build /build/frontend/dist /home/user/app/frontend/dist/
# ββ Runtime environment variables βββββββββββββββββββββββββββββββββββββββββββββ
# Every variable here has a local-safe default in the Python code; these
# overrides are ONLY active inside the container (start.bat sets none of them).
ENV HOME=/home/user \
PORT=7860 \
SERVE_FRONTEND=true \
PUBLIC_BASE_URL="" \
ALLOWED_ORIGINS="*" \
STORAGE_DIR=/tmp/storage \
JOBS_DB_DIR=/tmp/data \
HF_HOME=/home/user/.cache/huggingface \
HF_HUB_DISABLE_PROGRESS_BARS=1
EXPOSE 7860
# Working directory mirrors start.bat: `cd backend && uvicorn app.main:app`
# "app.main:app" resolves to /home/user/app/backend/app/main.py
WORKDIR /home/user/app/backend
# No --reload (dev-only). --host 0.0.0.0 required for HF Spaces to route traffic.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|