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 audio.py from agbalu/Matoub-4B: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/agbalu/Matoub-4B/resolve/main/audio.py
- Command line
-
hf download hf://agbalu/Matoub-4B/audio.py
-
curl -L -o audio.py https://huggingface.co/agbalu/Matoub-4B/resolve/main/audio.py
11 kB
| """Audit embedded source audio through resumable shard transactions.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import io | |
| import json | |
| import threading | |
| from collections.abc import Iterator | |
| from datetime import date | |
| from pathlib import Path | |
| from typing import BinaryIO | |
| import numpy as np | |
| import pyarrow.parquet as pq | |
| import soundfile as sf | |
| from pydantic import Field | |
| from .artifacts import Recording, Rejection, atomic_binary, digest_file, provenance, read_recordings, run_lock, verify_conservation, write_json | |
| from .configuration import Recipe, Specification | |
| from .pairs import pair_recordings | |
| from .partition import partition_recordings | |
| from .waveforms import Waveform, measure_signal, resample_waveform, text_key | |
| from .progress import Tracker, stage | |
| DECODE_LOCK = threading.Lock() | |
| class SourceFile(Specification): | |
| path: str | |
| sha256: str | |
| class Acquisition(Specification): | |
| revision: str | |
| files: tuple[SourceFile, ...] | |
| access_date: date | |
| class ShardResult(Specification): | |
| config_hash: str | |
| source_sha256: str | |
| rows: int = Field(ge=0) | |
| accepted: int = Field(ge=0) | |
| rejected: int = Field(ge=0) | |
| recordings_sha256: str | |
| rejects_sha256: str | |
| provenance: dict[str, object] | |
| def read_acquisition(recipe: Recipe) -> Acquisition: | |
| path = recipe.source / 'acquisition.stats.json' | |
| payload: object = json.loads(path.read_bytes()) | |
| if not isinstance(payload, dict) or not isinstance(payload.get('files'), list): | |
| message = f'invalid acquisition file list: {path}' | |
| raise TypeError(message) | |
| files: list[SourceFile] = [] | |
| for item in payload['files']: | |
| if not isinstance(item, dict) or not isinstance(item.get('path'), str): | |
| message = f'invalid acquisition entry at {path}: {item!r}' | |
| raise TypeError(message) | |
| if item['path'].endswith('.parquet'): | |
| files.append(SourceFile.model_validate({'path': item['path'], 'sha256': item.get('sha256')})) | |
| acquisition = Acquisition.model_validate({'revision': payload.get('revision'), 'files': tuple(files), 'access_date': payload.get('access_date')}) | |
| if acquisition.revision != recipe.source_revision or not files: | |
| message = f'source identity mismatch or no Parquet files at {path}: {acquisition.revision}' | |
| raise ValueError(message) | |
| return acquisition | |
| def source_files(recipe: Recipe) -> tuple[Path, ...]: | |
| path = recipe.source / 'acquisition.stats.json' | |
| acquisition = read_acquisition(recipe) | |
| expected: dict[str, str] = {} | |
| for item in acquisition.files: | |
| name = Path(item.path).name | |
| if name in expected: | |
| message = f'duplicate acquisition shard name {name!r} in {path}' | |
| raise ValueError(message) | |
| expected[name] = item.sha256 | |
| actual = tuple(sorted((recipe.source / 'data').glob('*.parquet'))) | |
| if set(expected) != {item.name for item in actual}: | |
| message = f'source shard set differs from {path}: expected={sorted(expected)} actual={actual}' | |
| raise ValueError(message) | |
| for shard in actual: | |
| observed = digest_file(shard) | |
| if observed != expected[shard.name]: | |
| message = f'checksum mismatch {shard}: observed={observed}, expected={expected[shard.name]}' | |
| raise ValueError(message) | |
| return actual | |
| def raw_rows(shard: Path) -> Iterator[tuple[int, object]]: | |
| with pq.ParquetFile(shard) as parquet: | |
| offset = 0 | |
| for batch in parquet.iter_batches(batch_size=128, columns=['audio', 'transcription', 'client_id'], use_threads=False): | |
| for value in batch.to_pylist(): | |
| yield (offset, value) | |
| offset += 1 | |
| verify_conservation(parquet.metadata.num_rows, offset, 0) | |
| def embedded_bytes(value: object, max_size_bytes: int) -> tuple[bytes, str, str]: | |
| if not isinstance(value, dict): | |
| message = f'expected audio record, got {type(value).__name__}' | |
| raise TypeError(message) | |
| audio, text, speaker = (value.get('audio'), value.get('transcription'), value.get('client_id')) | |
| if not isinstance(audio, dict) or not isinstance(audio.get('bytes'), bytes): | |
| message = 'missing embedded audio.bytes' | |
| raise TypeError(message) | |
| raw = audio['bytes'] | |
| if len(raw) > max_size_bytes or not raw: | |
| message = f'embedded audio size_bytes={len(raw)}, limit={max_size_bytes}' | |
| raise ValueError(message) | |
| if not isinstance(text, str) or not text_key(text): | |
| message = f'missing/empty transcription: {text!r}' | |
| raise ValueError(message) | |
| if not isinstance(speaker, str) or not speaker.strip(): | |
| message = f'missing/empty client_id: {speaker!r}' | |
| raise ValueError(message) | |
| return (raw, text, speaker) | |
| def decode_audio(raw: bytes) -> tuple[Waveform, int]: | |
| with DECODE_LOCK: | |
| waveform, rate_hz = sf.read(io.BytesIO(raw), dtype='float32', always_2d=True) | |
| if waveform.shape[1] != 1: | |
| message = f'expected mono audio, got channels={waveform.shape[1]}' | |
| raise ValueError(message) | |
| mono = np.asarray(waveform[:, 0], dtype=np.float32) | |
| measure_signal(mono, rate_hz) | |
| return (mono, int(rate_hz)) | |
| def recording_for(value: object, shard: Path, row: int, recipe: Recipe, retrieval_date: str) -> Recording: | |
| raw, text, speaker = embedded_bytes(value, recipe.max_embedded_bytes) | |
| waveform, rate_hz = decode_audio(raw) | |
| signal = measure_signal(waveform, rate_hz) | |
| duration_s = signal.samples / rate_hz | |
| if not recipe.min_duration_s <= duration_s <= recipe.max_duration_s: | |
| message = f'duration_s={duration_s} outside [{recipe.min_duration_s},{recipe.max_duration_s}]' | |
| raise ValueError(message) | |
| converted = resample_waveform(waveform, rate_hz, recipe.rate_hz) | |
| measure_signal(converted, recipe.rate_hz) | |
| identity = f'{recipe.source_revision}:{shard.name}:{row}' | |
| return Recording(record_id=hashlib.sha256(identity.encode()).hexdigest(), shard=shard.name, row=row, audio_sha256=hashlib.sha256(raw).hexdigest(), speaker=speaker, text=text, text_key=text_key(text), stored_rate_hz=rate_hz, samples=signal.samples, prepared_samples=converted.size, duration_s=duration_s, peak=signal.peak, rms_dbfs=signal.rms_dbfs, clipping_fraction=signal.clipping_fraction, source_id=recipe.source_id, source_revision=recipe.source_revision, license=recipe.license, retrieval_date=retrieval_date) | |
| def write_line(stream: BinaryIO, value: Specification) -> None: | |
| stream.write((value.model_dump_json() + '\n').encode()) | |
| def prepare_shard(shard: Path, recipe: Recipe, date: str) -> ShardResult: | |
| root = recipe.output / 'shards' / shard.stem | |
| stats_path = root / 'audit.stats.json' | |
| accepted_path, rejects_path = (root / 'recordings.jsonl', root / 'rejects.jsonl') | |
| source_digest = digest_file(shard) | |
| if stats_path.is_file(): | |
| result = ShardResult.model_validate_json(stats_path.read_bytes()) | |
| if result.config_hash != recipe.digest or result.source_sha256 != source_digest or result.recordings_sha256 != digest_file(accepted_path) or (result.rejects_sha256 != digest_file(rejects_path)): | |
| message = f'prepared artifact identity/checksum mismatch: {stats_path}' | |
| raise ValueError(message) | |
| verify_conservation(result.rows, result.accepted, result.rejected) | |
| return result | |
| rows = accepted = rejected = 0 | |
| with atomic_binary(accepted_path) as kept, atomic_binary(rejects_path) as rejects: | |
| for row, value in raw_rows(shard): | |
| rows += 1 | |
| try: | |
| record = recording_for(value, shard, row, recipe, date) | |
| except (ValueError, TypeError, sf.LibsndfileError) as error: | |
| rejected += 1 | |
| rejection = Rejection(record_id=f'{shard.name}:{row}', shard=shard.name, row=row, reason=str(error)) | |
| write_line(rejects, rejection) | |
| else: | |
| accepted += 1 | |
| write_line(kept, record) | |
| verify_conservation(rows, accepted, rejected) | |
| result = ShardResult(config_hash=recipe.digest, source_sha256=source_digest, rows=rows, accepted=accepted, rejected=rejected, recordings_sha256=digest_file(accepted_path), rejects_sha256=digest_file(rejects_path), provenance=provenance(recipe.digest)) | |
| write_json(stats_path, result.model_dump()) | |
| return result | |
| def prepare_audio(recipe: Recipe) -> dict[str, object]: | |
| with run_lock(recipe.output), stage('decode source audio'): | |
| shards = source_files(recipe) | |
| tracker = Tracker('decoded shards', len(shards), interval_s=5) | |
| counts: list[ShardResult] = [] | |
| retrieval_date = read_acquisition(recipe).access_date.isoformat() | |
| with atomic_binary(recipe.output / 'recordings.jsonl') as combined: | |
| for shard in shards: | |
| counts.append(prepare_shard(shard, recipe, retrieval_date)) | |
| path = recipe.output / 'shards' / shard.stem / 'recordings.jsonl' | |
| with path.open('rb') as stream: | |
| while (block := stream.read(1 << 20)): | |
| combined.write(block) | |
| tracker.advance() | |
| report: dict[str, object] = {**provenance(recipe.digest), 'input_rows': sum((item.rows for item in counts)), 'accepted_rows': sum((item.accepted for item in counts)), 'rejected_rows': sum((item.rejected for item in counts)), 'language_reviewed': False, 'recordings_sha256': digest_file(recipe.output / 'recordings.jsonl'), 'shards': [item.model_dump() for item in counts]} | |
| write_json(recipe.output / 'preparation.stats.json', report) | |
| return report | |
| def prepare_pairs(recipe: Recipe) -> dict[str, object]: | |
| with run_lock(recipe.output), stage('partition reference pairs'): | |
| records = tuple(read_recordings(recipe.output / 'recordings.jsonl')) | |
| partition = partition_recordings(records, recipe) | |
| pairing = pair_recordings(records, partition, recipe.seed) | |
| for split in ('train', 'validation', 'test'): | |
| if not any((pair.split == split for pair in pairing.pairs)): | |
| message = f'no eligible pairs in {split}: inspect speaker/script coverage' | |
| raise ValueError(message) | |
| with atomic_binary(recipe.output / 'pairs.jsonl') as stream: | |
| for pair in pairing.pairs: | |
| write_line(stream, pair) | |
| excluded = {**partition.excluded, **pairing.excluded} | |
| write_json(recipe.output / 'pair-rejects.json', excluded) | |
| verify_conservation(len(records), len(pairing.pairs), len(excluded)) | |
| report: dict[str, object] = {**provenance(recipe.digest), 'input_rows': len(records), 'paired_rows': len(pairing.pairs), 'excluded_rows': len(excluded), 'split_pairs': {split: sum((pair.split == split for pair in pairing.pairs)) for split in ('train', 'validation', 'test')}, 'recordings_sha256': digest_file(recipe.output / 'recordings.jsonl'), 'pairs_sha256': digest_file(recipe.output / 'pairs.jsonl')} | |
| write_json(recipe.output / 'pairs.stats.json', report) | |
| return report | |