Spaces:
Running
Running
| # Plain pip wheels throughout β the former PDAL dependency (conda-forge only, | |
| # broke Spaces builds) was replaced by laspy + pyproj, verified bit-identical. | |
| # Base image pinned by digest (python:3.11-slim) so a rebuild is reproducible. | |
| FROM python:3.11-slim@sha256:e031123e3d85762b141ad1cbc56452ba69c6e722ebf2f042cc0dc86c47c0d8b3 | |
| WORKDIR /app | |
| # CPU-only torch: ~1.5 GB smaller than the default CUDA build. Pinned to match CI; | |
| # torchvision is required by transformers' Mask2Former image processor. | |
| RUN pip install --no-cache-dir torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cpu | |
| # Postgres driver for the durable prod store (P1d). Installed separately from the | |
| # hash-pinned lock (same pattern as torch) β a prebuilt binary wheel, no libpq build. | |
| # Used only when DATABASE_URL is a postgres:// URL (Neon); SQLite is the default otherwise. | |
| RUN pip install --no-cache-dir "psycopg[binary]>=3.2,<3.3" | |
| # Clerk session-JWT verification (P2a dashboard auth): PyJWT + cryptography for RS256/JWKS. | |
| # Installed separately from the hash-pinned lock (prebuilt wheels). Used only when | |
| # CLERK_SECRET_KEY is set; otherwise the dashboards run on the dev-token shim. | |
| RUN pip install --no-cache-dir "pyjwt[crypto]>=2.8,<3" | |
| # Cloud Tasks client for serverless batch dispatch (B3). Installed separately from the | |
| # hash-pinned lock. Used only when the CLOUD_TASKS_* env is set (Cloud Run); otherwise batch | |
| # runs on the in-process worker and this stays dormant. | |
| RUN pip install --no-cache-dir "google-cloud-tasks>=2.16,<3" | |
| # S3-compatible client for the optional Cloudflare R2 LiDAR tile cache (A3). Installed | |
| # separately from the hash-pinned lock (prebuilt wheels). Used only when the R2_* env is | |
| # set; otherwise the tile cache is a no-op and tiles come straight from Douglas S3 / USGS. | |
| RUN pip install --no-cache-dir "boto3>=1.34,<2" | |
| # Error tracking (observability MVP). Installed separately from the hash-pinned lock. Used | |
| # only when SENTRY_DSN is set; otherwise Sentry init is a no-op and this stays dormant. | |
| RUN pip install --no-cache-dir "sentry-sdk>=2.0,<3" | |
| # Runtime deps from the hash-pinned lock (uv pip compile of requirements-api.txt). | |
| # --require-hashes refuses anything whose hash isn't listed, so the whole tree is | |
| # reproducible and tamper-evident. Regenerate the lock when requirements-api.txt changes. | |
| COPY requirements-api.lock . | |
| RUN pip install --no-cache-dir --require-hashes -r requirements-api.lock | |
| COPY pyproject.toml ./ | |
| COPY src/ src/ | |
| RUN pip install --no-cache-dir --no-deps . | |
| # Hugging Face Spaces runs the container as UID 1000 with ephemeral disk. | |
| RUN useradd -m -u 1000 user && mkdir -p /app/data && chown -R user:user /app | |
| USER user | |
| ENV HOME=/home/user | |
| ENV HF_HOME=/home/user/.cache/huggingface | |
| ENV MPLBACKEND=Agg | |
| ENV MPLCONFIGDIR=/tmp/mpl | |
| # Pre-bake the segmentation models into the image so ephemeral hosts don't | |
| # re-download them from Hugging Face after every restart. mask2former (~200 MB) | |
| # is always used; SAM (~375 MB) backs the opt-in SAM_RESTRICT path β baking it | |
| # avoids a ~1-2 min download on the first quote after each startup when enabled. | |
| # IDs + revisions are imported from the installed package (single source of truth), | |
| # so the baked weights always match what the code loads at runtime. | |
| RUN python -c "from transformers import AutoImageProcessor, AutoModelForUniversalSegmentation as M; \ | |
| from lawn_estimator.segmentation import LAWN_MODEL_ID as i, MODEL_REVISIONS as r; \ | |
| AutoImageProcessor.from_pretrained(i, revision=r[i]); \ | |
| M.from_pretrained(i, revision=r[i])" | |
| RUN python -c "from transformers import SamModel, SamProcessor; \ | |
| from lawn_estimator.restrict import SAM_MODEL as s, SAM_REVISION as v; \ | |
| SamModel.from_pretrained(s, revision=v); \ | |
| SamProcessor.from_pretrained(s, revision=v)" | |
| # EoMT-DINOv3 (~1.2 GB) backs the opt-in LAWN_CASCADE path β without the bake, | |
| # the first cascade quote after every restart pulls it from HF (minutes of stall). | |
| RUN python -c "from transformers import AutoImageProcessor, AutoModelForUniversalSegmentation as M; \ | |
| from lawn_estimator.segmentation import CASCADE_PRIMARY_MODEL_ID as i, MODEL_REVISIONS as r; \ | |
| AutoImageProcessor.from_pretrained(i, revision=r[i]); \ | |
| M.from_pretrained(i, revision=r[i])" | |
| # Statewide parcels GeoPackage (~1.1 GB, private HF dataset) backs the opt-in | |
| # PARCELS_FALLBACK path. HF_TOKEN arrives as a BuildKit secret (Space secret of | |
| # the same name); when absent (e.g. a local build) the bake is skipped and the | |
| # fallback simply reports the file as missing if ever enabled. | |
| RUN --mount=type=secret,id=HF_TOKEN,mode=0444,required=false \ | |
| python -c "import os; \ | |
| tok = open('/run/secrets/HF_TOKEN').read().strip() if os.path.exists('/run/secrets/HF_TOKEN') else None; \ | |
| from huggingface_hub import hf_hub_download; \ | |
| hf_hub_download('TempuraML/ne-parcels', 'ne_parcels.gpkg', repo_type='dataset', token=tok, local_dir='data/ne_parcels') if tok else print('HF_TOKEN absent - skipping parcels bake')" | |
| RUN mkdir -p data/lidar/tile_index data/outputs | |
| EXPOSE 8000 | |
| CMD ["uvicorn", "lawn_estimator.api:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] | |