Spaces:
Paused
Paused
Download api/model_server.py from MohsinElis/mfft-api: direct link, hf CLI and curl.
- Browser
- Download file 5.16 kB
-
https://huggingface.co/spaces/MohsinElis/mfft-api/resolve/main/api/model_server.py
- Command line
-
hf download hf://spaces/MohsinElis/mfft-api/api/model_server.py
-
curl -L -o model_server.py https://huggingface.co/spaces/MohsinElis/mfft-api/resolve/main/api/model_server.py
5.16 kB
| import time | |
| import torch | |
| import torch.nn.functional as F | |
| import numpy as np | |
| from PIL import Image | |
| from pathlib import Path | |
| from typing import Optional, Dict, Any | |
| import sys | |
| sys.path.insert(0, str(Path(__file__).parent.parent / "model" / "src")) | |
| from model import MFFTWithExplainability, build_mfft | |
| class ModelServer: | |
| """ | |
| Wraps the MFFT model for production inference. | |
| Handles model loading, preprocessing, prediction, and warmup. | |
| """ | |
| def __init__(self, checkpoint_path: Optional[str] = None, variant: str = "base", | |
| image_size: int = 384): | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.variant = variant | |
| self.image_size = image_size | |
| print(f"[ModelServer] Device: {self.device} | Variant: {variant} | Size: {image_size}") | |
| self.model = build_mfft(variant) | |
| self.model = self.model.to(self.device) | |
| self.is_loaded = False | |
| if checkpoint_path and Path(checkpoint_path).exists(): | |
| self.load_checkpoint(checkpoint_path) | |
| self.is_loaded = True | |
| print(f"[ModelServer] Model loaded from {checkpoint_path}") | |
| else: | |
| print(f"[ModelServer] No checkpoint found at {checkpoint_path}") | |
| print(f"[ModelServer] Running with untrained weights (random initialization)") | |
| self.warmup() | |
| def load_checkpoint(self, path: str): | |
| try: | |
| state = torch.load(path, map_location=self.device, weights_only=True) | |
| except Exception: | |
| state = torch.load(path, map_location=self.device, weights_only=False) | |
| raw_state = state["model_state_dict"] if isinstance(state, dict) and "model_state_dict" in state else state | |
| # Remap keys between cluster training pipeline and local model.py naming conventions | |
| cleaned_state = {} | |
| for k, v in raw_state.items(): | |
| nk = k.replace(".stages.", ".blocks.").replace("fga.gate.", "freq_guided_attn.freq_gate.") | |
| if "num_batches_tracked" in nk: | |
| continue | |
| cleaned_state[nk] = v | |
| self.model.load_state_dict(cleaned_state, strict=False) | |
| self.is_loaded = True | |
| def warmup(self, n_warmup: int = 3): | |
| dummy = torch.randn(1, 3, self.image_size, self.image_size).to(self.device) | |
| self.model.eval() | |
| with torch.no_grad(): | |
| for _ in range(n_warmup): | |
| _ = self.model(dummy) | |
| print(f"[ModelServer] Warmup complete ({n_warmup} iterations)") | |
| def predict(self, image: Image.Image) -> Dict[str, Any]: | |
| start = time.time() | |
| input_tensor = self._preprocess(image) | |
| logits, heatmaps = self.model(input_tensor, return_heatmap=True) | |
| probs = F.softmax(logits, dim=-1) | |
| pred = torch.argmax(probs, dim=-1).item() | |
| bands = self.model.decomposer(input_tensor) | |
| freq_magnitudes = [band.abs().mean().item() for band in bands] | |
| if len(freq_magnitudes) == 4: | |
| band_contribs = { | |
| "low_frequency": freq_magnitudes[0], | |
| "low_mid_frequency": freq_magnitudes[1], | |
| "mid_high_frequency": freq_magnitudes[2], | |
| "high_frequency": freq_magnitudes[3], | |
| } | |
| else: | |
| band_contribs = { | |
| "low_frequency": freq_magnitudes[0], | |
| "mid_frequency": freq_magnitudes[1] if len(freq_magnitudes) > 1 else 0.0, | |
| "high_frequency": freq_magnitudes[2] if len(freq_magnitudes) > 2 else 0.0, | |
| } | |
| elapsed = (time.time() - start) * 1000 | |
| result = { | |
| "prediction": pred, | |
| "real_prob": probs[0, 0].item(), | |
| "ai_prob": probs[0, 1].item(), | |
| "confidence": probs.max(dim=-1).values.item(), | |
| "heatmaps": heatmaps, | |
| "frequency_band_contributions": band_contribs, | |
| "processing_time_ms": elapsed, | |
| } | |
| return result | |
| def _preprocess(self, image: Image.Image) -> torch.Tensor: | |
| # Aspect-ratio preserving resize + center crop (matching training/eval pipeline) | |
| w, h = image.size | |
| target = self.image_size + 16 | |
| if w < h: | |
| new_w, new_h = target, max(target, int(h * target / w)) | |
| else: | |
| new_h, new_w = target, max(target, int(w * target / h)) | |
| image = image.convert("RGB").resize((new_w, new_h), Image.Resampling.BILINEAR) | |
| left = max(0, (new_w - self.image_size) // 2) | |
| top = max(0, (new_h - self.image_size) // 2) | |
| image = image.crop((left, top, left + self.image_size, top + self.image_size)) | |
| img_array = np.array(image, dtype=np.float32) / 255.0 | |
| mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) | |
| std = np.array([0.229, 0.224, 0.225], dtype=np.float32) | |
| img_array = ((img_array - mean) / std).astype(np.float32) | |
| tensor = torch.from_numpy(img_array).permute(2, 0, 1).unsqueeze(0) | |
| return tensor.to(self.device) | |