Download _net.py from SpeechAntiSpoofingBenchmarks/Forensics-WavLM-Base: direct link, hf CLI and curl.
- Browser
- Download file 9.67 kB
-
https://huggingface.co/SpeechAntiSpoofingBenchmarks/Forensics-WavLM-Base/resolve/main/_net.py
- Command line
-
hf download hf://SpeechAntiSpoofingBenchmarks/Forensics-WavLM-Base/_net.py
-
curl -L -o _net.py https://huggingface.co/SpeechAntiSpoofingBenchmarks/Forensics-WavLM-Base/resolve/main/_net.py
9.67 kB
| """Ported from HF eliya/forensics_0.3B_base_deepfake_classifier @ cfd11365b02167214dc3b392378809fef6ca3cfa, | |
| model.py (CC-BY-NC-4.0, Eliya Segev; byte-identical to eliya/forensics_0.3B_wavlm_oc_softmax_deepfake_classifier | |
| @ 2911b8f1f6112e7095c8ac1061e03b81baff95a2 model.py). Edits for the Arena wrapper: | |
| 1. DeepfakeDetector builds WavLMModel from the vendored microsoft/wavlm-large config | |
| (wavlm_large_config.json, HF @ c1423ed94bb01d80a3f5ce5bc39f6026a0f4828c) instead of | |
| WavLMModel.from_pretrained(...) -- no Hub weight download; the checkpoint overwrites every | |
| backbone weight (strict load, incl. pos_conv parametrizations.weight.original0/1). | |
| 2. @torch.no_grad() removed from DeepfakeDetector.forward (the wrapper runs under no_grad; lets ONNX trace). | |
| 3. Added WINDOW / load_audio_np (numpy port of inference.py load_audio: peak-normalise, centre crop / | |
| repeat-pad to 80000 samples; resample/downmix omitted -- the runner supplies 16 kHz mono). | |
| Everything else (AASIST pool, classifier, forward; defaults hidden_dim 320, gat_dims (128, 64)) is verbatim. | |
| Original docstring follows: | |
| WavLM-large + AASIST speech deepfake detector (inference only).""" | |
| import os | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from transformers import WavLMConfig, WavLMModel | |
| _DIR = os.path.dirname(os.path.abspath(__file__)) | |
| WAVLM_CONFIG = os.path.join(_DIR, "wavlm_large_config.json") | |
| WINDOW = 80000 # inference.py: 5.0 s * 16 kHz | |
| def load_audio_np(wav, n=WINDOW): | |
| """inference.py load_audio (after mono/16 kHz): peak-normalise, then repeat-pad or centre-crop.""" | |
| wav = np.asarray(wav, dtype=np.float32).reshape(-1) | |
| cur = wav.shape[0] | |
| if cur == 0: # (not in source) empty clip -> silence | |
| return np.zeros(n, dtype=np.float32) | |
| wav = (wav / (np.abs(wav).max() + np.float32(1e-8))).astype(np.float32) | |
| if cur < n: | |
| wav = np.tile(wav, (n + cur - 1) // cur)[:n] | |
| elif cur > n: | |
| s = (cur - n) // 2 | |
| wav = wav[s:s + n] | |
| return np.ascontiguousarray(wav) | |
| class _GraphAttentionLayer(nn.Module): | |
| def __init__(self, in_dim, out_dim, temperature): | |
| super().__init__() | |
| self.att_proj = nn.Linear(in_dim, out_dim) | |
| self.att_weight = nn.Parameter(torch.empty(out_dim, 1)) | |
| self.proj_with_att = nn.Linear(in_dim, out_dim) | |
| self.proj_without_att = nn.Linear(in_dim, out_dim) | |
| self.bn = nn.BatchNorm1d(out_dim) | |
| self.temp = temperature | |
| def forward(self, x): | |
| N = x.size(1) | |
| x_i = x.unsqueeze(2).expand(-1, -1, N, -1) | |
| att = torch.matmul(torch.tanh(self.att_proj(x_i * x_i.transpose(1, 2))), self.att_weight) / self.temp | |
| att = F.softmax(att, dim=-2) | |
| out = self.proj_with_att(torch.matmul(att.squeeze(-1), x)) + self.proj_without_att(x) | |
| s = out.shape | |
| return F.selu(self.bn(out.reshape(-1, s[-1])).reshape(s)) | |
| class _HtrgGraphAttentionLayer(nn.Module): | |
| def __init__(self, in_dim, out_dim, temperature): | |
| super().__init__() | |
| self.proj_type1 = nn.Linear(in_dim, in_dim) | |
| self.proj_type2 = nn.Linear(in_dim, in_dim) | |
| self.att_proj = nn.Linear(in_dim, out_dim) | |
| self.att_projM = nn.Linear(in_dim, out_dim) | |
| self.att_weight11 = nn.Parameter(torch.empty(out_dim, 1)) | |
| self.att_weight22 = nn.Parameter(torch.empty(out_dim, 1)) | |
| self.att_weight12 = nn.Parameter(torch.empty(out_dim, 1)) | |
| self.att_weightM = nn.Parameter(torch.empty(out_dim, 1)) | |
| self.proj_with_att = nn.Linear(in_dim, out_dim) | |
| self.proj_without_att = nn.Linear(in_dim, out_dim) | |
| self.proj_with_attM = nn.Linear(in_dim, out_dim) | |
| self.proj_without_attM = nn.Linear(in_dim, out_dim) | |
| self.bn = nn.BatchNorm1d(out_dim) | |
| self.temp = temperature | |
| def forward(self, x1, x2, master): | |
| n1 = x1.size(1) | |
| x = torch.cat([self.proj_type1(x1), self.proj_type2(x2)], dim=1) | |
| N = x.size(1) | |
| x_i = x.unsqueeze(2).expand(-1, -1, N, -1) | |
| att_map = torch.tanh(self.att_proj(x_i * x_i.transpose(1, 2))) | |
| board = torch.zeros_like(att_map[:, :, :, 0]).unsqueeze(-1) | |
| board[:, :n1, :n1] = torch.matmul(att_map[:, :n1, :n1], self.att_weight11) | |
| board[:, n1:, n1:] = torch.matmul(att_map[:, n1:, n1:], self.att_weight22) | |
| board[:, :n1, n1:] = torch.matmul(att_map[:, :n1, n1:], self.att_weight12) | |
| board[:, n1:, :n1] = torch.matmul(att_map[:, n1:, :n1], self.att_weight12) | |
| board = F.softmax(board / self.temp, dim=-2) | |
| out = self.proj_with_att(torch.matmul(board.squeeze(-1), x)) + self.proj_without_att(x) | |
| s = out.shape | |
| out = F.selu(self.bn(out.reshape(-1, s[-1])).reshape(s)) | |
| att_m = F.softmax(torch.matmul(torch.tanh(self.att_projM(x * master)), self.att_weightM) / self.temp, dim=-2) | |
| master = self.proj_with_attM(torch.matmul(att_m.squeeze(-1).unsqueeze(1), x)) + self.proj_without_attM(master) | |
| return out.narrow(1, 0, n1), out.narrow(1, n1, N - n1), master | |
| class _GraphPool(nn.Module): | |
| def __init__(self, k, in_dim): | |
| super().__init__() | |
| self.k = k | |
| self.proj = nn.Linear(in_dim, 1) | |
| def forward(self, h): | |
| scores = torch.sigmoid(self.proj(h)) | |
| n_keep = max(int(h.size(1) * self.k), 1) | |
| idx = torch.topk(scores, n_keep, dim=1)[1].expand(-1, -1, h.size(-1)) | |
| return torch.gather(h * scores, 1, idx) | |
| class AASISTPool(nn.Module): | |
| def __init__(self, feat_dim, num_layers, gat_dims=(128, 64), | |
| pool_ratios=(0.5, 0.7, 0.5, 0.5), temps=(2.0, 2.0, 100.0, 100.0)): | |
| super().__init__() | |
| g = list(gat_dims) | |
| self.proj = nn.Linear(feat_dim, g[0]) | |
| self.pos_S = nn.Parameter(torch.randn(1, num_layers, g[0])) | |
| self.master1 = nn.Parameter(torch.randn(1, 1, g[0])) | |
| self.master2 = nn.Parameter(torch.randn(1, 1, g[0])) | |
| self.GAT_S = _GraphAttentionLayer(g[0], g[0], temps[0]) | |
| self.GAT_T = _GraphAttentionLayer(g[0], g[0], temps[1]) | |
| self.HtrgGAT_11 = _HtrgGraphAttentionLayer(g[0], g[1], temps[2]) | |
| self.HtrgGAT_12 = _HtrgGraphAttentionLayer(g[1], g[1], temps[3]) | |
| self.HtrgGAT_21 = _HtrgGraphAttentionLayer(g[0], g[1], temps[2]) | |
| self.HtrgGAT_22 = _HtrgGraphAttentionLayer(g[1], g[1], temps[3]) | |
| self.pool_S = _GraphPool(pool_ratios[0], g[0]) | |
| self.pool_T = _GraphPool(pool_ratios[1], g[0]) | |
| self.pool_hS1 = _GraphPool(pool_ratios[2], g[1]) | |
| self.pool_hT1 = _GraphPool(pool_ratios[2], g[1]) | |
| self.pool_hS2 = _GraphPool(pool_ratios[2], g[1]) | |
| self.pool_hT2 = _GraphPool(pool_ratios[2], g[1]) | |
| self.output_dim = 5 * g[1] | |
| def forward(self, hidden_states): | |
| projected = self.proj(torch.stack(hidden_states, dim=1)) | |
| e_S = torch.max(torch.abs(projected), dim=2)[0] + self.pos_S | |
| e_T = torch.max(torch.abs(projected), dim=1)[0] | |
| out_S = self.pool_S(self.GAT_S(e_S)) | |
| out_T = self.pool_T(self.GAT_T(e_T)) | |
| out_T1, out_S1, m1 = self.HtrgGAT_11(out_T, out_S, self.master1) | |
| out_S1, out_T1 = self.pool_hS1(out_S1), self.pool_hT1(out_T1) | |
| a_T, a_S, a_m = self.HtrgGAT_12(out_T1, out_S1, m1) | |
| out_T1, out_S1, m1 = out_T1 + a_T, out_S1 + a_S, m1 + a_m | |
| out_T2, out_S2, m2 = self.HtrgGAT_21(out_T, out_S, self.master2) | |
| out_S2, out_T2 = self.pool_hS2(out_S2), self.pool_hT2(out_T2) | |
| a_T, a_S, a_m = self.HtrgGAT_22(out_T2, out_S2, m2) | |
| out_T2, out_S2, m2 = out_T2 + a_T, out_S2 + a_S, m2 + a_m | |
| out_T, out_S, master = torch.max(out_T1, out_T2), torch.max(out_S1, out_S2), torch.max(m1, m2) | |
| return torch.cat([torch.max(torch.abs(out_T), 1)[0], out_T.mean(1), | |
| torch.max(torch.abs(out_S), 1)[0], out_S.mean(1), master.squeeze(1)], dim=1) | |
| class MLPBlock(nn.Module): | |
| def __init__(self, dim): | |
| super().__init__() | |
| self.net = nn.Sequential(nn.Linear(dim, dim * 2), nn.GELU(), nn.Dropout(0.1), | |
| nn.Linear(dim * 2, dim), nn.GELU(), nn.Dropout(0.1)) | |
| def forward(self, x): | |
| return self.net(x) | |
| class Classifier(nn.Module): | |
| def __init__(self, input_dim, hidden_dim, num_layers, num_labels=1): | |
| super().__init__() | |
| self.input_proj = nn.Linear(input_dim, hidden_dim) if input_dim != hidden_dim else nn.Identity() | |
| self.mlp_blocks = nn.ModuleList([MLPBlock(hidden_dim) for _ in range(num_layers)]) | |
| self.layer_norms = nn.ModuleList([nn.LayerNorm(hidden_dim) for _ in range(num_layers)]) | |
| self.output_head = nn.Sequential(nn.Linear(hidden_dim, hidden_dim), nn.GELU(), | |
| nn.Dropout(0.1), nn.Linear(hidden_dim, num_labels)) | |
| def forward(self, x): | |
| x = self.input_proj(x) | |
| for mlp, ln in zip(self.mlp_blocks, self.layer_norms): | |
| x = x + mlp(ln(x)) | |
| return self.output_head(x) | |
| class DeepfakeDetector(nn.Module): | |
| def __init__(self, hidden_dim=320, classifier_layers=3, aasist_gat_dims=(128, 64)): | |
| super().__init__() | |
| self.wavlm = WavLMModel(WavLMConfig.from_json_file(WAVLM_CONFIG)) # edit 1 | |
| self.pool = AASISTPool(self.wavlm.config.hidden_size, self.wavlm.config.num_hidden_layers, aasist_gat_dims) | |
| self.classifier = Classifier(self.pool.output_dim, hidden_dim, classifier_layers) | |
| def forward(self, waveform): # edit 2: no @torch.no_grad() # (B, T) 16 kHz -> logit (B,); sigmoid(logit)=P(real) | |
| hs = list(self.wavlm(waveform, output_hidden_states=True).hidden_states[1:]) | |
| return self.classifier(self.pool(hs)).squeeze(-1) | |