KeisukeMiyamoto commited on
Commit
4df8719
·
verified ·
1 Parent(s): 82e11f0

Segment long audio with Silero VAD and transcribe batches of four

Browse files
Files changed (4) hide show
  1. README.md +7 -3
  2. app.py +21 -7
  3. requirements.txt +3 -0
  4. src/space/runtime.py +37 -8
README.md CHANGED
@@ -9,16 +9,20 @@ app_file: app.py
9
  pinned: false
10
  models:
11
  - KeisukeMiyamoto/lambda-audio-v6
12
- short_description: Japanese speech recognition for audio clips up to 30 seconds
13
  ---
14
 
15
  # LambdaAudio v6
16
 
17
  A simple Japanese speech recognition demo powered by
18
  [LambdaAudio v6](https://huggingface.co/KeisukeMiyamoto/lambda-audio-v6).
19
- Upload or record up to 30 seconds of audio, then select **文字起こし**.
 
 
 
20
 
21
- This Space uses **ZeroGPU**, with one transcription at a time. GPU availability
 
22
  and usage limits depend on the visitor's Hugging Face account.
23
 
24
  The FP16 Safetensors checkpoint is loaded into FP32 model parameters for inference.
 
9
  pinned: false
10
  models:
11
  - KeisukeMiyamoto/lambda-audio-v6
12
+ short_description: Japanese speech recognition with Silero VAD
13
  ---
14
 
15
  # LambdaAudio v6
16
 
17
  A simple Japanese speech recognition demo powered by
18
  [LambdaAudio v6](https://huggingface.co/KeisukeMiyamoto/lambda-audio-v6).
19
+ Upload or record Japanese audio, then select **文字起こし** (maximum file size: 200 MB).
20
+ Audio up to 30 seconds is transcribed directly. Longer recordings are segmented
21
+ with Silero VAD into speech clips of at most 20 seconds and transcribed in batches
22
+ of four. Results appear in chronological order as each batch finishes.
23
 
24
+ This Space uses **ZeroGPU**, with one recording at a time and a separate GPU
25
+ allocation for each batch. GPU availability
26
  and usage limits depend on the visitor's Hugging Face account.
27
 
28
  The FP16 Safetensors checkpoint is loaded into FP32 model parameters for inference.
app.py CHANGED
@@ -2,8 +2,12 @@ import spaces
2
 
3
  # isort: split
4
 
 
 
 
5
  import gradio as gr
6
  import torch
 
7
 
8
  from src.environment import load_hf_token
9
  from src.shared.transcription import transcribe
@@ -18,13 +22,22 @@ def create_app() -> gr.Blocks:
18
  components = load_space_components(load_hf_token())
19
  device = torch.device("cuda")
20
  components.model.to(device)
 
21
 
22
  @spaces.GPU(duration=30)
23
- def generate(audio: bytes) -> str:
24
- return transcribe(components.model, components.feature_extractor, components.tokenizer, config, device, [audio])[0].text
25
-
26
- def transcribe_recording(audio_path: str | None) -> str:
27
- return generate(read_recording(audio_path))
 
 
 
 
 
 
 
 
28
 
29
  # ---------------------------------------------------------
30
  # Provide one audio input and one transcription action.
@@ -32,7 +45,8 @@ def create_app() -> gr.Blocks:
32
  with gr.Blocks(title="LambdaAudio v6", delete_cache=(3600, 3600)) as app:
33
  gr.Markdown(
34
  "# LambdaAudio v6\n"
35
- "日本語の音声を文字起こしします。30秒以内の音声をアップロードするか、マイクで録音してください。\n\n"
 
36
  "[モデルの詳細](https://huggingface.co/KeisukeMiyamoto/lambda-audio-v6)"
37
  )
38
  audio = gr.Audio(sources=["upload", "microphone"], type="filepath", format="wav", label="音声", editable=False)
@@ -43,4 +57,4 @@ def create_app() -> gr.Blocks:
43
 
44
 
45
  if __name__ == "__main__":
46
- create_app().launch(max_file_size="20mb")
 
2
 
3
  # isort: split
4
 
5
+ from collections.abc import Iterator
6
+ from itertools import batched
7
+
8
  import gradio as gr
9
  import torch
10
+ from silero_vad import load_silero_vad
11
 
12
  from src.environment import load_hf_token
13
  from src.shared.transcription import transcribe
 
22
  components = load_space_components(load_hf_token())
23
  device = torch.device("cuda")
24
  components.model.to(device)
25
+ vad_model = load_silero_vad(onnx=True)
26
 
27
  @spaces.GPU(duration=30)
28
+ def generate(audio: list[bytes]) -> list[str]:
29
+ results = transcribe(components.model, components.feature_extractor, components.tokenizer, config, device, audio)
30
+ return [result.text for result in results]
31
+
32
+ def transcribe_recording(audio_path: str | None) -> Iterator[str]:
33
+ segments = read_recording(audio_path, vad_model)
34
+ if not segments:
35
+ yield "音声内に発話が見つかりませんでした。"
36
+ return
37
+ texts: list[str] = []
38
+ for batch in batched(segments, 4):
39
+ texts.extend(generate(list(batch)))
40
+ yield "\n".join(texts)
41
 
42
  # ---------------------------------------------------------
43
  # Provide one audio input and one transcription action.
 
45
  with gr.Blocks(title="LambdaAudio v6", delete_cache=(3600, 3600)) as app:
46
  gr.Markdown(
47
  "# LambdaAudio v6\n"
48
+ "日本語の音声をアップロードするか、マイクで録音してください。\n"
49
+ "30秒を超える音声は発話区間に分割し、順番に文字起こしします(最大200 MB)。\n\n"
50
  "[モデルの詳細](https://huggingface.co/KeisukeMiyamoto/lambda-audio-v6)"
51
  )
52
  audio = gr.Audio(sources=["upload", "microphone"], type="filepath", format="wav", label="音声", editable=False)
 
57
 
58
 
59
  if __name__ == "__main__":
60
+ create_app().launch(max_file_size="200mb")
requirements.txt CHANGED
@@ -4,10 +4,13 @@ einops>=0.8.0,<1.0.0
4
  huggingface-hub>=0.36.0,<2.0.0
5
  kernels==0.16.1
6
  numpy>=1.26.0,<3.0.0
 
7
  pyarrow>=15.0.0,<26.0.0
8
  pydantic>=2.0.0,<3.0.0
9
  python-dotenv>=1.1.0,<2.0.0
10
  safetensors==0.8.0
 
 
11
  spaces==0.51.3
12
  torch==2.13.0
13
  transformers==5.16.1
 
4
  huggingface-hub>=0.36.0,<2.0.0
5
  kernels==0.16.1
6
  numpy>=1.26.0,<3.0.0
7
+ onnxruntime==1.31.0
8
  pyarrow>=15.0.0,<26.0.0
9
  pydantic>=2.0.0,<3.0.0
10
  python-dotenv>=1.1.0,<2.0.0
11
  safetensors==0.8.0
12
+ silero-vad==6.2.3
13
+ soundfile>=0.12.0,<1.0.0
14
  spaces==0.51.3
15
  torch==2.13.0
16
  transformers==5.16.1
src/space/runtime.py CHANGED
@@ -1,10 +1,15 @@
 
1
  from pathlib import Path
2
  from typing import cast
3
 
4
  import av
5
  import gradio as gr
 
 
6
  from huggingface_hub import hf_hub_download
7
  from safetensors.torch import load_file
 
 
8
  from transformers import AutoFeatureExtractor, AutoTokenizer, SequenceFeatureExtractor
9
  from transformers.models.lfm2.configuration_lfm2 import Lfm2Config
10
 
@@ -13,7 +18,7 @@ from src.modeling.lambda_audio import LambdaAudioModel
13
  from src.modeling.lfm2_decoder import AudioLfm2ForCausalLM
14
  from src.modeling.loading import ModelComponents
15
  from src.modeling.zipformer import ZipformerConfig, ZipformerModel
16
- from src.shared.transcription import decode_audio_batch
17
 
18
 
19
  class SpaceConfig(ModelConfig):
@@ -52,18 +57,42 @@ def load_space_components(token: str) -> ModelComponents:
52
  )
53
 
54
 
55
- def read_recording(audio_path: str | None) -> bytes:
56
  # ---------------------------------------------------------
57
- # Reject missing, invalid, or oversized audio before requesting a GPU.
58
  # ---------------------------------------------------------
59
  if audio_path is None:
60
  raise gr.Error("音声をアップロードするか、録音してください。")
61
  path = Path(audio_path)
62
- if path.stat().st_size > 20 * 1024 * 1024:
63
- raise gr.Error("20 MB以下の音声ファイルを使用してください。")
64
  audio = path.read_bytes()
 
65
  try:
66
- decode_audio_batch([audio], SpaceConfig())
67
  except (av.FFmpegError, IndexError, ValueError) as error:
68
- raise gr.Error("30秒以内の、再生できる音声ファイルを使用してください。") from error
69
- return audio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
  from pathlib import Path
3
  from typing import cast
4
 
5
  import av
6
  import gradio as gr
7
+ import soundfile as sf
8
+ import torch
9
  from huggingface_hub import hf_hub_download
10
  from safetensors.torch import load_file
11
+ from silero_vad import get_speech_timestamps
12
+ from silero_vad.utils_vad import OnnxWrapper
13
  from transformers import AutoFeatureExtractor, AutoTokenizer, SequenceFeatureExtractor
14
  from transformers.models.lfm2.configuration_lfm2 import Lfm2Config
15
 
 
18
  from src.modeling.lfm2_decoder import AudioLfm2ForCausalLM
19
  from src.modeling.loading import ModelComponents
20
  from src.modeling.zipformer import ZipformerConfig, ZipformerModel
21
+ from src.shared.transcription import decode_audio
22
 
23
 
24
  class SpaceConfig(ModelConfig):
 
57
  )
