Vishal Patil commited on
Commit
e6b935f
·
verified ·
1 Parent(s): 8f10681

Update pipeline.py

Browse files
Files changed (1) hide show
  1. pipeline.py +37 -89
pipeline.py CHANGED
@@ -1,9 +1,7 @@
1
  """
2
  Speech Annotation Pipeline
3
- - Voice Activity Detection (VAD) via pyannote
4
  - Speaker Diarization via pyannote.audio
5
- - Transcription via WhisperX
6
- - Combines into structured ABAB-style annotations
7
  """
8
 
9
  import os
@@ -12,48 +10,43 @@ import csv
12
  import time
13
  import logging
14
  from pathlib import Path
15
- from dataclasses import dataclass, asdict
16
  from typing import List, Optional
17
 
18
  import torch
19
- import whisperx
20
  from pyannote.audio import Pipeline as PyannotePipeline
21
 
22
  logging.basicConfig(level=logging.INFO)
23
  logger = logging.getLogger(__name__)
24
 
25
- # ── Config ────────────────────────────────────────────────────────────────────
26
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
27
- COMPUTE_TYPE = "float16" if DEVICE == "cuda" else "int8"
28
- HF_TOKEN = os.environ.get("HF_TOKEN", "") # Required for pyannote models
29
- WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "base") # base / small / medium / large-v2
30
 
31
 
32
- # ── Data model ────────────────────────────────────────────────────────────────
33
  @dataclass
34
  class AnnotationSegment:
35
- speaker: str # A, B, C …
36
- start: float # seconds
37
- end: float # seconds
38
  text: str
39
 
40
  @property
41
- def start_fmt(self) -> str:
42
  m, s = divmod(int(self.start), 60)
43
  return f"{m:02d}:{s:02d}"
44
 
45
  @property
46
- def end_fmt(self) -> str:
47
  m, s = divmod(int(self.end), 60)
48
  return f"{m:02d}:{s:02d}"
49
 
50
- def to_abab(self) -> str:
51
  return f"{self.speaker} {self.start_fmt} {self.text}"
52
 
53
 
54
- # ── Label mapper ──────────────────────────────────────────────────────────────
55
- def _build_speaker_map(raw_labels: List[str]) -> dict:
56
- """Map pyannote speaker IDs (SPEAKER_00, …) to A, B, C, …"""
57
  letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
58
  seen, mapping = [], {}
59
  for lbl in raw_labels:
@@ -64,30 +57,25 @@ def _build_speaker_map(raw_labels: List[str]) -> dict:
64
  return mapping
65
 
66
 
67
- # ── Core pipeline ─────────────────────────────────────────────────────────────
68
  class SpeechAnnotationPipeline:
69
  def __init__(self):
70
  self._whisper_model = None
71
  self._diarization_pipeline = None
72
 
73
- # --- lazy loaders ---
74
  def _load_whisper(self):
75
  if self._whisper_model is None:
76
- logger.info(f"Loading WhisperX model '{WHISPER_MODEL}' on {DEVICE} …")
77
- self._whisper_model = whisperx.load_model(
78
- WHISPER_MODEL, DEVICE, compute_type=COMPUTE_TYPE
79
- )
80
  return self._whisper_model
81
 
82
  def _load_diarization(self):
83
  if self._diarization_pipeline is None:
84
  if not HF_TOKEN:
85
  raise EnvironmentError(
86
- "HF_TOKEN environment variable is required for pyannote diarization. "
87
- "Get a token at https://huggingface.co/settings/tokens and accept the "
88
- "model licence at https://hf.co/pyannote/speaker-diarization-3.1"
89
  )
