resemblyzer-torch-native / modeling_resemblyzer.py
Project Beatrice
Keep differentiable LSTM on cuDNN
a74a7e2
Raw
History Blame Contribute Delete
27.8 kB
"""Self-contained Resemblyzer inference with its WebRTC VAD preprocessing.
The Apache-2.0 Resemblyzer and BSD-licensed WebRTC implementations were
adapted for tensor-only batched execution; see LICENSE.
"""
from __future__ import annotations
import math
import operator
from collections.abc import Sequence
from dataclasses import dataclass
import torch
import torch.nn.functional as F
import torchaudio.functional as AF
from torch import nn
from transformers import PretrainedConfig, PreTrainedModel
from transformers.utils import ModelOutput
_TARGET_SAMPLING_RATE = 16_000
_PARTIAL_RATE = 1.3
_MIN_COVERAGE = 0.75
def _validate_sampling_rate(sampling_rate: int) -> int:
if isinstance(sampling_rate, bool):
raise TypeError("sampling_rate must be an integer")
try:
sampling_rate = operator.index(sampling_rate)
except TypeError as error:
raise TypeError("sampling_rate must be an integer") from error
if sampling_rate <= 0:
raise ValueError("sampling_rate must be positive")
return sampling_rate
def _as_waveform_sequence(input_values, input_lengths=None):
if input_lengths is not None:
if not isinstance(input_values, torch.Tensor) or input_values.ndim != 2:
raise ValueError(
"input_lengths requires dense input_values with shape [B, T]"
)
if not isinstance(input_lengths, torch.Tensor):
raise TypeError("input_lengths must be an integer tensor with shape [B]")
if (
input_lengths.dtype == torch.bool
or input_lengths.is_floating_point()
or input_lengths.is_complex()
):
raise TypeError("input_lengths must use an integer dtype")
if input_lengths.shape != (input_values.shape[0],):
raise ValueError("input_lengths must have shape [B]")
lengths = input_lengths.tolist()
if any(length <= 0 or length > input_values.shape[1] for length in lengths):
raise ValueError("input_lengths values are outside the dense input")
waveforms = tuple(
input_values[index, :length] for index, length in enumerate(lengths)
)
elif isinstance(input_values, torch.Tensor):
if input_values.ndim == 1:
waveforms = (input_values,)
elif input_values.ndim == 2:
waveforms = tuple(input_values.unbind(0))
else:
raise ValueError("input_values tensor must have shape [T] or [B, T]")
elif isinstance(input_values, Sequence):
waveforms = tuple(input_values)
else:
raise TypeError("input_values must be a tensor or a sequence of tensors")
if not waveforms:
raise ValueError("input_values must contain at least one waveform")
device = None
for waveform in waveforms:
if not isinstance(waveform, torch.Tensor) or waveform.ndim != 1:
raise ValueError("every waveform must be a one-dimensional torch.Tensor")
if waveform.numel() == 0:
raise ValueError("empty waveforms are not supported")
if not waveform.is_floating_point():
raise TypeError("waveforms must use a floating-point dtype")
if device is None:
device = waveform.device
elif waveform.device != device:
raise ValueError("all waveforms must be on the same device")
return waveforms
def _resample_waveform(waveform, original_rate, target_rate):
if original_rate == target_rate:
return waveform
return AF.resample(waveform, original_rate, target_rate)
def _segment_mean(values, counts):
lengths = torch.tensor(counts, device=values.device)
indices = torch.repeat_interleave(
torch.arange(len(counts), device=values.device),
lengths,
output_size=values.shape[0],
)
result = values.new_zeros((len(counts), *values.shape[1:]))
result.index_add_(0, indices, values)
return result.div_(lengths.reshape((-1,) + (1,) * (values.ndim - 1)))
def _slaney_filterbank(n_freqs: int, n_mels: int, max_frequency: float):
log_step = math.log(6.4) / 27.0
max_mel = 15.0 + math.log(8.0) / log_step
mel_points = torch.linspace(0.0, max_mel, n_mels + 2)
frequency_points = torch.where(
mel_points >= 15.0,
1000.0 * torch.exp(log_step * (mel_points - 15.0)),
(200.0 / 3.0) * mel_points,
)
frequencies = torch.linspace(0.0, max_frequency, n_freqs)
slopes = frequency_points.unsqueeze(0) - frequencies.unsqueeze(1)
differences = frequency_points[1:] - frequency_points[:-1]
filters = torch.minimum(
-slopes[:, :-2] / differences[:-1],
slopes[:, 2:] / differences[1:],
).clamp_min(0)
normalization = 2.0 / (frequency_points[2:] - frequency_points[:-2])
return (filters * normalization.unsqueeze(0)).transpose(0, 1)
def _trunc_div(numerator, denominator):
return torch.div(numerator, denominator, rounding_mode="trunc")
class VoiceActivityDetector(nn.Module):
"""Torch port of the 16 kHz, 30 ms, mode-3 WebRTC VAD path."""
def __init__(self):
super().__init__()
constants = {
"noise_weights": [[34, 62, 72, 66, 53, 25], [94, 66, 56, 62, 75, 103]],
"speech_weights": [[48, 82, 45, 87, 50, 47], [80, 46, 83, 41, 78, 81]],
"noise_means": [
[6738, 4892, 7065, 6715, 6771, 3369],
[7646, 3863, 7820, 7266, 5020, 4362],
],
"speech_means": [
[8306, 10085, 10078, 11823, 11843, 6309],
[9473, 9571, 10879, 7581, 8180, 7483],
],
"noise_stds": [
[378, 1064, 493, 582, 688, 593],
[474, 697, 475, 688, 421, 455],
],
"speech_stds": [
[555, 505, 567, 524, 585, 1231],
[509, 828, 492, 1540, 1079, 850],
],
}
for name, value in constants.items():
self.register_buffer(name, torch.tensor(value))
self.register_buffer("spectrum_weights", torch.tensor([6, 8, 10, 12, 14, 16]))
self.register_buffer(
"minimum_difference", torch.tensor([544, 544, 576, 576, 576, 576])
)
self.register_buffer(
"maximum_speech", torch.tensor([11392, 11392, 11520, 11520, 11520, 11520])
)
self.register_buffer(
"maximum_noise", torch.tensor([9216, 9088, 8960, 8832, 8704, 8576])
)
@staticmethod
def _all_pass(waveform: torch.Tensor, coefficient: float):
numerator = torch.tensor(
[coefficient / 2, 0.5], device=waveform.device, dtype=waveform.dtype
)
denominator = torch.tensor(
[1.0, coefficient], device=waveform.device, dtype=waveform.dtype
)
return torch.floor(AF.lfilter(waveform, denominator, numerator, clamp=False))
def _split(self, waveform):
upper = self._all_pass(waveform[..., ::2], 20972 / 32768)
lower = self._all_pass(waveform[..., 1::2], 5571 / 32768)
return upper - lower, upper + lower
def _features(self, waveform):
batch_size = waveform.shape[0]
quantized = torch.round(waveform * 32767).to(torch.int64)
quantized = (quantized + 32768).remainder(65536) - 32768
quantized = quantized.to(waveform.dtype)
upper = self._all_pass(quantized[..., ::2], 5243 / 8192)
lower = self._all_pass(quantized[..., 1::2], 1392 / 8192)
downsampled = upper + lower
high_4, low_4 = self._split(downsampled)
high_5, low_5 = self._split(high_4)
high_3, low_3 = self._split(low_4)
high_2, low_2 = self._split(low_3)
high_1, low_1 = self._split(low_2)
numerator = (
torch.tensor(
[6631, -13262, 6631], device=waveform.device, dtype=waveform.dtype
)
/ 16384
)
denominator = (
torch.tensor(
[16384, -7756, 5620], device=waveform.device, dtype=waveform.dtype
)
/ 16384
)
high_0 = AF.lfilter(low_1, denominator, numerator, clamp=False)
bands = (high_0, high_1, high_2, high_3, low_5, high_5)
lengths = (15, 15, 30, 60, 60, 60)
offsets = (368, 368, 272, 176, 176, 176)
features, power = [], []
for band, length, offset in zip(bands, lengths, offsets):
frames = band.reshape(batch_size, -1, length)
integer_frames = torch.floor(frames).to(torch.int64)
maximum = integer_frames.abs().amax(-1)
maximum_square = maximum.square()
maximum_bit = torch.floor(
torch.log2(maximum_square.clamp_min(1).double())
).to(torch.int64)
scaling = (maximum_bit + (length.bit_length() - 30)).clamp_min(0)
energy = (integer_frames.square() >> scaling[:, :, None]).sum(-1)
energy_bit = torch.floor(torch.log2(energy.clamp_min(1).double())).to(
torch.int64
)
normalization = energy_bit - 14
normalized = torch.where(
normalization < 0,
energy << (-normalization).clamp_min(0),
energy >> normalization.clamp_min(0),
)
log2_energy = 14336 + ((normalized & 0x3FFF) >> 4)
total_shifts = scaling + normalization
value = ((24660 * log2_energy) >> 19) + ((total_shifts * 24660) >> 9)
value = value.clamp_min(0) + offset
features.append(
torch.where(energy > 0, value, torch.full_like(value, offset))
)
contribution = torch.where(
total_shifts >= 0,
11,
normalized >> (-total_shifts).clamp_min(0),
)
power.append(torch.where(energy > 0, contribution, 0))
return torch.stack(features, -1).to(torch.int64), torch.stack(power).sum(0) > 10
@staticmethod
def _probability(features, means, stds):
inverse_std = (131072 + (stds >> 1)) // stds
inverse_variance = ((inverse_std >> 2).square()) >> 2
difference = (features[:, None] << 3) - means
delta = (inverse_variance * difference) >> 10
exponent = (delta * difference) >> 9
logarithm = (5909 * exponent) >> 12
negative = (-logarithm).to(torch.int16).to(torch.int64)
exponential = torch.bitwise_or(
torch.full_like(negative, 1024), torch.bitwise_and(negative, 1023)
)
complemented = torch.bitwise_xor(negative, torch.full_like(negative, 65535))
shift = (complemented.to(torch.int16).to(torch.int64) >> 10) + 1
exponential = torch.where(exponent < 22005, exponential >> shift, 0)
return inverse_std * exponential, delta
def _classify(self, features, active):
batch_size = features.shape[0]
noise_means = self.noise_means.expand(batch_size, -1, -1).clone()
speech_means = self.speech_means.expand(batch_size, -1, -1).clone()
noise_stds = self.noise_stds.expand(batch_size, -1, -1).clone()
speech_stds = self.speech_stds.expand(batch_size, -1, -1).clone()
low_values = torch.full(
(batch_size, 6, 16), 10000, dtype=torch.int64, device=features.device
)
ages = torch.zeros_like(low_values)
mean_values = torch.full(
(batch_size, 6), 1600, dtype=torch.int64, device=features.device
)
overhang = torch.zeros((batch_size,), dtype=torch.int64, device=features.device)
speech_count = torch.zeros_like(overhang)
model_frame_count = torch.zeros_like(overhang)
minimum_means = torch.tensor(
[640, 768], dtype=torch.int64, device=features.device
)[:, None]
maximum_means = (
torch.tensor(
[[72, 71, 70, 69, 68, 67], [73, 72, 71, 70, 69, 68]],
dtype=torch.int64,
device=features.device,
)
<< 7
)
speech_limits = (
torch.cat(
(
torch.tensor([12800], dtype=torch.int64, device=features.device),
self.maximum_speech[:-1],
)
)[:, None].transpose(0, 1)
+ 640
)
all_active = bool(active.all())
decisions = []
for frame_index in range(features.shape[1]):
frame = features[:, frame_index]
has_energy = active[:, frame_index]
old_noise_means = noise_means
old_speech_means = speech_means
old_noise_stds = noise_stds
old_speech_stds = speech_stds
if not all_active:
old_low_values = low_values
old_ages = ages
old_mean_values = mean_values
noise_probability, noise_delta = self._probability(
frame, noise_means, noise_stds
)
speech_probability, speech_delta = self._probability(
frame, speech_means, speech_stds
)
noise_probability = noise_probability * self.noise_weights
speech_probability = speech_probability * self.speech_weights
noise_total = noise_probability.sum(1)
speech_total = speech_probability.sum(1)
noise_log = torch.where(
noise_total > 0,
torch.floor(torch.log2(noise_total.double())).to(torch.int64),
-1,
)
speech_log = torch.where(
speech_total > 0,
torch.floor(torch.log2(speech_total.double())).to(torch.int64),
-1,
)
ratio = speech_log - noise_log
decision = has_energy & (
(ratio * 4 > 94).any(1)
| ((ratio * self.spectrum_weights).sum(1) >= 1100)
)
noise_total_q15 = (noise_total >> 12).to(torch.int16).to(torch.int64)
speech_total_q15 = (speech_total >> 12).to(torch.int16).to(torch.int64)
noise_first = torch.where(
noise_total_q15 > 0,
_trunc_div(
torch.bitwise_and(noise_probability[:, 0], 0xFFFFF000) << 2,
noise_total_q15.clamp_min(1),
),
16384,
)
speech_first = torch.where(
speech_total_q15 > 0,
_trunc_div(
torch.bitwise_and(speech_probability[:, 0], 0xFFFFF000) << 2,
speech_total_q15.clamp_min(1),
),
0,
)
noise_condition = torch.stack((noise_first, 16384 - noise_first), dim=1)
speech_condition = torch.stack(
(
speech_first,
torch.where(speech_total_q15 > 0, 16384 - speech_first, 0),
),
dim=1,
)
valid = ages < 100
candidates = torch.cat(
(torch.where(valid, low_values, 10000), frame[:, :, None]), 2
)
candidate_ages = torch.cat(
(
torch.where(valid, ages + 1, 101),
torch.ones(
(batch_size, 6, 1),
dtype=torch.int64,
device=features.device,
),
),
2,
)
order = torch.argsort(candidates, dim=2, stable=True)[:, :, :16]
low_values = torch.gather(candidates, 2, order)
ages = torch.gather(candidate_ages, 2, order)
if all_active:
if frame_index > 2:
current_median = low_values[:, :, 2]
elif frame_index > 0:
current_median = low_values[:, :, 0]
else:
current_median = torch.full_like(mean_values, 1600)
if frame_index > 0:
alpha = torch.where(current_median < mean_values, 6553, 32439)
else:
alpha = torch.zeros_like(mean_values)
else:
current_median = torch.where(
model_frame_count[:, None] > 2,
low_values[:, :, 2],
torch.where(
model_frame_count[:, None] > 0, low_values[:, :, 0], 1600
),
)
alpha = torch.where(
model_frame_count[:, None] > 0,
torch.where(current_median < mean_values, 6553, 32439),
0,
)
mean_values = (
(alpha + 1) * mean_values + (32767 - alpha) * current_median + 16384
) >> 15
noise_global = (noise_means * self.noise_weights).sum(1)
noise_update = (noise_condition * noise_delta) >> 11
adapted_noise = old_noise_means + ((noise_update * 655) >> 22)
adapted_noise = torch.where(
decision[:, None, None], old_noise_means, adapted_noise
)
correction = (mean_values << 4) - (noise_global >> 6)
adapted_noise = adapted_noise + ((correction[:, None] * 154) >> 9)
noise_means = torch.maximum(
torch.minimum(adapted_noise, maximum_means), minimum_means
)
speech_update = (speech_condition * speech_delta) >> 11
speech_step = (speech_update * 6554) >> 21
adapted_speech = old_speech_means + ((speech_step + 1) >> 1)
adapted_speech = torch.maximum(
torch.minimum(adapted_speech, speech_limits), minimum_means
)
speech_means = torch.where(
decision[:, None, None], adapted_speech, old_speech_means
)
speech_error = frame[:, None] - ((old_speech_means + 4) >> 3)
speech_variance = (speech_delta * speech_error) >> 3
speech_variance = (speech_condition >> 2) * (speech_variance - 4096)
speech_variance = speech_variance >> 4
speech_std_step = (
_trunc_div(speech_variance.abs(), old_speech_stds * 10)
* speech_variance.sign()
)
adapted_speech_std = old_speech_stds + ((speech_std_step + 128) >> 8)
speech_stds = torch.where(
decision[:, None, None],
adapted_speech_std.clamp_min(384),
old_speech_stds,
)
noise_error = frame[:, None] - (old_noise_means >> 3)
noise_variance = (noise_delta * noise_error) >> 3
noise_variance = ((noise_condition + 2) >> 2) * (noise_variance - 4096)
noise_variance = noise_variance >> 14
noise_std_step = (
_trunc_div(noise_variance.abs(), old_noise_stds) * noise_variance.sign()
)
adapted_noise_std = old_noise_stds + ((noise_std_step + 32) >> 6)
noise_stds = torch.where(
decision[:, None, None],
old_noise_stds,
adapted_noise_std.clamp_min(384),
)
noise_global = (noise_means * self.noise_weights).sum(1)
speech_global = (speech_means * self.speech_weights).sum(1)
separation = (speech_global >> 9) - (noise_global >> 9)
missing = (self.minimum_difference - separation).clamp_min(0)
speech_means = speech_means + (((13 * missing) >> 2)[:, None])
noise_means = noise_means - (((3 * missing) >> 2)[:, None])
speech_global = (speech_means * self.speech_weights).sum(1)
noise_global = (noise_means * self.noise_weights).sum(1)
speech_means = (
speech_means
- ((speech_global >> 7) - self.maximum_speech).clamp_min(0)[:, None]
)
noise_means = (
noise_means
- ((noise_global >> 7) - self.maximum_noise).clamp_min(0)[:, None]
)
if not all_active:
update = has_energy[:, None, None]
noise_means = torch.where(update, noise_means, old_noise_means)
speech_means = torch.where(update, speech_means, old_speech_means)
noise_stds = torch.where(update, noise_stds, old_noise_stds)
speech_stds = torch.where(update, speech_stds, old_speech_stds)
low_values = torch.where(update, low_values, old_low_values)
ages = torch.where(update, ages, old_ages)
mean_values = torch.where(
has_energy[:, None], mean_values, old_mean_values
)
model_frame_count = model_frame_count + has_energy.to(torch.int64)
emitted = decision | (overhang > 0)
next_count = speech_count + 1
speech_count = torch.where(
decision, next_count.clamp_max(6), torch.zeros_like(speech_count)
)
speech_overhang = torch.where(
next_count > 6,
torch.full_like(overhang, 3),
torch.full_like(overhang, 2),
)
overhang = torch.where(
decision, speech_overhang, (overhang - 1).clamp_min(0)
)
decisions.append(emitted)
return torch.stack(decisions, dim=1)
def forward(self, waveform, input_lengths=None):
if waveform.ndim != 2:
raise ValueError("VAD input must have shape [B, T]")
if input_lengths is None:
input_lengths = (waveform.shape[1],) * waveform.shape[0]
usable_lengths = tuple(length // 480 * 480 for length in input_lengths)
maximum = max(usable_lengths)
if maximum == 0:
return tuple(item[:0] for item in waveform)
waveform = waveform[:, :maximum]
features, active = self._features(waveform.float())
frame_lengths = torch.tensor(usable_lengths, device=waveform.device) // 480
valid_frames = (
torch.arange(features.shape[1], device=waveform.device)[None]
< frame_lengths[:, None]
)
flags = self._classify(features, active & valid_frames) & valid_frames
flags = flags.to(waveform.dtype)
averaged = F.avg_pool1d(F.pad(flags[:, None], (3, 4)), 8, stride=1)[:, 0]
smoothed = torch.round(averaged).to(torch.bool)
dilated = F.max_pool1d(
smoothed.to(waveform.dtype)[:, None], 7, stride=1, padding=3
)[:, 0].to(torch.bool)
masks = dilated.repeat_interleave(480, dim=1)
return tuple(
item[:length][mask[:length]]
for item, mask, length in zip(waveform, masks, usable_lengths)
)
class ResemblyzerConfig(PretrainedConfig):
model_type = "resemblyzer"
@dataclass
class ResemblyzerOutput(ModelOutput):
embeddings: torch.Tensor | None = None
class MelFrontend(nn.Module):
def __init__(self):
super().__init__()
self.register_buffer("window", torch.hann_window(400))
self.register_buffer("mel_filters", _slaney_filterbank(201, 40, 8_000.0))
def forward(self, waveform):
spectrum = torch.stft(
waveform,
400,
160,
400,
self.window,
center=True,
pad_mode="constant",
return_complex=True,
)
return torch.matmul(self.mel_filters, spectrum.abs().square()).transpose(0, 1)
class ResemblyzerModel(PreTrainedModel):
config_class = ResemblyzerConfig
main_input_name = "input_values"
def __init__(self, config):
super().__init__(config)
self.lstm = nn.LSTM(40, 256, 3, batch_first=True, dropout=0.0)
self.linear = nn.Linear(256, 256)
self.mel_frontend = MelFrontend()
self.voice_activity_detector = VoiceActivityDetector()
self.post_init()
def train(self, mode: bool = True):
super().train(mode)
# Dropout is zero, so this does not change the output. Keeping only the
# LSTM in training mode makes cuDNN retain the state required by backward.
self.lstm.train()
return self
@staticmethod
def _normalize_volume(waveform):
rms = waveform.square().mean().sqrt()
target = waveform.new_tensor(10 ** (-30 / 20))
gain = target / rms.clamp_min(torch.finfo(waveform.dtype).tiny)
normalized = waveform * torch.maximum(gain, waveform.new_tensor(1.0))
return torch.where(rms > 0, normalized, waveform)
def _partial_mels(self, waveform):
samples_per_frame = 160
n_samples = waveform.shape[-1]
n_frames = math.ceil((n_samples + 1) / samples_per_frame)
frame_step = round((_TARGET_SAMPLING_RATE / _PARTIAL_RATE) / samples_per_frame)
steps = max(1, n_frames - 160 + frame_step + 1)
starts = list(range(0, steps, frame_step))
coverage = (n_samples - starts[-1] * samples_per_frame) / (
160 * samples_per_frame
)
if coverage < _MIN_COVERAGE and len(starts) > 1:
starts.pop()
maximum = (starts[-1] + 160) * samples_per_frame
waveform = F.pad(waveform, (0, max(0, maximum - n_samples)))
mel = self.mel_frontend(waveform)
return mel.unfold(0, 160, frame_step)[: len(starts)].transpose(1, 2)
def _trim_silences(self, waveforms):
usable_lengths = tuple(waveform.shape[0] // 480 * 480 for waveform in waveforms)
maximum = max(usable_lengths)
batch = torch.stack(
[
F.pad(waveform[:length], (0, maximum - length))
for waveform, length in zip(waveforms, usable_lengths)
]
)
return self.voice_activity_detector(batch, usable_lengths)
def _encode(self, mels):
_, (hidden, _) = self.lstm(mels)
raw = F.relu(self.linear(hidden[-1]))
return F.normalize(raw, dim=1)
def forward(
self,
input_values: torch.Tensor | Sequence[torch.Tensor],
sampling_rate: int,
input_lengths: torch.Tensor | None = None,
*,
return_dict: bool | None = None,
) -> ResemblyzerOutput | tuple[torch.Tensor]:
sampling_rate = _validate_sampling_rate(sampling_rate)
return_dict = self.config.return_dict if return_dict is None else return_dict
waveforms = _as_waveform_sequence(input_values, input_lengths)
dtype = self.linear.weight.dtype
waveforms = tuple(
self._normalize_volume(
_resample_waveform(
waveform.float(),
sampling_rate,
_TARGET_SAMPLING_RATE,
)
)
for waveform in waveforms
)
waveforms = self._trim_silences(waveforms)
if any(waveform.numel() == 0 for waveform in waveforms):
raise ValueError("voice activity detection removed the entire waveform")
waveforms = tuple(waveform.to(dtype=dtype) for waveform in waveforms)
partials, counts = [], []
for waveform in waveforms:
item = self._partial_mels(waveform)
partials.append(item)
counts.append(item.shape[0])
partials = torch.cat(partials)
encoded = self._encode(partials)
result = F.normalize(
_segment_mean(encoded, counts),
dim=1,
)
if not return_dict:
return (result,)
return ResemblyzerOutput(embeddings=result)