Vishal Patil commited on
Commit
a04aea6
·
verified ·
1 Parent(s): 2970b9d

Update pipeline.py

Browse files
Files changed (1) hide show
  1. pipeline.py +67 -167
pipeline.py CHANGED
@@ -1,168 +1,68 @@
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
8
- import json
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
24
- class AnnotationSegment:
25
- speaker: str
26
- start: float
27
- end: float
28
- text: str
29
-
30
- @property
31
- def start_fmt(self):
32
- m, s = divmod(int(self.start), 60)
33
- return f"{m:02d}:{s:02d}"
34
-
35
- @property
36
- def end_fmt(self):
37
- m, s = divmod(int(self.end), 60)
38
- return f"{m:02d}:{s:02d}"
39
-
40
- def to_abab(self):
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)
151
-
152
- def to_csv(segments, path):
153
- with open(path, "w", newline="", encoding="utf-8") as f:
154
- writer = csv.writer(f)
155
- writer.writerow(["speaker", "start", "end", "text"])
156
- for s in segments:
157
- writer.writerow([s.speaker, s.start_fmt, s.end_fmt, s.text])
158
-
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()
168
- return _pipeline_instance
 
1
+ import tempfile
2
+ import traceback
 
 
 
 
 
 
 
 
 
 
3
  from pathlib import Path
4
+ import gradio as gr
5
+ from pipeline import get_pipeline, to_json, to_csv, to_abab_text
6
+
7
+ UPLOAD_DIR = Path(tempfile.gettempdir()) / "speech_annotation"
8
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
9
+
10
+ _last_segments = []
11
+
12
+ def process_audio(audio_path, num_speakers):
13
+ global _last_segments
14
+ if audio_path is None:
15
+ return "⚠️ Please upload an audio file first.", ""
16
+ try:
17
+ pipeline = get_pipeline()
18
+ n = int(num_speakers) if num_speakers and int(num_speakers) > 0 else 0
19
+ segments = pipeline.process(audio_path, num_speakers=n)
20
+ except Exception as e:
21
+ return f"❌ Error: {e}\n{traceback.format_exc()}", ""
22
+ if not segments:
23
+ return "⚠️ No speech detected.", ""
24
+ _last_segments = segments
25
+ abab = to_abab_text(segments)
26
+ unique = len(set(s.speaker for s in segments))
27
+ return f"✅ Done — {len(segments)} segments, {unique} speaker(s) detected", abab
28
+
29
+ def export_json():
30
+ if not _last_segments:
31
+ return None
32
+ out = str(UPLOAD_DIR / "annotation.json")
33
+ to_json(_last_segments, out)
34
+ return out
35
+
36
+ def export_csv():
37
+ if not _last_segments:
38
+ return None
39
+ out = str(UPLOAD_DIR / "annotation.csv")
40
+ to_csv(_last_segments, out)
41
+ return out
42
+
43
+ with gr.Blocks(title="Speech Annotation Pipeline") as demo:
44
+ gr.Markdown("# 🎙️ Speech Annotation Pipeline")
45
+ audio_input = gr.Audio(label="Upload Audio (.wav/.mp3/.flac)", type="filepath")
46
+ num_speakers = gr.Slider(minimum=0, maximum=10, step=1, value=0,
47
+ label="Number of speakers (0 = auto-detect)")
48
+ run_btn = gr.Button("▶ Run Annotation", variant="primary")
49
+ status_box = gr.Textbox(label="Status", value="Ready.", interactive=False)
50
+ abab_box = gr.Textbox(label="Transcript (ABAB)", lines=15, value="",
51
+ placeholder="Annotations will appear here...")
52
+ with gr.Row():
53
+ json_btn = gr.Button("Export JSON")
54
+ csv_btn = gr.Button("Export CSV")
55
+ json_file = gr.File(label="JSON")
56
+ csv_file = gr.File(label="CSV")
57
+
58
+ run_btn.click(
59
+ fn=process_audio,
60
+ inputs=[audio_input, num_speakers],
61
+ outputs=[status_box, abab_box],
62
+ queue=False
63
+ )
64
+ json_btn.click(fn=export_json, inputs=[], outputs=[json_file])
65
+ csv_btn.click(fn=export_csv, inputs=[], outputs=[csv_file])
66
+
67
+ if __name__ == "__main__":
68
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)