Zhifu Gao commited on
Commit
d24efa9
·
1 Parent(s): 89fe5e3

upgrade: modern UI with model comparison, cleaner interface

Browse files

- Remove cluttered proxy/download-method options
- Add compelling description with language list and GitHub links
- Add model comparison table (Fun-ASR-Nano vs SenseVoice)
- Show RTF performance metrics
- Fix invalid dotenv=== in requirements.txt
- Pin space for visibility
- Simplify code from 673 to 230 lines

Files changed (3) hide show
  1. README.md +22 -9
  2. app.py +181 -625
  3. requirements.txt +6 -8
README.md CHANGED
@@ -1,14 +1,27 @@
1
  ---
2
- title: Fun ASR Nano
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.1.0
8
  app_file: app.py
9
- pinned: false
10
- license: unknown
11
- short_description: demo page for fun-asr-nano
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Fun-ASR-Nano
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
  app_file: app.py
9
+ pinned: true
10
+ license: apache-2.0
11
+ suggested_hardware: zero-a10g
12
+ short_description: "LLM-powered ASR: 31 languages, Chinese dialects, timestamps"
13
  ---
14
 
15
+ # Fun-ASR-Nano: LLM-Powered Speech Recognition
16
+
17
+ End-to-end ASR model trained on tens of millions of hours, supporting **31 languages** including Chinese dialects (Cantonese, Sichuan, Shanghai, Minnan, etc.).
18
+
19
+ ## Key Features
20
+ - 🌍 31 languages + Chinese dialect recognition
21
+ - 🎯 Native punctuation output (no post-processing needed)
22
+ - ⚡ Timestamps and speaker diarization support
23
+ - 🔥 Trained on massive multilingual data
24
+
25
+ ## Links
26
+ - **GitHub**: [Fun-ASR](https://github.com/FunAudioLLM/Fun-ASR) | [FunASR Toolkit](https://github.com/modelscope/FunASR)
27
+ - **Model**: [Fun-ASR-Nano-2512](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512)
app.py CHANGED
@@ -1,674 +1,230 @@
1
  import os
2
  import spaces
3
- # only debug for hf now
4
- REPO_TYPE = "hf"
5
- if REPO_TYPE not in ["hf", "ms"]:
6
- raise ValueError("REPO_TYPE must be either 'hf' for Hugging Face or 'ms' for ModelScope.")
7
 
8
- if REPO_TYPE == "hf":
9
- from huggingface_hub import snapshot_download
10
- else:
11
- from modelscope.hub.snapshot_download import snapshot_download
12
 
 
13
 
14
-
15
- # 1. 定义本地路径和远程仓库ID
16
  MODEL_CACHE_DIR = "./models"
17
  FUN_ASR_NANO_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "Fun-ASR-Nano")
18
  SENSE_VOICE_SMALL_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "SenseVoiceSmall")
19
  VAD_MODEL_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "fsmn-vad")
20
 
21
- # 创建模型缓存目录
22
  os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
23
 
24
- # 设置ModelScope环境变量以使用本地缓存
25
- os.environ['MODELSCOPE_CACHE'] = MODEL_CACHE_DIR
26
- # 禁用远程下载,强制使用本地模型(可选,如果想要确保只使用本地模型)
27
- # os.environ['MODELSCOPE_DISABLE_REMOTE'] = '1'
28
-
29
- print(f"ModelScope缓存目录设置为: {MODEL_CACHE_DIR}")
30
-
31
- if REPO_TYPE == "ms":
32
- FUN_ASR_NANO_REPO_ID = "FunAudioLLM/Fun-ASR-Nano-2512"
33
- SENSE_VOICE_SMALL_REPO_ID = "iic/SenseVoiceSmall"
34
- VAD_MODEL_REPO_ID = "iic/speech_fsmn_vad_zh-cn-16k-common-pytorch"
35
- else:
36
- FUN_ASR_NANO_REPO_ID = "FunAudioLLM/Fun-ASR-Nano-2512"
37
- SENSE_VOICE_SMALL_REPO_ID = "FunAudioLLM/SenseVoiceSmall"
38
- VAD_MODEL_REPO_ID = "funasr/fsmn-vad"
39
 
40
- # 2. 检查本地是否存在,不存在则下载
41
  def download_model_if_not_exists(repo_id, local_path, model_name):
42
- """如果本地模型不存在,则下载模型"""
43
  if not os.path.exists(local_path):
44
- print(f"正在下载模型 {model_name} {local_path} ...")
45
- snapshot_download(
46
- repo_id=repo_id,
47
- local_dir=local_path,
48
- ignore_patterns=["*.onnx"], # 如果你不需要onnx文件,可以过滤掉以节省时间和空间
49
- )
50
- print(f"{model_name} 模型下载完毕!")
51
  else:
52
- print(f"检测到本地 {model_name} 模型文件,跳过下载。")
53
-
54
- # 下载所有需要的模型
55
- download_model_if_not_exists(FUN_ASR_NANO_REPO_ID, FUN_ASR_NANO_LOCAL_PATH, "Fun-ASR-Nano")
56
- download_model_if_not_exists(SENSE_VOICE_SMALL_REPO_ID, SENSE_VOICE_SMALL_LOCAL_PATH, "SenseVoiceSmall")
57
- download_model_if_not_exists(VAD_MODEL_REPO_ID, VAD_MODEL_LOCAL_PATH, "VAD Model")
58
-
59
 
