Upload 2 files
Browse files- audio_detector_inference.py +145 -0
- audio_model.py +243 -0
audio_detector_inference.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
audio_detector_inference.py
|
| 3 |
+
============================
|
| 4 |
+
Inference wrapper for AASISTDeepFake.
|
| 5 |
+
Mirrors the structure of text_detector_inference.py for consistency.
|
| 6 |
+
|
| 7 |
+
Usage
|
| 8 |
+
-----
|
| 9 |
+
from audio_detector_inference import AudioDetectorInference
|
| 10 |
+
|
| 11 |
+
detector = AudioDetectorInference(checkpoint="best_aasist.pt", threshold=0.5)
|
| 12 |
+
result = detector.predict(waveform_np_array, sample_rate=16000)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
from audio_model import AASISTDeepFake, SAMPLE_RATE, MAX_SAMPLES
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AudioDetectorInference:
|
| 22 |
+
"""
|
| 23 |
+
Thin wrapper around AASISTDeepFake for single-clip audio prediction.
|
| 24 |
+
|
| 25 |
+
Parameters
|
| 26 |
+
----------
|
| 27 |
+
checkpoint : str
|
| 28 |
+
Path to the best_aasist.pt state-dict file produced by training.
|
| 29 |
+
threshold : float
|
| 30 |
+
sigmoid(logit) >= threshold β Real.
|
| 31 |
+
Use 0.5 (default) or the optimal F1 threshold printed at the end
|
| 32 |
+
of training (the value labelled "Optimal threshold" in Cell 14).
|
| 33 |
+
device : torch.device | None
|
| 34 |
+
Auto-detects CUDA if None.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
checkpoint: str = "best_aasist.pt",
|
| 40 |
+
threshold: float = 0.5,
|
| 41 |
+
device: torch.device = None,
|
| 42 |
+
):
|
| 43 |
+
self.threshold = threshold
|
| 44 |
+
self.device = device or torch.device(
|
| 45 |
+
"cuda" if torch.cuda.is_available() else "cpu"
|
| 46 |
+
)
|
| 47 |
+
self.model = None
|
| 48 |
+
|
| 49 |
+
if os.path.exists(checkpoint):
|
| 50 |
+
print(f"[AudioDetector] Loading checkpoint: {checkpoint}")
|
| 51 |
+
self.model = AASISTDeepFake()
|
| 52 |
+
self.model.load_state_dict(
|
| 53 |
+
torch.load(checkpoint, map_location=self.device)
|
| 54 |
+
)
|
| 55 |
+
self.model.eval().to(self.device)
|
| 56 |
+
print(f"[AudioDetector] β
AASISTDeepFake ready "
|
| 57 |
+
f"(threshold={self.threshold})")
|
| 58 |
+
else:
|
| 59 |
+
print(
|
| 60 |
+
f"[AudioDetector] β οΈ '{checkpoint}' not found.\n"
|
| 61 |
+
f"[AudioDetector] Upload best_aasist.pt to the Space β "
|
| 62 |
+
f"audio predictions will fail until then."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 66 |
+
def _preprocess(self, x: np.ndarray, sr: int) -> torch.Tensor:
|
| 67 |
+
"""
|
| 68 |
+
Normalise, convert to mono, resample to 16 kHz, and
|
| 69 |
+
pad/trim to exactly MAX_SAMPLES (80 000).
|
| 70 |
+
|
| 71 |
+
Returns
|
| 72 |
+
-------
|
| 73 |
+
torch.Tensor of shape (1, 80 000), dtype float32, on CPU.
|
| 74 |
+
"""
|
| 75 |
+
x = x.astype(np.float32)
|
| 76 |
+
|
| 77 |
+
# ββ Normalise int16 recordings ββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
if np.abs(x).max() > 1.0:
|
| 79 |
+
x = x / 32768.0
|
| 80 |
+
|
| 81 |
+
# ββ Stereo β mono βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 82 |
+
if x.ndim == 2:
|
| 83 |
+
x = x.mean(axis=1)
|
| 84 |
+
|
| 85 |
+
# ββ Resample if needed ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 86 |
+
if sr != SAMPLE_RATE:
|
| 87 |
+
import librosa
|
| 88 |
+
print(f"[AudioDetector] Resampling {sr} Hz β {SAMPLE_RATE} Hz β¦")
|
| 89 |
+
x = librosa.resample(x, orig_sr=sr, target_sr=SAMPLE_RATE)
|
| 90 |
+
|
| 91 |
+
# ββ Pad or trim to exactly MAX_SAMPLES ββββββββββββββββββββββββββββββββ
|
| 92 |
+
if len(x) < MAX_SAMPLES:
|
| 93 |
+
x = np.pad(x, (0, MAX_SAMPLES - len(x)))
|
| 94 |
+
else:
|
| 95 |
+
x = x[:MAX_SAMPLES]
|
| 96 |
+
|
| 97 |
+
return torch.tensor(x, dtype=torch.float32).unsqueeze(0) # (1, 80000)
|
| 98 |
+
|
| 99 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 100 |
+
def predict(self, x: np.ndarray, sr: int) -> dict:
|
| 101 |
+
"""
|
| 102 |
+
Classify a single raw audio clip.
|
| 103 |
+
|
| 104 |
+
Parameters
|
| 105 |
+
----------
|
| 106 |
+
x : np.ndarray Raw waveform (any sample rate, mono or stereo).
|
| 107 |
+
sr : int Sample rate of x.
|
| 108 |
+
|
| 109 |
+
Returns
|
| 110 |
+
-------
|
| 111 |
+
dict with keys:
|
| 112 |
+
label : "Real" | "Fake"
|
| 113 |
+
real_prob : P(real) in [0, 1]
|
| 114 |
+
fake_prob : P(fake) in [0, 1]
|
| 115 |
+
confidence : probability of the predicted class in [0, 1]
|
| 116 |
+
"""
|
| 117 |
+
if self.model is None:
|
| 118 |
+
return {
|
| 119 |
+
"error": (
|
| 120 |
+
"Model not loaded β upload best_aasist.pt to the Space."
|
| 121 |
+
)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
wav = self._preprocess(x, sr).to(self.device)
|
| 125 |
+
|
| 126 |
+
with torch.no_grad():
|
| 127 |
+
logit = self.model(wav) # (1, 1)
|
| 128 |
+
real_prob = torch.sigmoid(logit).item()
|
| 129 |
+
|
| 130 |
+
fake_prob = 1.0 - real_prob
|
| 131 |
+
is_real = real_prob >= self.threshold
|
| 132 |
+
label = "Real" if is_real else "Fake"
|
| 133 |
+
confidence = real_prob if is_real else fake_prob
|
| 134 |
+
|
| 135 |
+
print(
|
| 136 |
+
f"[AudioDetector] real_prob={real_prob:.4f} "
|
| 137 |
+
f"fake_prob={fake_prob:.4f} β {label}"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
return {
|
| 141 |
+
"label": label,
|
| 142 |
+
"real_prob": round(real_prob, 4),
|
| 143 |
+
"fake_prob": round(fake_prob, 4),
|
| 144 |
+
"confidence": round(confidence, 4),
|
| 145 |
+
}
|
audio_model.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
audio_model.py
|
| 3 |
+
==============
|
| 4 |
+
AASISTDeepFake model definition β matches the training notebook exactly.
|
| 5 |
+
Import this in both training scripts and the Gradio app (via audio_detector_inference.py).
|
| 6 |
+
|
| 7 |
+
Architecture:
|
| 8 |
+
Raw waveform β SincConv β Downsample (32Γ) β Res2Block
|
| 9 |
+
β CNN (2 layers) β GraphAttn (Γ2) β AttentionPool β Classifier
|
| 10 |
+
|
| 11 |
+
Label convention (from training dataset enumerate(["fake", "real"])):
|
| 12 |
+
label = 0 β Fake
|
| 13 |
+
label = 1 β Real
|
| 14 |
+
sigmoid(logit) >= threshold β Real
|
| 15 |
+
sigmoid(logit) < threshold β Fake
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
|
| 23 |
+
# ββ Audio constants (must match training) βββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
SAMPLE_RATE = 16_000
|
| 25 |
+
MAX_DURATION = 5.0
|
| 26 |
+
MAX_SAMPLES = int(SAMPLE_RATE * MAX_DURATION) # 80 000 samples
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ββ Sub-modules βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
+
|
| 31 |
+
class SincConv(nn.Module):
|
| 32 |
+
"""
|
| 33 |
+
Learnable sinc-function band-pass filter bank.
|
| 34 |
+
Only 2Γout_channels parameters (one f_low, one f_high per filter).
|
| 35 |
+
Initialised from mel-scale frequency bands.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
@staticmethod
|
| 39 |
+
def to_mel(hz): return 2595 * np.log10(1 + hz / 700)
|
| 40 |
+
@staticmethod
|
| 41 |
+
def to_hz(mel): return 700 * (10 ** (mel / 2595) - 1)
|
| 42 |
+
|
| 43 |
+
def __init__(self, out_channels: int = 64, kernel_size: int = 512,
|
| 44 |
+
sample_rate: int = 16_000):
|
| 45 |
+
super().__init__()
|
| 46 |
+
self.out_channels = out_channels
|
| 47 |
+
self.kernel_size = kernel_size + 1 if kernel_size % 2 == 0 else kernel_size
|
| 48 |
+
self.sample_rate = sample_rate
|
| 49 |
+
|
| 50 |
+
low_hz, high_hz = 30, sample_rate / 2 - 100
|
| 51 |
+
mel = np.linspace(self.to_mel(low_hz), self.to_mel(high_hz), out_channels + 1)
|
| 52 |
+
hz = self.to_hz(mel)
|
| 53 |
+
|
| 54 |
+
self.low_hz_ = nn.Parameter(torch.Tensor(hz[:-1]).view(-1, 1))
|
| 55 |
+
self.band_hz_ = nn.Parameter(torch.Tensor(np.diff(hz)).view(-1, 1))
|
| 56 |
+
|
| 57 |
+
half = (self.kernel_size - 1) // 2
|
| 58 |
+
n = torch.arange(1, half + 1, dtype=torch.float32)
|
| 59 |
+
self.register_buffer('n_', (2 * np.pi * n / sample_rate).unsqueeze(0))
|
| 60 |
+
self.register_buffer('window_', torch.hamming_window(self.kernel_size))
|
| 61 |
+
|
| 62 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 63 |
+
low = 50 + torch.abs(self.low_hz_)
|
| 64 |
+
high = torch.clamp(low + 50 + torch.abs(self.band_hz_),
|
| 65 |
+
max=self.sample_rate / 2)
|
| 66 |
+
band = (high - low)[:, 0]
|
| 67 |
+
|
| 68 |
+
f1 = torch.matmul(low, self.n_)
|
| 69 |
+
f2 = torch.matmul(high, self.n_)
|
| 70 |
+
lp1 = torch.sin(f1) / (np.pi * self.n_ / (2 * np.pi))
|
| 71 |
+
lp2 = torch.sin(f2) / (np.pi * self.n_ / (2 * np.pi))
|
| 72 |
+
bp = (lp2 - lp1) / (2 * band[:, None])
|
| 73 |
+
|
| 74 |
+
centre = torch.zeros(self.out_channels, 1, device=bp.device)
|
| 75 |
+
filters = torch.cat([bp.flip(1), centre, bp], dim=1)
|
| 76 |
+
filters = filters * self.window_
|
| 77 |
+
|
| 78 |
+
x = x.unsqueeze(1)
|
| 79 |
+
return F.conv1d(x, filters.unsqueeze(1), padding=self.kernel_size // 2)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class Res2Block(nn.Module):
|
| 83 |
+
"""
|
| 84 |
+
Multi-scale residual block with inter-group accumulation.
|
| 85 |
+
Splits channels into `scale` groups; each group accumulates the previous.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
def __init__(self, channels: int, scale: int = 8, dilation: int = 1):
|
| 89 |
+
super().__init__()
|
| 90 |
+
assert channels % scale == 0, \
|
| 91 |
+
f"channels ({channels}) must be divisible by scale ({scale})"
|
| 92 |
+
self.scale = scale
|
| 93 |
+
width = channels // scale
|
| 94 |
+
self.convs = nn.ModuleList([
|
| 95 |
+
nn.Conv1d(width, width, 3, padding=dilation, dilation=dilation)
|
| 96 |
+
for _ in range(scale - 1)
|
| 97 |
+
])
|
| 98 |
+
self.bns = nn.ModuleList([nn.BatchNorm1d(width) for _ in range(scale - 1)])
|
| 99 |
+
|
| 100 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 101 |
+
chunks = torch.chunk(x, self.scale, dim=1)
|
| 102 |
+
out = [chunks[0]]
|
| 103 |
+
y = chunks[1]
|
| 104 |
+
for i, (conv, bn) in enumerate(zip(self.convs, self.bns)):
|
| 105 |
+
if i > 0:
|
| 106 |
+
y = y + chunks[i + 1]
|
| 107 |
+
y = F.gelu(bn(conv(y)))
|
| 108 |
+
out.append(y)
|
| 109 |
+
return torch.cat(out, dim=1)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class GraphAttn(nn.Module):
|
| 113 |
+
"""
|
| 114 |
+
Memory-efficient multi-head self-attention over temporal frames.
|
| 115 |
+
Sequences longer than 64 tokens are pooled before attention and
|
| 116 |
+
upsampled back for the residual addition.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
def __init__(self, dim: int, heads: int = 4):
|
| 120 |
+
super().__init__()
|
| 121 |
+
self.heads = heads
|
| 122 |
+
self.head_dim = dim // heads
|
| 123 |
+
self.qkv = nn.Linear(dim, dim * 3)
|
| 124 |
+
self.out = nn.Linear(dim, dim)
|
| 125 |
+
|
| 126 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 127 |
+
B, N, C = x.shape
|
| 128 |
+
if N > 64:
|
| 129 |
+
x_pool = F.adaptive_avg_pool1d(
|
| 130 |
+
x.transpose(1, 2), 64).transpose(1, 2)
|
| 131 |
+
else:
|
| 132 |
+
x_pool = x
|
| 133 |
+
|
| 134 |
+
Bp, Np, Cp = x_pool.shape
|
| 135 |
+
qkv = (self.qkv(x_pool)
|
| 136 |
+
.reshape(Bp, Np, 3, self.heads, self.head_dim)
|
| 137 |
+
.permute(2, 0, 3, 1, 4))
|
| 138 |
+
q, k, v = qkv.unbind(0)
|
| 139 |
+
attn = torch.softmax(
|
| 140 |
+
q @ k.transpose(-2, -1) / (self.head_dim ** 0.5), dim=-1)
|
| 141 |
+
out = (attn @ v).transpose(1, 2).reshape(Bp, Np, Cp)
|
| 142 |
+
out = self.out(out)
|
| 143 |
+
|
| 144 |
+
# Upsample back to original length for the residual connection
|
| 145 |
+
out = F.interpolate(
|
| 146 |
+
out.transpose(1, 2), size=N,
|
| 147 |
+
mode='linear', align_corners=False).transpose(1, 2)
|
| 148 |
+
return out
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class AttentionPool(nn.Module):
|
| 152 |
+
"""Soft-attention weighted pooling over a sequence."""
|
| 153 |
+
|
| 154 |
+
def __init__(self, dim: int):
|
| 155 |
+
super().__init__()
|
| 156 |
+
self.attn = nn.Linear(dim, 1)
|
| 157 |
+
|
| 158 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 159 |
+
w = torch.softmax(self.attn(x), dim=1) # (B, T, 1)
|
| 160 |
+
return (w * x).sum(dim=1) # (B, dim)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ββ Main model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
+
|
| 165 |
+
class AASISTDeepFake(nn.Module):
|
| 166 |
+
"""
|
| 167 |
+
AASISTDeepFake β memory-efficient raw-waveform audio spoof detector.
|
| 168 |
+
|
| 169 |
+
Input : (B, 80 000) float32 waveform, normalised to [-1, 1]
|
| 170 |
+
Output : (B, 1) raw logit β sigmoid β P(real)
|
| 171 |
+
|
| 172 |
+
Prediction:
|
| 173 |
+
sigmoid(logit) >= threshold β Real (label 1)
|
| 174 |
+
sigmoid(logit) < threshold β Fake (label 0)
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
def __init__(
|
| 178 |
+
self,
|
| 179 |
+
sinc_ch: int = 64,
|
| 180 |
+
sinc_kernel: int = 512,
|
| 181 |
+
hidden: int = 128,
|
| 182 |
+
graph_heads: int = 4,
|
| 183 |
+
n_graph: int = 2,
|
| 184 |
+
):
|
| 185 |
+
super().__init__()
|
| 186 |
+
self.sinc = SincConv(sinc_ch, sinc_kernel, SAMPLE_RATE)
|
| 187 |
+
self.bn_sinc = nn.BatchNorm1d(sinc_ch)
|
| 188 |
+
|
| 189 |
+
# Aggressive downsampling: T β T/32 (kills OOM on long sequences)
|
| 190 |
+
self.downsample = nn.Sequential(
|
| 191 |
+
nn.Conv1d(sinc_ch, sinc_ch, kernel_size=8, stride=8),
|
| 192 |
+
nn.BatchNorm1d(sinc_ch), nn.GELU(),
|
| 193 |
+
nn.Conv1d(sinc_ch, sinc_ch, kernel_size=4, stride=4),
|
| 194 |
+
nn.BatchNorm1d(sinc_ch), nn.GELU(),
|
| 195 |
+
)
|
| 196 |
+
self.encoder = nn.Sequential(
|
| 197 |
+
Res2Block(sinc_ch), nn.BatchNorm1d(sinc_ch), nn.GELU(),
|
| 198 |
+
)
|
| 199 |
+
self.cnn = nn.Sequential(
|
| 200 |
+
nn.Conv1d(sinc_ch, hidden, kernel_size=3, padding=1),
|
| 201 |
+
nn.BatchNorm1d(hidden), nn.GELU(),
|
| 202 |
+
nn.Conv1d(hidden, hidden, kernel_size=3, padding=1),
|
| 203 |
+
nn.BatchNorm1d(hidden), nn.GELU(),
|
| 204 |
+
)
|
| 205 |
+
self.graph_layers = nn.ModuleList(
|
| 206 |
+
[GraphAttn(hidden, graph_heads) for _ in range(n_graph)])
|
| 207 |
+
self.layer_norms = nn.ModuleList(
|
| 208 |
+
[nn.LayerNorm(hidden) for _ in range(n_graph)])
|
| 209 |
+
self.pool = AttentionPool(hidden)
|
| 210 |
+
self.classifier = nn.Sequential(
|
| 211 |
+
nn.LayerNorm(hidden),
|
| 212 |
+
nn.Linear(hidden, 64),
|
| 213 |
+
nn.GELU(),
|
| 214 |
+
nn.Dropout(0.4),
|
| 215 |
+
nn.Linear(64, 1),
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 219 |
+
x = torch.abs(self.sinc(x)) # (B, sinc_ch, T)
|
| 220 |
+
x = F.gelu(self.bn_sinc(x))
|
| 221 |
+
x = self.downsample(x) # (B, sinc_ch, T/32)
|
| 222 |
+
x = self.encoder(x)
|
| 223 |
+
x = self.cnn(x) # (B, hidden, T/32)
|
| 224 |
+
x = x.transpose(1, 2) # (B, T/32, hidden)
|
| 225 |
+
for attn, ln in zip(self.graph_layers, self.layer_norms):
|
| 226 |
+
x = ln(x + attn(x))
|
| 227 |
+
pooled = self.pool(x) # (B, hidden)
|
| 228 |
+
return self.classifier(pooled) # (B, 1)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# ββ Helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
+
|
| 233 |
+
def load_audio_model(
|
| 234 |
+
checkpoint: str,
|
| 235 |
+
device: torch.device = None,
|
| 236 |
+
) -> AASISTDeepFake:
|
| 237 |
+
"""Load a trained AASISTDeepFake from a .pt state-dict checkpoint."""
|
| 238 |
+
if device is None:
|
| 239 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 240 |
+
model = AASISTDeepFake()
|
| 241 |
+
model.load_state_dict(torch.load(checkpoint, map_location=device))
|
| 242 |
+
model.eval().to(device)
|
| 243 |
+
return model
|