deepshield-api / backend /audio_detector.py
Venkatkalyan21
Deploy clean backend to Hugging Face
1970a5b
Raw
History Blame Contribute Delete
7 kB
"""
AudioDeepfakeDetector β€” Wav2Vec2-based audio deepfake detection
Uses Facebook's Wav2Vec2-base for feature extraction + lightweight classification head.
Detects AI-generated or voice-cloned audio (TTS, VC spoofing).
"""
import torch
import torch.nn as nn
import numpy as np
from pathlib import Path
from typing import Optional
try:
import librosa
LIBROSA_AVAILABLE = True
except ImportError:
LIBROSA_AVAILABLE = False
try:
from transformers import Wav2Vec2Model, Wav2Vec2FeatureExtractor
WAV2VEC_AVAILABLE = True
except ImportError:
WAV2VEC_AVAILABLE = False
# ─────────────────────────────────────────────────────────────────
# Classifier Head (on top of Wav2Vec2 hidden states)
# ─────────────────────────────────────────────────────────────────
class AudioClassifierHead(nn.Module):
"""
Pools temporal hidden states from Wav2Vec2 and classifies Real/Fake.
Input: (B, T, D) β€” Wav2Vec2 last_hidden_state
Output: (B, 1) β€” raw logits (apply sigmoid for probability)
"""
def __init__(self, input_dim: int = 768):
super().__init__()
self.attention_pool = nn.Sequential(
nn.Linear(input_dim, 128),
nn.Tanh(),
nn.Linear(128, 1),
)
self.classifier = nn.Sequential(
nn.Linear(input_dim, 256),
nn.LayerNorm(256),
nn.ReLU(inplace=True),
nn.Dropout(0.4),
nn.Linear(256, 64),
nn.ReLU(inplace=True),
nn.Dropout(0.2),
nn.Linear(64, 1),
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
# Attention-weighted pooling
weights = torch.softmax(self.attention_pool(hidden_states), dim=1) # (B, T, 1)
pooled = (hidden_states * weights).sum(dim=1) # (B, D)
return self.classifier(pooled) # (B, 1)
# ─────────────────────────────────────────────────────────────────
# Main Audio Deepfake Detector
# ─────────────────────────────────────────────────────────────────
class AudioDeepfakeDetector(nn.Module):
"""
Wav2Vec2-base + Attention-Pooling Classifier for audio deepfake detection.
Pipeline:
Raw waveform (16 kHz mono)
β†’ Wav2Vec2 feature extractor (normalisation)
β†’ Wav2Vec2 transformer encoder β†’ hidden states (T Γ— 768)
β†’ Attention-weighted pooling β†’ 768-dim vector
β†’ FC classifier head β†’ fake probability
"""
SAMPLE_RATE = 16_000
MAX_DURATION_SEC = 10.0 # Clip to 10 s for speed
def __init__(self, pretrained: bool = True, freeze_base: bool = True):
super().__init__()
if not WAV2VEC_AVAILABLE:
raise RuntimeError(
"transformers library not installed.\n"
"Run: pip install transformers>=4.30.0"
)
model_name = "facebook/wav2vec2-base"
self.feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_name)
self.wav2vec2 = Wav2Vec2Model.from_pretrained(model_name)
# Optionally freeze Wav2Vec2 backbone for efficient fine-tuning
if freeze_base:
for param in self.wav2vec2.parameters():
param.requires_grad = False
hidden_dim = self.wav2vec2.config.hidden_size # 768
self.head = AudioClassifierHead(hidden_dim)
# ── Forward ─────────────────────────────────────────────────
def forward(
self,
input_values: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
outputs = self.wav2vec2(input_values, attention_mask=attention_mask)
return self.head(outputs.last_hidden_state) # (B, 1) logits
# ── Inference helpers ────────────────────────────────────────
def predict_proba(self, waveform: np.ndarray, device: str = "cpu") -> float:
"""
Args:
waveform: 1-D numpy float32 array at 16 kHz
device: 'cpu' or 'cuda'
Returns:
Fake probability in [0, 1]
"""
max_samples = int(self.MAX_DURATION_SEC * self.SAMPLE_RATE)
if len(waveform) > max_samples:
waveform = waveform[:max_samples]
inputs = self.feature_extractor(
waveform,
sampling_rate=self.SAMPLE_RATE,
return_tensors="pt",
padding=True,
)
input_values = inputs.input_values.to(device)
self.eval()
with torch.no_grad():
logits = self.forward(input_values)
prob = torch.sigmoid(logits).squeeze().item()
return float(prob)
# ── Static helpers ───────────────────────────────────────────
@staticmethod
def load_audio(path: str, target_sr: int = 16_000) -> np.ndarray:
"""Load any audio/video file and resample to 16 kHz mono."""
if not LIBROSA_AVAILABLE:
raise RuntimeError("librosa not installed. Run: pip install librosa>=0.10.0")
waveform, _ = librosa.load(path, sr=target_sr, mono=True)
return waveform.astype(np.float32)
@staticmethod
def load(path: str, device: str = "cpu") -> "AudioDeepfakeDetector":
"""Load a trained detector from a .pth file."""
model = AudioDeepfakeDetector(pretrained=False, freeze_base=False)
state = torch.load(path, map_location=device, weights_only=True)
model.load_state_dict(state)
model.eval()
return model
# ── Quick sanity check ────────────────────────────────────────────
if __name__ == "__main__":
if WAV2VEC_AVAILABLE:
model = AudioDeepfakeDetector(pretrained=False, freeze_base=False)
dummy = torch.randn(2, 16_000) # 1-second batch
logits = model(dummy)
total = sum(p.numel() for p in model.parameters())
print(f"Output shape : {logits.shape}")
print(f"Total params : {total:,}")
print("AudioDeepfakeDetector OK βœ“")
else:
print("transformers not installed β€” skipping sanity check.")