Vishal Patil commited on
Commit
43787ec
·
verified ·
1 Parent(s): 1e60763

Update pipeline.py

Browse files
Files changed (1) hide show
  1. pipeline.py +100 -102
pipeline.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- Speech Annotation Pipeline
3
- - Speaker Diarization via pyannote.audio
4
- - Transcription via openai-whisper
5
  """
6
 
7
  import os
@@ -9,25 +9,15 @@ import json
9
  import csv
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 huggingface_hub import login
19
- from pyannote.audio import Pipeline as PyannotePipeline
20
-
21
  logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger(__name__)
23
 
24
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
25
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
26
- WHISPER_MODEL = os.environ.get("WHISPER_MODEL", "base")
27
- NUM_SPEAKERS = int(os.environ.get("NUM_SPEAKERS", "0")) # 0 = auto-detect
28
-
29
- if HF_TOKEN:
30
- login(token=HF_TOKEN)
31
 
32
 
33
  @dataclass
@@ -51,102 +41,110 @@ class AnnotationSegment:
51
  return f"{self.speaker} {self.start_fmt} {self.text}"
52
 
53
 
54
- def _build_speaker_map(raw_labels):
55
- letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
56
- seen, mapping = [], {}
57
- for lbl in raw_labels:
58
- if lbl not in seen:
59
- seen.append(lbl)
60
- for i, lbl in enumerate(seen):
61
- mapping[lbl] = letters[i] if i < len(letters) else f"SPK{i}"
62
- return mapping
63
 
64
 
65
  class SpeechAnnotationPipeline:
66
- def __init__(self):
67
- self._whisper_model = None
68
- self._diarization_pipeline = None
69
-
70
- def _load_whisper(self):
71
- if self._whisper_model is None:
72
- logger.info(f"Loading Whisper model '{WHISPER_MODEL}' on {DEVICE}...")
73
- self._whisper_model = whisper.load_model(WHISPER_MODEL, device=DEVICE)
74
- return self._whisper_model
75
-
76
- def _load_diarization(self):
77
- if self._diarization_pipeline is None:
78
- if not HF_TOKEN:
79
- raise EnvironmentError(
80
- "HF_TOKEN not set. Add it in Space Settings > Variables and secrets."
81
- )
82
- logger.info("Loading pyannote diarization pipeline...")
83
- self._diarization_pipeline = PyannotePipeline.from_pretrained(
84
- "pyannote/speaker-diarization-3.1",
85
- use_auth_token=HF_TOKEN,
86
  )
87
- if DEVICE == "cuda":
88
- self._diarization_pipeline = self._diarization_pipeline.to(torch.device("cuda"))
89
- return self._diarization_pipeline
90
-
91
- def _transcribe(self, audio_path):
92
- logger.info("Step 1/2 - Transcribing with Whisper...")
93
- model = self._load_whisper()
94
- result = model.transcribe(audio_path, verbose=False)
95
- return result["segments"]
96
-
97
- def _diarize(self, audio_path, num_speakers=0, min_speakers=None, max_speakers=None):
98
- logger.info("Step 2/2 - Diarizing speakers...")
99
- pipeline = self._load_diarization()
100
-
101
- # Build diarization params
102
- params = {}
 
 
 
 
 
 
 
 
103
  if num_speakers and num_speakers > 0:
104
- params["num_speakers"] = num_speakers
105
- logger.info(f"Using fixed num_speakers={num_speakers}")
106
- else:
107
- if min_speakers:
108
- params["min_speakers"] = min_speakers
109
- if max_speakers:
110
- params["max_speakers"] = max_speakers
111
-
112
- diarization = pipeline(audio_path, **params)
113
- segments = []
114
- for turn, _, speaker in diarization.itertracks(yield_label=True):
115
- segments.append({"start": turn.start, "end": turn.end, "speaker": speaker})
116
- return segments
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- def _assign_speakers(self, transcript_segments, diarization_segments):
119
- raw_labels = [d["speaker"] for d in diarization_segments]
120
- speaker_map = _build_speaker_map(raw_labels)
121
- results = []
122
- for seg in transcript_segments:
123
- t_start, t_end = seg["start"], seg["end"]
124
- text = seg.get("text", "").strip()
125
- if not text:
126
- continue
127
- best_speaker, best_overlap = "A", 0.0
128
- for d in diarization_segments:
129
- overlap = max(0, min(t_end, d["end"]) - max(t_start, d["start"]))
130
- if overlap > best_overlap:
131
- best_overlap = overlap
132
- best_speaker = d["speaker"]
133
- results.append(AnnotationSegment(
134
- speaker=speaker_map.get(best_speaker, best_speaker),
135
- start=t_start, end=t_end, text=text,
136
  ))
137
- return results
138
 
139
- def process(self, audio_path, num_speakers=0, min_speakers=None, max_speakers=None):
140
- t0 = time.time()
141
- transcript = self._transcribe(audio_path)
142
- diarization = self._diarize(audio_path, num_speakers, min_speakers, max_speakers)
143
- annotations = self._assign_speakers(transcript, diarization)
144
- logger.info(f"Done in {time.time()-t0:.1f}s — {len(annotations)} segments")
145
- return annotations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
 
148
  def to_json(segments, path):
149
- data = [{"speaker": s.speaker, "start": round(s.start,3), "end": round(s.end,3),
150
  "start_fmt": s.start_fmt, "end_fmt": s.end_fmt, "text": s.text} for s in segments]
151
  with open(path, "w", encoding="utf-8") as f:
152
  json.dump(data, f, indent=2, ensure_ascii=False)
@@ -161,9 +159,9 @@ def to_csv(segments, path):
161
  def to_abab_text(segments):
162
  return "\n".join(s.to_abab() for s in segments)
163
 
164
- _pipeline_instance = None
165
 
166
- def get_pipeline():
167
  global _pipeline_instance
168
  if _pipeline_instance is None:
169
  _pipeline_instance = SpeechAnnotationPipeline()
 
1
  """
