Spaces:
Sleeping
Sleeping
Commit Β·
998bc6e
1
Parent(s): 7a5bb5d
Initial commit for Hugging Face deployment
Browse files- .dockerignore +27 -0
- Dockerfile +39 -0
- backend/inference.py +4 -0
- backend/main.py +9 -1
- backend/utils/model_fetcher.py +36 -0
- docker-compose.yml +1 -0
- frontend/.dockerignore +10 -0
- frontend/src/App.tsx +1 -1
- upload_to_r2.py +41 -0
.dockerignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python / Backend
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
ENV/
|
| 9 |
+
.env
|
| 10 |
+
|
| 11 |
+
# Large Files / Data
|
| 12 |
+
data/images/
|
| 13 |
+
data/masks/
|
| 14 |
+
*.keras
|
| 15 |
+
*.h5
|
| 16 |
+
*.pkl
|
| 17 |
+
|
| 18 |
+
# Project
|
| 19 |
+
# frontend/ # Remove this to allow frontend build in multi-stage Dockerfile
|
| 20 |
+
node_modules/
|
| 21 |
+
.git
|
| 22 |
+
.github
|
| 23 |
+
.gitignore
|
| 24 |
+
.dockerignore
|
| 25 |
+
implementation_plan.md
|
| 26 |
+
task.md
|
| 27 |
+
walkthrough.md
|
Dockerfile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build the frontend
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Final image with Python backend
|
| 10 |
+
FROM python:3.10-slim
|
| 11 |
+
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# Install system dependencies for OpenCV/Pillow
|
| 15 |
+
RUN apt-get update && apt-get install -y \
|
| 16 |
+
libglib2.0-0 \
|
| 17 |
+
libsm6 \
|
| 18 |
+
libxext6 \
|
| 19 |
+
libxrender-dev \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
# Copy requirements and install
|
| 23 |
+
COPY backend/requirements.txt .
|
| 24 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copy built frontend from Stage 1
|
| 27 |
+
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 28 |
+
|
| 29 |
+
# Copy backend and supporting code
|
| 30 |
+
COPY backend/ ./backend/
|
| 31 |
+
COPY model/ ./model/
|
| 32 |
+
COPY utils/ ./utils/
|
| 33 |
+
|
| 34 |
+
# Hugging Face Spaces uses port 7860 by default
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
EXPOSE 7860
|
| 37 |
+
|
| 38 |
+
# We use the PORT environment variable in main.py
|
| 39 |
+
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
backend/inference.py
CHANGED
|
@@ -8,6 +8,10 @@ import sys
|
|
| 8 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 9 |
from model.unet import build_unet
|
| 10 |
from model.loss_metrics import get_metrics, bce_dice_loss
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# Path to the saved model weights
|
| 13 |
MODEL_PATH = os.path.join(os.path.dirname(__file__), "../model/saved_models/oil_spill_unet_best.keras")
|
|
|
|
| 8 |
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 9 |
from model.unet import build_unet
|
| 10 |
from model.loss_metrics import get_metrics, bce_dice_loss
|
| 11 |
+
from utils.model_fetcher import download_if_missing
|
| 12 |
+
|
| 13 |
+
# Force download if missing before initialization
|
| 14 |
+
download_if_missing()
|
| 15 |
|
| 16 |
# Path to the saved model weights
|
| 17 |
MODEL_PATH = os.path.join(os.path.dirname(__file__), "../model/saved_models/oil_spill_unet_best.keras")
|
backend/main.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 2 |
from fastapi.responses import Response, JSONResponse
|
|
|
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
from PIL import Image
|
| 5 |
import io
|
|
@@ -22,6 +23,11 @@ app.add_middleware(
|
|
| 22 |
allow_headers=["*"],
|
| 23 |
)
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
@app.get("/health")
|
| 26 |
def health_check():
|
| 27 |
"""Health check endpoint to ensure API and Model are ready."""
|
|
@@ -69,4 +75,6 @@ async def predict_spill(file: UploadFile = File(...)):
|
|
| 69 |
|
| 70 |
if __name__ == "__main__":
|
| 71 |
import uvicorn
|
| 72 |
-
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 2 |
from fastapi.responses import Response, JSONResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from PIL import Image
|
| 6 |
import io
|
|
|
|
| 23 |
allow_headers=["*"],
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# Serves the built frontend files
|
| 27 |
+
# Ensure 'frontend/dist' exists after the build stage in Docker
|
| 28 |
+
if os.path.exists("frontend/dist"):
|
| 29 |
+
app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
|
| 30 |
+
|
| 31 |
@app.get("/health")
|
| 32 |
def health_check():
|
| 33 |
"""Health check endpoint to ensure API and Model are ready."""
|
|
|
|
| 75 |
|
| 76 |
if __name__ == "__main__":
|
| 77 |
import uvicorn
|
| 78 |
+
# Hugging Face Spaces defaults to port 7860
|
| 79 |
+
port = int(os.environ.get("PORT", 7860))
|
| 80 |
+
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)
|
backend/utils/model_fetcher.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import urllib.request
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
def download_if_missing():
|
| 6 |
+
"""
|
| 7 |
+
Checks if the model weights exist locally.
|
| 8 |
+
If not, attempts to download them from the URL provided in environment variables.
|
| 9 |
+
"""
|
| 10 |
+
model_url = os.getenv("MODEL_URL")
|
| 11 |
+
# Default local path relative to backend directory
|
| 12 |
+
model_path = os.path.join(os.path.dirname(__file__), "../../model/saved_models/oil_spill_unet_best.keras")
|
| 13 |
+
|
| 14 |
+
# Ensure directory exists
|
| 15 |
+
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
| 16 |
+
|
| 17 |
+
if not os.path.exists(model_path):
|
| 18 |
+
if not model_url or model_url == "https://your-r2-public-url/oil_spill_unet_best.keras":
|
| 19 |
+
print("β οΈ MODEL_URL not configured. Skipping download.")
|
| 20 |
+
print("π‘ Please provide a valid URL in your docker-compose.yml to enable automated fetching.")
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
print(f"π Model weights missing. Initializing download from: {model_url}")
|
| 24 |
+
try:
|
| 25 |
+
# Using urllib as it's built-in and sufficient for this simple GET
|
| 26 |
+
urllib.request.urlretrieve(model_url, model_path)
|
| 27 |
+
print("β
Model weights downloaded successfully.")
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"β Failed to download model: {e}")
|
| 30 |
+
print("π‘ Fallback: The system will use an untrained stub model for now.")
|
| 31 |
+
else:
|
| 32 |
+
print("π¦ Production model weights detected locally. Skipping download.")
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
# Test run
|
| 36 |
+
download_if_missing()
|
docker-compose.yml
CHANGED
|
@@ -12,6 +12,7 @@ services:
|
|
| 12 |
- ./model:/app/model
|
| 13 |
environment:
|
| 14 |
- PYTHONUNBUFFERED=1
|
|
|
|
| 15 |
|
| 16 |
frontend:
|
| 17 |
build:
|
|
|
|
| 12 |
- ./model:/app/model
|
| 13 |
environment:
|
| 14 |
- PYTHONUNBUFFERED=1
|
| 15 |
+
- MODEL_URL=https://pub-b6e6ec55d6e84dcdb100466edde874a2.r2.dev/oil_spill_unet_best.keras
|
| 16 |
|
| 17 |
frontend:
|
| 18 |
build:
|
frontend/.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
build
|
| 4 |
+
.env
|
| 5 |
+
.env.*
|
| 6 |
+
!.env.example
|
| 7 |
+
.git
|
| 8 |
+
.gitignore
|
| 9 |
+
.dockerignore
|
| 10 |
+
README.md
|
frontend/src/App.tsx
CHANGED
|
@@ -181,7 +181,7 @@ function Dashboard() {
|
|
| 181 |
formData.append("file", selectedFile);
|
| 182 |
|
| 183 |
try {
|
| 184 |
-
const response = await fetch("
|
| 185 |
method: "POST",
|
| 186 |
body: formData,
|
| 187 |
});
|
|
|
|
| 181 |
formData.append("file", selectedFile);
|
| 182 |
|
| 183 |
try {
|
| 184 |
+
const response = await fetch("/predict", {
|
| 185 |
method: "POST",
|
| 186 |
body: formData,
|
| 187 |
});
|
upload_to_r2.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import boto3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# 1. Credentials (Ready to use from your screenshot)
|
| 5 |
+
ACCESS_KEY = "a9f685d13afd9a290e54dca612ef83d2"
|
| 6 |
+
SECRET_KEY = "8284c338275d0e6ebb3e6e697ba5999f6409908cb92c08fe8351da4af728c875"
|
| 7 |
+
ACCOUNT_ID = "659b46b7d1773d3742cd10b2c9194dd3"
|
| 8 |
+
BUCKET_NAME = "oil-spill-detection"
|
| 9 |
+
|
| 10 |
+
# 2. Path to your model file
|
| 11 |
+
# Note: Ensure oil_spill_unet_best.keras is in this same folder!
|
| 12 |
+
FILE_PATH = "oil_spill_unet_best.keras"
|
| 13 |
+
|
| 14 |
+
def upload():
|
| 15 |
+
# Setup the Cloudflare R2 Client via S3 API
|
| 16 |
+
s3 = boto3.client(
|
| 17 |
+
service_name='s3',
|
| 18 |
+
endpoint_url=f'https://{ACCOUNT_ID}.r2.cloudflarestorage.com',
|
| 19 |
+
aws_access_key_id=ACCESS_KEY,
|
| 20 |
+
aws_secret_access_key=SECRET_KEY,
|
| 21 |
+
region_name='auto'
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
print(f"Starting high-speed API upload of {FILE_PATH}...")
|
| 25 |
+
|
| 26 |
+
if not os.path.exists(FILE_PATH):
|
| 27 |
+
print(f"ERROR: File '{FILE_PATH}' not found in the current directory.")
|
| 28 |
+
print("FIX: Make sure you have downloaded it from Google Drive to this folder.")
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
s3.upload_file(FILE_PATH, BUCKET_NAME, "oil_spill_unet_best.keras")
|
| 33 |
+
print("\n" + "="*40)
|
| 34 |
+
print("SUCCESS! Your model is now on Cloudflare R2.")
|
| 35 |
+
print("="*40)
|
| 36 |
+
print("You can now run 'docker-compose up --build' and the system will pull the model automatically.")
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"UPLOAD FAILED: {e}")
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
upload()
|