Shabir
some changes in the app.py
e826a24
Raw
History Blame Contribute Delete
6.25 kB
"""
DeepFace Service — Facial analysis for video frames.
Model: DeepFace (opencv backend)
Endpoint: POST /analyze
"""
import base64
import io
import logging
from contextlib import asynccontextmanager
from typing import Optional
import cv2
import numpy as np
from fastapi import FastAPI
from PIL import Image
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
DeepFace downloads model weights lazily on first call, so we do a
warm-up inference during startup to avoid a cold-start delay on the
first real request.
"""
logger.info("Warming up DeepFace models …")
try:
from deepface import DeepFace # noqa: PLC0415
# 1×1 black image — just enough to trigger weight download without
# actually detecting a face (the exception is silently ignored).
dummy = np.zeros((64, 64, 3), dtype=np.uint8)
try:
DeepFace.analyze(dummy, actions=["emotion", "age", "gender"], enforce_detection=False, detector_backend="retinaface")
except Exception: # noqa: BLE001
pass
logger.info("DeepFace warm-up done")
except Exception as exc: # noqa: BLE001
logger.warning("DeepFace warm-up failed (non-fatal): %s", exc)
yield
app = FastAPI(title="ViralClip DeepFace Service", lifespan=lifespan)
# ---------------------------------------------------------------------------
# Request / Response schemas
# ---------------------------------------------------------------------------
class BBox(BaseModel):
x: int
y: int
w: int
h: int
class EmotionResult(BaseModel):
dominant: str
scores: dict[str, float]
class FaceResult(BaseModel):
face_index: int
emotion: EmotionResult
age: int
gender: str
gender_confidence: float
bbox: BBox
class AnalyzeRequest(BaseModel):
frame_b64: str
class AnalyzeResponse(BaseModel):
faces: list[FaceResult]
face_count: int
dominant_emotion: Optional[str]
frame_width: Optional[int] = None
frame_height: Optional[int] = None
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parse_face(idx: int, face_data: dict) -> FaceResult:
"""Convert a single DeepFace result dict into a FaceResult."""
# Emotion scores
raw_emotions: dict = face_data.get("emotion", {})
emotion_scores = {k: round(float(v) / 100.0, 4) for k, v in raw_emotions.items()}
dominant_emotion: str = face_data.get("dominant_emotion", max(emotion_scores, key=lambda k: emotion_scores[k]))
# Gender
gender_raw: dict = face_data.get("gender", {})
if isinstance(gender_raw, dict):
# {"Man": 94.2, "Woman": 5.8}
dominant_gender = max(gender_raw, key=lambda k: gender_raw[k])
gender_conf = round(float(gender_raw[dominant_gender]) / 100.0, 4)
else:
dominant_gender = str(gender_raw)
gender_conf = float(face_data.get("gender_confidence", 1.0))
# BBox
region: dict = face_data.get("region", {})
bbox = BBox(
x=int(region.get("x", 0)),
y=int(region.get("y", 0)),
w=int(region.get("w", 0)),
h=int(region.get("h", 0)),
)
return FaceResult(
face_index=idx,
emotion=EmotionResult(dominant=dominant_emotion, scores=emotion_scores),
age=int(face_data.get("age", 0)),
gender=dominant_gender,
gender_confidence=gender_conf,
bbox=bbox,
)
# ---------------------------------------------------------------------------
# Endpoint
# ---------------------------------------------------------------------------
@app.post("/analyze")
async def analyze(req: AnalyzeRequest):
frame_width: Optional[int] = None
frame_height: Optional[int] = None
try:
from deepface import DeepFace # noqa: PLC0415
# Decode image → numpy BGR array — pad base64 if needed
b64 = req.frame_b64
b64 += "=" * (-len(b64) % 4)
img_bytes = base64.b64decode(b64)
pil_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")
frame_width, frame_height = pil_image.size
frame_rgb = np.array(pil_image)
frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
results = DeepFace.analyze(
frame_bgr,
actions=["emotion", "age", "gender"],
enforce_detection=True,
detector_backend="retinaface",
)
# DeepFace may return a single dict or a list of dicts
if isinstance(results, dict):
results = [results]
faces = [_parse_face(i, r) for i, r in enumerate(results)]
# dominant_emotion: from the face whose top emotion score is highest
dominant_emotion: Optional[str] = None
if faces:
best_face = max(
faces,
key=lambda f: f.emotion.scores.get(f.emotion.dominant, 0.0),
)
dominant_emotion = best_face.emotion.dominant
return AnalyzeResponse(
faces=faces,
face_count=len(faces),
dominant_emotion=dominant_emotion,
frame_width=frame_width,
frame_height=frame_height,
)
except Exception as exc: # noqa: BLE001
err_msg = str(exc).lower()
# DeepFace raises ValueError or a custom exception when no face is found
if "face" in err_msg and ("not" in err_msg or "detect" in err_msg or "found" in err_msg):
# frame_width/frame_height are set above the DeepFace.analyze
# call (which is what raises this), so they're populated
# unless image decoding itself failed — in which case they
# stay at their None default.
return AnalyzeResponse(
faces=[], face_count=0, dominant_emotion=None,
frame_width=frame_width, frame_height=frame_height,
)
logger.exception("DeepFace inference failed")
return {"error": str(exc)}
@app.get("/health")
async def health():
return {"status": "ok", "model": "deepface"}