EYEDOL commited on
Commit
ef40fdd
·
verified ·
1 Parent(s): db4586b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +345 -56
app.py CHANGED
@@ -1,84 +1,373 @@
1
- import gradio as gr
2
- import numpy as np
3
- import soundfile as sf
4
  import tempfile
5
- from faster_whisper import WhisperModel
 
 
 
 
6
 
7
- # Fastest practical model
8
- model = WhisperModel(
9
- "turbo",
10
- device="cpu",
11
- compute_type="int8"
12
- )
13
 
14
- full_transcript = ""
15
- last_segment = ""
16
 
17
- def transcribe(audio):
18
 
19
- global full_transcript
20
- global last_segment
21
 
22
- if audio is None:
23
- return full_transcript
24
 
25
- sr, data = audio
 
 
 
 
 
26
 
27
- if len(data) < sr // 2:
28
- return full_transcript
29
 
30
- with tempfile.NamedTemporaryFile(suffix=".wav") as f:
 
 
31
 
32
- sf.write(f.name, data, sr)
 
33
 
34
- segments, info = model.transcribe(
35
- f.name,
36
- language="en",
37
- vad_filter=True,
38
- beam_size=1,
39
- best_of=1,
40
- temperature=0
41
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- current_text = " ".join(
44
- segment.text.strip()
45
- for segment in segments
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  )
47
 
48
- if not current_text:
49
- return full_transcript
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- if current_text != last_segment:
 
 
 
 
52
 
53
- if full_transcript:
54
- full_transcript += " "
55
 
56
- full_transcript += current_text
57
- last_segment = current_text
58
 
59
- return full_transcript
60
 
 
61
 
62
- with gr.Blocks() as demo:
63
 
64
- gr.Markdown("# Real-Time English Speech Recognition")
 
 
 
 
65
 
66
- audio = gr.Audio(
67
- sources=["microphone"],
68
- streaming=True,
69
- type="numpy"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  )
71
 
72
- output = gr.Textbox(
73
- label="Transcript",
74
- lines=12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  )
76
 
77
- audio.stream(
78
- fn=transcribe,
79
- inputs=audio,
80
- outputs=output,
81
- stream_every=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  )
83
 
84
- demo.launch()
 
 
 
 
 
 
1
+ import os
 
 
2
  import tempfile
3
+ import torch
4
+ import soundfile as sf
5
+ from transformers import pipeline
6
+ import gradio as gr
7
+ from pydub import AudioSegment
8
 
9
+ # =========================================================
10
+ # Whisper Tiny Hausa ASR
11
+ # =========================================================
 
 
 
12
 
13
+ MODEL_ID = "EYEDOL/whisper-tiny-hausa3"
 
14
 
15
+ DEVICE = 0 if torch.cuda.is_available() else -1
16
 
17
+ # Cache pipeline
18
+ ASR_PIPELINE = None
19
 
20
+ def get_asr_pipeline():
21
+ global ASR_PIPELINE
22
 
23
+ if ASR_PIPELINE is None:
24
+ ASR_PIPELINE = pipeline(
25
+ "automatic-speech-recognition",
26
+ model=MODEL_ID,
27
+ device=DEVICE
28
+ )
29
 
30
+ return ASR_PIPELINE
 
31
 
32
+ # =========================================================
33
+ # Utilities
34
+ # =========================================================
35
 
36
+ def save_numpy_to_wav(np_tuple):
37
+ samplerate, data = np_tuple
38
 
39
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
40
+
41
+ sf.write(tmp.name, data, samplerate)
42
+
43
+ return tmp.name
44
+
45
+
46
+ def get_duration_seconds(path):
47
+ try:
48
+ info = sf.info(path)
49
+ return info.duration
50
+ except Exception:
51
+ seg = AudioSegment.from_file(path)
52
+ return len(seg) / 1000.0
53
+
54
+
55
+ def split_audio_file(path, chunk_length_ms=25000, overlap_ms=500):
56
+ audio = AudioSegment.from_file(path)
57
+
58
+ duration_ms = len(audio)
59
+
60
+ chunks = []
61
+
62
+ start = 0
63
+
64
+ while start < duration_ms:
65
+
66
+ end = min(start + chunk_length_ms, duration_ms)
67
+
68
+ chunk = audio[start:end]
69
+
70
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
71
+
72
+ chunk.export(tmp.name, format="wav")
73
+
74
+ chunks.append((tmp.name, start, end))
75
 
76
+ start += max(1, chunk_length_ms - overlap_ms)
77
+
78
+ return chunks
79
+
80
+
81
+ def transcribe_file(asr_pipeline, path, return_timestamps=False):
82
+
83
+ if return_timestamps:
84
+ return asr_pipeline(path, return_timestamps=True)
85
+
86
+ return asr_pipeline(path)
87
+
88
+ # =========================================================
89
+ # Main transcription function
90
+ # =========================================================
91
+
92
+ def transcribe(
93
+ audio_input,
94
+ allow_longform_with_timestamps=False,
95
+ chunk_length_seconds=25,
96
+ overlap_seconds=0.5,
97
+ ):
98
+
99
+ if audio_input is None:
100
+ return {"error": "No audio provided."}
101
+
102
+ # Convert mic numpy input -> wav
103
+ created_tmp_input = False
104
+
105
+ if isinstance(audio_input, tuple):
106
+ audio_path = save_numpy_to_wav(audio_input)
107
+ created_tmp_input = True
108
+ else:
109
+ audio_path = audio_input
110
+
111
+ duration_s = get_duration_seconds(audio_path)
112
+
113
+ asr = get_asr_pipeline()
114
+
115
+ # =====================================================
116
+ # SHORT AUDIO
117
+ # =====================================================
118
+
119
+ if duration_s <= 30:
120
+
121
+ out = transcribe_file(
122
+ asr,
123
+ audio_path,
124
+ return_timestamps=False
125
  )
126
 
127
+ text = out.get("text", out) if isinstance(out, dict) else str(out)
128
+
129
+ segments = [{
130
+ "start_s": 0.0,
131
+ "end_s": duration_s,
132
+ "text": text
133
+ }]
134
+
135
+ if created_tmp_input:
136
+ try:
137
+ os.unlink(audio_path)
138
+ except:
139
+ pass
140
+
141
+ return {
142
+ "full_text": text,
143
+ "segments": segments
144
+ }
145
+
146
+ # =====================================================
147
+ # LONG AUDIO WITH WHISPER TIMESTAMPS
148
+ # =====================================================
149
+
150
+ if allow_longform_with_timestamps:
151
+
152
+ try:
153
 
154
+ out = transcribe_file(
155
+ asr,
156
+ audio_path,
157
+ return_timestamps=True
158
+ )
159
 
160
+ full_text = out.get("text", "")
 
161
 
162
+ segments = []
 
163
 
164
+ if "chunks" in out:
165
 
166
+ for c in out["chunks"]:
167
 
168
+ ts = c.get("timestamp", [None, None])
169
 
170
+ segments.append({
171
+ "start_s": ts[0],
172
+ "end_s": ts[1],
173
+ "text": c.get("text", "")
174
+ })
175
 
176
+ else:
177
+
178
+ segments = [{
179
+ "start_s": 0.0,
180
+ "end_s": duration_s,
181
+ "text": full_text
182
+ }]
183
+
184
+ if created_tmp_input:
185
+ try:
186
+ os.unlink(audio_path)
187
+ except:
188
+ pass
189
+
190
+ return {
191
+ "full_text": full_text,
192
+ "segments": segments
193
+ }
194
+
195
+ except Exception as e:
196
+ print("Long-form failed. Falling back to chunking:", e)
197
+
198
+ # =====================================================
199
+ # CHUNKING FALLBACK
200
+ # =====================================================
201
+
202
+ chunk_length_ms = int(chunk_length_seconds * 1000)
203
+ overlap_ms = int(overlap_seconds * 1000)
204
+
205
+ chunks = split_audio_file(
206
+ audio_path,
207
+ chunk_length_ms=chunk_length_ms,
208
+ overlap_ms=overlap_ms
209
  )
210
 
211
+ stitched = []
212
+ segments = []
213
+
214
+ for chunk_path, start_ms, end_ms in chunks:
215
+
216
+ try:
217
+
218
+ out = transcribe_file(
219
+ asr,
220
+ chunk_path,
221
+ return_timestamps=False
222
+ )
223
+
224
+ text = out.get("text", out) if isinstance(out, dict) else str(out)
225
+
226
+ except Exception as e:
227
+
228
+ text = f"[ERROR: {e}]"
229
+
230
+ segments.append({
231
+ "start_s": start_ms / 1000.0,
232
+ "end_s": end_ms / 1000.0,
233
+ "text": text
234
+ })
235
+
236
+ stitched.append(text)
237
+
238
+ try:
239
+ os.unlink(chunk_path)
240
+ except:
241
+ pass
242
+
243
+ if created_tmp_input:
244
+ try:
245
+ os.unlink(audio_path)
246
+ except:
247
+ pass
248
+
249
+ full_text = " ".join([x for x in stitched if x])
250
+
251
+ return {
252
+ "full_text": full_text,
253
+ "segments": segments
254
+ }
255
+
256
+ # =========================================================
257
+ # Gradio UI
258
+ # =========================================================
259
+
260
+ with gr.Blocks(title="Whisper Tiny Hausa ASR") as demo:
261
+
262
+ gr.Markdown(
263
+ """
264
+ # Whisper Tiny Hausa ASR
265
+
266
+ Upload audio or record with microphone.
267
+ Supports long audio transcription.
268
+ """
269
  )
270
 
271
+ with gr.Row():
272
+
273
+ with gr.Column(scale=2):
274
+
275
+ mic_input = gr.Audio(
276
+ label="Record Audio",
277
+ type="numpy"
278
+ )
279
+
280
+ file_input = gr.Audio(
281
+ label="Upload Audio File",
282
+ type="filepath"
283
+ )
284
+
285
+ source = gr.Radio(
286
+ ["Use microphone input", "Use uploaded file"],
287
+ value="Use microphone input",
288
+ label="Input source"
289
+ )
290
+
291
+ longform = gr.Checkbox(
292
+ label="Use Whisper timestamps",
293
+ value=True
294
+ )
295
+
296
+ chunk_len = gr.Slider(
297
+ minimum=10,
298
+ maximum=120,
299
+ value=25,
300
+ step=5,
301
+ label="Chunk length (seconds)"
302
+ )
303
+
304
+ overlap = gr.Slider(
305
+ minimum=0.0,
306
+ maximum=5.0,
307
+ value=0.5,
308
+ step=0.5,
309
+ label="Chunk overlap (seconds)"
310
+ )
311
+
312
+ transcribe_btn = gr.Button("Transcribe")
313
+
314
+ with gr.Column(scale=3):
315
+
316
+ full_text_out = gr.Textbox(
317
+ label="Full transcription",
318
+ lines=8
319
+ )
320
+
321
+ segments_out = gr.JSON(
322
+ label="Segments"
323
+ )
324
+
325
+ def handle_transcription(
326
+ mic_input,
327
+ file_input,
328
+ source_choice,
329
+ use_longform,
330
+ chunk_len_s,
331
+ overlap_s
332
+ ):
333
+
334
+ audio_src = (
335
+ mic_input
336
+ if source_choice == "Use microphone input"
337
+ else file_input
338
+ )
339
+
340
+ result = transcribe(
341
+ audio_src,
342
+ allow_longform_with_timestamps=use_longform,
343
+ chunk_length_seconds=chunk_len_s,
344
+ overlap_seconds=overlap_s
345
+ )
346
+
347
+ if "error" in result:
348
+ return result["error"], []
349
+
350
+ return result["full_text"], result["segments"]
351
+
352
+ transcribe_btn.click(
353
+ fn=handle_transcription,
354
+ inputs=[
355
+ mic_input,
356
+ file_input,
357
+ source,
358
+ longform,
359
+ chunk_len,
360
+ overlap
361
+ ],
362
+ outputs=[
363
+ full_text_out,
364
+ segments_out
365
+ ],
366
  )
367
 
368
+ # =========================================================
369
+ # Launch
370
+ # =========================================================
371
+
372
+ if __name__ == "__main__":
373
+ demo.launch()