Spaces:
Sleeping
Sleeping
File size: 5,710 Bytes
11aa20d 823741b 11aa20d 2df033f 11aa20d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """
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
|