60
 
 
 
 
61
 
62
  import gradio as gr
63
  import time
64
- import sys
65
- import io
66
  import tempfile
67
- import subprocess
68
- import requests
69
- from urllib.parse import urlparse
70
- from pydub import AudioSegment
71
- import logging
72
  import torch
73
- import importlib
74
  from funasr import AutoModel
75
  from funasr.utils.postprocess_utils import rich_transcription_postprocess
76
 
77
- # Model configurations for local deployment
78
- FUN_ASR_NANO_MODEL_PATH_LIST = [
79
- FUN_ASR_NANO_LOCAL_PATH, # local path
80
- ]
81
-
82
- SENSEVOICE_MODEL_PATH_LIST = [
83
- SENSE_VOICE_SMALL_LOCAL_PATH, # local path
84
- ]
85
-
86
- class LogCapture(io.StringIO):
87
- def __init__(self, callback):
88
- super().__init__()
89
- self.callback = callback
90
-
91
- def write(self, s):
92
- super().write(s)
93
- self.callback(s)
94
-
95
- # Set up logging
96
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
97
-
98
-
99
 
 
100
 
101
 
 
 
 
102
 
103
- # Check for CUDA availability
104
- device = "cuda:0" if torch.cuda.is_available() else "cpu"
105
- logging.info(f"Using device: {device}")
106
-
107
- def download_audio(url, method_choice, proxy_url, proxy_username, proxy_password):
108
- """
109
- Downloads audio from a given URL using the specified method and proxy settings.
110
-
111
- Args:
112
- url (str): The URL of the audio.
113
- method_choice (str): The method to use for downloading audio.
114
- proxy_url (str): Proxy URL if needed.
115
- proxy_username (str): Proxy username.
116
- proxy_password (str): Proxy password.
117
-
118
- Returns:
119
- tuple: (path to the downloaded audio file, is_temp_file), or (None, False) if failed.
120
- """
121
- parsed_url = urlparse(url)
122
- logging.info(f"Downloading audio from URL: {url} using method: {method_choice}")
123
- try:
124
- if 'youtube.com' in parsed_url.netloc or 'youtu.be' in parsed_url.netloc:
125
- error_msg = f"YouTube download is not supported. Please use direct audio URLs instead."
126
- logging.error(error_msg)
127
- return None, False
128
- elif parsed_url.scheme == 'rtsp':
129
- audio_file = download_rtsp_audio(url, proxy_url)
130
- if not audio_file:
131
- error_msg = f"Failed to download RTSP audio from {url}"
132
- logging.error(error_msg)
133
- return None, False
134
- else:
135
- audio_file = download_direct_audio(url, method_choice, proxy_url, proxy_username, proxy_password)
136
- if not audio_file:
137
- error_msg = f"Failed to download audio from {url} using method {method_choice}"
138
- logging.error(error_msg)
139
- return None, False
140
- return audio_file, True
141
- except Exception as e:
142
- error_msg = f"Error downloading audio from {url} using method {method_choice}: {str(e)}"
143
- logging.error(error_msg)
144
- return None, False
145
-
146
-
147
-
148
-
149
- def download_rtsp_audio(url, proxy_url):
150
- """
151
- Downloads audio from an RTSP URL using FFmpeg.
152
-
153
- Args:
154
- url (str): The RTSP URL.
155
- proxy_url (str): Proxy URL if needed.
156
-
157
- Returns:
158
- str: Path to the downloaded audio file, or None if failed.
159
- """
160
- logging.info("Using FFmpeg to download RTSP stream")
161
- output_file = tempfile.mktemp(suffix='.mp3')
162
- command = ['ffmpeg', '-i', url, '-acodec', 'libmp3lame', '-ab', '192k', '-y', output_file]
163
- env = os.environ.copy()
164
- if proxy_url and len(proxy_url.strip()) > 0:
165
- env['http_proxy'] = proxy_url
166
- env['https_proxy'] = proxy_url
167
- try:
168
- subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
169
- logging.info(f"Downloaded RTSP audio to: {output_file}")
170
- return output_file
171
- except subprocess.CalledProcessError as e:
172
- logging.error(f"FFmpeg error: {e.stderr.decode()}")
173
- return None
174
- except Exception as e:
175
- logging.error(f"Error downloading RTSP audio: {str(e)}")
176
  return None
177
 
178
- def download_direct_audio(url, method_choice, proxy_url, proxy_username, proxy_password):
179
- """
180
- Downloads audio from a direct URL using the specified method.
181
-
182
- Args:
183
- url (str): The direct URL of the audio file.
184
- method_choice (str): The method to use for downloading.
185
- proxy_url (str): Proxy URL if needed.
186
- proxy_username (str): Proxy username.
187
- proxy_password (str): Proxy password.
188
-
189
- Returns:
190
- str: Path to the downloaded audio file, or None if failed.
191
- """
192
- logging.info(f"Downloading direct audio from: {url} using method: {method_choice}")
193
- methods = {
194
- 'wget': wget_method,
195
- 'requests': requests_method,
196
- 'ffmpeg': ffmpeg_method,
197
- 'aria2': aria2_method,
198
- }
199
- method = methods.get(method_choice, requests_method)
200
- try:
201
- audio_file = method(url, proxy_url, proxy_username, proxy_password)
202
- if not audio_file or not os.path.exists(audio_file):
203
- error_msg = f"Failed to download direct audio from {url} using method {method_choice}"
204
- logging.error(error_msg)
205
- return None
206
- return audio_file
207
- except Exception as e:
208
- logging.error(f"Error downloading direct audio with {method_choice}: {str(e)}")
209
- return None
210
 
