indrajit4533's picture
Add reproducible validation evidence kit (harness + results + README)
c863b63 verified
Raw History Blame Contribute Delete
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