90
- logger.info("Loading pyannote diarization pipeline …")
91
  self._diarization_pipeline = PyannotePipeline.from_pretrained(
92
  "pyannote/speaker-diarization-3.1",
93
  use_auth_token=HF_TOKEN,
@@ -96,25 +84,14 @@ class SpeechAnnotationPipeline:
96
  self._diarization_pipeline = self._diarization_pipeline.to(torch.device("cuda"))
97
  return self._diarization_pipeline
98
 
99
- # --- steps ---
100
- def _transcribe(self, audio_path: str) -> dict:
101
- logger.info("Step 1/3 – Transcribing with WhisperX …")
102
  model = self._load_whisper()
103
- audio = whisperx.load_audio(audio_path)
104
- result = model.transcribe(audio, batch_size=16)
105
-
106
- logger.info("Step 1b – Aligning word timestamps …")
107
- align_model, metadata = whisperx.load_align_model(
108
- language_code=result["language"], device=DEVICE
109
- )
110
- result = whisperx.align(
111
- result["segments"], align_model, metadata, audio, DEVICE,
112
- return_char_alignments=False,
113
- )
114
- return result
115
-
116
- def _diarize(self, audio_path: str) -> list:
117
- logger.info("Step 2/3 – Speaker diarization …")
118
  pipeline = self._load_diarization()
119
  diarization = pipeline(audio_path)
120
  segments = []
@@ -122,85 +99,56 @@ class SpeechAnnotationPipeline:
122
  segments.append({"start": turn.start, "end": turn.end, "speaker": speaker})
123
  return segments
124
 
125
- def _assign_speakers(self, transcript_segments: list, diarization_segments: list) -> List[AnnotationSegment]:
126
- """Assign speaker labels to transcript segments using maximum-overlap rule."""
127
- logger.info("Step 3/3 – Merging transcription + diarization …")
128
-
129
  raw_labels = [d["speaker"] for d in diarization_segments]
130
  speaker_map = _build_speaker_map(raw_labels)
131
-
132
  results = []
133
  for seg in transcript_segments:
134
  t_start, t_end = seg["start"], seg["end"]
135
  text = seg.get("text", "").strip()
136
  if not text:
137
  continue
138
-
139
- # Find diarization segment with max overlap
140
  best_speaker, best_overlap = "A", 0.0
141
  for d in diarization_segments:
142
  overlap = max(0, min(t_end, d["end"]) - max(t_start, d["start"]))
143
  if overlap > best_overlap:
144
  best_overlap = overlap
145
  best_speaker = d["speaker"]
146
-
147
  results.append(AnnotationSegment(
148
  speaker=speaker_map.get(best_speaker, best_speaker),
149
- start=t_start,
150
- end=t_end,
151
- text=text,
152
  ))
153
  return results
154
 
155
- # --- public API ---
156
- def process(self, audio_path: str) -> List[AnnotationSegment]:
157
  t0 = time.time()
158
- audio_path = str(Path(audio_path).resolve())
159
-
160
  transcript = self._transcribe(audio_path)
161
  diarization = self._diarize(audio_path)
162
- annotations = self._assign_speakers(transcript["segments"], diarization)
163
-
164
- elapsed = time.time() - t0
165
- logger.info(f"Done in {elapsed:.1f}s – {len(annotations)} segments")
166
  return annotations
167
 
168
 
169
- # ── Export helpers ─────────────────────────────────────────────────────────────
170
- def to_json(segments: List[AnnotationSegment], path: str):
171
- data = [
172
- {
173
- "speaker": s.speaker,
174
- "start": round(s.start, 3),
175
- "end": round(s.end, 3),
176
- "start_fmt": s.start_fmt,
177
- "end_fmt": s.end_fmt,
178
- "text": s.text,
179
- }
180
- for s in segments
181
- ]
182
  with open(path, "w", encoding="utf-8") as f:
183
  json.dump(data, f, indent=2, ensure_ascii=False)
184
 
185
-
186
- def to_csv(segments: List[AnnotationSegment], path: str):
187
  with open(path, "w", newline="", encoding="utf-8") as f:
188
  writer = csv.writer(f)
189
  writer.writerow(["speaker", "start", "end", "text"])
190
  for s in segments:
191
  writer.writerow([s.speaker, s.start_fmt, s.end_fmt, s.text])
192
 
193
-
194
- def to_abab_text(segments: List[AnnotationSegment]) -> str:
195
  return "\n".join(s.to_abab() for s in segments)
196
 
 
197
 
198
- # ── Singleton instance ─────────────────────────────────────────────────────────
199
- _pipeline_instance: Optional[SpeechAnnotationPipeline] = None
200
-
201
-
202
- def get_pipeline() -> SpeechAnnotationPipeline:
203
  global _pipeline_instance
204
  if _pipeline_instance is None:
205
  _pipeline_instance = SpeechAnnotationPipeline()
206
- return _pipeline_instance
 
1
  """
2
  Speech Annotation Pipeline
 
3
  - Speaker Diarization via pyannote.audio
4
+ - Transcription via openai-whisper (CPU-friendly, no av dependency)
 
5
  """
6
 
7
  import os
 
10
  import time
11
  import logging
12
  from pathlib import Path
13
+ from dataclasses import dataclass
14
  from typing import List, Optional
15
 
16
  import torch
17
+ import whisper
18
  from pyannote.audio import Pipeline as PyannotePipeline
19
 
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
 
23
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
24
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
25
+ WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "base")
 
26
 
27
 
 
28
  @dataclass
29
  class AnnotationSegment:
30
+ speaker: str
31
+ start: float
32
+ end: float
33
  text: str
34
 
35
  @property
36
+ def start_fmt(self):
37
  m, s = divmod(int(self.start), 60)
38
  return f"{m:02d}:{s:02d}"
39
 
40
  @property
41
+ def end_fmt(self):
42
  m, s = divmod(int(self.end), 60)