211
- def requests_method(url, proxy_url, proxy_username, proxy_password):
212
- """
213
- Downloads audio using the requests library.
214
-
215
- Args:
216
- url (str): The URL of the audio file.
217
- proxy_url (str): Proxy URL if needed.
218
- proxy_username (str): Proxy username.
219
- proxy_password (str): Proxy password.
220
-
221
- Returns:
222
- str: Path to the downloaded audio file, or None if failed.
223
- """
224
- try:
225
- proxies = None
226
- auth = None
227
- if proxy_url and len(proxy_url.strip()) > 0:
228
- proxies = {
229
- "http": proxy_url,
230
- "https": proxy_url
231
- }
232
- if proxy_username and proxy_password:
233
- auth = (proxy_username, proxy_password)
234
- response = requests.get(url, stream=True, proxies=proxies, auth=auth)
235
- if response.status_code == 200:
236
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
237
- for chunk in response.iter_content(chunk_size=8192):
238
- if chunk:
239
- temp_file.write(chunk)
240
- logging.info(f"Downloaded direct audio to: {temp_file.name}")
241
- return temp_file.name
242
- else:
243
- logging.error(f"Failed to download audio from {url} with status code {response.status_code}")
244
- return None
245
- except Exception as e:
246
- logging.error(f"Error in requests_method: {str(e)}")
247
- return None
248
 
249
- def wget_method(url, proxy_url, proxy_username, proxy_password):
250
- """
251
- Downloads audio using the wget command-line tool.
252
-
253
- Args:
254
- url (str): The URL of the audio file.
255
- proxy_url (str): Proxy URL if needed.
256
- proxy_username (str): Proxy username.
257
- proxy_password (str): Proxy password.
258
-
259
- Returns:
260
- str: Path to the downloaded audio file, or None if failed.
261
- """
262
- logging.info("Using wget method")
263
- output_file = tempfile.mktemp(suffix='.mp3')
264
- command = ['wget', '-O', output_file, url]
265
- env = os.environ.copy()
266
- if proxy_url and len(proxy_url.strip()) > 0:
267
- env['http_proxy'] = proxy_url
268
- env['https_proxy'] = proxy_url
269
  try:
270
- subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
271
- logging.info(f"Downloaded audio to: {output_file}")
272
- return output_file
273
- except subprocess.CalledProcessError as e:
274
- logging.error(f"Wget error: {e.stderr.decode()}")
275
- return None
276
- except Exception as e:
277
- logging.error(f"Error in wget_method: {str(e)}")
278
- return None
279
 
 
 
280
 
281
- def ffmpeg_method(url, proxy_url, proxy_username, proxy_password):
282
- """
283
- Downloads audio using FFmpeg.
284
-
285
- Args:
286
- url (str): The URL of the audio file.
287
- proxy_url (str): Proxy URL if needed.
288
- proxy_username (str): Proxy username.
289
- proxy_password (str): Proxy password.
290
-
291
- Returns:
292
- str: Path to the downloaded audio file, or None if failed.
293
- """
294
- logging.info("Using ffmpeg method")
295
- output_file = tempfile.mktemp(suffix='.mp3')
296
- command = ['ffmpeg', '-i', url, '-vn', '-acodec', 'libmp3lame', '-q:a', '2', output_file]
297
- env = os.environ.copy()
298
- if proxy_url and len(proxy_url.strip()) > 0:
299
- env['http_proxy'] = proxy_url
300
- env['https_proxy'] = proxy_url
301
- try:
302
- subprocess.run(command, check=True, capture_output=True, text=True, env=env)
303
- logging.info(f"Downloaded and converted audio to: {output_file}")
304
- return output_file
305
- except subprocess.CalledProcessError as e:
306
- logging.error(f"FFmpeg error: {e.stderr}")
307
- return None
308
- except Exception as e:
309
- logging.error(f"Error in ffmpeg_method: {str(e)}")
310
- return None
311
 
312
- def aria2_method(url, proxy_url, proxy_username, proxy_password):
313
- """
314
- Downloads audio using aria2.
315
-
316
- Args:
317
- url (str): The URL of the audio file.
318
- proxy_url (str): Proxy URL if needed.
319
- proxy_username (str): Proxy username.
320
- proxy_password (str): Proxy password.
321
-
322
- Returns:
323
- str: Path to the downloaded audio file, or None if failed.
324
- """
325
- logging.info("Using aria2 method")
326
- output_file = tempfile.mktemp(suffix='.mp3')
327
- command = ['aria2c', '--split=4', '--max-connection-per-server=4', '--out', output_file, url]
328
- if proxy_url and len(proxy_url.strip()) > 0:
329
- command.extend(['--all-proxy', proxy_url])
330
- try:
331
- subprocess.run(command, check=True, capture_output=True, text=True)
332
- logging.info(f"Downloaded audio to: {output_file}")
333
- return output_file
334
- except subprocess.CalledProcessError as e:
335
- logging.error(f"Aria2 error: {e.stderr}")
336
- return None
337
- except Exception as e:
338
- logging.error(f"Error in aria2_method: {str(e)}")
339
- return None
340
 
