| 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): |
| state = torch.load(path, map_location=self.device, weights_only=True) |
| if "model_state_dict" in state: |
| self.model.load_state_dict(state["model_state_dict"]) |
| else: |
| self.model.load_state_dict(state) |
| 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)") |
|
|
| @torch.no_grad() |
| 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] |
|
|
| 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": { |
| "low_frequency": freq_magnitudes[0], |
| "mid_frequency": freq_magnitudes[1], |
| "high_frequency": freq_magnitudes[2], |
| }, |
| "processing_time_ms": elapsed, |
| } |
| return result |
|
|
| def _preprocess(self, image: Image.Image) -> torch.Tensor: |
| image = image.convert("RGB").resize( |
| (self.image_size, self.image_size), Image.Resampling.LANCZOS) |
| 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) |
|
|