Download handler.py from Shinzmann/cnn-age-v0: direct link, hf CLI and curl.
- Browser
- Download file 4.77 kB
-
https://huggingface.co/Shinzmann/cnn-age-v0/resolve/main/handler.py
- Command line
-
hf download hf://Shinzmann/cnn-age-v0/handler.py
-
curl -L -o handler.py https://huggingface.co/Shinzmann/cnn-age-v0/resolve/main/handler.py
4.77 kB
| """Hugging Face Inference Endpoints handler for the Kámárí CNN age model. | |
| A cheap, platform-diverse fallback for the age model: host `cnn-age-v0` on a small CPU | |
| Inference Endpoint (scale-to-zero) and point the gateway's third CNN fallback at it. Mirrors | |
| the Modal serve logic: detect and crop the largest face, then estimate age, under-18 | |
| probability, uncertainty, and a sharpness-based quality. | |
| Send the raw image bytes with `Content-Type: image/jpeg`. Returns: | |
| {estimated_age, p_under_18, uncertainty, face_quality, faces_detected, model_version} | |
| """ | |
| import base64 | |
| import io | |
| import os | |
| import numpy as np | |
| import timm | |
| import torch | |
| import torch.nn as nn | |
| from PIL import Image | |
| MEAN = [0.485, 0.456, 0.406] | |
| STD = [0.229, 0.224, 0.225] | |
| MODEL_VERSION = "cnn_v0.1.0-hf" | |
| BACKBONE = os.environ.get("CNN_BACKBONE", "tf_efficientnetv2_s") | |
| class AgeNet(nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.backbone = timm.create_model(BACKBONE, pretrained=False, num_classes=0) | |
| d = self.backbone.num_features | |
| self.age = nn.Linear(d, 1) | |
| self.minor = nn.Linear(d, 1) | |
| self.logvar = nn.Linear(d, 1) | |
| def forward(self, x): | |
| f = self.backbone(x) | |
| return self.age(f).squeeze(1), self.minor(f).squeeze(1), self.logvar(f).squeeze(1) | |
| def _to_bytes(data) -> bytes: | |
| """Accept the many shapes HF may hand a custom handler: raw bytes, a PIL image, | |
| a base64 / data-URL string, or a dict wrapping any of those under `inputs`.""" | |
| if isinstance(data, dict): | |
| data = data.get("inputs", data) | |
| if isinstance(data, (bytes, bytearray)): | |
| return bytes(data) | |
| if isinstance(data, str): | |
| if data.startswith("data:"): | |
| data = data.split(",", 1)[1] | |
| return base64.b64decode(data) | |
| buf = io.BytesIO() # already a PIL image | |
| data.convert("RGB").save(buf, format="JPEG") | |
| return buf.getvalue() | |
| class EndpointHandler: | |
| def __init__(self, path: str = ""): | |
| import cv2 # noqa: F401 - imported lazily so the model loads even if cv2 is slow | |
| model = AgeNet() | |
| model.load_state_dict( | |
| torch.load(os.path.join(path, "best.pt"), map_location="cpu", weights_only=True)) | |
| model.eval() | |
| torch.set_num_threads(max(1, os.cpu_count() or 2)) | |
| self.model = model | |
| self.cv2 = cv2 | |
| self.cascade = cv2.CascadeClassifier( | |
| cv2.data.haarcascades + "haarcascade_frontalface_default.xml") | |
| def __call__(self, data): | |
| try: | |
| return self._infer(_to_bytes(data)) | |
| except Exception: | |
| return {"estimated_age": 0.0, "p_under_18": 0.5, "uncertainty": 1.0, | |
| "face_quality": 0.0, "faces_detected": 0, "model_version": MODEL_VERSION} | |
| def _infer(self, image_bytes: bytes) -> dict: | |
| cv2 = self.cv2 | |
| try: | |
| img = Image.open(io.BytesIO(image_bytes)).convert("RGB") | |
| except Exception: | |
| return {"estimated_age": 0.0, "p_under_18": 0.5, "uncertainty": 1.0, | |
| "face_quality": 0.0, "faces_detected": 0, "model_version": MODEL_VERSION} | |
| w, h = img.size | |
| gray = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2GRAY) | |
| faces = self.cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, | |
| minSize=(48, 48)) | |
| if len(faces) == 0: | |
| return {"estimated_age": 0.0, "p_under_18": 0.5, "uncertainty": 1.0, | |
| "face_quality": 0.0, "faces_detected": 0, "model_version": MODEL_VERSION} | |
| fx, fy, fw, fh = (int(v) for v in max(faces, key=lambda f: f[2] * f[3])) | |
| mx, my = int(fw * 0.3), int(fh * 0.3) | |
| x0, y0 = max(0, fx - mx), max(0, fy - my) | |
| x1, y1 = min(w, fx + fw + mx), min(h, fy + fh + my) | |
| face = img.crop((x0, y0, x1, y1)).resize((224, 224)) | |
| arr = np.asarray(face, np.float32) / 255.0 | |
| sharp = float(cv2.Laplacian(cv2.cvtColor(np.asarray(face), cv2.COLOR_RGB2GRAY), | |
| cv2.CV_64F).var()) | |
| quality = float(np.clip(0.55 + sharp / 800.0, 0.0, 1.0)) | |
| x = ((arr - MEAN) / STD).transpose(2, 0, 1)[None].astype(np.float32) | |
| with torch.no_grad(): | |
| age, minor_logit, logvar = self.model(torch.from_numpy(x)) | |
| p_under = 1.0 / (1.0 + float(np.exp(-float(minor_logit[0])))) | |
| uncertainty = float(np.clip(float(np.exp(0.5 * float(logvar[0]))) / 10.0, 0, 1)) | |
| return { | |
| "estimated_age": round(float(age[0]), 1), | |
| "p_under_18": round(p_under, 3), | |
| "uncertainty": round(uncertainty, 3), | |
| "face_quality": round(quality, 3), | |
| "faces_detected": int(len(faces)), | |
| "model_version": MODEL_VERSION, | |
| } | |