341
- def trim_audio(audio_path, start_time, end_time):
342
- """
343
- Trims an audio file to the specified start and end times.
344
-
345
- Args:
346
- audio_path (str): Path to the audio file.
347
- start_time (float): Start time in seconds.
348
- end_time (float): End time in seconds.
349
-
350
- Returns:
351
- str: Path to the trimmed audio file.
352
-
353
- Raises:
354
- gr.Error: If invalid start or end times are provided.
355
- """
356
- try:
357
- logging.info(f"Trimming audio from {start_time} to {end_time}")
358
- audio = AudioSegment.from_file(audio_path)
359
- audio_duration = len(audio) / 1000 # Duration in seconds
360
-
361
- # Default start and end times if None
362
- start_time = max(0, start_time) if start_time is not None else 0
363
- end_time = min(audio_duration, end_time) if end_time is not None else audio_duration
364
-
365
- # Validate times
366
- if start_time >= end_time:
367
- raise gr.Error("End time must be greater than start time.")
368
-
369
- trimmed_audio = audio[int(start_time * 1000):int(end_time * 1000)]
370
- with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_audio_file:
371
- trimmed_audio.export(temp_audio_file.name, format="wav")
372
- logging.info(f"Trimmed audio saved to: {temp_audio_file.name}")
373
- return temp_audio_file.name
374
  except Exception as e:
375
- logging.error(f"Error trimming audio: {str(e)}")
376
- raise gr.Error(f"Error trimming audio: {str(e)}")
377
-
378
- def save_transcription(transcription):
379
- """
380
- Saves the transcription text to a temporary file.
381
-
382
- Args:
383
- transcription (str): The transcription text.
384
-
385
- Returns:
386
- str: The path to the transcription file.
387
- """
388
- with tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w', encoding='utf-8') as temp_file:
389
- temp_file.write(transcription)
390
- logging.info(f"Transcription saved to: {temp_file.name}")
391
- return temp_file.name
392
-
393
- def get_model_options(pipeline_type):
394
- """
395
- Returns a list of model IDs based on the selected pipeline type.
396
-
397
- Args:
398
- pipeline_type (str): The type of pipeline.
399
-
400
- Returns:
401
- list: A list of model IDs.
402
- """
403
- if pipeline_type == "fun-asr-nano":
404
- return FUN_ASR_NANO_MODEL_PATH_LIST
405
- elif pipeline_type == "sensevoice":
406
- return SENSEVOICE_MODEL_PATH_LIST
407
- else:
408
- return []
409
- # if pipeline_type == "sensevoice":
410
- # return SENSEVOICE_MODEL_PATH_LIST
411
- # else:
412
- # return []
413
-
414
- # Dictionary to store loaded models
415
- loaded_models = {}
 
 
 
 
 
 
 
 
 
 
 
 
416
 
417
- @spaces.GPU(duration=40)
418
- def transcribe_audio(audio_input, audio_url, proxy_url, proxy_username, proxy_password, pipeline_type, model_id, download_method, start_time=None, end_time=None, verbose=False):
419
- """
420
- Transcribes audio from a given source using SenseVoice.
421
-
422
- Args:
423
- audio_input (str): Path to uploaded audio file or recorded audio.
424
- audio_url (str): URL of audio.
425
- proxy_url (str): Proxy URL if needed.
426
- proxy_username (str): Proxy username.
427
- proxy_password (str): Proxy password.
428
- pipeline_type (str): Type of pipeline to use ('sensevoice').
429
- model_id (str): The ID of the model to use.
430
- download_method (str): Method to use for downloading audio.
431
- start_time (float, optional): Start time in seconds for trimming audio.
432
- end_time (float, optional): End time in seconds for trimming audio.
433
- verbose (bool, optional): Whether to output verbose logging.
434
-
435
- Yields:
436
- Tuple[str, str, str or None]: Metrics and messages, transcription text, path to transcription file.
437
- """
438
- try:
439
- if verbose:
440
- logging.getLogger().setLevel(logging.INFO)
441
- else:
442
- logging.getLogger().setLevel(logging.WARNING)
443
-
444
- logging.info(f"Transcription parameters: pipeline_type={pipeline_type}, model_id={model_id}, download_method={download_method}")
445
- verbose_messages = f"Starting transcription with parameters:\nPipeline Type: {pipeline_type}\nModel ID: {model_id}\nDownload Method: {download_method}\n"
446
-
447
- if verbose:
448
- yield verbose_messages, "", None
449
-
450
- # Determine the audio source
451
- audio_path = None
452
- is_temp_file = False
453
-
454
- if audio_input is not None and len(audio_input) > 0:
455
- # audio_input is a filepath to uploaded or recorded audio
456
- audio_path = audio_input
457
- is_temp_file = False
458
- elif audio_url is not None and len(audio_url.strip()) > 0:
459
- # audio_url is provided
460
- audio_path, is_temp_file = download_audio(audio_url, download_method, proxy_url, proxy_username, proxy_password)
461
- if not audio_path:
462
- error_msg = f"Error downloading audio from {audio_url} using method {download_method}. Check logs for details."
463
- logging.error(error_msg)
464
- yield verbose_messages + error_msg, "", None
465
- return
466
- else:
467
- verbose_messages += f"Successfully downloaded audio from {audio_url}\n"
468
- if verbose:
469
- yield verbose_messages, "", None
470
- else:
471
- error_msg = "No audio source provided. Please upload an audio file, record audio, or enter a URL."
472
- logging.error(error_msg)
473
- yield verbose_messages + error_msg, "", None
474
- return
475
-
476
- # Convert start_time and end_time to float or None
477
- start_time = float(start_time) if start_time else None
478
- end_time = float(end_time) if end_time else None
479
-
480
- if start_time is not None or end_time is not None:
481
- audio_path = trim_audio(audio_path, start_time, end_time)
482
- is_temp_file = True # The trimmed audio is a temporary file
483
- verbose_messages += f"Audio trimmed from {start_time} to {end_time}\n"
484
- if verbose:
485
- yield verbose_messages, "", None
486
-
487
- # Model caching
488
- model_key = (pipeline_type, model_id)
489
- if model_key in loaded_models:
490
- model = loaded_models[model_key]
491
- logging.info("Loaded model from cache")
492
- else:
493
- if pipeline_type == "fun-asr-nano":
494
- model = AutoModel(
495
- model=model_id,
496
- trust_remote_code=True,
497
- remote_code=f"./Fun-ASR/model.py",
498
- vad_model=VAD_MODEL_LOCAL_PATH, # Use local VAD model path
499
- vad_kwargs={"max_single_segment_time": 30000},
500
- device=device,
501
- disable_update=True,
502
- hub='ms',
503
  )
