Skip to content

Commit 9041074

Browse files
authored
Merge pull request #3494 from wuhongsheng/silero_vad
feat(vad): add Silero VAD adapter
2 parents 79a0ed2 + e8ea59a commit 9041074

7 files changed

Lines changed: 220 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ from funasr import AutoModel
189189
model = AutoModel(model="paraformer-zh", vad_model="fsmn-vad", punc_model="ct-punc", spk_model="cam++", device="cuda")
190190
result = model.generate(input="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav", hotword="关键词 20")
191191

192+
# Optional Silero VAD (install first: python -m pip install "funasr[silero]")
193+
model = AutoModel(
194+
model="paraformer-zh", vad_model="silero-vad", device="cuda",
195+
vad_kwargs={"silero_threshold": 0.5, "silero_min_silence_duration_ms": 100},
196+
)
197+
result = model.generate(input="audio.wav")
192198

193199
# Streaming real-time (feed audio chunk by chunk)
194200
import soundfile as sf

README_zh.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@ from funasr import AutoModel
239239
model = AutoModel(model="paraformer-zh", vad_model="fsmn-vad", punc_model="ct-punc", spk_model="cam++", device="cuda")
240240
result = model.generate(input="https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav", hotword="关键词 20")
241241

242+
# 使用 Silero VAD(先安装:python -m pip install "funasr[silero]")
243+
model = AutoModel(
244+
model="paraformer-zh", vad_model="silero-vad", device="cpu",
245+
vad_kwargs={"silero_threshold": 0.5, "silero_min_silence_duration_ms": 100},
246+
)
247+
result = model.generate(input="audio.wav")
248+
242249
# 中/英/日 + 中文方言
243250
model = AutoModel(model="FunAudioLLM/Fun-ASR-Nano-2512", hub="hf", trust_remote_code=True,
244251
vad_model="fsmn-vad", vad_kwargs={"max_single_segment_time": 30000}, device="cuda")

funasr/auto/auto_model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,12 @@ def build_model(**kwargs):
536536
kwargs contains the resolved configuration.
537537
"""
538538
assert "model" in kwargs
539+
# Silero VAD is loaded by its optional Python package rather than a
540+
# FunASR model repository. Supplying model_conf keeps it on the normal
541+
# AutoModel construction path while bypassing hub config resolution.
542+
if kwargs["model"] in {"silero-vad", "silero_vad"}:
543+
kwargs.setdefault("model_conf", {})
544+
kwargs["model"] = "SileroVad"
539545
if "model_conf" not in kwargs:
540546
logging.info("download models from model hub: {}".format(kwargs.get("hub", "ms")))
541547
kwargs = download_model(**kwargs)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Silero VAD adapter for the FunASR AutoModel pipeline."""