43
  return f"{m:02d}:{s:02d}"
44
 
45
+ def to_abab(self):
46
  return f"{self.speaker} {self.start_fmt} {self.text}"
47
 
48
 
49
+ def _build_speaker_map(raw_labels):
 
 
50
  letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
51
  seen, mapping = [], {}
52
  for lbl in raw_labels:
 
57
  return mapping
58
 
59
 
 
60
  class SpeechAnnotationPipeline:
61
  def __init__(self):
62
  self._whisper_model = None
63
  self._diarization_pipeline = None
64
 
 
65
  def _load_whisper(self):
66
  if self._whisper_model is None:
67
+ logger.info(f"Loading Whisper model '{WHISPER_MODEL}' on {DEVICE}...")
68
+ self._whisper_model = whisper.load_model(WHISPER_MODEL, device=DEVICE)
 
 
69
  return self._whisper_model
70
 
71
  def _load_diarization(self):
72
  if self._diarization_pipeline is None:
73
  if not HF_TOKEN:
74
  raise EnvironmentError(
75
+ "HF_TOKEN environment variable is required. "
76
+ "Set it in Space Settings > Variables and secrets."
 
77
  )
78
+ logger.info("Loading pyannote diarization pipeline...")
79
  self._diarization_pipeline = PyannotePipeline.from_pretrained(
80
  "pyannote/speaker-diarization-3.1",
81
  use_auth_token=HF_TOKEN,
 
84
  self._diarization_pipeline = self._diarization_pipeline.to(torch.device("cuda"))
85
  return self._diarization_pipeline
86
 
87
+ def _transcribe(self, audio_path):
88
+ logger.info("Step 1/2 - Transcribing with Whisper...")
 
89
  model = self._load_whisper()
90
+ result = model.transcribe(audio_path, verbose=False)
91
+ return result["segments"]
92
+
93
+ def _diarize(self, audio_path):
94
+ logger.info("Step 2/2 - Diarizing speakers...")
 
 
 
 
 
 
 
 
 
 
95
  pipeline = self._load_diarization()
96
  diarization = pipeline(audio_path)
97
  segments = []
 
99
  segments.append({"start": turn.start, "end": turn.end, "speaker": speaker})
100
  return segments
101
 
102
+ def _assign_speakers(self, transcript_segments, diarization_segments):
 
 
 
103
  raw_labels = [d["speaker"] for d in diarization_segments]
104
  speaker_map = _build_speaker_map(raw_labels)
 
105
  results = []
106
  for seg in transcript_segments:
107
  t_start, t_end = seg["start"], seg["end"]
108
  text = seg.get("text", "").strip()
109
  if not text:
110
  continue
 
 
111
  best_speaker, best_overlap = "A", 0.0
112
  for d in diarization_segments:
113
  overlap = max(0, min(t_end, d["end"]) - max(t_start, d["start"]))
114
  if overlap > best_overlap:
115
  best_overlap = overlap
116
  best_speaker = d["speaker"]
 
117
  results.append(AnnotationSegment(
118
  speaker=speaker_map.get(best_speaker, best_speaker),
119
+ start=t_start, end=t_end, text=text,
 
 
120
  ))
121
  return results
122
 
123
+ def process(self, audio_path):
 
124
  t0 = time.time()
 
 
125
  transcript = self._transcribe(audio_path)
126
  diarization = self._diarize(audio_path)
127
+ annotations = self._assign_speakers(transcript, diarization)
128
+ logger.info(f"Done in {time.time()-t0:.1f}s — {len(annotations)} segments")
 
 
129
  return annotations
130
 
131
 
132
+ def to_json(segments, path):
133
+ data = [{"speaker": s.speaker, "start": round(s.start,3), "end": round(s.end,3),
134
+ "start_fmt": s.start_fmt, "end_fmt": s.end_fmt, "text": s.text} for s in segments]
 
 
 
 
 
 
 
 
 
 
135
  with open(path, "w", encoding="utf-8") as f:
136
  json.dump(data, f, indent=2, ensure_ascii=False)
137
 
138
+ def to_csv(segments, path):
 
139
  with open(path, "w", newline="", encoding="utf-8") as f:
140
  writer = csv.writer(f)
141
  writer.writerow(["speaker", "start", "end", "text"])
142
  for s in segments:
143
  writer.writerow([s.speaker, s.start_fmt, s.end_fmt, s.text])
144
 
145
+ def to_abab_text(segments):
 
146
  return "\n".join(s.to_abab() for s in segments)
147
 
148
+ _pipeline_instance = None
149
 
150
+ def get_pipeline():
 
 
 
 
151
  global _pipeline_instance
152
  if _pipeline_instance is None:
153
  _pipeline_instance = SpeechAnnotationPipeline()
154
+ return _pipeline_instance