mazesmazes commited on
Commit
569f1aa
·
verified ·
1 Parent(s): ac77d85

Update custom model files, README, and requirements

Browse files
Files changed (2) hide show
  1. asr_pipeline.py +26 -9
  2. 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 super().__call__({"raw": array, "sampling_rate": sr}, **kwargs)
176
  result: dict[str, Any] = {}
177
  texts = []
178
  for s, e in bounds:
179
- out = super().__call__({"raw": array[s:e], "sampling_rate": sr}, **kwargs)
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 assign_speakers_to_words(
810
- cls, words: list[dict], activity: np.ndarray, keep: list[int]
811
- ) -> list[dict]:
812
- """Give each word the kept speaker most active over its span.
 
 
 
813
 
814
- A word where no kept speaker reaches MIN_WORD_ACTIVITY (aligner put it
815
- in a pause) takes the previous word's speaker -- the next word's at
816
- the start of the recording.
 
817
  """
818
- names = {c: f"SPEAKER_{i}" for i, c in enumerate(keep)}
819
  cols = np.asarray(keep)
820
- picked: list[str | None] = []
821
- for word in words:
822
- lo = min(int(word["start"] / cls.FRAME_S), len(activity) - 1)
823
- hi = max(lo + 1, min(int(np.ceil(word["end"] / cls.FRAME_S)), len(activity)))
 
 
 
824
  mean = activity[lo:hi, cols].mean(axis=0)
825
  best = int(mean.argmax())
826
- picked.append(names[keep[best]] if mean[best] >= cls.MIN_WORD_ACTIVITY else None)
827
  known = [p for p in picked if p is not None]
828
- last = known[0] if known else names[keep[0]]
829
- for word, p in zip(words, picked):
 
830
  last = p if p is not None else last
831
- word["speaker"] = last
 
 
 
 
 
 
 
 
 
 
 
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