GlowSenseAI / skin_api.py
Tayyaba11's picture
deploy backend
6e08e39
Raw
History Blame Contribute Delete
7.35 kB
"""
DermLIP Skin Analysis Microservice
====================================
Standalone FastAPI service that loads the DermLIP ViT-B-16 model ONCE at
startup (pre-warm) and exposes an HTTP inference endpoint.
Run:
python skin_api.py
-- or --
uvicorn skin_api:app --host 0.0.0.0 --port 8001
Endpoints:
GET /health → {"status": "ready"} once model is loaded
POST /analyze → accepts multipart image, returns top skin conditions
"""
import io
import time
import torch
import open_clip
from PIL import Image
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import uvicorn
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_HUB = "hf-hub:redlessone/DermLIP_ViT-B-16"
# Skincare attributes — mirrors test_dermlip.py exactly
ATTRIBUTES = [
"visible acne",
"oily shine",
"dry flaky skin",
"redness or irritation",
"dark spots or hyperpigmentation",
"uneven texture",
"visible pores",
"under-eye dark circles",
"scalp flaking or dandruff",
"scalp redness",
"greasy scalp",
]
# Multiple prompt templates per attribute (same as test_dermlip.py)
TEMPLATES = [
"A close-up photo of skin showing {}.",
"This photo shows {}.",
"Visible signs of {}.",
]
TOP_K =2 # Return top 2 conditions for the UI
# ---------------------------------------------------------------------------
# Global model state (loaded at startup)
# ---------------------------------------------------------------------------
_model = None
_preprocess = None
_tokenizer = None
_text_features = None # precomputed text embeddings (constant across requests)
_model_ready = False
def _load_model():
"""Load DermLIP and pre-compute text embeddings. Called during startup."""
global _model, _preprocess, _tokenizer, _text_features, _model_ready
print(f"[skin_api] Loading DermLIP on {DEVICE} …")
t0 = time.time()
_model, _, _preprocess = open_clip.create_model_and_transforms(MODEL_HUB)
_model = _model.to(DEVICE).eval()
_tokenizer = open_clip.get_tokenizer(MODEL_HUB)
# Pre-compute text features once — reused for every image request
prompts = [t.format(a) for a in ATTRIBUTES for t in TEMPLATES]
text_tokens = _tokenizer(prompts).to(DEVICE)
with torch.no_grad():
txt_f = _model.encode_text(text_tokens)
txt_f = txt_f / txt_f.norm(dim=-1, keepdim=True)
_text_features = txt_f # shape: [num_attributes * num_templates, embed_dim]
_model_ready = True
print(f"[skin_api] Model ready in {time.time() - t0:.1f}s (device={DEVICE})")
# ---------------------------------------------------------------------------
# Lifespan — pre-warm on startup
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Pre-warm the model synchronously before accepting any requests."""
_load_model()
yield
# Cleanup (optional)
print("[skin_api] Shutting down.")
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
app = FastAPI(
title="DermLIP Skin Analysis Service",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Main backend will proxy; restrict further if needed
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/health")
def health():
if not _model_ready:
raise HTTPException(status_code=503, detail="Model not ready yet")
return {"status": "ready", "device": DEVICE}
@app.post("/analyze")
async def analyze(file: UploadFile = File(...)):
"""
Accepts a skin image and returns the top-K conditions detected.
Response:
{
"conditions": [
{"label": "visible acne", "display": "Acne / Breakouts", "score": 0.342, "rank": 1},
...
],
"device": "cpu"
}
"""
if not _model_ready:
raise HTTPException(status_code=503, detail="Model is still loading. Please retry in a moment.")
# Validate file type
allowed = {"image/jpeg", "image/png", "image/webp", "image/jpg"}
if file.content_type not in allowed:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type '{file.content_type}'. Use JPEG, PNG, or WebP."
)
# Read and preprocess image
try:
raw = await file.read()
pil_image = Image.open(io.BytesIO(raw)).convert("RGB")
image_tensor = _preprocess(pil_image).unsqueeze(0).to(DEVICE)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Could not process image: {e}")
# Inference
with torch.no_grad():
img_f = _model.encode_image(image_tensor)
img_f = img_f / img_f.norm(dim=-1, keepdim=True)
# Similarity: [1, num_attributes * num_templates]
sims = (img_f @ _text_features.T).squeeze(0)
# Average template scores per attribute → [num_attributes]
sims = sims.view(len(ATTRIBUTES), len(TEMPLATES)).mean(dim=1)
# Top-K
k = min(TOP_K, len(ATTRIBUTES))
topk = torch.topk(sims, k=k)
conditions = []
for rank, (score, idx) in enumerate(
zip(topk.values.tolist(), topk.indices.tolist()), start=1
):
label = ATTRIBUTES[idx]
conditions.append({
"label": label,
"display": label,
"score": round(score, 4),
"rank": rank,
})
return {"conditions": conditions, "device": DEVICE}
def perform_inference(pil_image):
"""Internal function to run analysis without HTTP overhead."""
global _model, _preprocess, _text_features
if not _model_ready:
return None
# Preprocess and Inference
image_tensor = _preprocess(pil_image).unsqueeze(0).to(DEVICE)
with torch.no_grad():
img_f = _model.encode_image(image_tensor)
img_f = img_f / img_f.norm(dim=-1, keepdim=True)
sims = (img_f @ _text_features.T).squeeze(0)
sims = sims.view(len(ATTRIBUTES), len(TEMPLATES)).mean(dim=1)
# Top-K logic
k = min(TOP_K, len(ATTRIBUTES))
topk = torch.topk(sims, k=k)
conditions = []
for rank, (score, idx) in enumerate(zip(topk.values.tolist(), topk.indices.tolist()), start=1):
label = ATTRIBUTES[idx]
conditions.append({
"label": label,
"display": label,
"score": round(score, 4),
"rank": rank,
})
return conditions
def is_model_ready():
return _model_ready
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
uvicorn.run("skin_api:app", host="0.0.0.0", port=8001, reload=False)