2
+ Speech Annotation Pipeline using Deepgram
3
+ - Transcription + Speaker Diarization in one API call
4
+ - Much faster and more accurate than pyannote on mixed audio
5
  """
6
 
7
  import os
 
9
  import csv
10
  import time
11
  import logging
12
+ import httpx
13
  from pathlib import Path
14
  from dataclasses import dataclass
15
  from typing import List, Optional
16
 
 
 
 
 
 
17
  logging.basicConfig(level=logging.INFO)
18
  logger = logging.getLogger(__name__)
19
 
20
+ DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "")
 
 
 
 
 
 
21
 
22
 
23
  @dataclass
 
41
  return f"{self.speaker} {self.start_fmt} {self.text}"
42
 
43
 
44
+ SPEAKER_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
45
+
46
+ def _speaker_label(idx: int) -> str:
47
+ return SPEAKER_LETTERS[idx] if idx < len(SPEAKER_LETTERS) else f"SPK{idx}"
 
 
 
 
 
48
 
49
 
50
  class SpeechAnnotationPipeline:
51
+
52
+ def process(self, audio_path: str, num_speakers: int = 0, **kwargs) -> List[AnnotationSegment]:
53
+ if not DEEPGRAM_API_KEY:
54
+ raise EnvironmentError(
55
+ "DEEPGRAM_API_KEY not set. Add it in Space Settings > Variables and secrets."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  )
57
+
58
+ logger.info(f"Sending audio to Deepgram: {audio_path}")
59
+ t0 = time.time()
60
+
61
+ # Read audio file
62
+ audio_bytes = Path(audio_path).read_bytes()
63
+ ext = Path(audio_path).suffix.lower().lstrip(".")
64
+ mime = {
65
+ "wav": "audio/wav",
66
+ "mp3": "audio/mpeg",
67
+ "mp4": "audio/mp4",
68
+ "m4a": "audio/mp4",
69
+ "flac": "audio/flac",
70
+ "ogg": "audio/ogg",
71
+ }.get(ext, "audio/wav")
72
+
73
+ # Build Deepgram request params
74
+ params = {
75
+ "model": "nova-2",
76
+ "diarize": "true",
77
+ "punctuate": "true",
78
+ "utterances": "true",
79
+ "smart_format": "true",
80
+ }
81
  if num_speakers and num_speakers > 0:
82
+ params["diarize_version"] = "latest"
83
+
84
+ headers = {
85
+ "Authorization": f"Token {DEEPGRAM_API_KEY}",
86
+ "Content-Type": mime,
87
+ }
88
+
89
+ # Call Deepgram API
90
+ response = httpx.post(
91
+ "https://api.deepgram.com/v1/listen",
92
+ params=params,
93
+ headers=headers,
94
+ content=audio_bytes,
95
+ timeout=300,
96
+ )
97
+ response.raise_for_status()
98
+ data = response.json()
99
+
100
+ # Parse utterances (already speaker-separated)
101
+ utterances = data.get("results", {}).get("utterances", [])
102
+ if not utterances:
103
+ # Fallback: parse words
104
+ words = data.get("results", {}).get("channels", [{}])[0]\
105
+ .get("alternatives", [{}])[0].get("words", [])
106
+ utterances = self._words_to_utterances(words)
107
 
108
+ segments = []
109
+ for utt in utterances:
110
+ speaker_idx = utt.get("speaker", 0)
111
+ segments.append(AnnotationSegment(
112
+ speaker=_speaker_label(speaker_idx),
113
+ start=utt.get("start", 0),
114
+ end=utt.get("end", 0),
115
+ text=utt.get("transcript", "").strip(),
 
 
 
 
 
 
 
 
 
 
116
  ))
 
117
 
118
+ logger.info(f"Done in {time.time()-t0:.1f}s — {len(segments)} segments")
119
+ return segments
120
+
121
+ def _words_to_utterances(self, words):
122
+ """Group words into utterances by speaker."""
123
+ if not words:
124
+ return []
125
+ utterances = []
126
+ current = {"speaker": words[0].get("speaker", 0),
127
+ "start": words[0].get("start", 0),
128
+ "end": words[0].get("end", 0),
129
+ "transcript": words[0].get("punctuated_word", words[0].get("word", ""))}
130
+ for word in words[1:]:
131
+ if word.get("speaker") == current["speaker"]:
132
+ current["end"] = word.get("end", current["end"])
133
+ current["transcript"] += " " + word.get("punctuated_word", word.get("word", ""))
134
+ else:
135
+ utterances.append(current)
136
+ current = {
137
+ "speaker": word.get("speaker", 0),
138
+ "start": word.get("start", 0),
139
+ "end": word.get("end", 0),
140
+ "transcript": word.get("punctuated_word", word.get("word", "")),
141
+ }
142
+ utterances.append(current)
143
+ return utterances
144
 
145
 
146
  def to_json(segments, path):
147
+ data = [{"speaker": s.speaker, "start": round(s.start, 3), "end": round(s.end, 3),
148
  "start_fmt": s.start_fmt, "end_fmt": s.end_fmt, "text": s.text} for s in segments]
149
  with open(path, "w", encoding="utf-8") as f:
150
  json.dump(data, f, indent=2, ensure_ascii=False)
 
159
  def to_abab_text(segments):
160
  return "\n".join(s.to_abab() for s in segments)
161
 
162
+ _pipeline_instance: Optional[SpeechAnnotationPipeline] = None
163
 
164
+ def get_pipeline() -> SpeechAnnotationPipeline:
165
  global _pipeline_instance
166
  if _pipeline_instance is None:
167
  _pipeline_instance = SpeechAnnotationPipeline()