deepfake-detector / src /model.py
Isit1's picture
Fix startup crash: skip torch.hub GitHub API validation (rate limit)
823741b verified
Raw History Blame Contribute Delete
5.71 kB
"""
Vision-KAN + Vision-LSTM hybrid deepfake detector.
This module is a faithful, refactored copy of the inference path in
`notebooks/test_mac.ipynb`. The architecture, weights loading, preprocessing,
and the real/fake decision rule are IDENTICAL to the original notebook β€” only
reorganized into importable functions so the demo app and the notebook share
one source of truth.
Decision rule (unchanged from the original project):
prob = sigmoid output of the model
prob >= 0.5 -> REAL
prob < 0.5 -> FAKE
"""
import os
import numpy as np
import torch
import torch.nn as nn
from PIL import Image
DEVICE = torch.device("cpu")
# ImageNet normalization β€” exactly as used during training/inference.
_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 1, 3)
_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 1, 3)
# ──────────────────────────────────────────────────────────────────────────
# Model definition (unchanged from test_mac.ipynb)
# ──────────────────────────────────────────────────────────────────────────
class CombinedModel(nn.Module):
"""Late-fusion of a Vision-KAN backbone and a Vision-LSTM backbone.
Each backbone emits a scalar logit. The two logits are concatenated and
passed through a small MLP, then a sigmoid produces a probability in [0, 1].
"""
def __init__(self, KAN, vision_lstm):
super(CombinedModel, self).__init__()
self.KAN = KAN
self.vision_lstm = vision_lstm
self.fc1 = nn.Linear(2, 512)
self.fc2 = nn.Linear(512, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
KAN_output = self.KAN(x)
lstm_output = self.vision_lstm(x)
combined = torch.cat((KAN_output, lstm_output), dim=1)
x = self.fc1(combined)
x = self.fc2(x)
x = self.sigmoid(x)
return x
# ──────────────────────────────────────────────────────────────────────────
# Build + load
# ──────────────────────────────────────────────────────────────────────────
def build_model():
"""Reconstruct the exact Vision-KAN + Vision-LSTM hybrid (random weights)."""
from VisionKAN import create_model
KAN = create_model(
model_name="deit_tiny_patch16_224_KAN",
pretrained=False,
hdim_kan=192,
num_classes=1,
drop_rate=0.0,
drop_path_rate=0.05,
img_size=224,
batch_size=32,
)
vision_lstm = torch.hub.load(
"nx-ai/vision-lstm",
"VisionLSTM",
dim=192,
depth=24,
patch_size=16,
input_shape=(3, 224, 224),
output_shape=(1,),
drop_path_rate=0.05,
stride=None,
trust_repo=True,
skip_validation=True, # avoid GitHub API rate limit on shared Space IPs
)
return CombinedModel(KAN, vision_lstm).to(DEVICE)
def load_model(weights_path="as_model_best.pt"):
"""Build the model and load trained weights, returning it in eval mode."""
model = build_model()
if not os.path.exists(weights_path):
raise FileNotFoundError(
f"Trained weights not found at '{weights_path}'. "
f"Download as_model_best.pt (see README) and place it here."
)
state_dict = torch.load(weights_path, map_location=DEVICE)
model.load_state_dict(state_dict)
model.eval()
return model
# ──────────────────────────────────────────────────────────────────────────
# Preprocessing (unchanged from test_mac.ipynb -> preprocess_pil)
# ──────────────────────────────────────────────────────────────────────────
def preprocess_pil(img: Image.Image) -> torch.Tensor:
"""Resize to 224x224, scale to [0,1], ImageNet-normalize, return CHW tensor."""
img = img.convert("RGB").resize((224, 224))
img_np = np.asarray(img, dtype=np.float32) / 255.0
img_np = (img_np - _MEAN) / _STD
img_np = img_np.transpose(2, 0, 1)
return torch.tensor(img_np, dtype=torch.float32)
# ──────────────────────────────────────────────────────────────────────────
# Prediction
# ──────────────────────────────────────────────────────────────────────────
@torch.no_grad()
def predict(model, img: Image.Image):
"""Run the model on a PIL image.
Returns (label, probability) where probability is the raw sigmoid output
and label is decided with the original threshold: prob >= 0.5 -> REAL.
"""
x = preprocess_pil(img).unsqueeze(0).to(DEVICE)
prob = model(x).item()
label = "REAL" if prob >= 0.5 else "FAKE"
return label, prob