Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| def health(): | |
| if not _model_ready: | |
| raise HTTPException(status_code=503, detail="Model not ready yet") | |
| return {"status": "ready", "device": DEVICE} | |
| 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) | |