Download validation/backend/model.py from indrajit4533/voiceguard: direct link, hf CLI and curl.
- Browser
- Download file 6.76 kB
-
https://huggingface.co/indrajit4533/voiceguard/resolve/main/validation/backend/model.py
- Command line
-
hf download hf://indrajit4533/voiceguard/validation/backend/model.py
-
curl -L -o model.py https://huggingface.co/indrajit4533/voiceguard/resolve/main/validation/backend/model.py
6.76 kB
| """ | |
| VoiceGuard RawNet Neural Network Architecture | |
| Matches checkpoint: backend/models/voiceguard_int8.pt | |
| Architecture: | |
| - SincConv front-end with learnable band-pass and high-pass filters | |
| - 3 Residual blocks with dilated/strided 1D convolutions and residual shortcuts | |
| - Max pooling (downsampling temporal resolution) | |
| - Bidirectional 2-layer GRU (quantizable to INT8) | |
| - Linear classification head for 5 classes: | |
| ['synthetic_indic', 'diffwave', 'melgan', 'gt', 'wavenet'] | |
| """ | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from typing import List | |
| class SincConv(nn.Module): | |
| """ | |
| Parameterized Sinc Convolutional Layer. | |
| Learns band-pass filters directly from raw audio waveforms. | |
| """ | |
| def __init__( | |
| self, | |
| out_channels: int = 64, | |
| kernel_size: int = 129, | |
| sample_rate: int = 16000, | |
| min_low_hz: int = 50, | |
| min_band_hz: int = 50, | |
| ): | |
| super().__init__() | |
| if kernel_size % 2 == 0: | |
| kernel_size += 1 | |
| self.out_channels = out_channels | |
| self.kernel_size = kernel_size | |
| self.sample_rate = sample_rate | |
| self.min_low_hz = min_low_hz | |
| self.min_band_hz = min_band_hz | |
| # Learnable filter parameters matching checkpoint | |
| self.high_pass_filter = nn.Parameter(torch.Tensor(out_channels, 1, 1)) | |
| self.band_pass_filter = nn.Parameter(torch.Tensor(out_channels, 1, 1)) | |
| # Initialize filter frequencies linearly if not loaded | |
| low_hz = torch.linspace(min_low_hz, sample_rate / 2 - min_band_hz, out_channels).view(out_channels, 1, 1) | |
| band_hz = torch.full((out_channels, 1, 1), min_band_hz, dtype=torch.float32) | |
| self.high_pass_filter.data.copy_(low_hz) | |
| self.band_pass_filter.data.copy_(band_hz) | |
| def _sinc(self, x: torch.Tensor) -> torch.Tensor: | |
| """Numerically stable sinc function: sin(pi*x) / (pi*x).""" | |
| return torch.where( | |
| x == 0, | |
| torch.ones_like(x), | |
| torch.sin(math.pi * x) / (math.pi * x + 1e-12), | |
| ) | |
| def get_filters(self, device: torch.device) -> torch.Tensor: | |
| """Construct the bank of sinc band-pass filters with Hamming window.""" | |
| t_half = (self.kernel_size - 1) // 2 | |
| # Discrete time indices: [-(K-1)/2, ..., (K-1)/2] | |
| t = torch.arange(-t_half, t_half + 1, dtype=torch.float32, device=device) / self.sample_rate | |
| # Ensure valid positive frequencies | |
| f_low = self.min_low_hz + torch.abs(self.high_pass_filter.squeeze(-1)).to(device) # [out_channels, 1] | |
| f_band = self.min_band_hz + torch.abs(self.band_pass_filter.squeeze(-1)).to(device) # [out_channels, 1] | |
| f_high = torch.clamp(f_low + f_band, max=self.sample_rate / 2 - 1) | |
| # 2 * f2 * sinc(2 * f2 * t) - 2 * f1 * sinc(2 * f1 * t) | |
| t = t.unsqueeze(0) # [1, kernel_size] | |
| h_high = 2.0 * f_high * self._sinc(2.0 * f_high * t) | |
| h_low = 2.0 * f_low * self._sinc(2.0 * f_low * t) | |
| filters = h_high - h_low # [out_channels, kernel_size] | |
| # Hamming window for spectral leakage control | |
| window = 0.54 - 0.46 * torch.cos(2.0 * math.pi * torch.arange(self.kernel_size, device=device) / (self.kernel_size - 1)) | |
| filters = filters * window.unsqueeze(0) | |
| # Normalize energy per filter | |
| filters = filters / (filters.abs().sum(dim=1, keepdim=True) + 1e-12) | |
| return filters.unsqueeze(1) # [out_channels, 1, kernel_size] | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Input: [B, 1, T] | |
| Output: [B, out_channels, T] | |
| """ | |
| filters = self.get_filters(x.device) | |
| return F.conv1d(x, filters, stride=1, padding=self.kernel_size // 2) | |
| class ResidualBlock(nn.Module): | |
| """Residual convolutional block with optional downsampling shortcut.""" | |
| def __init__(self, in_channels: int, out_channels: int, stride: int = 1): | |
| super().__init__() | |
| self.conv1 = nn.Conv1d( | |
| in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False | |
| ) | |
| self.bn1 = nn.BatchNorm1d(out_channels) | |
| self.relu = nn.ReLU(inplace=True) | |
| self.conv2 = nn.Conv1d( | |
| out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False | |
| ) | |
| self.bn2 = nn.BatchNorm1d(out_channels) | |
| if stride != 1 or in_channels != out_channels: | |
| self.shortcut = nn.Sequential( | |
| nn.Conv1d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), | |
| nn.BatchNorm1d(out_channels), | |
| ) | |
| else: | |
| self.shortcut = nn.Sequential() | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| residual = self.shortcut(x) | |
| out = self.relu(self.bn1(self.conv1(x))) | |
| out = self.bn2(self.conv2(out)) | |
| out = self.relu(out + residual) | |
| return out | |
| class VoiceGuardRawNet(nn.Module): | |
| """ | |
| VoiceGuard Deepfake Detection Network matching voiceguard_int8.pt. | |
| Processes raw 16kHz audio waveforms directly. | |
| """ | |
| CLASS_LABELS: List[str] = [ | |
| "synthetic_indic", | |
| "diffwave", | |
| "melgan", | |
| "gt", | |
| "wavenet", | |
| ] | |
| def __init__(self, num_classes: int = 5): | |
| super().__init__() | |
| self.sinc_conv = SincConv( | |
| out_channels=64, | |
| kernel_size=129, | |
| sample_rate=16000, | |
| min_low_hz=50, | |
| min_band_hz=50, | |
| ) | |
| self.res_block1 = ResidualBlock(64, 64, stride=1) | |
| self.res_block2 = ResidualBlock(64, 128, stride=2) | |
| self.res_block3 = ResidualBlock(128, 128, stride=1) | |
| self.pool = nn.MaxPool1d(kernel_size=10, stride=10, padding=0) | |
| self.gru = nn.GRU( | |
| input_size=128, | |
| hidden_size=256, | |
| num_layers=2, | |
| batch_first=True, | |
| bidirectional=True, | |
| ) | |
| self.fc = nn.Linear(512, num_classes) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Input: [B, 24000] or [B, 1, 24000] (1.5 seconds of 16kHz audio) | |
| Output: [B, 5] logits | |
| """ | |
| if x.ndim == 2: | |
| x = x.unsqueeze(1) | |
| out = self.sinc_conv(x) # [B, 64, 24000] | |
| out = self.res_block1(out) # [B, 64, 24000] | |
| out = self.res_block2(out) # [B, 128, 12000] | |
| out = self.res_block3(out) # [B, 128, 12000] | |
| out = self.pool(out) # [B, 128, 1200] | |
| out = out.transpose(1, 2) # [B, 1200, 128] | |
| out, _ = self.gru(out) # [B, 1200, 512] | |
| out = out[:, -1, :] # [B, 512] - last time step | |
| logits = self.fc(out) # [B, 5] | |
| return logits | |