pavankumarvk commited on
Commit
e950836
Β·
verified Β·
1 Parent(s): 6810b1c

Upload 2 files

Browse files
Files changed (2) hide show
  1. audio_detector_inference.py +145 -0
  2. 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