Automatic Speech Recognition
Transformers
Safetensors
English
asr_model
feature-extraction
asr
speech-recognition
speech-to-text
audio
speech-llm
word-timestamps
speaker-diarization
qwen
granite-speech
custom_code
Instructions to use mazesmazes/tiny-audio with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use mazesmazes/tiny-audio with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="mazesmazes/tiny-audio", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("mazesmazes/tiny-audio", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update custom model files, README, and requirements
Browse files- asr_pipeline.py +26 -9
- diarization.py +34 -16
asr_pipeline.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
import re
|
| 4 |
from pathlib import Path
|
| 5 |
-
from typing import Any
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
import torch
|
|
@@ -54,6 +54,19 @@ def chunk_bounds(
|
|
| 54 |
return bounds
|
| 55 |
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
_THINK_TAG_RE = re.compile(r"<think>.*?</think>\s*", flags=re.DOTALL)
|
| 58 |
_MIN_REPEATS = 3
|
| 59 |
_TRAILING_CHAR_RE = re.compile(rf"(.)\1{{{_MIN_REPEATS - 1},}}$")
|
|
@@ -172,11 +185,11 @@ class ASRPipeline(transformers.AutomaticSpeechRecognitionPipeline):
|
|
| 172 |
|
| 173 |
bounds = chunk_bounds(array, sr)
|
| 174 |
if len(bounds) == 1:
|
| 175 |
-
return
|
| 176 |
result: dict[str, Any] = {}
|
| 177 |
texts = []
|
| 178 |
for s, e in bounds:
|
| 179 |
-
out =
|
| 180 |
texts.append(out["text"])
|
| 181 |
# Per-step logprobs (output_scores=True) run on across chunks.
|
| 182 |
for key in ("top1_logprob", "top2_logprob"):
|
|
@@ -185,6 +198,15 @@ class ASRPipeline(transformers.AutomaticSpeechRecognitionPipeline):
|
|
| 185 |
result["text"] = " ".join(t for t in texts if t)
|
| 186 |
return result
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
def _transcribe_timed(
|
| 189 |
self,
|
| 190 |
inputs,
|
|
@@ -209,12 +231,7 @@ class ASRPipeline(transformers.AutomaticSpeechRecognitionPipeline):
|
|
| 209 |
sr = audio.get("sampling_rate", 16000)
|
| 210 |
|
| 211 |
bounds = chunk_bounds(array, sr)
|
| 212 |
-
texts = [
|
| 213 |
-
super(ASRPipeline, self).__call__({"raw": array[s:e], "sampling_rate": sr}, **kwargs)[
|
| 214 |
-
"text"
|
| 215 |
-
]
|
| 216 |
-
for s, e in bounds
|
| 217 |
-
]
|
| 218 |
result: dict[str, Any] = {"text": " ".join(t for t in texts if t)}
|
| 219 |
|
| 220 |
try:
|
|
|
|
| 2 |
|
| 3 |
import re
|
| 4 |
from pathlib import Path
|
| 5 |
+
from typing import Any, cast
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
import torch
|
|
|
|
| 54 |
return bounds
|
| 55 |
|
| 56 |
|
| 57 |
+
# Below this RMS (-100 dBFS) a chunk is digital silence: exact zeros, as in
|
| 58 |
+
# edited or remixed recordings. Given one, the model answers with a memorized
|
| 59 |
+
# training sentence ("The film was directed by the same director who directed
|
| 60 |
+
# 'The Man with the Moustache'") -- ten such chunks cost 2.3 WER on one AMI
|
| 61 |
+
# meeting -- so it is skipped. Quiet real speech sits near -60 dBFS.
|
| 62 |
+
SILENCE_RMS = 1e-5
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def is_silent(audio: np.ndarray) -> bool:
|
| 66 |
+
"""True for digital silence (or an empty array): nothing for the model to hear."""
|
| 67 |
+
return audio.size == 0 or float(np.sqrt(np.mean(np.square(audio)))) < SILENCE_RMS
|
| 68 |
+
|
| 69 |
+
|
| 70 |
_THINK_TAG_RE = re.compile(r"<think>.*?</think>\s*", flags=re.DOTALL)
|
| 71 |
_MIN_REPEATS = 3
|
| 72 |
_TRAILING_CHAR_RE = re.compile(rf"(.)\1{{{_MIN_REPEATS - 1},}}$")
|
|
|
|
| 185 |
|
| 186 |
bounds = chunk_bounds(array, sr)
|
| 187 |
if len(bounds) == 1:
|
| 188 |
+
return self._transcribe_chunk(array, sr, **kwargs)
|
| 189 |
result: dict[str, Any] = {}
|
| 190 |
texts = []
|
| 191 |
for s, e in bounds:
|
| 192 |
+
out = self._transcribe_chunk(array[s:e], sr, **kwargs)
|
| 193 |
texts.append(out["text"])
|
| 194 |
# Per-step logprobs (output_scores=True) run on across chunks.
|
| 195 |
for key in ("top1_logprob", "top2_logprob"):
|
|
|
|
| 198 |
result["text"] = " ".join(t for t in texts if t)
|
| 199 |
return result
|
| 200 |
|
| 201 |
+
def _transcribe_chunk(self, chunk: np.ndarray, sample_rate: int, **kwargs) -> dict:
|
| 202 |
+
"""One model call on one chunk; digital silence is "" without calling the model."""
|
| 203 |
+
if is_silent(chunk):
|
| 204 |
+
return {"text": ""}
|
| 205 |
+
# One input in, one dict out; the parent is typed for batches as well.
|
| 206 |
+
return cast(
|
| 207 |
+
"dict", super().__call__({"raw": chunk, "sampling_rate": sample_rate}, **kwargs)
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
def _transcribe_timed(
|
| 211 |
self,
|
| 212 |
inputs,
|
|
|
|
| 231 |
sr = audio.get("sampling_rate", 16000)
|
| 232 |
|
| 233 |
bounds = chunk_bounds(array, sr)
|
| 234 |
+
texts = [self._transcribe_chunk(array[s:e], sr, **kwargs)["text"] for s, e in bounds]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
result: dict[str, Any] = {"text": " ".join(t for t in texts if t)}
|
| 236 |
|
| 237 |
try:
|
diarization.py
CHANGED
|
@@ -806,27 +806,45 @@ class NemotronDiarizer:
|
|
| 806 |
return sorted(out, key=lambda seg: (seg["start"], seg["end"]))
|
| 807 |
|
| 808 |
@classmethod
|
| 809 |
-
def
|
| 810 |
-
cls,
|
| 811 |
-
|
| 812 |
-
|
|
|
|
|
|
|
|
|
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
the
|
|
|
|
| 817 |
"""
|
| 818 |
-
names = {c: f"SPEAKER_{i}" for i, c in enumerate(keep)}
|
| 819 |
cols = np.asarray(keep)
|
| 820 |
-
picked: list[
|
| 821 |
-
for
|
| 822 |
-
|
| 823 |
-
|
|
|
|
|
|
|
|
|
|
| 824 |
mean = activity[lo:hi, cols].mean(axis=0)
|
| 825 |
best = int(mean.argmax())
|
| 826 |
-
picked.append(
|
| 827 |
known = [p for p in picked if p is not None]
|
| 828 |
-
last = known[0] if known else
|
| 829 |
-
|
|
|
|
| 830 |
last = p if p is not None else last
|
| 831 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 832 |
return words
|
|
|
|
| 806 |
return sorted(out, key=lambda seg: (seg["start"], seg["end"]))
|
| 807 |
|
| 808 |
@classmethod
|
| 809 |
+
def speaker_columns(
|
| 810 |
+
cls,
|
| 811 |
+
spans: list[tuple[float | None, float | None]],
|
| 812 |
+
activity: np.ndarray,
|
| 813 |
+
keep: list[int],
|
| 814 |
+
) -> list[int]:
|
| 815 |
+
"""The kept activity column that said each `(start_s, end_s)` word.
|
| 816 |
|
| 817 |
+
The kept speaker most active over the word's span. A word with no
|
| 818 |
+
time, or where no kept speaker reaches MIN_WORD_ACTIVITY (it sits in
|
| 819 |
+
a pause), takes the previous word's speaker -- the next word's at the
|
| 820 |
+
start of the recording.
|
| 821 |
"""
|
|
|
|
| 822 |
cols = np.asarray(keep)
|
| 823 |
+
picked: list[int | None] = []
|
| 824 |
+
for start, end in spans:
|
| 825 |
+
if start is None or end is None:
|
| 826 |
+
picked.append(None)
|
| 827 |
+
continue
|
| 828 |
+
lo = min(int(start / cls.FRAME_S), len(activity) - 1)
|
| 829 |
+
hi = max(lo + 1, min(int(np.ceil(end / cls.FRAME_S)), len(activity)))
|
| 830 |
mean = activity[lo:hi, cols].mean(axis=0)
|
| 831 |
best = int(mean.argmax())
|
| 832 |
+
picked.append(int(cols[best]) if mean[best] >= cls.MIN_WORD_ACTIVITY else None)
|
| 833 |
known = [p for p in picked if p is not None]
|
| 834 |
+
last = known[0] if known else keep[0]
|
| 835 |
+
out = []
|
| 836 |
+
for p in picked:
|
| 837 |
last = p if p is not None else last
|
| 838 |
+
out.append(last)
|
| 839 |
+
return out
|
| 840 |
+
|
| 841 |
+
@classmethod
|
| 842 |
+
def assign_speakers_to_words(
|
| 843 |
+
cls, words: list[dict], activity: np.ndarray, keep: list[int]
|
| 844 |
+
) -> list[dict]:
|
| 845 |
+
"""Label each `{"start", "end"}` word `SPEAKER_i` by `speaker_columns`."""
|
| 846 |
+
names = {c: f"SPEAKER_{i}" for i, c in enumerate(keep)}
|
| 847 |
+
spans = [(w["start"], w["end"]) for w in words]
|
| 848 |
+
for word, col in zip(words, cls.speaker_columns(spans, activity, keep)):
|
| 849 |
+
word["speaker"] = names[col]
|
| 850 |
return words
|