# SPDX-License-Identifier: CC-BY-NC-SA-4.0 # Copyright 2026 TeraSpace. See LICENSE.md for encoder attribution. """TeraAnaly: one frozen speech encoder and independently callable task heads. Local model execution needs only Python's standard library and PyTorch. Transformers enables AutoModel(..., trust_remote_code=True); huggingface_hub is used only when downloading a component from a Hub repository. """ import json import math import mmap import re import struct import sys import unicodedata from pathlib import Path from typing import Any, Sequence import torch import torch.nn.functional as F from torch import Tensor, nn SAMPLE_RATE = 16000 HOP_SAMPLES = 320 FRAME_SECONDS = 0.02 WINDOW_SAMPLES = 1024 DEFAULT_MIN_PAUSE_SECONDS = 0.10 SHARED_LAYERS = (4, 8, 16, 24) STRESS_VOWEL_ORDER = "аеёиоуыэюя" RUSSIAN_VOWELS = frozenset(STRESS_VOWEL_ORDER) RUSSIAN_WORD = re.compile(r"[А-Яа-яЁё][А-Яа-яЁё\u0301]*") def _resample(audio: Tensor, source_rate: int, target_rate: int = 16000) -> Tensor: if source_rate == target_rate: return audio # Windowed sinc low-pass reconstruction, in bounded chunks. length = math.ceil(audio.numel() * target_rate / source_rate) radius = 32 taps = torch.arange(-radius + 1, radius + 1, device=audio.device) cutoff = min(1.0, target_rate / source_rate) output = [] for start in range(0, length, 16384): positions = torch.arange(start, min(start + 16384, length), device=audio.device) positions = positions.to(torch.float64) * source_rate / target_rate indices = positions.floor().long()[:, None] + taps[None, :] offsets = positions[:, None] - indices window = (0.5 + 0.5 * torch.cos(math.pi * offsets / radius)).clamp_min(0) weights = torch.sinc(offsets * cutoff) * cutoff * window weights *= (offsets.abs() <= radius).to(weights.dtype) weights /= weights.sum(dim=1, keepdim=True) values = audio[indices.clamp(0, audio.numel() - 1)] output.append((values * weights.to(values.dtype)).sum(dim=1)) return torch.cat(output) def read_wav(path: str | Path) -> Tensor: """Read PCM or IEEE float WAV, average channels, and return 16 kHz audio.""" data = memoryview(Path(path).read_bytes()) if len(data) < 12 or data[:4].tobytes() != b"RIFF" or data[8:12].tobytes() != b"WAVE": raise ValueError("Expected a RIFF WAV file") fmt = raw = None offset = 12 while offset + 8 <= len(data): kind = data[offset : offset + 4].tobytes() size = struct.unpack_from("= 40: encoding = struct.unpack_from(" dict[str, Tensor]: """Map dense F32/F16/BF16 tensors without a safetensors dependency.""" if sys.byteorder != "little": raise ValueError("Safetensors reader requires a little-endian host") with path.open("rb") as stream: raw_size = stream.read(8) if len(raw_size) != 8: raise ValueError("Truncated safetensors header") size = struct.unpack(" Tensor: """Return windows centered halfway into each alignment frame.""" left = WINDOW_SAMPLES // 2 - HOP_SAMPLES // 2 right = WINDOW_SAMPLES // 2 mode = "reflect" if waveform.numel() > right else "replicate" padded = F.pad(waveform[None, None], (left, right), mode=mode)[0, 0] windows = padded.unfold(0, WINDOW_SAMPLES, HOP_SAMPLES) if count > windows.shape[0]: raise ValueError( f"Alignment requires {count} frames, but the audio provides only {windows.shape[0]}" ) return windows[:count] def _yin_pitch( windows: Tensor, *, min_hz: float, max_hz: float, threshold: float, silence_rms: float, ) -> tuple[Tensor, Tensor]: """Return Hz (zero for unvoiced) and voiced mask for one batch of windows.""" length = windows.shape[1] min_lag = max(2, math.floor(SAMPLE_RATE / max_hz)) max_lag = min(length // 2, math.ceil(SAMPLE_RATE / min_hz)) if max_lag <= min_lag + 1: raise ValueError("Pitch range does not fit the analysis window") centered = windows - windows.mean(dim=1, keepdim=True) rms = windows.square().mean(dim=1).sqrt() # YIN is scale invariant; normalize before numerical stabilization. scale = centered.square().mean(dim=1, keepdim=True).sqrt() centered = centered / scale.clamp_min(torch.finfo(centered.dtype).tiny) spectrum = torch.fft.rfft(centered, n=length * 2, dim=1) correlation = torch.fft.irfft(spectrum.abs().square(), n=length * 2, dim=1) lags = torch.arange(max_lag + 1, device=windows.device) powers = F.pad(centered.square().cumsum(dim=1), (1, 0)) difference = ( powers[:, length - lags] + powers[:, length, None] - powers[:, lags] - 2 * correlation[:, : max_lag + 1] ).clamp_min_(0) cumulative = difference[:, 1:].cumsum(dim=1) cmndf = torch.ones_like(difference) cmndf[:, 1:] = difference[:, 1:] * lags[1:] / cumulative.clamp_min(1e-12) candidates = cmndf[:, min_lag : max_lag + 1] valleys = (candidates < threshold) & (candidates <= cmndf[:, min_lag - 1 : max_lag]) valleys &= candidates <= F.pad(candidates[:, 1:], (0, 1), value=float("inf")) has_valley = valleys.any(dim=1) first_valley = valleys.to(torch.int32).argmax(dim=1) best = candidates.argmin(dim=1) lag = torch.where(has_valley, first_valley, best) + min_lag selected = cmndf.gather(1, lag[:, None])[:, 0] voiced = (selected < 0.25) & (rms >= silence_rms) before = cmndf.gather(1, (lag - 1)[:, None])[:, 0] after = cmndf.gather(1, (lag + 1).clamp(max=max_lag)[:, None])[:, 0] denominator = before - 2 * selected + after safe_denominator = torch.where( denominator.abs() > 1e-8, denominator, torch.ones_like(denominator) ) delta = torch.where( (denominator.abs() > 1e-8) & (lag < max_lag), 0.5 * (before - after) / safe_denominator, torch.zeros_like(selected), ).clamp(-1, 1) hz = SAMPLE_RATE / (lag.to(torch.float32) + delta) return torch.where(voiced, hz, torch.zeros_like(hz)), voiced def _pause_statistics(waveform: Tensor, *, min_pause_seconds: float = DEFAULT_MIN_PAUSE_SECONDS, pause_threshold_db: float = -35.0) -> dict[str, Any]: """Find quiet internal intervals on a 20 ms RMS grid; retain edge silence.""" if not math.isfinite(min_pause_seconds) or min_pause_seconds < FRAME_SECONDS: raise ValueError("Minimum pause duration must be finite and at least 20 ms") if not math.isfinite(pause_threshold_db) or pause_threshold_db >= 0: raise ValueError("Pause threshold must be a finite negative dB offset") count = math.ceil(waveform.numel() / HOP_SAMPLES) padded = F.pad(waveform, (0, count * HOP_SAMPLES - waveform.numel())) rms = padded.reshape(count, HOP_SAMPLES).square().mean(dim=1).sqrt() # Avoid understating RMS in the last partial frame. last_samples = waveform.numel() - (count - 1) * HOP_SAMPLES rms[-1] *= math.sqrt(HOP_SAMPLES / last_samples) cutoff = float(rms.max()) * 10 ** (pause_threshold_db / 20) quiet = (rms <= cutoff).tolist() pauses = [] leading = trailing = None position = 0 while position < count: if not quiet[position]: position += 1 continue start = position while position < count and quiet[position]: position += 1 begin = start * FRAME_SECONDS end = min(position * FRAME_SECONDS, waveform.numel() / SAMPLE_RATE) span = {"start": round(begin, 6), "end": round(end, 6), "duration_seconds": round(end - begin, 6)} if start == 0: leading = span if position == count: trailing = span if start > 0 and position < count and end - begin + 1e-9 >= min_pause_seconds: pauses.append(span) return {"pauses": pauses, "leading_silence": leading, "trailing_silence": trailing, "average_pause_duration_seconds": math.fsum(pause["duration_seconds"] for pause in pauses) / len(pauses) if pauses else 0.0, "pause_count": len(pauses), "pause_method": "20 ms RMS below peak-relative threshold, no absolute floor; quiet internal intervals only; leading/trailing silence excluded", "pause_threshold_db": pause_threshold_db, "pause_rms_cutoff": cutoff, "min_pause_seconds": min_pause_seconds} def w2vbert_fbank(waveform: Tensor, *, spectral_statistics: bool = False, raw_features: bool = False): """Kaldi-style 80-bin log mel, unbiased CMVN, stacked 10 ms frames. Matches SeamlessM4TFeatureExtractor for a single 16 kHz recording, including its complex64 FFT rounding and padding value of 1. No NumPy/torchaudio. """ if waveform.ndim != 1 or waveform.numel() < 560 or not torch.isfinite(waveform).all(): raise ValueError("w2v-BERT needs finite mono 16 kHz audio of at least 35 ms") audio = (waveform.detach().cpu().float() * 32768).double() windows = audio.unfold(0, 400, 160).clone() windows -= windows.mean(1, keepdim=True) windows[:, 1:] -= 0.97 * windows[:, :-1].clone() windows[:, 0] *= 0.03 window = torch.hann_window(400, periodic=False, dtype=torch.float64).pow(0.85) spectrum = torch.fft.rfft(windows * window, n=512).to(torch.complex64).to(torch.complex128).abs().square() frequencies = 1127 * torch.log1p(torch.arange(257, dtype=torch.float64) * (16000 / 512) / 700) centers = torch.linspace(1127 * math.log1p(20 / 700), 1127 * math.log1p(8000 / 700), 82, dtype=torch.float64) lower = (frequencies[:, None] - centers[:-2]) / (centers[1:-1] - centers[:-2]) upper = (centers[2:] - frequencies[:, None]) / (centers[2:] - centers[1:-1]) filters = torch.minimum(lower, upper).clamp_min(0) features = (spectrum @ filters).clamp_min(1.192092955078125e-7).log() if spectral_statistics: mean = features.mean(0) spectral = torch.cat((mean-mean.mean(),features.std(0,correction=0))).float() if raw_features: raw = (features-features.mean()).float() features = (features - features.mean(0)) / (features.var(0, correction=1) + 1e-7).sqrt() valid = features.shape[0] // 2 if features.shape[0] % 2: features = F.pad(features, (0, 0, 0, 1), value=1) if raw_features:raw = F.pad(raw,(0,0,0,1)) result = (features.reshape(1, -1, 160).float(), valid) if spectral_statistics:result=(*result,spectral) if raw_features:result=(*result,raw.reshape(-1,160)[:valid]) return result class _BertFFN(nn.Module): def __init__(self, config: dict[str, Any], activation: str = "silu") -> None: super().__init__() self.intermediate_dense = nn.Linear(config["hidden_size"], config["intermediate_size"]) self.output_dense = nn.Linear(config["intermediate_size"], config["hidden_size"]) self.activation = activation def forward(self, x: Tensor) -> Tensor: x = self.intermediate_dense(x) return self.output_dense(F.relu(x) if self.activation == "relu" else F.silu(x)) class _BertAttention(nn.Module): def __init__(self, config: dict[str, Any], relative: bool = True) -> None: super().__init__() hidden = config["hidden_size"] self.heads = config["num_attention_heads"] self.width = hidden // self.heads for name in ("linear_q", "linear_k", "linear_v", "linear_out"): setattr(self, name, nn.Linear(hidden, hidden)) self.left = config["left_max_position_embeddings"] self.right = config["right_max_position_embeddings"] self.distance_embedding = nn.Embedding(self.left + self.right + 1, self.width) if relative else None def forward(self, x: Tensor, valid: int | Tensor) -> Tensor: batch, length, hidden = x.shape def project(name): return getattr(self, name)(x).reshape(batch, length, self.heads, self.width).transpose(1, 2) q, k, v = (project(name) for name in ("linear_q", "linear_k", "linear_v")) bias = None if self.distance_embedding is not None: positions = torch.arange(length, device=x.device) distances = (positions[None] - positions[:, None]).clamp(-self.left, self.right) + self.left # Compute Q dot each of the 73 relative vectors once, then gather. relative = q @ self.distance_embedding.weight.T bias = relative.gather(-1, distances[None, None].expand(batch, self.heads, -1, -1)) / math.sqrt(self.width) if isinstance(valid, Tensor) or valid < length: if bias is None: bias = x.new_zeros(1, 1, 1, length) limits = valid[:, None, None, None] if isinstance(valid, Tensor) else valid bias = bias.masked_fill(torch.arange(length, device=x.device)[None, None, None] >= limits, float("-inf")) attended = F.scaled_dot_product_attention(q, k, v, attn_mask=bias, dropout_p=0.0) return self.linear_out(attended.transpose(1, 2).reshape(batch, length, hidden)) class _BertConv(nn.Module): def __init__(self, config: dict[str, Any]) -> None: super().__init__() hidden, kernel = config["hidden_size"], config["conv_depthwise_kernel_size"] self.layer_norm = nn.LayerNorm(hidden, eps=config["layer_norm_eps"]) self.pointwise_conv1 = nn.Conv1d(hidden, 2 * hidden, 1, bias=False) self.depthwise_conv = nn.Conv1d(hidden, hidden, kernel, groups=hidden, bias=False) self.depthwise_layer_norm = nn.LayerNorm(hidden, eps=config["layer_norm_eps"]) self.pointwise_conv2 = nn.Conv1d(hidden, hidden, 1, bias=False) def forward(self, x: Tensor, valid: int | Tensor) -> Tensor: x = self.layer_norm(x) if isinstance(valid, Tensor) or valid < x.shape[1]: limits = valid[:, None, None] if isinstance(valid, Tensor) else valid x = x.masked_fill(torch.arange(x.shape[1], device=x.device)[None, :, None] >= limits, 0) x = F.glu(self.pointwise_conv1(x.transpose(1, 2)), dim=1) x = self.depthwise_conv(F.pad(x, (self.depthwise_conv.kernel_size[0] - 1, 0))) x = F.silu(self.depthwise_layer_norm(x.transpose(1, 2))) return self.pointwise_conv2(x.transpose(1, 2)).transpose(1, 2) class _BertLayer(nn.Module): def __init__(self, config: dict[str, Any]) -> None: super().__init__() for name in ("ffn1_layer_norm", "self_attn_layer_norm", "ffn2_layer_norm", "final_layer_norm"): setattr(self, name, nn.LayerNorm(config["hidden_size"], eps=config["layer_norm_eps"])) self.ffn1 = _BertFFN(config) self.ffn2 = _BertFFN(config) self.self_attn = _BertAttention(config) self.conv_module = _BertConv(config) def forward(self, x: Tensor, valid: int | Tensor) -> Tensor: x = x + 0.5 * self.ffn1(self.ffn1_layer_norm(x)) x = x + self.self_attn(self.self_attn_layer_norm(x), valid) x = x + self.conv_module(x, valid) return self.final_layer_norm(x + 0.5 * self.ffn2(self.ffn2_layer_norm(x))) def ctc_forced_spans(log_probs: Tensor, tokens: Sequence[int], blank: int) -> list[tuple[int, int]]: """Viterbi on CTC's blank/character graph, including repeated characters.""" if log_probs.ndim != 2 or not tokens or not torch.isfinite(log_probs).all(): raise ValueError("Expected finite CTC frame log probabilities and a nonempty target") if any(type(token) is not int or not 0 <= token < log_probs.shape[1] or token == blank for token in tokens): raise ValueError("Invalid CTC target token") needed = len(tokens) + sum(a == b for a, b in zip(tokens, tokens[1:])) if log_probs.shape[0] < needed: raise ValueError("Audio is too short for this transcript's CTC path") emissions = torch.full((len(tokens) * 2 + 1,), blank, dtype=torch.long) emissions[1::2] = torch.tensor(tokens) probabilities = log_probs.detach().float().cpu()[:, emissions] skip = torch.zeros(len(emissions), dtype=torch.bool) skip[2:] = (emissions[2:] != blank) & (emissions[2:] != emissions[:-2]) scores = torch.full((len(emissions),), float("-inf")) scores[:2] = probabilities[0, :2] predecessors = torch.empty(probabilities.shape, dtype=torch.int8) predecessors[0] = 0 for frame in range(1, len(probabilities)): previous1 = F.pad(scores[:-1], (1, 0), value=float("-inf")) previous2 = F.pad(scores[:-2], (2, 0), value=float("-inf")).masked_fill(~skip, float("-inf")) best, transition = torch.stack((scores, previous1, previous2)).max(0) scores = best + probabilities[frame] predecessors[frame] = transition.to(torch.int8) state = len(emissions) - 2 + scores[-2:].argmax().item() if not math.isfinite(scores[state].item()): raise ValueError("No valid forced CTC alignment") path = [state] for frame in range(len(probabilities) - 1, 0, -1): state -= predecessors[frame, state].item() path.append(state) path.reverse() positions = [[] for _ in tokens] for frame, state in enumerate(path): if state % 2: positions[state // 2].append(frame) if any(not frames for frames in positions): raise ValueError("CTC path omitted a target character") return [(frames[0], frames[-1] + 1) for frames in positions] def wav2vec_vowel_features(alignment: dict[str, Any], hidden: Tensor, *, hidden_size: int = 1024) -> tuple[Tensor, list[dict[str, Any]]]: """Pool frozen audio embeddings; do not use energy or word spelling as inputs. Each row is vowel mean/std, mean minus the word's vowel mean, vowel identity, and three duration measurements, on the native 20 ms encoder grid. """ text = alignment["normalized_text"] characters = alignment["characters"] if hidden.ndim != 2 or hidden.shape[1] != hidden_size or len(characters) != len(text): raise ValueError("Invalid alignment or hidden features") if not torch.isfinite(hidden).all(): raise ValueError("Non-finite wav2vec features") rows, words = [], [] hidden = hidden.float() for match in RUSSIAN_WORD.finditer(text): vowels = [i for i in range(match.start(), match.end()) if text[i].lower() in RUSSIAN_VOWELS] if not vowels: continue means, deviations, durations = [], [], [] for index in vowels: item = characters[index] if item.get("char") != text[index]: raise ValueError("Character alignment does not match text") start = round(item["start"] / FRAME_SECONDS) end = round(item["end"] / FRAME_SECONDS) if not 0 <= start < end <= hidden.shape[0]: raise ValueError(f"Hidden features do not cover vowel {index}") span = hidden[start:end] means.append(span.mean(0)) deviations.append(span.std(0, correction=0)) durations.append(end - start) mean = torch.stack(means) word_mean = mean.mean(0) word_frames = (characters[match.end() - 1]["end"] - characters[match.start()]["start"]) / FRAME_SECONDS row_start = len(rows) for index, avg, std, duration in zip(vowels, means, deviations, durations): identity = hidden.new_zeros(len(STRESS_VOWEL_ORDER)) identity[STRESS_VOWEL_ORDER.index(text[index].lower())] = 1 timing = hidden.new_tensor([ math.log(duration), duration / max(word_frames, 1), duration / (sum(durations) / len(durations)), ]) rows.append(torch.cat((avg, std, avg - word_mean, identity, timing))) words.append({"word": match.group(), "start_index": match.start(), "end_index": match.end(), "vowels": vowels, "row_start": row_start, "row_end": len(rows)}) dimension = hidden_size * 3 + len(STRESS_VOWEL_ORDER) + 3 return (torch.stack(rows) if rows else hidden.new_empty((0, dimension))), words def shared_recording_features(taps: dict[int, Tensor], layers: Sequence[int] = SHARED_LAYERS, statistics: dict[str, Any] | None = None) -> Tensor: values = [value for layer in layers for value in (taps[layer].float().mean(0), taps[layer].float().std(0, correction=0))] if statistics is not None: # Voice and prosody cues are computed once alongside the encoder. pitch = [statistics.get(name) for name in ("average_pitch_hz", "pitch_p10_hz", "pitch_median_hz", "pitch_p90_hz")] pitch = [math.log2(value) if value is not None and value > 0 else 0.0 for value in pitch] duration = statistics["duration_seconds"] pause_fraction = sum(item["duration_seconds"] for item in statistics.get("pauses", [])) / max(duration, 1e-6) extra = pitch + [(statistics.get("pitch_range_semitones") or 0.0) / 12, statistics.get("voiced_fraction", 0.0), math.log(max(duration, 0.01)), pause_fraction] values.append(values[0].new_tensor(extra)) return torch.cat(values) def recording_statistics(waveform, frame_count, device): windows = _frame_audio(waveform.to(device), frame_count) peak_rms = windows.square().mean(1).sqrt().max().item() gate = peak_rms * 0.05 if peak_rms > 0 else float('inf') pitch, voiced = [], [] for chunk in windows.split(512): p, v = _yin_pitch(chunk, min_hz=50, max_hz=800, threshold=0.15, silence_rms=gate) pitch.append(p.cpu()) voiced.append(v.cpu()) pitch, voiced = torch.cat(pitch), torch.cat(voiced) values = pitch[voiced] quantiles = torch.quantile(values, torch.tensor([0.1, 0.5, 0.9])) if values.numel() else None return { 'average_pitch_hz': float(values.mean()) if values.numel() else None, 'pitch_p10_hz': float(quantiles[0]) if quantiles is not None else None, 'pitch_median_hz': float(quantiles[1]) if quantiles is not None else None, 'pitch_p90_hz': float(quantiles[2]) if quantiles is not None else None, 'pitch_range_semitones': 12 * math.log2(float(quantiles[2] / quantiles[0])) if quantiles is not None else None, 'voiced_fraction': float(voiced.float().mean()), 'duration_seconds': waveform.numel() / SAMPLE_RATE, 'pauses': _pause_statistics(waveform)['pauses'], } def align(text, logits, vocab, blank, frame_count): text = ' '.join(unicodedata.normalize('NFKC', text).split()) indices, tokens = [], [] for i, char in enumerate(text): token = '|' if char == ' ' else char.lower() if token == 'ё' and token not in vocab: token = 'е' if token in vocab and vocab[token] != blank: indices.append(i) tokens.append(vocab[token]) elif char.isalpha() or char.isdigit(): raise ValueError(f'Unsupported character {char!r}; spell out numbers and use Russian text') if not tokens: raise ValueError('Text has no supported spoken characters') spans = ctc_forced_spans(logits.log_softmax(-1), tokens, blank) starts = [start for start, _ in spans] ends = [min(end, frame_count) for _, end in spans] boundaries = [starts[0]] + [(ends[i] + starts[i+1]) // 2 for i in range(len(spans)-1)] + [ends[-1]] mapped = {index: (boundaries[i], boundaries[i+1]) for i, index in enumerate(indices)} characters, position = [], boundaries[0] for i, char in enumerate(text): start, end = mapped.get(i, (position, position)) characters.append({'char': char, 'start': start * FRAME_SECONDS, 'end': end * FRAME_SECONDS}) position = end return {'normalized_text': text, 'characters': characters} # Transformers is optional for local PyTorch use, and supplies AutoConfig on HF. try: from transformers import PretrainedConfig as _ConfigBase except ImportError: class _ConfigBase: def __init__(self, **kwargs): self.__dict__.update(kwargs) def to_dict(self): return dict(self.__dict__) class TeraAnalyConfig(_ConfigBase): model_type = 'tera_analy' def __init__(self, encoder_config=None, heads=None, ctc=None, vocab=None, global_layers=None, use_prosody_features=True, labels=None, source=None, **kwargs): super().__init__(**kwargs) self.encoder_config = encoder_config or {} self.heads = heads or {} self.ctc = ctc or {} self.vocab = vocab or {} self.global_layers = global_layers or [4, 8, 16, 24] self.use_prosody_features = use_prosody_features self.labels = labels or {} self.source = source or {} def _resolve_file(source, name, **options): directory = Path(source).expanduser() if directory.is_dir(): path = directory / name if not path.is_file(): raise FileNotFoundError(path) return path try: from huggingface_hub import hf_hub_download except ImportError as error: raise ImportError('Install huggingface_hub to download models, or pass a local model folder') from error allowed = {key: options[key] for key in ('revision', 'cache_dir', 'token', 'local_files_only', 'force_download') if key in options} return Path(hf_hub_download(str(source), name, **allowed)) def _load_config(source, config, options): options = dict(options) if config is None: path = _resolve_file(source, 'config.json', **options) config = TeraAnalyConfig(**json.loads(path.read_text(encoding='utf-8'))) if not Path(source).expanduser().is_dir() and re.fullmatch('[0-9a-f]{40}', path.parent.name): options['revision'] = path.parent.name elif isinstance(config, dict): config = TeraAnalyConfig(**config) commit = getattr(config, '_commit_hash', None) if commit: options['revision'] = commit dtype = options.get('dtype', options.get('torch_dtype')) if dtype not in (None, 'auto', 'float32', torch.float32): raise ValueError('This release uses float32 inference; omit dtype or use torch.float32') return config, options def _load_state(module, source, filename, options): module.load_state_dict(_read_safetensors(_resolve_file(source, filename, **options)), strict=True, assign=True) class _Component(nn.Module): filename = None @classmethod def from_pretrained(cls, source, *, config=None, device='cpu', **options): """Load only this component's safetensors (stress also loads CTC).""" config, options = _load_config(source, config, options) with torch.device('meta'): module = cls(config) module._load_weights(source, options) module.to(device).eval() if isinstance(module, TeraEncoder): module.requires_grad_(False) return module def _load_weights(self, source, options): _load_state(self, source, self.filename, options) @property def device(self): return next(self.parameters()).device def _is_batch(value): return isinstance(value, (list, tuple)) def _audio_items(waveform, sample_rate): batched = _is_batch(waveform) or isinstance(waveform, Tensor) and waveform.ndim == 2 items = list(waveform) if batched else [waveform] rates = list(sample_rate) if _is_batch(sample_rate) else [sample_rate]*len(items) if len(rates) != len(items) or any(not isinstance(rate, int) or rate <= 0 for rate in rates): raise ValueError('Supply one positive integer sample rate per waveform, or one rate for all') if any(not isinstance(item, Tensor) or item.ndim != 1 for item in items): raise ValueError('Expected a mono waveform, a list of mono waveforms, or a [batch, samples] tensor') return items, rates, batched def _matching_texts(states, text): if _is_batch(states): if not _is_batch(text) or len(text) != len(states): raise ValueError('Supply one matching transcript per batch item') texts = list(text) else: texts = [text] if any(not isinstance(item, str) or not item.strip() for item in texts): raise ValueError('Text must contain the matching audio transcript') return texts class TeraEncoder(_Component): """Frozen 24-layer Conformer; returns states for any number of task heads.""" filename = 'encoder.safetensors' def __init__(self, config): super().__init__() self.config = config c = config.encoder_config self.feature_projection = nn.Module() self.feature_projection.layer_norm = nn.LayerNorm(160, eps=c['layer_norm_eps']) self.feature_projection.projection = nn.Linear(160, 1024) self.layers = nn.ModuleList(_BertLayer(c) for _ in range(c['num_hidden_layers'])) self.required_layers = sorted(set(config.global_layers) | {config.heads['stress']['layer'], config.ctc['layer']} | {h['layer'] for name in ('age', 'gender') for h in config.heads[name]['frames']}) self.requires_grad_(False) @torch.no_grad() def forward(self, waveform, sample_rate=16000): """Accept one mono waveform, or a batch of different-length waveforms. Single input returns a states dictionary; list/2D input returns a list of dictionaries in input order. A sample rate list is also accepted. The neural encoder runs once for the padded, masked batch. Frontend and prosody statistics are computed separately for each recording. """ items, rates, batched = _audio_items(waveform, sample_rate) if not items: return [] prepared = [] for item, rate in zip(items, rates): audio = _resample(item.detach().cpu().float(), rate) features, valid, spectral, raw = w2vbert_fbank(audio, spectral_statistics=True, raw_features=True) if features.shape[1] > 2000: raise ValueError('Use audio clips of at most 40 seconds') prepared.append((audio, features, valid, spectral, raw)) length = max(item[1].shape[1] for item in prepared) features = torch.cat([F.pad(item[1], (0, 0, 0, length-item[1].shape[1])) for item in prepared]) counts = torch.tensor([item[2] for item in prepared], device=self.device) valid = prepared[0][2] if len(items) == 1 else counts x = self.feature_projection.projection(self.feature_projection.layer_norm(features.to(self.device))) x = x.masked_fill(torch.arange(length, device=x.device)[None, :, None] >= counts[:, None, None], 0) taps = [{} for _ in items] for number, layer in enumerate(self.layers, 1): x = layer(x, valid) if number in self.required_layers: for index, item in enumerate(prepared): taps[index][number] = x[index, :item[2]] states = [] for hidden, (audio, _, count, spectral, raw) in zip(taps, prepared): statistics = recording_statistics(audio, count, self.device) if self.config.use_prosody_features else None recording = shared_recording_features(hidden, self.config.global_layers, statistics) states.append({'hidden_states': hidden, 'recording_features': recording, 'spectral_features': spectral.to(self.device), 'raw_features': raw.to(self.device), 'frame_count': count, 'frame_seconds': FRAME_SECONDS}) return states if batched else states[0] class _RecordingMLP(nn.Module): def __init__(self, dimension, widths, outputs): super().__init__() self.register_buffer('mean', torch.zeros(dimension)) self.register_buffer('scale', torch.ones(dimension)) layers, previous = [], dimension for width in widths: layers.extend((nn.Linear(previous, width), nn.ReLU(), nn.Dropout(.25))) previous = width layers.append(nn.Linear(previous, outputs)) self.network = nn.Sequential(*layers) def forward(self, features): features = features.to(self.mean.device) return self.network(((features-self.mean)/self.scale).clamp(-10, 10)) def _recording_input(states, dimension, spectral=False): if _is_batch(states): return (torch.stack([_recording_input(item, dimension, spectral) for item in states]) if states else torch.empty(0, dimension)) values = states['recording_features'] if spectral or values.numel() != dimension: values = torch.cat((values, states['spectral_features'].to(values.device))) if values.numel() != dimension: raise ValueError('Encoder states do not match this head configuration') return values class EmotionHead(_Component): filename = 'emotion.safetensors' def __init__(self, config): super().__init__() spec = config.heads['emotion'] self.labels = config.labels['emotion'] self.recording = _RecordingMLP(spec['dimension'], spec['widths'], len(self.labels)) def logits(self, states): return self.recording(_recording_input(states, self.recording.mean.numel())) @torch.inference_mode() def forward(self, states): indices = self.logits(states).argmax(-1) return [self.labels[index] for index in indices.tolist()] if _is_batch(states) else self.labels[indices.item()] class _FrameAttributeHead(nn.Module): def __init__(self, spec, outputs): super().__init__() width, projection, dimension = spec['width'], spec['projection_dim'], spec['global_dim'] self.layer, self.frame_stride = spec['layer'], spec.get('frame_stride', 2) self.register_buffer('ssl_mean', torch.zeros(1024)) self.register_buffer('ssl_projection', torch.zeros(1024, projection)) self.register_buffer('mean', torch.zeros(dimension)) self.register_buffer('scale', torch.ones(dimension)) self.ssl_norm, self.mel_norm = nn.LayerNorm(projection), nn.LayerNorm(160) self.frame = nn.Linear(projection+160, width) self.temporal = nn.Conv1d(width, width, 5, padding=2, groups=width) self.attention = nn.Sequential(nn.Linear(width, 64), nn.Tanh(), nn.Linear(64, 1)) self.body = nn.Sequential(nn.Linear(dimension+width*2, 256), nn.ReLU(), nn.Dropout(.25)) self.output = nn.Linear(256, outputs) def forward(self, states): batched = _is_batch(states) items = states if batched else [states] if not items: return self.mean.new_empty((0, self.output.out_features)) sequences = [] for item in items: ssl = item['hidden_states'][self.layer].to(self.mean.device).float() raw = item['raw_features'].to(self.mean.device).float() sequences.append(torch.cat(((ssl-self.ssl_mean) @ self.ssl_projection, raw), -1)[::self.frame_stride]) frames = nn.utils.rnn.pad_sequence(sequences, batch_first=True) lengths = torch.tensor([item.shape[0] for item in sequences], device=self.mean.device) mask = torch.arange(frames.shape[1], device=self.mean.device)[None] >= lengths[:, None] dimension = self.ssl_projection.shape[1] x = torch.cat((self.ssl_norm(frames[:, :, :dimension]), self.mel_norm(frames[:, :, dimension:])), -1) x = F.gelu(self.frame(x)).masked_fill(mask[:, :, None], 0) x = F.gelu(x+self.temporal(x.transpose(1, 2)).transpose(1, 2)) weights = self.attention(x).squeeze(-1).masked_fill(mask, float('-inf')).softmax(-1) mean = (x*weights[:, :, None]).sum(1) std = ((x-mean[:, None]).square()*weights[:, :, None]).sum(1).clamp_min(1e-5).sqrt() recording = _recording_input(items, self.mean.numel(), spectral=True).to(self.mean.device) normalized = ((recording-self.mean)/self.scale).clamp(-10, 10) output = self.output(self.body(torch.cat((normalized, mean, std), -1))) return output if batched else output[0] class _AttributeHead(_Component): task = None def __init__(self, config): super().__init__() spec = config.heads[self.task] self.labels = config.labels.get(self.task) outputs = 1 if self.task == 'age' else len(self.labels) self.recording = _RecordingMLP(spec['dimension'], spec['widths'], outputs) self.frames = nn.ModuleList(_FrameAttributeHead(item, outputs) for item in spec['frames']) self.blend_weight = spec['blend_weight'] def logits(self, states): value = self.recording(_recording_input(states, self.recording.mean.numel())) for frame_head in self.frames: frame_value = frame_head(states) weight = self.blend_weight if weight == 1: value = frame_value elif weight != 0: value = ((1-weight)*value+weight*frame_value if self.task == 'age' else ((1-weight)*value.softmax(-1)+weight*frame_value.softmax(-1)).clamp_min(1e-9).log()) return value class AgeHead(_AttributeHead): filename, task = 'age.safetensors', 'age' @torch.inference_mode() def forward(self, states): values = self.logits(states) return ([int(round(value*100)) for value in values.flatten().tolist()] if _is_batch(states) else int(round(values.item()*100))) class GenderHead(_AttributeHead): filename, task = 'gender.safetensors', 'gender' @torch.inference_mode() def forward(self, states): indices = self.logits(states).argmax(-1) return [self.labels[index] for index in indices.tolist()] if _is_batch(states) else self.labels[indices.item()] class CTCHead(_Component): """Native 20 ms character emissions; used internally by StressHead.""" filename = 'ctc.safetensors' def __init__(self, config): super().__init__() self.layer, self.blank = config.ctc['layer'], config.ctc['blank'] self.vocab = config.vocab self.norm = nn.LayerNorm(1024) self.temporal = nn.Conv1d(1024, 1024, 5, padding=2, groups=1024) self.network = nn.Sequential(nn.Linear(1024, config.ctc['hidden_size']), nn.GELU(), nn.Dropout(.15), nn.Linear(config.ctc['hidden_size'], config.ctc['output_size'])) def forward(self, states): batched = _is_batch(states) items = states if batched else [states] if not items: return [] sequences = [self.norm(item['hidden_states'][self.layer].to(self.device).float()) for item in items] values = nn.utils.rnn.pad_sequence(sequences, batch_first=True) values = values+self.temporal(values.transpose(1, 2)).transpose(1, 2) logits = self.network(values) return [item[:state['frame_count']] for item, state in zip(logits, items)] if batched else logits[0] @torch.inference_mode() def align(self, states, text): texts = _matching_texts(states, text) if _is_batch(states): return [align(transcript, logits, self.vocab, self.blank, state['frame_count']) for state, transcript, logits in zip(states, texts, self(states))] return align(texts[0], self(states), self.vocab, self.blank, states['frame_count']) class StressHead(_Component): """States + transcript -> CTC timings -> word-local stressed text.""" def __init__(self, config): super().__init__() spec = config.heads['stress'] self.layer = spec['layer'] self.ranker = _RecordingMLP(spec['dimension'], spec['widths'], 1) self.ctc_head = CTCHead(config) def _load_weights(self, source, options): _load_state(self.ranker, source, 'stress.safetensors', options) self.ctc_head._load_weights(source, options) @torch.inference_mode() def forward(self, states, text): _matching_texts(states, text) batched = _is_batch(states) items = states if batched else [states] if not items: return [] aligned = self.ctc_head.align(states, text) alignments = aligned if batched else [aligned] pooled = [wav2vec_vowel_features(alignment, state['hidden_states'][self.layer], hidden_size=1024) for state, alignment in zip(items, alignments)] scores = self.ranker(torch.cat([item[0] for item in pooled])).squeeze(-1) if not torch.isfinite(scores).all(): raise ValueError('Non-finite stress scores') results, offset = [], 0 for alignment, (features, words) in zip(alignments, pooled): rendered = list(alignment['normalized_text']) for word in words: chosen = scores[offset+word['row_start']:offset+word['row_end']].argmax().item() index = word['vowels'][chosen] rendered[index] = '+'+rendered[index] results.append(''.join(rendered)) offset += features.shape[0] return results if batched else results[0] class TeraAnaly(nn.Module): """Independently callable encoder, emotion, age, gender, stress and CTC.""" config_class = TeraAnalyConfig def __init__(self, config): super().__init__() self.config = config self.encoder = TeraEncoder(config) self.emotion_head = EmotionHead(config) self.age_head = AgeHead(config) self.gender_head = GenderHead(config) self.stress_head = StressHead(config) @property def ctc_head(self): return self.stress_head.ctc_head @property def device(self): return self.encoder.device @classmethod def register_for_auto_class(cls, auto_class='AutoModel'): cls._auto_class = auto_class if isinstance(auto_class, str) else auto_class.__name__ @classmethod def from_pretrained(cls, source, *args, config=None, device='cpu', **options): """Load separate safetensors; also called by HF AutoModel.""" if args: raise TypeError('Pass options as keyword arguments') config, options = _load_config(source, config, options) with torch.device('meta'): model = cls(config) for module in (model.encoder, model.emotion_head, model.age_head, model.gender_head, model.stress_head): module._load_weights(source, options) device_map = options.get('device_map') if device_map == 'auto': device = 'cuda' if torch.cuda.is_available() else 'cpu' elif device_map is not None: if not isinstance(device_map, (str, torch.device)): raise ValueError('Use a single device or call model.to(device)') device = device_map model.to(device).eval() model.encoder.requires_grad_(False) return model @staticmethod def read_wav(path): return read_wav(path) @torch.inference_mode() def forward(self, waveform, text, sample_rate=16000): """Return one result dict, or a list for a waveform/text batch.""" items, _, batched = _audio_items(waveform, sample_rate) _matching_texts(items if batched else waveform, text) states = self.encoder(waveform, sample_rate) outputs = {'age': self.age_head(states), 'gender': self.gender_head(states), 'emotion': self.emotion_head(states), 'stressed_text': self.stress_head(states, text)} return ([dict(zip(outputs, values)) for values in zip(*outputs.values())] if batched else outputs)