mfft-api / api /model_server.py
MohsinElis's picture
Deploy production MFFT detection API with verified 20-epoch weights
37b0e8b verified
Raw History Blame Contribute Delete
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)")
@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]
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)