Text-to-Speech
Transformers
Safetensors
Kabyle
agbalu_speech
feature-extraction
kabyle
taqbaylit
berber
amazigh
speech-synthesis
voice-cloning
cross-lingual-voice-cloning
expressive-speech
low-resource
custom_code
Instructions to use agbalu/Matoub-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use agbalu/Matoub-4B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-to-speech", model="agbalu/Matoub-4B", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("agbalu/Matoub-4B", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download verification.py from agbalu/Matoub-4B: direct link, hf CLI and curl.
- Browser
- Download file 6.73 kB
-
https://huggingface.co/agbalu/Matoub-4B/resolve/main/verification.py
- Command line
-
hf download hf://agbalu/Matoub-4B/verification.py
-
curl -L -o verification.py https://huggingface.co/agbalu/Matoub-4B/resolve/main/verification.py
6.73 kB
| """Measure generated Kabyle content, speaker identity and predicted speech quality.""" | |
| from __future__ import annotations | |
| import re | |
| import unicodedata | |
| from pathlib import Path | |
| import numpy as np | |
| import onnxruntime as ort | |
| import requests | |
| import torch | |
| from huggingface_hub import hf_hub_download | |
| from numpy.typing import NDArray | |
| from pydantic import Field | |
| from torch import Tensor, nn | |
| from torch.nn import functional | |
| from torchaudio.compliance.kaldi import fbank | |
| from torchaudio.models import squim_subjective_base | |
| from transformers import Wav2Vec2CTCTokenizer, Wav2Vec2FeatureExtractor, Wav2Vec2ForCTC | |
| from .artifacts import atomic_binary, digest_file, run_lock | |
| from .configuration import Specification | |
| from .evaluation import ASR_ID, ASR_REVISION, edit_count, extract_input | |
| from .waveforms import Waveform, measure_signal, resample_waveform | |
| VOICE_ID = 'Wespeaker/wespeaker-voxceleb-resnet34-LM' | |
| VOICE_REVISION = 'f0c48c298fd835726c27956a5d617bad7115627e' | |
| VOICE_SHA256 = '7bb2f06e9df17cdf1ef14ee8a15ab08ed28e8d0ef5054ee135741560df2ec068' | |
| MINIMUM_SPEAKER_SAMPLES = 400 | |
| QUALITY_SHA256 = 'dd36e1e4a5e3b36a5e7c32c936097b57b3221663a0529704171a3bb792699718' | |
| INSTRUMENTS = {'asr': {'id': ASR_ID, 'revision': ASR_REVISION}, 'speaker': {'id': VOICE_ID, 'revision': VOICE_REVISION, 'sha256': VOICE_SHA256}, 'speech_quality': {'id': 'torchaudio/SQUIM_SUBJECTIVE', 'sha256': QUALITY_SHA256}} | |
| def quality_checkpoint() -> Path: | |
| path = Path(torch.hub.get_dir()) / 'torchaudio/models/squim_subjective_bvcc_daps.pth' | |
| with run_lock(path.parent): | |
| if not path.exists(): | |
| url = 'https://download.pytorch.org/torchaudio/models/squim_subjective_bvcc_daps.pth' | |
| with requests.get(url, stream=True, timeout=(10, 60)) as response: | |
| response.raise_for_status() | |
| with atomic_binary(path) as stream: | |
| for block in response.iter_content(chunk_size=1024 * 1024): | |
| stream.write(block) | |
| if digest_file(path) != QUALITY_SHA256: | |
| message = f'speech-quality checkpoint checksum mismatch at {path}' | |
| raise ValueError(message) | |
| return path | |
| class Quality(Specification): | |
| hypothesis: str | |
| character_errors: int = Field(ge=0) | |
| characters: int = Field(ge=1) | |
| voice_cosine: float = Field(ge=-1, le=1) | |
| predicted_mos: float = Field(ge=1, le=5) | |
| duration_s: float = Field(gt=0) | |
| peak: float = Field(ge=0) | |
| rms_dbfs: float | |
| def cer(self) -> float: | |
| return self.character_errors / self.characters | |
| def spoken_text(text: str) -> str: | |
| plain = unicodedata.normalize('NFC', re.sub('<\\|[^<>]*\\|>', '', text)).lower() | |
| result = ' '.join(''.join((char if char.isalnum() else ' ' for char in plain)).split()) | |
| if not result: | |
| message = f'no spoken text in {text!r}' | |
| raise ValueError(message) | |
| return result | |
| def speaker_features(waveform: Waveform) -> NDArray[np.float32]: | |
| audio = resample_waveform(waveform, 24000, 16000) | |
| if audio.size < MINIMUM_SPEAKER_SAMPLES: | |
| message = f'speaker verification needs 400 samples at 16 kHz, got {audio.size}' | |
| raise ValueError(message) | |
| features: Tensor = fbank(torch.from_numpy(audio).reshape(1, -1) * (1 << 15), num_mel_bins=80, frame_length=25, frame_shift=10, dither=0, sample_frequency=16000, window_type='hamming', use_energy=False) | |
| return np.asarray((features - features.mean(0)).unsqueeze(0).numpy(), dtype=np.float32) | |
| class SpeechVerifier: | |
| def __init__(self, device: torch.device) -> None: | |
| self.device = device | |
| path = Path(hf_hub_download(VOICE_ID, 'voxceleb_resnet34_LM.onnx', revision=VOICE_REVISION, etag_timeout=30)) | |
| if digest_file(path) != VOICE_SHA256: | |
| message = f'speaker-model checksum mismatch at {path}' | |
| raise ValueError(message) | |
| options = ort.SessionOptions() | |
| options.inter_op_num_threads, options.intra_op_num_threads = (1, 4) | |
| self.speaker = ort.InferenceSession(str(path), sess_options=options, providers=['CPUExecutionProvider']) | |
| self.recognizer: nn.Module = Wav2Vec2ForCTC.from_pretrained(ASR_ID, revision=ASR_REVISION) | |
| self.quality: nn.Module = squim_subjective_base() | |
| self.quality.load_state_dict(torch.load(quality_checkpoint(), weights_only=True, map_location='cpu')) | |
| for model in (self.recognizer, self.quality): | |
| nn.Module.to(model, device=device) | |
| nn.Module.train(model, False) | |
| self.extractor = Wav2Vec2FeatureExtractor.from_pretrained(ASR_ID, revision=ASR_REVISION) | |
| self.tokenizer = Wav2Vec2CTCTokenizer.from_pretrained(ASR_ID, revision=ASR_REVISION) | |
| def embed(self, waveform: Waveform) -> Tensor: | |
| output: object = self.speaker.run(['embs'], {'feats': speaker_features(waveform)})[0] | |
| if not isinstance(output, np.ndarray) or output.shape != (1, 256) or (not np.isfinite(output).all()): | |
| message = f'invalid speaker embedding from {VOICE_ID}' | |
| raise ValueError(message) | |
| tensor = torch.from_numpy(np.asarray(output, dtype=np.float32)) | |
| if float(tensor.norm()) == 0: | |
| message = 'speaker encoder returned a zero vector' | |
| raise ValueError(message) | |
| return functional.normalize(tensor, dim=-1) | |
| def assess(self, waveform: Waveform, text: str, reference: Waveform) -> Quality: | |
| signal = measure_signal(waveform, 24000) | |
| if signal.rms_dbfs is None: | |
| message = 'generated speech is silent' | |
| raise ValueError(message) | |
| audio = resample_waveform(waveform, 24000, 16000) | |
| output = self.recognizer(input_values=extract_input(self.extractor, audio, self.device)) | |
| logits = getattr(output, 'logits', None) | |
| if not isinstance(logits, Tensor): | |
| message = 'ASR returned no logits' | |
| raise TypeError(message) | |
| raw = str(self.tokenizer.decode(logits[0].argmax(-1).tolist())) | |
| hypothesis = spoken_text(raw) if raw.strip() else '' | |
| expected = spoken_text(text) | |
| score = self.quality(torch.from_numpy(audio).reshape(1, -1).to(self.device), torch.from_numpy(resample_waveform(reference, 24000, 16000)).reshape(1, -1).to(self.device)) | |
| if not isinstance(score, Tensor): | |
| message = 'speech-quality model returned no MOS tensor' | |
| raise TypeError(message) | |
| return Quality(hypothesis=hypothesis, character_errors=edit_count(expected, hypothesis), characters=len(expected), voice_cosine=float((self.embed(waveform) * self.embed(reference)).sum().clamp(-1, 1)), predicted_mos=float(score[0]), duration_s=signal.samples / signal.rate_hz, peak=signal.peak, rms_dbfs=signal.rms_dbfs) | |