littlebird13 commited on
Commit
775fc2e
·
verified ·
1 Parent(s): ae45bf2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +374 -0
app.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2026 The Alibaba Qwen team.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """
5
+ Qwen3-ASR Demo for Huggingface Spaces with ZeroGPU support.
6
+ Showcases the 1.7B model with timestamp visualization.
7
+ """
8
+
9
+ import base64
10
+ import io
11
+ import os
12
+ from typing import Any, Dict, List, Optional, Tuple, Union
13
+
14
+ import gradio as gr
15
+ import numpy as np
16
+ import spaces
17
+ import torch
18
+ from huggingface_hub import login
19
+ from scipy.io.wavfile import write as wav_write
20
+
21
+ # Login to Hugging Face with token from environment variable
22
+ HF_TOKEN = os.environ.get("HF_TOKEN")
23
+ if HF_TOKEN:
24
+ login(token=HF_TOKEN)
25
+
26
+ # Global model instance (lazy loaded)
27
+ _asr_model = None
28
+
29
+
30
+ def _title_case_display(s: str) -> str:
31
+ s = (s or "").strip()
32
+ s = s.replace("_", " ")
33
+ return " ".join([w[:1].upper() + w[1:] if w else "" for w in s.split()])
34
+
35
+
36
+ def _build_choices_and_map(items: Optional[List[str]]) -> Tuple[List[str], Dict[str, str]]:
37
+ if not items:
38
+ return [], {}
39
+ display = [_title_case_display(x) for x in items]
40
+ mapping = {d: r for d, r in zip(display, items)}
41
+ return display, mapping
42
+
43
+
44
+ def _normalize_audio(wav, eps=1e-12, clip=True):
45
+ x = np.asarray(wav)
46
+
47
+ if np.issubdtype(x.dtype, np.integer):
48
+ info = np.iinfo(x.dtype)
49
+ if info.min < 0:
50
+ y = x.astype(np.float32) / max(abs(info.min), info.max)
51
+ else:
52
+ mid = (info.max + 1) / 2.0
53
+ y = (x.astype(np.float32) - mid) / mid
54
+ elif np.issubdtype(x.dtype, np.floating):
55
+ y = x.astype(np.float32)
56
+ m = np.max(np.abs(y)) if y.size else 0.0
57
+ if m > 1.0 + 1e-6:
58
+ y = y / (m + eps)
59
+ else:
60
+ raise TypeError(f"Unsupported dtype: {x.dtype}")
61
+
62
+ if clip:
63
+ y = np.clip(y, -1.0, 1.0)
64
+
65
+ if y.ndim > 1:
66
+ y = np.mean(y, axis=-1).astype(np.float32)
67
+
68
+ return y
69
+
70
+
71
+ def _audio_to_tuple(audio: Any) -> Optional[Tuple[np.ndarray, int]]:
72
+ """
73
+ Accept gradio audio:
74
+ - {"sampling_rate": int, "data": np.ndarray}
75
+ - (sr, np.ndarray) [some gradio versions]
76
+ Return: (wav_float32_mono, sr)
77
+ """
78
+ if audio is None:
79
+ return None
80
+
81
+ if isinstance(audio, dict) and "sampling_rate" in audio and "data" in audio:
82
+ sr = int(audio["sampling_rate"])
83
+ wav = _normalize_audio(audio["data"])
84
+ return wav, sr
85
+
86
+ if isinstance(audio, tuple) and len(audio) == 2:
87
+ a0, a1 = audio
88
+ if isinstance(a0, int):
89
+ sr = int(a0)
90
+ wav = _normalize_audio(a1)
91
+ return wav, sr
92
+ if isinstance(a1, int):
93
+ wav = _normalize_audio(a0)
94
+ sr = int(a1)
95
+ return wav, sr
96
+
97
+ return None
98
+
99
+
100
+ def _parse_audio_any(audio: Any) -> Union[str, Tuple[np.ndarray, int]]:
101
+ if audio is None:
102
+ raise ValueError("Audio is required.")
103
+ at = _audio_to_tuple(audio)
104
+ if at is not None:
105
+ return at
106
+ raise ValueError("Unsupported audio input format.")
107
+
108
+
109
+ def _make_timestamp_html(audio_upload: Any, timestamps: Any) -> str:
110
+ """
111
+ Build HTML with per-token audio slices, using base64 data URLs.
112
+ """
113
+ at = _audio_to_tuple(audio_upload)
114
+ if at is None:
115
+ return "<div style='color:#666'>No audio available for visualization.</div>"
116
+ audio, sr = at
117
+
118
+ if not timestamps:
119
+ return "<div style='color:#666'>No timestamps to visualize.</div>"
120
+ if not isinstance(timestamps, list):
121
+ return "<div style='color:#666'>Invalid timestamp format.</div>"
122
+
123
+ html_content = """
124
+ <style>
125
+ .word-alignment-container { display: flex; flex-wrap: wrap; gap: 10px; }
126
+ .word-box {
127
+ border: 1px solid #ddd; border-radius: 8px; padding: 10px;
128
+ background-color: #f9f9f9; box-shadow: 0 2px 4px rgba(0,0,0,0.06);
129
+ text-align: center;
130
+ }
131
+ .word-text { font-size: 18px; font-weight: 700; margin-bottom: 5px; }
132
+ .word-time { font-size: 12px; color: #666; margin-bottom: 8px; }
133
+ .word-audio audio { width: 140px; height: 30px; }
134
+ details { border: 1px solid #ddd; border-radius: 6px; padding: 10px; background-color: #f7f7f7; }
135
+ summary { font-weight: 700; cursor: pointer; }
136
+ </style>
137
+ """
138
+
139
+ html_content += """
140
+ <details open>
141
+ <summary>Timestamps Visualization (click each word to hear the audio segment)</summary>
142
+ <div class="word-alignment-container" style="margin-top: 14px;">
143
+ """
144
+
145
+ for item in timestamps:
146
+ if not isinstance(item, dict):
147
+ continue
148
+ word = str(item.get("text", "") or "")
149
+ start = item.get("start_time", None)
150
+ end = item.get("end_time", None)
151
+ if start is None or end is None:
152
+ continue
153
+
154
+ start = float(start)
155
+ end = float(end)
156
+ if end <= start:
157
+ continue
158
+
159
+ start_sample = max(0, int(start * sr))
160
+ end_sample = min(len(audio), int(end * sr))
161
+ if end_sample <= start_sample:
162
+ continue
163
+
164
+ seg = audio[start_sample:end_sample]
165
+ seg_i16 = (np.clip(seg, -1.0, 1.0) * 32767.0).astype(np.int16)
166
+
167
+ mem = io.BytesIO()
168
+ wav_write(mem, sr, seg_i16)
169
+ mem.seek(0)
170
+ b64 = base64.b64encode(mem.read()).decode("utf-8")
171
+ audio_src = f"data:audio/wav;base64,{b64}"
172
+
173
+ html_content += f"""
174
+ <div class="word-box">
175
+ <div class="word-text">{word}</div>
176
+ <div class="word-time">{start:.3f}s - {end:.3f}s</div>
177
+ <div class="word-audio">
178
+ <audio controls preload="none" src="{audio_src}"></audio>
179
+ </div>
180
+ </div>
181
+ """
182
+
183
+ html_content += "</div></details>"
184
+ return html_content
185
+
186
+
187
+ def get_model():
188
+ """Lazy load the model using vLLM backend for faster inference."""
189
+ global _asr_model
190
+ if _asr_model is None:
191
+ from qwen_asr import Qwen3ASRModel
192
+
193
+ # Use vLLM backend for much faster inference
194
+ _asr_model = Qwen3ASRModel.LLM(
195
+ model="Qwen/Qwen3-ASR-1.7B",
196
+ gpu_memory_utilization=0.9,
197
+ max_model_len=4096,
198
+ forced_aligner="Qwen/Qwen3-ForcedAligner-0.6B",
199
+ forced_aligner_kwargs=dict(
200
+ dtype=torch.bfloat16,
201
+ device_map="cuda",
202
+ ),
203
+ max_inference_batch_size=16,
204
+ )
205
+ return _asr_model
206
+
207
+
208
+ # Supported languages
209
+ SUPPORTED_LANGUAGES = [
210
+ "Chinese", "Cantonese", "English", "Arabic", "German", "French",
211
+ "Spanish", "Portuguese", "Indonesian", "Italian", "Korean", "Russian",
212
+ "Thai", "Vietnamese", "Japanese", "Turkish", "Hindi", "Malay",
213
+ "Dutch", "Swedish", "Danish", "Finnish", "Polish", "Czech",
214
+ "Filipino", "Persian", "Greek", "Romanian", "Hungarian", "Macedonian"
215
+ ]
216
+
217
+ lang_choices_disp, lang_map = _build_choices_and_map(SUPPORTED_LANGUAGES)
218
+ lang_choices = ["Auto"] + lang_choices_disp
219
+
220
+
221
+ @spaces.GPU(duration=120)
222
+ def transcribe(audio_upload: Any, lang_disp: str, return_ts: bool):
223
+ """
224
+ Main transcription function with ZeroGPU support.
225
+ """
226
+ if audio_upload is None:
227
+ return "", "", None, "<div style='color:#666'>Please upload an audio file first.</div>"
228
+
229
+ try:
230
+ audio_obj = _parse_audio_any(audio_upload)
231
+ except ValueError as e:
232
+ return "", "", None, f"<div style='color:red'>Error: {str(e)}</div>"
233
+
234
+ language = None
235
+ if lang_disp and lang_disp != "Auto":
236
+ language = lang_map.get(lang_disp, lang_disp)
237
+
238
+ # Get model (lazy loaded)
239
+ asr = get_model()
240
+
241
+ # Perform transcription
242
+ results = asr.transcribe(
243
+ audio=audio_obj,
244
+ language=language,
245
+ return_time_stamps=return_ts,
246
+ )
247
+
248
+ if not isinstance(results, list) or len(results) != 1:
249
+ return "", "", None, "<div style='color:red'>Unexpected result format.</div>"
250
+
251
+ r = results[0]
252
+
253
+ # Extract timestamps
254
+ ts_payload = None
255
+ if return_ts and hasattr(r, "time_stamps") and r.time_stamps:
256
+ ts_payload = [
257
+ dict(
258
+ text=getattr(t, "text", ""),
259
+ start_time=getattr(t, "start_time", 0),
260
+ end_time=getattr(t, "end_time", 0),
261
+ )
262
+ for t in r.time_stamps
263
+ ]
264
+
265
+ # Generate visualization HTML
266
+ viz_html = ""
267
+ if return_ts and ts_payload:
268
+ viz_html = _make_timestamp_html(audio_upload, ts_payload)
269
+
270
+ return (
271
+ getattr(r, "language", "") or "",
272
+ getattr(r, "text", "") or "",
273
+ ts_payload,
274
+ viz_html,
275
+ )
276
+
277
+
278
+ def visualize_timestamps(audio_upload: Any, timestamps_json: Any):
279
+ """Generate timestamp visualization from existing results."""
280
+ if timestamps_json is None:
281
+ return "<div style='color:#666'>No timestamps available. Please run transcription with timestamps enabled first.</div>"
282
+ return _make_timestamp_html(audio_upload, timestamps_json)
283
+
284
+
285
+ # Build Gradio interface
286
+ theme = gr.themes.Soft(
287
+ font=[gr.themes.GoogleFont("Source Sans Pro"), "Arial", "sans-serif"],
288
+ )
289
+
290
+ css = """
291
+ .gradio-container {max-width: none !important;}
292
+ .main-title {text-align: center; margin-bottom: 20px;}
293
+ """
294
+
295
+ with gr.Blocks(theme=theme, css=css, title="Qwen3-ASR Demo") as demo:
296
+ gr.Markdown(
297
+ """
298
+ # Qwen3-ASR Demo
299
+
300
+ **Model:** `Qwen3-ASR-1.7B` with `Qwen3-ForcedAligner-0.6B` | **Backend:** vLLM (high-speed inference)
301
+
302
+ Qwen3-ASR is a state-of-the-art automatic speech recognition model that supports **30+ languages** with high accuracy.
303
+ This demo showcases the 1.7B model which provides excellent multilingual recognition capabilities.
304
+
305
+ **Features:**
306
+ - Multi-language ASR (Chinese, English, Japanese, Korean, and 26+ more languages)
307
+ - Word/character-level timestamp alignment
308
+ - Interactive timestamp visualization - hear each word/character segment!
309
+ - Powered by vLLM for fast inference
310
+ """
311
+ )
312
+
313
+ with gr.Row():
314
+ with gr.Column(scale=2):
315
+ audio_in = gr.Audio(
316
+ label="Upload Audio",
317
+ type="numpy",
318
+ sources=["upload", "microphone"],
319
+ )
320
+ lang_in = gr.Dropdown(
321
+ label="Language (leave 'Auto' for automatic detection)",
322
+ choices=lang_choices,
323
+ value="Auto",
324
+ interactive=True,
325
+ )
326
+ ts_in = gr.Checkbox(
327
+ label="Enable Timestamps (recommended for visualization)",
328
+ value=True,
329
+ )
330
+ btn = gr.Button("Transcribe", variant="primary", size="lg")
331
+
332
+ with gr.Column(scale=2):
333
+ out_lang = gr.Textbox(label="Detected Language", lines=1, interactive=False)
334
+ out_text = gr.Textbox(label="Transcription Result", lines=10, interactive=False)
335
+
336
+ with gr.Column(scale=3):
337
+ out_ts = gr.JSON(label="Timestamps (JSON)")
338
+ viz_btn = gr.Button("Re-visualize Timestamps", variant="secondary")
339
+
340
+ with gr.Row():
341
+ out_ts_html = gr.HTML(label="Timestamps Visualization")
342
+
343
+ # Examples
344
+ gr.Markdown("### Examples")
345
+ gr.Examples(
346
+ examples=[
347
+ ["https://github.com/QwenLM/Qwen2-Audio/raw/refs/heads/main/assets/audio/1272-128104-0000.flac", "Auto", True],
348
+ ],
349
+ inputs=[audio_in, lang_in, ts_in],
350
+ label="Click to try an example",
351
+ )
352
+
353
+ # Event handlers
354
+ btn.click(
355
+ transcribe,
356
+ inputs=[audio_in, lang_in, ts_in],
357
+ outputs=[out_lang, out_text, out_ts, out_ts_html],
358
+ )
359
+ viz_btn.click(
360
+ visualize_timestamps,
361
+ inputs=[audio_in, out_ts],
362
+ outputs=[out_ts_html],
363
+ )
364
+
365
+ gr.Markdown(
366
+ """
367
+ ---
368
+ **Links:** [Qwen3-ASR on Hugging Face](https://huggingface.co/Qwen/Qwen3-ASR-1.7B) | [GitHub Repository](https://github.com/Qwen/Qwen3-ASR)
369
+ """
370
+ )
371
+
372
+
373
+ if __name__ == "__main__":
374
+ demo.launch()