ehabnegm/Egyption_ASR / processing_cohere_asr.py
ehabnegm's picture
download
raw
20.6 kB
import logging
import math
import random
from pathlib import Path
import librosa
import numpy as np
import torch
import torch.nn.functional as F
from safetensors.torch import load_file as safetensors_load_file
from torch import nn
from transformers import AutoFeatureExtractor, AutoTokenizer, BatchFeature
from transformers.feature_extraction_sequence_utils import SequenceFeatureExtractor
from transformers.processing_utils import ProcessorMixin
from .configuration_cohere_asr import _dynamo_disable
logger = logging.getLogger(__name__)
DITHER_CONSTANT = 1e-5
class FilterbankFeatures(nn.Module):
"""Filterbank features extraction module.
Follows NeMo's FilterbankFeatures implementation.
"""
window: torch.Tensor
fb: torch.Tensor
def __init__(
self,
sample_rate=16000,
n_window_size=320,
n_window_stride=160,
window="hann",
normalize="per_feature",
n_fft=None,
preemph=0.97,
nfilt=64,
lowfreq=0,
highfreq=None,
log=True,
log_zero_guard_type="add",
log_zero_guard_value=2**-24,
dither=DITHER_CONSTANT,
pad_to=16,
max_duration=30,
frame_splicing=1,
exact_pad=False,
pad_value=0,
mag_power=2.0,
use_grads=False,
rng=None,
nb_augmentation_prob=0.0,
nb_max_freq=4000,
mel_norm="slaney",
stft_exact_pad=False,
stft_conv=False,
device="cpu",
):
super().__init__()
if stft_conv or stft_exact_pad:
logger.warning(
"torch_stft compatibility flags are deprecated; " "forcing behavior to default torch.stft path."
)
if exact_pad and n_window_stride % 2 == 1:
raise NotImplementedError(f"{self} received exact_pad=True with odd hop length ({n_window_stride}).")
if (
n_window_size is None
or n_window_stride is None
or not isinstance(n_window_size, int)
or not isinstance(n_window_stride, int)
or n_window_size <= 0
or n_window_stride <= 0
):
raise ValueError("n_window_size and n_window_stride must be positive ints.")
self.log_zero_guard_value = log_zero_guard_value
self.sample_rate = sample_rate
self.win_length = n_window_size
self.hop_length = n_window_stride
self.n_fft = n_fft or 2 ** math.ceil(math.log2(self.win_length))
self.stft_pad_amount = (self.n_fft - self.hop_length) // 2 if exact_pad else None
self.exact_pad = exact_pad
self.max_duration = max_duration
torch_windows = {
"hann": torch.hann_window,
"hamming": torch.hamming_window,
"blackman": torch.blackman_window,
"bartlett": torch.bartlett_window,
"none": None,
}
window_fn = torch_windows.get(window)
window_tensor = window_fn(self.win_length, periodic=False) if window_fn else None
self.register_buffer("window", window_tensor)
self.normalize = normalize
self.log = log
self.dither = dither
self.frame_splicing = frame_splicing
self.nfilt = nfilt
self.preemph = preemph
self.pad_to = pad_to
highfreq = highfreq or sample_rate / 2
self.pad_min_duration = 0.0
self.pad_direction = "both"
self.pad_value = pad_value
self.mag_power = mag_power
self.nb_augmentation_prob = nb_augmentation_prob
filterbanks = torch.tensor(
librosa.filters.mel(
sr=sample_rate, n_fft=self.n_fft, n_mels=nfilt, fmin=lowfreq, fmax=highfreq, norm=mel_norm
),
dtype=torch.float,
).unsqueeze(0)
self.register_buffer("fb", filterbanks)
max_length = self.get_seq_len(torch.tensor(max_duration * sample_rate, dtype=torch.float))
max_pad = pad_to - (max_length % pad_to) if pad_to > 0 else 0
self.max_length = max_length + max_pad
if log_zero_guard_type not in ["add", "clamp"]:
raise ValueError("log_zero_guard_type must be 'add' or 'clamp'.")
self.log_zero_guard_type = log_zero_guard_type
self.use_grads = use_grads
if not use_grads:
self.forward = torch.no_grad()(self.forward)
self._rng = random.Random() if rng is None else rng
if self.nb_augmentation_prob > 0.0:
if nb_max_freq >= sample_rate / 2:
self.nb_augmentation_prob = 0.0
else:
self._nb_max_fft_bin = int((nb_max_freq / sample_rate) * self.n_fft)
if self.window is None:
raise RuntimeError("Expected a window tensor for STFT feature extraction.")
if self.fb is None:
raise RuntimeError("Expected mel filterbank weights for feature extraction.")
self.window = self.window.to(dtype=torch.bfloat16)
self.fb = self.fb.to(dtype=torch.bfloat16)
self.generator = torch.Generator(device=device)
self.generator.manual_seed(0)
@_dynamo_disable
def _apply_dither(self, x, seq_len_time):
"""Apply deterministic per-sample dither outside torch.compile.
Each sample is seeded by its valid waveform length so that dither noise
is batch-composition invariant (a sample's features depend only on its
own content, not on what else is in the batch).
"""
if self.dither <= 0:
return x
for i in range(x.shape[0]):
valid_samples = min(int(seq_len_time[i].item()), x.shape[1])
if valid_samples <= 0:
continue
self.generator.manual_seed(valid_samples)
noise = torch.randn(
(valid_samples,),
dtype=x.dtype,
device=x.device,
generator=self.generator,
)
x[i, :valid_samples] += self.dither * noise
return x
@_dynamo_disable
def stft(self, x):
with torch.amp.autocast(x.device.type, enabled=False):
return torch.view_as_real(
torch.stft(
x,
n_fft=self.n_fft,
hop_length=self.hop_length,
win_length=self.win_length,
center=not self.exact_pad,
window=self.window.to(dtype=torch.float, device=x.device),
return_complex=True,
pad_mode="constant",
)
)
def log_zero_guard_value_fn(self, x):
if isinstance(self.log_zero_guard_value, str):
if self.log_zero_guard_value == "tiny":
return torch.finfo(x.dtype).tiny
if self.log_zero_guard_value == "eps":
return torch.finfo(x.dtype).eps
raise ValueError("log_zero_guard_value must be number, 'tiny', or 'eps' when str.")
return self.log_zero_guard_value
def get_seq_len(self, seq_len):
pad_amount = self.stft_pad_amount * 2 if self.stft_pad_amount is not None else self.n_fft // 2 * 2
seq_len = torch.floor_divide((seq_len + pad_amount - self.n_fft), self.hop_length)
return seq_len.to(dtype=torch.long)
def splice_frames(self, x, frame_splicing):
seq = [x]
for n in range(1, frame_splicing):
seq.append(torch.cat([x[:, :, :n], x[:, :, n:]], dim=2))
return torch.cat(seq, dim=1)
def normalize_batch(self, x, seq_len, normalize_type):
if normalize_type != "per_feature":
raise ValueError("Only per_feature normalization is supported.")
batch_size = x.shape[0]
max_time = x.shape[2]
time_steps = torch.arange(max_time, device=x.device).unsqueeze(0).expand(batch_size, max_time)
valid_mask = time_steps < seq_len.unsqueeze(1)
x_mean_num = torch.where(valid_mask.unsqueeze(1), x, 0.0).sum(axis=2)
x_mean_den = valid_mask.sum(axis=1)
x_mean = x_mean_num / x_mean_den.unsqueeze(1)
x_std = torch.sqrt(
torch.sum(
torch.where(valid_mask.unsqueeze(1), x - x_mean.unsqueeze(2), 0.0) ** 2,
axis=2,
)
/ (x_mean_den.unsqueeze(1) - 1.0)
)
x_std = x_std.masked_fill(x_std.isnan(), 0.0)
x_std += DITHER_CONSTANT
return (x - x_mean.unsqueeze(2)) / x_std.unsqueeze(2), x_mean, x_std
def forward(self, x, seq_len, linear_spec=False):
if x.shape[1] < self.sample_rate * self.pad_min_duration:
pad_amount = int(self.sample_rate * self.pad_min_duration) - x.shape[1]
if self.pad_direction == "right":
x = F.pad(x, (0, pad_amount), value=self.pad_value)
elif self.pad_direction == "left":
x = F.pad(x, (pad_amount, 0), value=self.pad_value)
elif self.pad_direction == "both":
left_pad = pad_amount // 2
right_pad = pad_amount - left_pad
x = F.pad(x, (left_pad, right_pad), value=self.pad_value)
else:
raise ValueError(f"Invalid pad_direction: {self.pad_direction}")
seq_len = torch.tensor([x.shape[1]], dtype=torch.float, device=x.device)
seq_len_time = seq_len
seq_len_unfixed = self.get_seq_len(seq_len)
seq_len = torch.where(seq_len == 0, torch.zeros_like(seq_len_unfixed), seq_len_unfixed)
if self.stft_pad_amount is not None:
x = torch.nn.functional.pad(
x.unsqueeze(1), (self.stft_pad_amount, self.stft_pad_amount), "constant"
).squeeze(1)
x = self._apply_dither(x, seq_len_time)
if self.preemph is not None:
timemask = torch.arange(x.shape[1], device=x.device).unsqueeze(0) < seq_len_time.unsqueeze(1)
x = torch.cat((x[:, 0].unsqueeze(1), x[:, 1:] - self.preemph * x[:, :-1]), dim=1)
x = x.masked_fill(~timemask, 0.0)
x = self.stft(x)
guard = 0 if not self.use_grads else DITHER_CONSTANT
x = torch.sqrt(x.pow(2).sum(-1) + guard)
if self.mag_power != 1.0:
x = x.pow(self.mag_power)
if linear_spec:
return x, seq_len
with torch.amp.autocast(x.device.type, enabled=False):
x = torch.matmul(self.fb.to(x.dtype), x)
if self.log:
if self.log_zero_guard_type == "add":
x = torch.log(x + self.log_zero_guard_value_fn(x))
elif self.log_zero_guard_type == "clamp":
x = torch.log(torch.clamp(x, min=self.log_zero_guard_value_fn(x)))
else:
raise ValueError("log_zero_guard_type was not understood")
if self.frame_splicing > 1:
x = self.splice_frames(x, self.frame_splicing)
if self.normalize:
x, _, _ = self.normalize_batch(x, seq_len, normalize_type=self.normalize)
max_len = x.size(-1)
mask = torch.arange(max_len, device=x.device)
mask = mask.repeat(x.size(0), 1) >= seq_len.unsqueeze(1)
x = x.masked_fill(mask.unsqueeze(1).to(device=x.device), self.pad_value)
del mask
if self.pad_to == "max":
x = nn.functional.pad(x, (0, self.max_length - x.size(-1)), value=self.pad_value)
elif self.pad_to > 0:
pad_amt = x.size(-1) % self.pad_to
if pad_amt != 0:
x = nn.functional.pad(x, (0, self.pad_to - pad_amt), value=self.pad_value)
return x, seq_len
class CohereAsrFeatureExtractor(SequenceFeatureExtractor):
"""HF-compatible feature extractor wrapping FilterbankFeatures."""
model_input_names = ["input_features"]
def __init__(
self,
feature_size=64,
sampling_rate=16000,
padding_value=0.0,
max_duration=30,
n_window_size=320,
n_window_stride=160,
window="hann",
normalize="per_feature",
n_fft=None,
preemph=0.97,
lowfreq=0,
highfreq=None,
log=True,
log_zero_guard_type="add",
log_zero_guard_value=2**-24,
dither=DITHER_CONSTANT,
pad_to=16,
frame_splicing=1,
exact_pad=False,
mag_power=2.0,
nb_augmentation_prob=0.0,
nb_max_freq=4000,
mel_norm="slaney",
stft_exact_pad=False,
stft_conv=False,
device="cpu",
**kwargs,
):
super().__init__(
feature_size=feature_size,
sampling_rate=sampling_rate,
padding_value=padding_value,
**kwargs,
)
self.max_duration = max_duration
self.hop_length = n_window_stride
self._device = str(device)
self._fb_config = dict(
sample_rate=sampling_rate,
n_window_size=n_window_size,
n_window_stride=n_window_stride,
window=window,
normalize=normalize,
n_fft=n_fft,
preemph=preemph,
nfilt=feature_size,
lowfreq=lowfreq,
highfreq=highfreq,
log=log,
log_zero_guard_type=log_zero_guard_type,
log_zero_guard_value=log_zero_guard_value,
dither=dither,
pad_to=pad_to,
max_duration=max_duration,
frame_splicing=frame_splicing,
exact_pad=exact_pad,
pad_value=padding_value,
mag_power=mag_power,
nb_augmentation_prob=nb_augmentation_prob,
nb_max_freq=nb_max_freq,
mel_norm=mel_norm,
stft_exact_pad=stft_exact_pad,
stft_conv=stft_conv,
device=device,
)
self._filterbank = None
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
fe = super().from_pretrained(pretrained_model_name_or_path, **kwargs)
model_dir = Path(pretrained_model_name_or_path)
if model_dir.is_dir():
_maybe_load_preprocessor_buffers_from_checkpoint(feature_extractor=fe, model_dir=model_dir)
return fe
@property
def filterbank(self):
if self._filterbank is None:
fb = FilterbankFeatures(**self._fb_config)
fb.eval()
self._filterbank = fb.to(self._device)
return self._filterbank
def get_seq_len(self, seq_len):
return self.filterbank.get_seq_len(seq_len)
def __call__(
self,
raw_speech,
sampling_rate=None,
return_tensors=None,
**kwargs,
):
"""Extract mel features from raw waveform input."""
if sampling_rate is not None and int(sampling_rate) != int(self.sampling_rate):
raise ValueError(f"Expected sampling_rate={self.sampling_rate}, got {sampling_rate}")
if isinstance(raw_speech, np.ndarray):
if raw_speech.ndim == 1:
raw_speech = [raw_speech]
else:
raw_speech = [s for s in raw_speech]
elif isinstance(raw_speech, torch.Tensor):
if raw_speech.ndim == 1:
raw_speech = [raw_speech.detach().cpu().numpy()]
else:
raw_speech = [s.detach().cpu().numpy() for s in raw_speech]
elif not isinstance(raw_speech, (list, tuple)):
raise TypeError("raw_speech must be an array/tensor or list of arrays.")
normalized = []
for sample in raw_speech:
arr = np.asarray(sample, dtype=np.float32)
if arr.ndim != 1:
raise ValueError("Each audio sample must be 1D waveform.")
normalized.append(arr)
seq_len = torch.tensor([s.shape[0] for s in normalized], dtype=torch.long)
max_len = max(s.shape[0] for s in normalized)
padded = np.zeros((len(normalized), max_len), dtype=np.float32)
for i, s in enumerate(normalized):
padded[i, : s.shape[0]] = s
audio_tensor = torch.from_numpy(padded).to(self._device)
seq_len = seq_len.to(self._device)
with torch.no_grad():
input_features, length = self.filterbank(audio_tensor, seq_len)
result = BatchFeature({"input_features": input_features.cpu(), "length": length.cpu()})
if return_tensors is not None:
result = result.convert_to_tensors(return_tensors)
return result
class CohereAsrProcessor(ProcessorMixin):
"""HF-compatible processor for Cohere ASR.
``ProcessorMixin._get_arguments_from_pretrained`` resolves sub-component
class names by looking them up inside the ``transformers`` package, which
fails for custom remote-code classes. We override ``from_pretrained`` to
use ``AutoFeatureExtractor`` / ``AutoTokenizer`` instead -- those honour
``auto_map`` and ``trust_remote_code``.
"""
attributes = ["feature_extractor", "tokenizer"]
feature_extractor_class = "CohereAsrFeatureExtractor"
tokenizer_class = "CohereAsrTokenizer"
def __init__(self, feature_extractor=None, tokenizer=None, **kwargs):
if feature_extractor is None:
raise ValueError(
"CohereAsrProcessor requires a CohereAsrFeatureExtractor instance. " "Got feature_extractor=None."
)
if tokenizer is None:
raise ValueError("CohereAsrProcessor requires a CohereAsrTokenizer instance. " "Got tokenizer=None.")
# Bypass super().__init__ which calls get_possibly_dynamic_module to
# validate sub-component types. That lookup searches the transformers
# package namespace and fails for remote-code classes. We set the
# attributes directly instead -- the type checks above are sufficient.
self.feature_extractor = feature_extractor
self.tokenizer = tokenizer
self.chat_template = kwargs.get("chat_template", None)
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, **kwargs):
trust_remote_code = kwargs.pop("trust_remote_code", True)
feature_extractor = AutoFeatureExtractor.from_pretrained(
pretrained_model_name_or_path,
trust_remote_code=trust_remote_code,
**kwargs,
)
tokenizer = AutoTokenizer.from_pretrained(
pretrained_model_name_or_path,
trust_remote_code=trust_remote_code,
**kwargs,
)
return cls(feature_extractor=feature_extractor, tokenizer=tokenizer)
def __call__(
self,
audio=None,
text=None,
sampling_rate=None,
return_tensors=None,
**kwargs,
):
"""Run audio feature extraction and optional text tokenization."""
if audio is None:
raise ValueError("audio is required for CohereAsrProcessor.")
result = self.feature_extractor(audio, sampling_rate=sampling_rate, return_tensors=return_tensors)
if text is not None:
add_special_tokens = kwargs.pop("add_special_tokens", False)
text_inputs = self.tokenizer(
text,
return_tensors=return_tensors,
add_special_tokens=add_special_tokens,
**kwargs,
)
result["input_ids"] = text_inputs["input_ids"]
if "attention_mask" in text_inputs:
result["attention_mask"] = text_inputs["attention_mask"]
return result
def batch_decode(self, *args, **kwargs):
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
return self.tokenizer.decode(*args, **kwargs)
def _maybe_load_preprocessor_buffers_from_checkpoint(
feature_extractor: CohereAsrFeatureExtractor, model_dir: Path
) -> None:
"""
Load exported frontend buffers if they exist in checkpoint weights.
"""
safetensor_path = model_dir / "model.safetensors"
if not safetensor_path.exists():
return
try:
state = safetensors_load_file(safetensor_path.as_posix())
except Exception:
return
fb = state.get("preprocessor.featurizer.fb")
window = state.get("preprocessor.featurizer.window")
if fb is None or window is None:
return
fb_module = feature_extractor.filterbank
target_device = fb_module.fb.device
target_dtype = fb_module.fb.dtype
fb_module.fb = fb.to(device=target_device, dtype=target_dtype)
fb_module.window = window.to(device=target_device, dtype=target_dtype)

Xet Storage Details

Size:
20.6 kB
·
Xet hash:
a6e46cf473652210e78d76f270c381f14b1f3a84c3e2037f8652714efa84d3de

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.