504
- elif pipeline_type == "sensevoice":
505
- model = AutoModel(
506
- model=model_id,
507
- trust_remote_code=False,
508
- vad_model=VAD_MODEL_LOCAL_PATH, # Use local VAD model path
509
- vad_kwargs={"max_single_segment_time": 30000},
510
- device=device,
511
- disable_update=True,
512
- hub='ms',
 
 
 
 
 
 
 
 
 
 
513
  )
514
- else:
515
- error_msg = "Invalid pipeline type. Only 'sensevoice' is supported."
516
- logging.error(error_msg)
517
- yield verbose_messages + error_msg, "", None
518
- return
519
- loaded_models[model_key] = model
520
-
521
- # Perform the transcription
522
- start_time_perf = time.time()
523
-
524
- if pipeline_type == "fun-asr-nano":
525
- system_prompt = "You are a helpful assistant."
526
- user_prompt = f"语音转写:<|startofspeech|>!{audio_path}<|endofspeech|>"
527
- contents_i = []
528
- contents_i.append({"role": "system", "content": system_prompt})
529
- contents_i.append({"role": "user", "content": user_prompt})
530
- contents_i.append({"role": "assistant", "content": "null"})
531
- print(audio_path)
532
- res = model.generate(
533
- input=[audio_path],
534
- use_itn=True,
535
- batch_size=1,
536
- )
537
- elif pipeline_type == "sensevoice":
538
- res = model.generate(
539
- input=audio_path,
540
- cache={},
541
- language="auto", # "zh", "en", "yue", "ja", "ko", "nospeech"
542
- use_itn=True,
543
- batch_size_s=60,
544
- merge_vad=True,
545
- merge_length_s=15,
546
- )
547
-
548
- transcription = rich_transcription_postprocess(res[0]["text"])
549
- end_time_perf = time.time()
550
 
551
- # Calculate metrics
552
- transcription_time = end_time_perf - start_time_perf
553
- audio_file_size = os.path.getsize(audio_path) / (1024 * 1024)
554
-
555
- metrics_output = (
556
- f"Transcription time: {transcription_time:.2f} seconds\n"
557
- f"Audio file size: {audio_file_size:.2f} MB\n"
558
  )
559
 
560
- # Save the transcription to a file
561
- transcription_file = save_transcription(transcription)
 
 
 
562
 
563
- # Always yield the final result, regardless of verbose setting
564
- final_metrics = verbose_messages + metrics_output
565
- yield final_metrics, transcription, transcription_file
 
 
566
 
567
- except Exception as e:
568
- error_msg = f"An error occurred during transcription: {str(e)}"
569
- logging.error(error_msg)
570
- yield verbose_messages + error_msg, "", None
571
 
