Spaces:
Sleeping
Sleeping
Segment long audio with Silero VAD and transcribe batches of four
Browse files- README.md +7 -3
- app.py +21 -7
- requirements.txt +3 -0
- 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
|
| 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
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
This Space uses **ZeroGPU**, with one
|
|
|
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"日本語の音声を
|
|
|
|
| 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="
|
|
|
|
| 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
|
| 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 |
-
#
|
| 58 |
# ---------------------------------------------------------
|
| 59 |
if audio_path is None:
|
| 60 |
raise gr.Error("音声をアップロードするか、録音してください。")
|
| 61 |
path = Path(audio_path)
|
| 62 |
-
if path.stat().st_size >
|
| 63 |
-
raise gr.Error("
|
| 64 |
audio = path.read_bytes()
|
|
|
|
| 65 |
try:
|
| 66 |
-
|
| 67 |
except (av.FFmpegError, IndexError, ValueError) as error:
|
| 68 |
-
raise gr.Error("
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|