funasr/models/silero_vad/model.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Adapter that makes Silero VAD return FunASR-compatible millisecond segments."""
2+
3+
import time
4+
5+
import torch
6+
7+
from funasr.register import tables
8+
from funasr.utils.load_utils import load_audio_text_image_video
9+
10+
11+
@tables.register("model_classes", "SileroVad")
12+
class SileroVad(torch.nn.Module):
13+
"""Offline Silero VAD adapter used by ``AutoModel(vad_model='silero-vad')``.
14+
15+
Requires the official ``silero-vad`` Python package.
16+
"""
17+
18+
def __init__(self, **kwargs):
19+
super().__init__()
20+
self.anchor = torch.nn.Parameter(torch.empty(0), requires_grad=False)
21+
try:
22+
from silero_vad import get_speech_timestamps, load_silero_vad
23+
except ImportError as error:
24+
raise ImportError(
25+
"Silero VAD requires the optional dependency. Install it with "
26+
'`python -m pip install "funasr[silero]"` or '
27+
"`python -m pip install silero-vad`."
28+
) from error
29+
self.onnx = bool(kwargs.get("silero_onnx", False))
30+
self.model = load_silero_vad(onnx=self.onnx)
31+
self.get_speech_timestamps = get_speech_timestamps
32+
33+
@staticmethod
34+
def _split_long_segments(segments, max_single_segment_time):
35+
if not max_single_segment_time:
36+
return segments
37+
limit_ms = int(max_single_segment_time)
38+
if limit_ms <= 0:
39+
raise ValueError(
40+
"max_single_segment_time must resolve to a positive millisecond value"
41+
)
42+
split = []
43+
for start, end in segments:
44+
while end - start > limit_ms:
45+
split.append([start, start + limit_ms])
46+
start += limit_ms
47+
split.append([start, end])
48+
return split
49+
50+
def inference(self, data_in, key=None, **kwargs):
51+
sample_rate = int(kwargs.get("silero_sampling_rate", 16000))
52+
if sample_rate not in (8000, 16000):
53+
raise ValueError("Silero VAD supports silero_sampling_rate=8000 or 16000")
54+
audio_list = load_audio_text_image_video(
55+
data_in,
56+
fs=sample_rate,
57+
audio_fs=kwargs.get("fs", sample_rate),
58+
data_type=kwargs.get("data_type", "sound"),
59+
)
60+
if not isinstance(audio_list, list):
61+
audio_list = [audio_list]
62+
63+
started = time.perf_counter()
64+
results = []
65+
for index, audio in enumerate(audio_list):
66+
device = torch.device("cpu") if self.onnx else self.anchor.device
67+
waveform = torch.as_tensor(audio, dtype=torch.float32).flatten().to(device)
68+
timestamps = self.get_speech_timestamps(
69+
waveform,
70+
self.model,
71+
sampling_rate=sample_rate,
72+
threshold=kwargs.get("silero_threshold", 0.5),
73+
min_speech_duration_ms=kwargs.get("silero_min_speech_duration_ms", 250),
74+
min_silence_duration_ms=kwargs.get(
75+
"silero_min_silence_duration_ms", 100
76+
),
77+
speech_pad_ms=kwargs.get("silero_speech_pad_ms", 30),
78+
)
79+
segments = [
80+
[
81+
int(item["start"] * 1000 / sample_rate),
82+
int(item["end"] * 1000 / sample_rate),
83+
]
84+
for item in timestamps
85+
]
86+
segments = self._split_long_segments(
87+
segments, kwargs.get("max_single_segment_time")
88+
)
89+
results.append(
90+
{"key": key[index] if key else str(index), "value": segments}
91+
)
92+
elapsed = time.perf_counter() - started
93+
total_samples = sum(len(torch.as_tensor(audio)) for audio in audio_list)
94+
return results, {
95+
"batch_data_time": total_samples / sample_rate,
96+
"forward": elapsed,
97+
}

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
"train": [
4949
"rapidfuzz>=3.0.0",
5050
],
51+
"silero": [
52+
"silero-vad>=6.0.0",
53+
],
5154
# all: The modules should be optionally installled due to some reason.
5255
# Please consider moving them to "install" occasionally
5356
"all": [

tests/test_silero_vad_adapter.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import unittest
2+
from importlib.util import find_spec
3+
from unittest.mock import patch
4+
5+
import torch
6+
7+
from funasr.auto.auto_model import AutoModel
8+
from funasr.models.silero_vad.model import SileroVad
9+
10+
11+
@unittest.skipUnless(find_spec("silero_vad"), "silero-vad is not installed")
12+
class TestSileroVadAdapter(unittest.TestCase):
13+
def _timestamps_stub(self, waveform, model, sampling_rate, **options):
14+
self.assertEqual(sampling_rate, 16000)
15+
self.assertEqual(options["threshold"], 0.6)
16+
return [{"start": 1600, "end": 17600}]
17+
18+
def _load_stub(self, *args, **kwargs):
19+
self.assertEqual(kwargs, {"onnx": False})
20+
return torch.nn.Identity()
21+
22+
@patch("silero_vad.get_speech_timestamps")
23+
@patch("silero_vad.load_silero_vad")
24+
def test_returns_funasr_millisecond_segments_and_honors_max_length(
25+
self, load_model, timestamps
26+
):
27+
load_model.side_effect = self._load_stub
28+
timestamps.side_effect = self._timestamps_stub
29+
model = SileroVad()
30+
results, metadata = model.inference(
31+
data_in=[torch.zeros(32000)],
32+
key=["sample"],
33+
silero_threshold=0.6,
34+
max_single_segment_time=500,
35+
)
36+
37+
self.assertEqual(
38+
results, [{"key": "sample", "value": [[100, 600], [600, 1100]]}]
39+
)
40+
self.assertEqual(metadata["batch_data_time"], 2.0)
41+
load_model.assert_called_once_with(onnx=False)
42+
43+
@patch("silero_vad.get_speech_timestamps")
44+
@patch("silero_vad.load_silero_vad")
45+
def test_rejects_unsupported_sampling_rate(self, load_model, timestamps):
46+
load_model.side_effect = self._load_stub
47+
model = SileroVad()
48+
with self.assertRaisesRegex(ValueError, "8000 or 16000"):
49+
model.inference(data_in=[torch.zeros(16000)], silero_sampling_rate=44100)
50+
51+
@patch("silero_vad.get_speech_timestamps")
52+
@patch("silero_vad.load_silero_vad")
53+
def test_auto_model_alias_uses_the_existing_vad_build_path(
54+
self, load_model, timestamps
55+
):
56+
load_model.side_effect = self._load_stub
57+
model, resolved = AutoModel.build_model(model="silero-vad", device="cpu")
58+
self.assertIsInstance(model, SileroVad)
59+
self.assertEqual(resolved["model"], "SileroVad")
60+
61+
@patch("silero_vad.get_speech_timestamps")
62+
@patch("silero_vad.load_silero_vad")
63+
def test_waveform_follows_the_adapter_device(self, load_model, timestamps):
64+
load_model.side_effect = self._load_stub
65+
66+
def timestamps_stub(waveform, model, sampling_rate, **options):
67+
self.assertEqual(waveform.device.type, "meta")
68+
return []
69+
70+
timestamps.side_effect = timestamps_stub
71+
model = SileroVad().to("meta")
72+
results, _ = model.inference(data_in=[torch.zeros(16000)], key=["sample"])
73+
self.assertEqual(results, [{"key": "sample", "value": []}])
74+
75+
@patch("silero_vad.get_speech_timestamps")
76+
@patch("silero_vad.load_silero_vad")
77+
def test_onnx_waveform_stays_on_cpu(self, load_model, timestamps):
78+
load_model.return_value = object()
79+
80+
def timestamps_stub(waveform, model, sampling_rate, **options):
81+
self.assertEqual(waveform.device.type, "cpu")
82+
return []
83+
84+
timestamps.side_effect = timestamps_stub
85+
model = SileroVad(silero_onnx=True).to("meta")
86+
results, _ = model.inference(data_in=[torch.zeros(16000)], key=["sample"])
87+
self.assertEqual(results, [{"key": "sample", "value": []}])
88+
load_model.assert_called_once_with(onnx=True)
89+
90+
def test_rejects_negative_max_segment_length(self):
91+
with self.assertRaisesRegex(ValueError, "positive"):
92+
SileroVad._split_long_segments([[100, 1100]], -500)
93+
94+
def test_rejects_sub_millisecond_max_segment_length(self):
95+
with self.assertRaisesRegex(ValueError, "positive"):
96+
SileroVad._split_long_segments([[100, 1100]], 0.5)
97+
98+
99+
if __name__ == "__main__":
100+
unittest.main()

0 commit comments

Comments
 (0)