Spaces:
Sleeping
Sleeping
n0v33n
commited on
Commit
·
287ab0c
1
Parent(s):
4dc492e
initail commit
Browse files- DockerFile +31 -0
- main.py +58 -0
- model.py +51 -0
- requirement.txt +4 -0
DockerFile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python slim image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working dir
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system deps needed for Pillow
|
| 8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
build-essential \
|
| 10 |
+
libjpeg-dev \
|
| 11 |
+
zlib1g-dev \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Copy requirements first for better caching
|
| 15 |
+
COPY requirements.txt /app/
|
| 16 |
+
|
| 17 |
+
# Install python deps
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
+
|
| 20 |
+
# Copy app
|
| 21 |
+
COPY . /app
|
| 22 |
+
|
| 23 |
+
# Expose port (Spaces usually route; commonly 8080 works)
|
| 24 |
+
EXPOSE 8080
|
| 25 |
+
|
| 26 |
+
# Use a non-root user (optional but nice)
|
| 27 |
+
RUN useradd --create-home appuser && chown -R appuser /app
|
| 28 |
+
USER appuser
|
| 29 |
+
|
| 30 |
+
# Run with Uvicorn
|
| 31 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "1"]
|
main.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
from model import DummyGenderModel
|
| 6 |
+
from PIL import Image
|
| 7 |
+
import io
|
| 8 |
+
import time
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="Radiograph Gender Predictor (placeholder)",
|
| 12 |
+
description="Returns a random gender prediction for an uploaded radiograph image. Replace the DummyGenderModel with your trained model when ready.",
|
| 13 |
+
version="0.1.0")
|
| 14 |
+
|
| 15 |
+
model = DummyGenderModel() # placeholder. replace with your real model object when available.
|
| 16 |
+
|
| 17 |
+
class PredictResponse(BaseModel):
|
| 18 |
+
id: str
|
| 19 |
+
prediction: str
|
| 20 |
+
confidence: float
|
| 21 |
+
probabilities: Dict[str, float]
|
| 22 |
+
model_version: str
|
| 23 |
+
runtime_ms: int
|
| 24 |
+
timestamp: float
|
| 25 |
+
|
| 26 |
+
@app.get("/health")
|
| 27 |
+
def health():
|
| 28 |
+
return {"status": "ok", "model_loaded": model.is_loaded(), "model_version": model.version}
|
| 29 |
+
|
| 30 |
+
@app.post("/predict", response_model=PredictResponse)
|
| 31 |
+
async def predict(file: UploadFile = File(...)):
|
| 32 |
+
"""
|
| 33 |
+
Accepts an image file (radiograph). Returns a JSON with a random gender prediction.
|
| 34 |
+
Content-type should be one of common image types: image/jpeg, image/png, image/tiff, etc.
|
| 35 |
+
"""
|
| 36 |
+
start = time.time()
|
| 37 |
+
# basic content-type check (optional)
|
| 38 |
+
if not file.content_type.startswith("image/"):
|
| 39 |
+
raise HTTPException(status_code=400, detail="Uploaded file must be an image.")
|
| 40 |
+
|
| 41 |
+
contents = await file.read()
|
| 42 |
+
try:
|
| 43 |
+
img = Image.open(io.BytesIO(contents)).convert("RGB")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
raise HTTPException(status_code=400, detail=f"Unable to parse image: {e}")
|
| 46 |
+
|
| 47 |
+
# call the placeholder model (random)
|
| 48 |
+
result = model.predict(img)
|
| 49 |
+
|
| 50 |
+
runtime_ms = int((time.time() - start) * 1000)
|
| 51 |
+
response = {
|
| 52 |
+
"id": str(uuid.uuid4()),
|
| 53 |
+
"prediction": result["label"],
|
| 54 |
+
"model_version": model.version,
|
| 55 |
+
"runtime_ms": runtime_ms,
|
| 56 |
+
"timestamp": time.time()
|
| 57 |
+
}
|
| 58 |
+
return JSONResponse(content=response)
|
model.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Dummy model module.
|
| 3 |
+
|
| 4 |
+
This file contains a placeholder model class `DummyGenderModel` that returns
|
| 5 |
+
random predictions. Replace the `predict` method or the whole class with your
|
| 6 |
+
trained model loading + inference logic when ready.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import random
|
| 10 |
+
from typing import Dict
|
| 11 |
+
from PIL.Image import Image
|
| 12 |
+
|
| 13 |
+
class DummyGenderModel:
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.version = "dummy-v0.1"
|
| 16 |
+
self._loaded = True
|
| 17 |
+
|
| 18 |
+
def is_loaded(self) -> bool:
|
| 19 |
+
return self._loaded
|
| 20 |
+
|
| 21 |
+
def predict(self, img: Image) -> Dict:
|
| 22 |
+
"""
|
| 23 |
+
Return a random prediction. Expects a PIL Image.
|
| 24 |
+
|
| 25 |
+
Returns a dict:
|
| 26 |
+
{
|
| 27 |
+
"label": "male" or "female",
|
| 28 |
+
"confidence": float between 0.5 and 1.0,
|
| 29 |
+
"probabilities": {"male": p_male, "female": p_female}
|
| 30 |
+
}
|
| 31 |
+
"""
|
| 32 |
+
# random base probability
|
| 33 |
+
p_male = random.random()
|
| 34 |
+
p_female = 1.0 - p_male
|
| 35 |
+
|
| 36 |
+
# calibrate to avoid extremely low confidences — push to [0.5, 1.0]
|
| 37 |
+
p_male = 0.5 + 0.5 * p_male
|
| 38 |
+
p_female = 1.0 - p_male
|
| 39 |
+
|
| 40 |
+
if p_male >= p_female:
|
| 41 |
+
label = "male"
|
| 42 |
+
# confidence = p_male
|
| 43 |
+
else:
|
| 44 |
+
label = "female"
|
| 45 |
+
# confidence = p_female
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
"label": label,
|
| 49 |
+
# "confidence": round(confidence, 4),
|
| 50 |
+
# "probabilities": {"male": round(p_male, 4), "female": round(p_female, 4)}
|
| 51 |
+
}
|
requirement.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
pillow
|
| 4 |
+
python-multipart
|