deepshield-api / backend /model.py
Venkatkalyan21
Deploy clean backend to Hugging Face
1970a5b
Raw
History Blame Contribute Delete
6.82 kB
"""
HybridDeepfakeDetector β€” Dual-branch Spatial + Frequency CNN
Architecture for IEEE Research Paper on Deepfake Video Detection
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
try:
import timm
TIMM_AVAILABLE = True
except ImportError:
TIMM_AVAILABLE = False
import torchvision.models as tv_models
# ─────────────────────────────────────────────
# Frequency Analysis Branch
# ─────────────────────────────────────────────
class FrequencyBranch(nn.Module):
"""
Extracts GAN fingerprint artifacts from the frequency domain.
GAN generators leave periodic patterns in the DCT/FFT spectrum
that are invisible to the human eye but detectable by CNNs.
"""
def __init__(self, out_dim: int = 128):
super().__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 112x112
nn.Conv2d(32, 64, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 56x56
nn.Conv2d(64, 128, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2), # 28x28
nn.Conv2d(128, out_dim, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_dim),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1)), # 1x1
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Convert to grayscale: (B, 1, H, W)
gray = 0.299 * x[:, 0:1] + 0.587 * x[:, 1:2] + 0.114 * x[:, 2:3]
# 2D FFT β†’ log-magnitude spectrum
fft = torch.fft.fft2(gray)
magnitude = torch.abs(fft)
magnitude = torch.log(magnitude + 1e-8)
# Normalize per sample
b = magnitude.shape[0]
m = magnitude.view(b, -1)
mn = m.mean(dim=1, keepdim=True).view(b, 1, 1, 1)
std = m.std(dim=1, keepdim=True).view(b, 1, 1, 1) + 1e-8
magnitude = (magnitude - mn) / std
return self.conv_layers(magnitude).flatten(1) # (B, out_dim)
# ─────────────────────────────────────────────
# Spatial Branch (EfficientNet-B0 backbone)
# ─────────────────────────────────────────────
class SpatialBranch(nn.Module):
def __init__(self, pretrained: bool = True):
super().__init__()
if TIMM_AVAILABLE:
self.backbone = timm.create_model(
"efficientnet_b0",
pretrained=pretrained,
num_classes=0, # Remove classifier head
global_pool="avg",
)
self.out_dim = self.backbone.num_features # 1280
else:
# Fallback: MobileNetV3-Small from torchvision
backbone = tv_models.mobilenet_v3_small(pretrained=pretrained)
self.backbone = nn.Sequential(*list(backbone.children())[:-2],
nn.AdaptiveAvgPool2d(1), nn.Flatten())
self.out_dim = 576
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.backbone(x) # (B, out_dim)
# ─────────────────────────────────────────────
# Fusion Classifier
# ─────────────────────────────────────────────
class FusionClassifier(nn.Module):
def __init__(self, spatial_dim: int, freq_dim: int):
super().__init__()
combined = spatial_dim + freq_dim
self.fc = nn.Sequential(
nn.Linear(combined, 512),
nn.BatchNorm1d(512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512, 128),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(128, 1),
)
def forward(self, spatial_feat, freq_feat):
x = torch.cat([spatial_feat, freq_feat], dim=1)
return self.fc(x) # (B, 1) β€” raw logits
# ─────────────────────────────────────────────
# HybridDeepfakeDetector (Main Model)
# ─────────────────────────────────────────────
class HybridDeepfakeDetector(nn.Module):
"""
Novel dual-branch architecture combining:
- Spatial branch (EfficientNet-B0): captures texture/semantic artifacts
- Frequency branch (FFT-CNN): captures GAN frequency fingerprints
Fused via FC layers for binary Real/Fake classification.
"""
def __init__(self, pretrained: bool = True, freq_dim: int = 128):
super().__init__()
self.spatial = SpatialBranch(pretrained=pretrained)
self.freq = FrequencyBranch(out_dim=freq_dim)
self.fusion = FusionClassifier(self.spatial.out_dim, freq_dim)
def forward(self, x: torch.Tensor):
s = self.spatial(x)
f = self.freq(x)
return self.fusion(s, f) # (B, 1) logits
def predict_proba(self, x: torch.Tensor) -> torch.Tensor:
"""Returns fake probability in [0, 1]."""
with torch.no_grad():
logits = self.forward(x)
return torch.sigmoid(logits).squeeze(1) # (B,)
@staticmethod
def load(path: str, device: str = "cpu") -> "HybridDeepfakeDetector":
model = HybridDeepfakeDetector(pretrained=False)
state = torch.load(path, map_location=device)
model.load_state_dict(state)
model.eval()
return model
# ─────────────────────────────────────────────
# Quick sanity check
# ─────────────────────────────────────────────
if __name__ == "__main__":
model = HybridDeepfakeDetector(pretrained=False)
dummy = torch.randn(4, 3, 224, 224)
out = model.predict_proba(dummy)
total = sum(p.numel() for p in model.parameters())
print(f"Output shape : {out.shape}")
print(f"Total params : {total:,}")
print("Model OK βœ“")