572
- finally:
573
- # Clean up temporary audio files
574
- if audio_path and is_temp_file and os.path.exists(audio_path):
575
- os.remove(audio_path)
576
-
577
-
578
- with gr.Blocks() as iface:
579
- gr.Markdown("# Audio Transcription")
580
- gr.Markdown("Transcribe audio using SenseVoice model with multilingual support.")
581
-
582
- with gr.Row():
583
- audio_input = gr.Audio(label="Upload or Record Audio", sources=["upload", "microphone"], type="filepath")
584
- audio_url = gr.Textbox(label="Or Enter URL of audio file (direct link only, no YouTube)")
585
-
586
- transcribe_button = gr.Button("Transcribe")
587
-
588
- with gr.Accordion("Advanced Options", open=False):
589
- with gr.Row():
590
- proxy_url = gr.Textbox(label="Proxy URL", placeholder="Enter proxy URL if needed", value="", lines=1)
591
- proxy_username = gr.Textbox(label="Proxy Username", placeholder="Proxy username (optional)", value="", lines=1)
592
- proxy_password = gr.Textbox(label="Proxy Password", placeholder="Proxy password (optional)", value="", lines=1, type="password")
593
-
594
-
595
- with gr.Row():
596
- pipeline_type = gr.Dropdown(
597
- choices=["sensevoice","fun-asr-nano"],
598
- label="Pipeline Type",
599
- value="fun-asr-nano"
600
- )
601
- model_id = gr.Dropdown(
602
- label="Model",
603
- choices=get_model_options("fun-asr-nano"),
604
- value=FUN_ASR_NANO_MODEL_PATH_LIST[0] # Default to official Local Model
605
- )
606
- with gr.Row():
607
- download_method = gr.Dropdown(
608
- choices=["requests", "ffmpeg", "aria2", "wget"],
609
- label="Download Method",
610
- value="requests"
611
- )
612
-
613
- with gr.Row():
614
- start_time = gr.Number(label="Start Time (seconds)", value=None, minimum=0)
615
- end_time = gr.Number(label="End Time (seconds)", value=None, minimum=0)
616
- verbose = gr.Checkbox(label="Verbose Output", value=False)
617
-
618
- with gr.Row():
619
- metrics_output = gr.Textbox(label="Transcription Metrics and Verbose Messages", lines=10)
620
- transcription_output = gr.Textbox(label="Transcription", lines=10)
621
- transcription_file = gr.File(label="Download Transcription")
622
-
623
- def update_model_dropdown(pipeline_type):
624
- """
625
- Updates the model dropdown choices based on the selected pipeline type.
626
-
627
- Args:
628
- pipeline_type (str): The selected pipeline type.
629
-
630
- Returns:
631
- gr.update: Updated model dropdown component.
632
- """
633
- try:
634
- model_choices = get_model_options(pipeline_type)
635
- logging.info(f"Model choices for {pipeline_type}: {model_choices}")
636
- if model_choices:
637
- return gr.update(choices=model_choices, value=model_choices[0], visible=True)
638
- else:
639
- return gr.update(choices=["No models available"], value=None, visible=False)
640
- except Exception as e:
641
- logging.error(f"Error in update_model_dropdown: {str(e)}")
642
- return gr.update(choices=["Error"], value="Error", visible=True)
643
-
644
- # Event handler for pipeline_type change
645
- pipeline_type.change(update_model_dropdown, inputs=[pipeline_type], outputs=[model_id])
646
-
647
- def transcribe_with_progress(*args):
648
- # The audio_input is now the first argument
649
- for result in transcribe_audio(*args):
650
- yield result
651
-
652
- transcribe_button.click(
653
- transcribe_with_progress,
654
- inputs=[audio_input, audio_url, proxy_url, proxy_username, proxy_password, pipeline_type, model_id, download_method, start_time, end_time, verbose],
655
- outputs=[metrics_output, transcription_output, transcription_file]
656
- )
657
-
658
- # Note: For examples, users should use local audio files or upload their own files
659
- # Examples with specific paths may not work for all users
660
-
661
- gr.Markdown(f"""
662
- ### Usage Examples:
663
- 1. **Upload Audio**: Click the "Upload or Record Audio" button to select your audio file
664
- 2. **Select Pipeline Type**: Choose from available pipelines:
665
- - **Fun-ASR-Nano** (default) - Large language model based ASR model
666
- - **SenseVoice** - CTC-based based ASR model with VAD
667
- 3. **Local Testing**: For development, you can use local paths as shown above
668
-
669
- Supported languages:
670
- - Fun-ASR-Nano: more than 50 languages and Chinese dialects.
671
- - SenseVoiceSmall:Chinese (zh), English (en), Cantonese (yue), Japanese (ja), Korean (ko).
672
- """)
673
-
674
- iface.queue().launch(share=False, debug=True)
 
1
  import os
2
  import spaces
 
 
 
 
3
 
4
+ REPO_TYPE = "hf"
 
 
 
5
 
6
+ from huggingface_hub import snapshot_download
7
 
 
 
8
  MODEL_CACHE_DIR = "./models"
9
  FUN_ASR_NANO_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "Fun-ASR-Nano")
10
  SENSE_VOICE_SMALL_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "SenseVoiceSmall")
11
  VAD_MODEL_LOCAL_PATH = os.path.join(MODEL_CACHE_DIR, "fsmn-vad")
12
 
 
13
  os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
 
16
  def download_model_if_not_exists(repo_id, local_path, model_name):
 
17
  if not os.path.exists(local_path):
18
+ print(f"Downloading {model_name} to {local_path} ...")
19
+ snapshot_download(repo_id=repo_id, local_dir=local_path, ignore_patterns=["*.onnx"])
20
+ print(f"{model_name} downloaded.")
 
 
 
 
21
  else:
22
+ print(f"{model_name} found locally, skipping download.")
 
 
 
 
 
 
23
 