58
 
59
 
60
+ def read_recording(audio_path: str | None, vad_model: OnnxWrapper) -> list[bytes]:
61
  # ---------------------------------------------------------
62
+ # Decode and validate audio before requesting a GPU.
63
  # ---------------------------------------------------------
64
  if audio_path is None:
65
  raise gr.Error("音声をアップロードするか、録音してください。")
66
  path = Path(audio_path)
67
+ if path.stat().st_size > 200 * 1024 * 1024:
68
+ raise gr.Error("200 MB以下の音声ファイルを使用してください。")
69
  audio = path.read_bytes()
70
+ config = SpaceConfig()
71
  try:
72
+ waveform = decode_audio(audio, config.sampling_rate)
73
  except (av.FFmpegError, IndexError, ValueError) as error:
74
+ raise gr.Error("再生できる音声ファイルを使用してください。") from error
75
+ if waveform.size <= config.sampling_rate * config.max_audio_seconds:
76
+ return [audio]
77
+
78
+ # ---------------------------------------------------------
79
+ # Split longer recordings at speech boundaries, at most 20 seconds.
80
+ # Encode lossless clips in chronological order for batched ASR.
81
+ # ---------------------------------------------------------
82
+ timestamps = get_speech_timestamps(
83
+ torch.from_numpy(waveform),
84
+ vad_model,
85
+ sampling_rate=config.sampling_rate,
86
+ max_speech_duration_s=20,
87
+ min_silence_duration_ms=300,
88
+ speech_pad_ms=100,
89
+ )
90
+ segments: list[bytes] = []
91
+ segment_samples = 20 * config.sampling_rate
92
+ for timestamp in timestamps:
93
+ for start in range(timestamp["start"], timestamp["end"], segment_samples):
94
+ end = min(start + segment_samples, timestamp["end"])
95
+ buffer = BytesIO()
96
+ sf.write(buffer, waveform[start:end], config.sampling_rate, format="WAV", subtype="FLOAT")
97
+ segments.append(buffer.getvalue())
98
+ return segments