Phoneme wake word engine: student+teacher models, INT8 export, C engine, enrollment tooling
f6aec75 verified | """Log-mel feature frontend. | |
| 40 log-mel bands, 16 kHz, 25 ms window / 10 ms hop. Deliberately matches | |
| what is cheap to compute on the ESP32-S3 later (esp-dsp / TFLite-Micro | |
| audio frontend), so the on-device features can be made to line up with | |
| training. | |
| """ | |
| import torch | |
| import torchaudio | |
| SAMPLE_RATE = 16000 | |
| N_MELS = 40 | |
| WIN_LENGTH = 400 # 25 ms | |
| HOP_LENGTH = 160 # 10 ms | |
| N_FFT = 512 | |
| class LogMel(torch.nn.Module): | |
| def __init__(self): | |
| super().__init__() | |
| self.mel = torchaudio.transforms.MelSpectrogram( | |
| sample_rate=SAMPLE_RATE, | |
| n_fft=N_FFT, | |
| win_length=WIN_LENGTH, | |
| hop_length=HOP_LENGTH, | |
| n_mels=N_MELS, | |
| center=True, | |
| power=2.0, | |
| ) | |
| EMA_ALPHA = 0.02 # ~0.5 s time constant at 10 ms hop | |
| def forward(self, waveform): | |
| """waveform (B, samples) -> features (B, T, N_MELS). | |
| Normalization is a causal per-channel EMA mean subtraction so the | |
| exact same computation can run frame-by-frame on the device. | |
| """ | |
| mel = self.mel(waveform) # (B, n_mels, T) | |
| logmel = torch.log(mel + 1e-6) | |
| a = self.EMA_ALPHA | |
| ema = torchaudio.functional.lfilter( | |
| logmel, | |
| a_coeffs=torch.tensor([1.0, -(1.0 - a)], device=logmel.device), | |
| b_coeffs=torch.tensor([a, 0.0], device=logmel.device), | |
| clamp=False, | |
| ) | |
| logmel = logmel - ema | |
| return logmel.transpose(1, 2) # (B, T, n_mels) | |
| def num_frames(num_samples): | |
| """Frame count produced for a waveform length (center=True).""" | |
| return num_samples // HOP_LENGTH + 1 | |