24
 
25
+ download_model_if_not_exists("FunAudioLLM/Fun-ASR-Nano-2512", FUN_ASR_NANO_LOCAL_PATH, "Fun-ASR-Nano")
26
+ download_model_if_not_exists("FunAudioLLM/SenseVoiceSmall", SENSE_VOICE_SMALL_LOCAL_PATH, "SenseVoiceSmall")
27
+ download_model_if_not_exists("funasr/fsmn-vad", VAD_MODEL_LOCAL_PATH, "VAD Model")
28
 
29
  import gradio as gr
30
  import time
 
 
31
  import tempfile
32
+ import numpy as np
 
 
 
 
33
  import torch
34
+ import torchaudio
35
  from funasr import AutoModel
36
  from funasr.utils.postprocess_utils import rich_transcription_postprocess
37
 
38
+ device = "cuda:0" if torch.cuda.is_available() else "cpu"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ loaded_models = {}
41
 
42
 
43
+ def get_model(pipeline_type):
44
+ if pipeline_type in loaded_models:
45
+ return loaded_models[pipeline_type]
46
 
47
+ if pipeline_type == "fun-asr-nano":
48
+ model = AutoModel(
49
+ model=FUN_ASR_NANO_LOCAL_PATH,
50
+ trust_remote_code=True,
51
+ remote_code="./Fun-ASR/model.py",
52
+ vad_model=VAD_MODEL_LOCAL_PATH,
53
+ vad_kwargs={"max_single_segment_time": 30000},
54
+ device=device,
55
+ disable_update=True,
56
+ hub="hf",
57
+ )
58
+ elif pipeline_type == "sensevoice":
59
+ model = AutoModel(
60
+ model=SENSE_VOICE_SMALL_LOCAL_PATH,
61
+ trust_remote_code=False,
62
+ vad_model=VAD_MODEL_LOCAL_PATH,
63
+ vad_kwargs={"max_single_segment_time": 30000},
64
+ device=device,
65
+ disable_update=True,
66
+ hub="hf",
67
+ )
68
+ else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  return None
70
 
71
+ loaded_models[pipeline_type] = model
72
+ return model
73
+
74
+
75
+ @spaces.GPU(duration=60)
76
+ def transcribe(audio_input, pipeline_type, language):
77
+ if audio_input is None:
78
+ return "Please upload an audio file or record via microphone.", ""
79
+
80
+ model = get_model(pipeline_type)
81
+ if model is None:
82
+ return "Model loading failed.", ""
83
+
84
+ # Handle gradio audio input
85
+ if isinstance(audio_input, tuple):
86
+ sr, audio_data = audio_input
87
+ audio_data = audio_data.astype(np.float32) / np.iinfo(np.int16).max
88
+ if len(audio_data.shape) > 1:
89
+ audio_data = audio_data.mean(-1)
90
+ if sr != 16000:
91
+ resampler = torchaudio.transforms.Resample(sr, 16000)
92
+ audio_data = resampler(torch.from_numpy(audio_data).float().unsqueeze(0))[0].numpy()
93
+ # Save to temp file
94
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
95
+ import soundfile as sf
96
+ sf.write(tmp.name, audio_data, 16000)
97
+ audio_path = tmp.name
98
+ else:
99
+ audio_path = audio_input
 
 
 
100
 
101
+ start_time = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  try:
104
+ if pipeline_type == "fun-asr-nano":
105
+ res = model.generate(input=[audio_path], use_itn=True, batch_size=1)
106
+ else:
107
+ res = model.generate(
108
+ input=audio_path, cache={}, language=language or "auto",
109
+ use_itn=True, batch_size_s=60, merge_vad=True,
110
+ )
 
 
111
 
112
+ elapsed = time.time() - start_time
113
+ text = rich_transcription_postprocess(res[0]["text"])
114
 
