""" 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