""" Student model for LoRaVoiceLink: a compact spectrogram U-Net that maps a Codec2-degraded magnitude spectrogram to an enhanced one. Kept deliberately small (edge-deployment target: real-time inference on a Jetson) — unlike the teacher models used only offline during data preparation, this is the model that actually ships. """ import torch import torch.nn as nn def conv_block(in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1), nn.BatchNorm2d(out_ch), nn.LeakyReLU(0.2, inplace=True), ) class SpectrogramUNet(nn.Module): """ Input: (B, 1, F, T) degraded magnitude spectrogram — F and T must be multiples of 8 (three pooling stages); see dataset.py's pad_to_multiple(). Output: (B, 1, F, T) enhanced magnitude spectrogram, produced by predicting a [0, 1] mask and multiplying it against the input (standard, more stable to train than predicting raw magnitude directly). """ def __init__(self, base_channels: int = 32): super().__init__() c = base_channels self.enc1 = conv_block(1, c) self.enc2 = conv_block(c, c * 2) self.enc3 = conv_block(c * 2, c * 4) self.bottleneck = conv_block(c * 4, c * 8) self.pool = nn.MaxPool2d(2) self.up3 = nn.ConvTranspose2d(c * 8, c * 4, kernel_size=2, stride=2) self.dec3 = conv_block(c * 8, c * 4) self.up2 = nn.ConvTranspose2d(c * 4, c * 2, kernel_size=2, stride=2) self.dec2 = conv_block(c * 4, c * 2) self.up1 = nn.ConvTranspose2d(c * 2, c, kernel_size=2, stride=2) self.dec1 = conv_block(c * 2, c) self.out_conv = nn.Conv2d(c, 1, kernel_size=1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) b = self.bottleneck(self.pool(e3)) d3 = self.up3(b) d3 = self.dec3(torch.cat([d3, e3], dim=1)) d2 = self.up2(d3) d2 = self.dec2(torch.cat([d2, e2], dim=1)) d1 = self.up1(d2) d1 = self.dec1(torch.cat([d1, e1], dim=1)) mask = torch.sigmoid(self.out_conv(d1)) return mask * x def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) if __name__ == "__main__": model = SpectrogramUNet() dummy = torch.randn(2, 1, 256, 128) out = model(dummy) print("output shape:", out.shape) print("parameters:", f"{count_parameters(model):,}")