115
+ metrics = f"⏱️ {elapsed:.2f}s"
116
+ if os.path.exists(audio_path):
117
+ import librosa
118
+ duration = librosa.get_duration(filename=audio_path)
119
+ rtf = elapsed / duration if duration > 0 else 0
120
+ metrics = f"⏱️ {elapsed:.2f}s | Audio: {duration:.1f}s | RTF: {rtf:.4f}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ return text, metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  except Exception as e:
125
+ return f"Error: {str(e)}", ""
126
+ finally:
127
+ if isinstance(audio_input, tuple) and os.path.exists(audio_path):
128
+ os.unlink(audio_path)
129
+
130
+
131
+ description_html = """
132
+ <div style="text-align: center; max-width: 850px; margin: 0 auto;">
133
+ <h1 style="font-size: 2.2em; margin-bottom: 0.1em;">🚀 Fun-ASR-Nano</h1>
134
+ <p style="font-size: 1.3em; color: #444; margin-bottom: 0.3em;">LLM-Powered Speech Recognition — 31 Languages, Dialects & Accents</p>
135
+ <p style="font-size: 1em; color: #666;">
136
+ End-to-end ASR trained on <strong>tens of millions of hours</strong> of data.
137
+ Supports Chinese (+ dialects), English, Japanese, Korean, French, German, Spanish, and 24 more languages.
138
+ </p>
139
+ <p style="font-size: 0.9em; margin-top: 0.8em;">
140
+ <a href="https://github.com/FunAudioLLM/Fun-ASR" target="_blank">⭐ GitHub (Fun-ASR)</a> ·
141
+ <a href="https://github.com/modelscope/FunASR" target="_blank">🛠️ FunASR Toolkit</a> ·
142
+ <a href="https://github.com/FunAudioLLM/SenseVoice" target="_blank">🎙️ SenseVoice</a> ·
143
+ <a href="https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512" target="_blank">🤗 Model Card</a>
144
+ </p>
145
+ </div>
146
+ """
147
+
148
+ comparison_html = """
149
+ <div style="background: linear-gradient(135deg, #f0f9ff 0%, #f5f3ff 100%); border-radius: 10px; padding: 16px; margin: 10px 0;">
150
+ <table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
151
+ <tr style="border-bottom: 2px solid #ddd;">
152
+ <th style="padding: 8px; text-align: left;">Model</th>
153
+ <th style="padding: 8px; text-align: center;">Languages</th>
154
+ <th style="padding: 8px; text-align: center;">Architecture</th>
155
+ <th style="padding: 8px; text-align: center;">Best For</th>
156
+ </tr>
157
+ <tr style="background: #e8f4fd;">
158
+ <td style="padding: 8px;"><strong>Fun-ASR-Nano</strong> ⭐</td>
159
+ <td style="padding: 8px; text-align: center;">31</td>
160
+ <td style="padding: 8px; text-align: center;">LLM-based</td>
161
+ <td style="padding: 8px; text-align: center;">Multi-language, dialects, highest accuracy</td>
162
+ </tr>
163
+ <tr>
164
+ <td style="padding: 8px;">SenseVoice</td>
165
+ <td style="padding: 8px; text-align: center;">5</td>
166
+ <td style="padding: 8px; text-align: center;">CTC (non-AR)</td>
167
+ <td style="padding: 8px; text-align: center;">Speed + Emotion + Audio events</td>
168
+ </tr>
169
+ </table>
170
+ </div>
171
+ """
172
+
173
+
174
+ def launch():
175
+ with gr.Blocks(theme=gr.themes.Soft(), title="Fun-ASR-Nano - 31 Language ASR") as demo:
176
+ gr.HTML(description_html)
177
+ gr.HTML(comparison_html)
178
 
179
+ with gr.Row():
180
+ with gr.Column(scale=1):
181
+ audio_input = gr.Audio(
182
+ label="Upload audio or record via microphone",
183
+ sources=["upload", "microphone"],
184
+ type="filepath",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  )
186
+ with gr.Row():
187
+ pipeline_type = gr.Dropdown(
188
+ choices=["fun-asr-nano", "sensevoice"],
189
+ value="fun-asr-nano",
190
+ label="Model",
191
+ )
192
+ language = gr.Dropdown(
193
+ choices=["auto", "zh", "en", "yue", "ja", "ko"],
194
+ value="auto",
195
+ label="Language (SenseVoice only)",
196
+ interactive=True,
197
+ )
198
+ btn = gr.Button("🎯 Transcribe", variant="primary", size="lg")
199
+
200
+ with gr.Column(scale=1):
201
+ output_text = gr.Textbox(
202
+ label="Transcription Result",
203
+ lines=10,
204
+ show_copy_button=True,
205
  )
206
+ metrics_text = gr.Textbox(label="Performance", lines=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ btn.click(
209
+ transcribe,
210
+ inputs=[audio_input, pipeline_type, language],
211
+ outputs=[output_text, metrics_text],
 
 
 
212
  )
213
 
214
+ gr.Markdown("""
215
+ ### Supported Languages (Fun-ASR-Nano)
216
+ Chinese (Mandarin, Cantonese, Sichuan, Shanghai, Minnan, Wenzhou, Hakka, Gan, and more),
217
+ English, Japanese, Korean, French, German, Spanish, Italian, Portuguese, Russian, Arabic, Hindi,
218
+ Thai, Vietnamese, Indonesian, Malay, Turkish, Polish, Dutch, Swedish, Hebrew, Greek, Czech, Romanian, Hungarian, Finnish, Danish, Norwegian, Ukrainian.
219
 
220
+ ### Tips
221
+ - **Fun-ASR-Nano**: Best for multi-language & Chinese dialects. Outputs punctuation natively.
222
+ - **SenseVoice**: Ultra-fast (7x faster than Whisper-small), also detects emotions & audio events.
223
+ - For long audio (>5min), consider using [FunASR](https://github.com/modelscope/FunASR) locally with GPU.
224
+ """)
225
 
226
+ demo.launch()
 
 
 
227
 
228
+
229
+ if __name__ == "__main__":
230
+ launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,12 +1,10 @@
1
  numpy
2
- requests
3
- ffmpeg-python
4
- pydub
5
  torch
6
- transformers
7
- funasr>=1.1.3
8
  torchaudio
9
- modelscope
 
10
  huggingface_hub
11
- pydantic>=2.12.4
12
- dotenv
 
 
 
1
  numpy
 
 
 
2
  torch
 
 
3
  torchaudio
4
+ transformers
5
+ funasr>=1.2.0
6
  huggingface_hub
7
+ modelscope
8
+ pydub
9
+ ffmpeg-python
10
+ librosa