realtimevad 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- realtimevad-0.0.4/LICENSE +21 -0
- realtimevad-0.0.4/PKG-INFO +174 -0
- realtimevad-0.0.4/README.md +150 -0
- realtimevad-0.0.4/RealtimeVAD/__init__.py +8 -0
- realtimevad-0.0.4/RealtimeVAD/assets/LICENSE-silero-vad.txt +23 -0
- realtimevad-0.0.4/RealtimeVAD/assets/silero_vad_v6.2.1.onnx +0 -0
- realtimevad-0.0.4/RealtimeVAD/iterator.py +84 -0
- realtimevad-0.0.4/RealtimeVAD/silero.py +76 -0
- realtimevad-0.0.4/RealtimeVAD/vad.py +436 -0
- realtimevad-0.0.4/pyproject.toml +41 -0
- realtimevad-0.0.4/realtimevad.egg-info/PKG-INFO +174 -0
- realtimevad-0.0.4/realtimevad.egg-info/SOURCES.txt +18 -0
- realtimevad-0.0.4/realtimevad.egg-info/dependency_links.txt +1 -0
- realtimevad-0.0.4/realtimevad.egg-info/requires.txt +5 -0
- realtimevad-0.0.4/realtimevad.egg-info/top_level.txt +1 -0
- realtimevad-0.0.4/setup.cfg +4 -0
- realtimevad-0.0.4/tests/test_iterator.py +52 -0
- realtimevad-0.0.4/tests/test_package.py +5 -0
- realtimevad-0.0.4/tests/test_silero.py +45 -0
- realtimevad-0.0.4/tests/test_vad.py +369 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kolja Beigel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: realtimevad
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Fast speech-end detection for low-latency voice assistants
|
|
5
|
+
Author: Kolja Beigel
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/KoljaB/RealtimeVAD
|
|
8
|
+
Project-URL: Issues, https://github.com/KoljaB/RealtimeVAD/issues
|
|
9
|
+
Keywords: vad,voice activity detection,endpointing,turn detection,voice assistant,realtime
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.21
|
|
20
|
+
Requires-Dist: onnxruntime>=1.16
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# RealtimeVAD
|
|
26
|
+
|
|
27
|
+
*Know that the speaker stopped, about 110 ms before Silero does.*
|
|
28
|
+
|
|
29
|
+
[RealtimeSTT](https://github.com/KoljaB/RealtimeSTT) · [RealtimeTTS](https://github.com/KoljaB/RealtimeTTS) · [Live demo](https://github.com/KoljaB/RealtimeVAD/blob/main/demo/README.md) · [Spec](https://github.com/KoljaB/RealtimeVAD/blob/main/SPEC.md)
|
|
30
|
+
|
|
31
|
+
A voice assistant can't do anything until it knows the user has stopped
|
|
32
|
+
talking. Speech recognition, the LLM call and the voice output all wait for
|
|
33
|
+
that one decision, and a normal voice activity detector takes a few hundred
|
|
34
|
+
milliseconds to make it.
|
|
35
|
+
|
|
36
|
+
RealtimeVAD makes it sooner. It keeps [Silero VAD](https://github.com/snakers4/silero-vad)
|
|
37
|
+
to decide what is speech, and adds a second, faster check: every 10 ms it
|
|
38
|
+
compares the audio level with the room's noise. The moment the audio goes
|
|
39
|
+
quiet, you get an event, long before Silero's probability has fallen.
|
|
40
|
+
|
|
41
|
+
- **Input:** 16 kHz mono audio in chunks of any size (float32, int16 or int16 bytes).
|
|
42
|
+
- **Output:** `start`, `pause` and `end` events, plus the audio of the running segment at any time.
|
|
43
|
+
- **Lead:** speech-off a median 110 ms before Silero's probability drops, and 214 ms before Silero's `VADIterator` reports the end, on 3,238 real turns.
|
|
44
|
+
- **Price:** it also fires in short gaps between words, about ten times as often as Silero.
|
|
45
|
+
- **Runs on:** one CPU thread, far faster than realtime. Needs only numpy and onnxruntime; the Silero model is bundled, no torch.
|
|
46
|
+
- **Status:** alpha (0.0.4). The API may still change.
|
|
47
|
+
|
|
48
|
+
## Live demo
|
|
49
|
+
|
|
50
|
+
https://github.com/user-attachments/assets/d01900dd-9601-4f3d-8c88-db55e8bc83a1
|
|
51
|
+
|
|
52
|
+
The video uses a synthetic voice and shows a median lead of 92 ms. The 110 ms above is the median on real voice-assistant turns.
|
|
53
|
+
|
|
54
|
+
[demo/](https://github.com/KoljaB/RealtimeVAD/blob/main/demo/README.md) runs this page on your own microphone. It shows, turn by turn, how much sooner
|
|
55
|
+
RealtimeVAD knew that you stopped than Silero alone, on the same audio.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install realtimevad
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from RealtimeVAD import VAD
|
|
65
|
+
|
|
66
|
+
vad = VAD()
|
|
67
|
+
for chunk in microphone_chunks():
|
|
68
|
+
for event in vad.feed(chunk):
|
|
69
|
+
if event.kind == "start":
|
|
70
|
+
stream_to_asr(vad.audio()) # includes the audio just before speech began
|
|
71
|
+
elif event.kind == "pause":
|
|
72
|
+
speculate(vad.audio()) # the speaker may be done; this isn't the end yet
|
|
73
|
+
elif event.kind == "end":
|
|
74
|
+
finalize(event.segment.audio)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Events
|
|
78
|
+
|
|
79
|
+
| Event | Meaning | When |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `start` | Speech has begun | After about 64 ms of confident speech, so clicks don't trigger it |
|
|
82
|
+
| `pause` | The speaker may have stopped (speech-off) | Within about 10 ms of the audio going quiet |
|
|
83
|
+
| `end` | The segment is over | 0.24 s after speech-off, unless speech resumes. Never later than Silero alone |
|
|
84
|
+
| `onset` | Speech may have begun | At the first confident frame, before `start` confirms it. Off by default: `VAD(onset_events=True)` |
|
|
85
|
+
|
|
86
|
+
`vad.audio()` returns the running segment at any time, starting about 0.3 s
|
|
87
|
+
before the speech. `end` carries the finished segment with its audio, start
|
|
88
|
+
time, end time and the time the speech stopped.
|
|
89
|
+
|
|
90
|
+
If your assistant talks while the microphone is open, pass
|
|
91
|
+
`vad.feed(chunk, playback=True)` for those chunks so its own voice is handled
|
|
92
|
+
as possible echo. Every threshold and duration can be configured;
|
|
93
|
+
[SPEC.md](https://github.com/KoljaB/RealtimeVAD/blob/main/SPEC.md) lists what is guaranteed.
|
|
94
|
+
|
|
95
|
+
## How to use `pause`
|
|
96
|
+
|
|
97
|
+
`pause` is early and often wrong, on purpose. It fires on every breath, every
|
|
98
|
+
short gap between words and every stop consonant, so most `pause` events are
|
|
99
|
+
followed by more speech.
|
|
100
|
+
|
|
101
|
+
- **Use it to start work you can cancel:** finalize the transcript, call the
|
|
102
|
+
LLM, prepare the voice output. Cancel when speech continues. A wrong `pause`
|
|
103
|
+
then costs almost nothing, and a right one saves hundreds of milliseconds.
|
|
104
|
+
- **Never treat it as the end of the turn.** Only `end` closes a segment.
|
|
105
|
+
- **Can't cancel quickly?** Ignore `pause` and wait for `end`. The extra
|
|
106
|
+
false alarms would cost you more than they save.
|
|
107
|
+
|
|
108
|
+
## Already using Silero?
|
|
109
|
+
|
|
110
|
+
Change the import:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from silero_vad import load_silero_vad
|
|
114
|
+
from RealtimeVAD import VADIterator # was: from silero_vad import VADIterator
|
|
115
|
+
|
|
116
|
+
vad_iterator = VADIterator(load_silero_vad(), sampling_rate=16000)
|
|
117
|
+
speech_dict = vad_iterator(chunk, return_seconds=True) # {'start': ...} / {'end': ...} / None
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The constructor and the results stay the same, but `end` arrives as soon as the
|
|
121
|
+
audio goes quiet. Without a model argument, it uses the bundled Silero model and
|
|
122
|
+
doesn't need torch.
|
|
123
|
+
|
|
124
|
+
## Results
|
|
125
|
+
|
|
126
|
+
Measured on 3,238 turns from a live voice assistant, each with its trailing
|
|
127
|
+
silence, with default settings. Silero's decisions are computed from the very
|
|
128
|
+
same frame probabilities, so both see identical input. "Speech-off" is
|
|
129
|
+
RealtimeVAD's last `pause` before the turn ended, against the first frame of
|
|
130
|
+
Silero's final run below its threshold.
|
|
131
|
+
|
|
132
|
+
| RealtimeVAD speech-off earlier than … | median | p25 – p75 | p90 | earlier in |
|
|
133
|
+
|---|---|---|---|---|
|
|
134
|
+
| Silero, probability below 0.5 (its default threshold) | 110 ms | 84 – 190 ms | 242 ms | 98 % |
|
|
135
|
+
| Silero, probability below 0.35 (`VADIterator`'s end threshold) | 120 ms | 98 – 200 ms | 252 ms | 98 % |
|
|
136
|
+
| Silero `VADIterator` end, default 100 ms minimum silence | 214 ms | 192 – 298 ms | 356 ms | 99 % |
|
|
137
|
+
|
|
138
|
+
| Speech-off signals followed by more speech | per second of speech | turns without any |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| RealtimeVAD `pause` | 1.6 | 16 % |
|
|
141
|
+
| Silero, probability below 0.5 | 0.15 | 85 % |
|
|
142
|
+
|
|
143
|
+
Reproduce with your own recordings (16 kHz mono WAVs, one turn each, with
|
|
144
|
+
trailing silence):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python benchmarks/speech_off_lead.py <wav dir> out.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Limits
|
|
151
|
+
|
|
152
|
+
- The numbers come from one speaker and one capture chain: browser capture
|
|
153
|
+
with noise suppression and echo cancellation, mostly quiet rooms.
|
|
154
|
+
- The lead needs audio that actually goes quiet after a word, and noise
|
|
155
|
+
suppression helps that. In the 147 noisier turns (room level above −70 dBFS)
|
|
156
|
+
the median lead was 92–132 ms. Without noise suppression in a loud room,
|
|
157
|
+
expect less.
|
|
158
|
+
- Earlier is not the same as correct: whether a speech-off sometimes lands
|
|
159
|
+
inside a soft word ending was not checked against word timings.
|
|
160
|
+
- The lead is for speech-off, not for the turn end. `end` waits 0.24 s after
|
|
161
|
+
speech-off in both cases; its own gain over Silero is smaller and depends on
|
|
162
|
+
the noise.
|
|
163
|
+
- 16 kHz mono only.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install -e ".[dev]"
|
|
169
|
+
pytest
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License and credits
|
|
173
|
+
|
|
174
|
+
MIT. The bundled speech model is [Silero VAD](https://github.com/snakers4/silero-vad) (MIT).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# RealtimeVAD
|
|
2
|
+
|
|
3
|
+
*Know that the speaker stopped, about 110 ms before Silero does.*
|
|
4
|
+
|
|
5
|
+
[RealtimeSTT](https://github.com/KoljaB/RealtimeSTT) · [RealtimeTTS](https://github.com/KoljaB/RealtimeTTS) · [Live demo](https://github.com/KoljaB/RealtimeVAD/blob/main/demo/README.md) · [Spec](https://github.com/KoljaB/RealtimeVAD/blob/main/SPEC.md)
|
|
6
|
+
|
|
7
|
+
A voice assistant can't do anything until it knows the user has stopped
|
|
8
|
+
talking. Speech recognition, the LLM call and the voice output all wait for
|
|
9
|
+
that one decision, and a normal voice activity detector takes a few hundred
|
|
10
|
+
milliseconds to make it.
|
|
11
|
+
|
|
12
|
+
RealtimeVAD makes it sooner. It keeps [Silero VAD](https://github.com/snakers4/silero-vad)
|
|
13
|
+
to decide what is speech, and adds a second, faster check: every 10 ms it
|
|
14
|
+
compares the audio level with the room's noise. The moment the audio goes
|
|
15
|
+
quiet, you get an event, long before Silero's probability has fallen.
|
|
16
|
+
|
|
17
|
+
- **Input:** 16 kHz mono audio in chunks of any size (float32, int16 or int16 bytes).
|
|
18
|
+
- **Output:** `start`, `pause` and `end` events, plus the audio of the running segment at any time.
|
|
19
|
+
- **Lead:** speech-off a median 110 ms before Silero's probability drops, and 214 ms before Silero's `VADIterator` reports the end, on 3,238 real turns.
|
|
20
|
+
- **Price:** it also fires in short gaps between words, about ten times as often as Silero.
|
|
21
|
+
- **Runs on:** one CPU thread, far faster than realtime. Needs only numpy and onnxruntime; the Silero model is bundled, no torch.
|
|
22
|
+
- **Status:** alpha (0.0.4). The API may still change.
|
|
23
|
+
|
|
24
|
+
## Live demo
|
|
25
|
+
|
|
26
|
+
https://github.com/user-attachments/assets/d01900dd-9601-4f3d-8c88-db55e8bc83a1
|
|
27
|
+
|
|
28
|
+
The video uses a synthetic voice and shows a median lead of 92 ms. The 110 ms above is the median on real voice-assistant turns.
|
|
29
|
+
|
|
30
|
+
[demo/](https://github.com/KoljaB/RealtimeVAD/blob/main/demo/README.md) runs this page on your own microphone. It shows, turn by turn, how much sooner
|
|
31
|
+
RealtimeVAD knew that you stopped than Silero alone, on the same audio.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install realtimevad
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from RealtimeVAD import VAD
|
|
41
|
+
|
|
42
|
+
vad = VAD()
|
|
43
|
+
for chunk in microphone_chunks():
|
|
44
|
+
for event in vad.feed(chunk):
|
|
45
|
+
if event.kind == "start":
|
|
46
|
+
stream_to_asr(vad.audio()) # includes the audio just before speech began
|
|
47
|
+
elif event.kind == "pause":
|
|
48
|
+
speculate(vad.audio()) # the speaker may be done; this isn't the end yet
|
|
49
|
+
elif event.kind == "end":
|
|
50
|
+
finalize(event.segment.audio)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Events
|
|
54
|
+
|
|
55
|
+
| Event | Meaning | When |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `start` | Speech has begun | After about 64 ms of confident speech, so clicks don't trigger it |
|
|
58
|
+
| `pause` | The speaker may have stopped (speech-off) | Within about 10 ms of the audio going quiet |
|
|
59
|
+
| `end` | The segment is over | 0.24 s after speech-off, unless speech resumes. Never later than Silero alone |
|
|
60
|
+
| `onset` | Speech may have begun | At the first confident frame, before `start` confirms it. Off by default: `VAD(onset_events=True)` |
|
|
61
|
+
|
|
62
|
+
`vad.audio()` returns the running segment at any time, starting about 0.3 s
|
|
63
|
+
before the speech. `end` carries the finished segment with its audio, start
|
|
64
|
+
time, end time and the time the speech stopped.
|
|
65
|
+
|
|
66
|
+
If your assistant talks while the microphone is open, pass
|
|
67
|
+
`vad.feed(chunk, playback=True)` for those chunks so its own voice is handled
|
|
68
|
+
as possible echo. Every threshold and duration can be configured;
|
|
69
|
+
[SPEC.md](https://github.com/KoljaB/RealtimeVAD/blob/main/SPEC.md) lists what is guaranteed.
|
|
70
|
+
|
|
71
|
+
## How to use `pause`
|
|
72
|
+
|
|
73
|
+
`pause` is early and often wrong, on purpose. It fires on every breath, every
|
|
74
|
+
short gap between words and every stop consonant, so most `pause` events are
|
|
75
|
+
followed by more speech.
|
|
76
|
+
|
|
77
|
+
- **Use it to start work you can cancel:** finalize the transcript, call the
|
|
78
|
+
LLM, prepare the voice output. Cancel when speech continues. A wrong `pause`
|
|
79
|
+
then costs almost nothing, and a right one saves hundreds of milliseconds.
|
|
80
|
+
- **Never treat it as the end of the turn.** Only `end` closes a segment.
|
|
81
|
+
- **Can't cancel quickly?** Ignore `pause` and wait for `end`. The extra
|
|
82
|
+
false alarms would cost you more than they save.
|
|
83
|
+
|
|
84
|
+
## Already using Silero?
|
|
85
|
+
|
|
86
|
+
Change the import:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from silero_vad import load_silero_vad
|
|
90
|
+
from RealtimeVAD import VADIterator # was: from silero_vad import VADIterator
|
|
91
|
+
|
|
92
|
+
vad_iterator = VADIterator(load_silero_vad(), sampling_rate=16000)
|
|
93
|
+
speech_dict = vad_iterator(chunk, return_seconds=True) # {'start': ...} / {'end': ...} / None
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The constructor and the results stay the same, but `end` arrives as soon as the
|
|
97
|
+
audio goes quiet. Without a model argument, it uses the bundled Silero model and
|
|
98
|
+
doesn't need torch.
|
|
99
|
+
|
|
100
|
+
## Results
|
|
101
|
+
|
|
102
|
+
Measured on 3,238 turns from a live voice assistant, each with its trailing
|
|
103
|
+
silence, with default settings. Silero's decisions are computed from the very
|
|
104
|
+
same frame probabilities, so both see identical input. "Speech-off" is
|
|
105
|
+
RealtimeVAD's last `pause` before the turn ended, against the first frame of
|
|
106
|
+
Silero's final run below its threshold.
|
|
107
|
+
|
|
108
|
+
| RealtimeVAD speech-off earlier than … | median | p25 – p75 | p90 | earlier in |
|
|
109
|
+
|---|---|---|---|---|
|
|
110
|
+
| Silero, probability below 0.5 (its default threshold) | 110 ms | 84 – 190 ms | 242 ms | 98 % |
|
|
111
|
+
| Silero, probability below 0.35 (`VADIterator`'s end threshold) | 120 ms | 98 – 200 ms | 252 ms | 98 % |
|
|
112
|
+
| Silero `VADIterator` end, default 100 ms minimum silence | 214 ms | 192 – 298 ms | 356 ms | 99 % |
|
|
113
|
+
|
|
114
|
+
| Speech-off signals followed by more speech | per second of speech | turns without any |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| RealtimeVAD `pause` | 1.6 | 16 % |
|
|
117
|
+
| Silero, probability below 0.5 | 0.15 | 85 % |
|
|
118
|
+
|
|
119
|
+
Reproduce with your own recordings (16 kHz mono WAVs, one turn each, with
|
|
120
|
+
trailing silence):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python benchmarks/speech_off_lead.py <wav dir> out.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Limits
|
|
127
|
+
|
|
128
|
+
- The numbers come from one speaker and one capture chain: browser capture
|
|
129
|
+
with noise suppression and echo cancellation, mostly quiet rooms.
|
|
130
|
+
- The lead needs audio that actually goes quiet after a word, and noise
|
|
131
|
+
suppression helps that. In the 147 noisier turns (room level above −70 dBFS)
|
|
132
|
+
the median lead was 92–132 ms. Without noise suppression in a loud room,
|
|
133
|
+
expect less.
|
|
134
|
+
- Earlier is not the same as correct: whether a speech-off sometimes lands
|
|
135
|
+
inside a soft word ending was not checked against word timings.
|
|
136
|
+
- The lead is for speech-off, not for the turn end. `end` waits 0.24 s after
|
|
137
|
+
speech-off in both cases; its own gain over Silero is smaller and depends on
|
|
138
|
+
the noise.
|
|
139
|
+
- 16 kHz mono only.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install -e ".[dev]"
|
|
145
|
+
pytest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License and credits
|
|
149
|
+
|
|
150
|
+
MIT. The bundled speech model is [Silero VAD](https://github.com/snakers4/silero-vad) (MIT).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
silero_vad_v6.2.1.onnx comes from Silero VAD (https://github.com/snakers4/silero-vad).
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2020-present Silero Team
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Drop-in for ``silero_vad.VADIterator``: same constructor, call and results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .silero import SAMPLE_RATE
|
|
10
|
+
from .vad import FRAME_S, VAD
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _SileroModel:
|
|
14
|
+
"""Adapts a model from ``silero_vad.load_silero_vad()`` to a frame -> probability call."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, model) -> None:
|
|
17
|
+
self._model = model
|
|
18
|
+
try:
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
self._tensor = torch.from_numpy
|
|
22
|
+
except ImportError:
|
|
23
|
+
self._tensor = lambda a: a
|
|
24
|
+
|
|
25
|
+
def __call__(self, frame: np.ndarray) -> float:
|
|
26
|
+
p = self._model(self._tensor(np.ascontiguousarray(frame)), SAMPLE_RATE)
|
|
27
|
+
return float(p.item() if hasattr(p, "item") else p)
|
|
28
|
+
|
|
29
|
+
def reset(self) -> None:
|
|
30
|
+
self._model.reset_states()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VADIterator:
|
|
34
|
+
"""Like Silero's ``VADIterator``, but ``end`` comes as soon as the audio goes quiet.
|
|
35
|
+
|
|
36
|
+
``model`` may be a Silero model (used as given) or None (the bundled one).
|
|
37
|
+
Extra keyword arguments go to :class:`VAD`. Chunks may have any size.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
model=None,
|
|
43
|
+
threshold: float = 0.5,
|
|
44
|
+
sampling_rate: int = 16000,
|
|
45
|
+
min_silence_duration_ms: int = 100,
|
|
46
|
+
speech_pad_ms: int = 30,
|
|
47
|
+
neg_threshold: Optional[float] = None,
|
|
48
|
+
**options,
|
|
49
|
+
) -> None:
|
|
50
|
+
if sampling_rate != SAMPLE_RATE:
|
|
51
|
+
raise ValueError("RealtimeVAD supports 16000 Hz only")
|
|
52
|
+
if neg_threshold is None:
|
|
53
|
+
neg_threshold = max(threshold - 0.15, 0.01)
|
|
54
|
+
self.sampling_rate = sampling_rate
|
|
55
|
+
self.speech_pad_samples = sampling_rate * speech_pad_ms / 1000
|
|
56
|
+
self.vad = VAD(
|
|
57
|
+
start_threshold=threshold,
|
|
58
|
+
continue_threshold=neg_threshold,
|
|
59
|
+
start_s=FRAME_S, # Silero starts on one frame
|
|
60
|
+
end_s=min_silence_duration_ms / 1000,
|
|
61
|
+
preroll_s=speech_pad_ms / 1000,
|
|
62
|
+
probability=None if model is None else _SileroModel(model),
|
|
63
|
+
**options,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def triggered(self) -> bool:
|
|
68
|
+
return self.vad.in_speech
|
|
69
|
+
|
|
70
|
+
def reset_states(self) -> None:
|
|
71
|
+
self.vad.reset()
|
|
72
|
+
|
|
73
|
+
def __call__(self, x, return_seconds: bool = False, time_resolution: int = 1) -> Optional[dict]:
|
|
74
|
+
result = {}
|
|
75
|
+
for event in self.vad.feed(np.asarray(x)):
|
|
76
|
+
if event.kind == "start": # Silero: start of the first speech frame, minus the pad
|
|
77
|
+
sample = (event.time - FRAME_S) * self.sampling_rate - self.speech_pad_samples
|
|
78
|
+
elif event.kind == "end": # Silero: where speech stopped, plus the pad
|
|
79
|
+
sample = event.segment.speech_end * self.sampling_rate + self.speech_pad_samples
|
|
80
|
+
else:
|
|
81
|
+
continue
|
|
82
|
+
sample = max(0, int(round(sample)))
|
|
83
|
+
result[event.kind] = round(sample / self.sampling_rate, time_resolution) if return_seconds else sample
|
|
84
|
+
return result or None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Stateful Silero v6 ONNX inference on 512-sample, 16 kHz frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
SAMPLE_RATE = 16_000
|
|
11
|
+
FRAME = 512
|
|
12
|
+
MODEL = Path(__file__).parent / "assets" / "silero_vad_v6.2.1.onnx"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Silero:
|
|
16
|
+
"""Silero v6 with recovery from its "deaf" state.
|
|
17
|
+
|
|
18
|
+
On a long stream the recurrent state can drift into an attractor where the
|
|
19
|
+
probability stays near 0 even through loud speech (low babble, near-silent
|
|
20
|
+
pauses). After ``restore_s`` of p < 0.1 the state is put back to where the
|
|
21
|
+
model last heard confident speech (p >= 0.9), or zeroed before any. A zero
|
|
22
|
+
state is the worse restart point: it scores the next utterance lower and
|
|
23
|
+
later. Costs no extra inference; 0 disables it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, model: str | Path = MODEL, threads: int = 1, restore_s: float = 1.0) -> None:
|
|
27
|
+
import onnxruntime as ort
|
|
28
|
+
|
|
29
|
+
options = ort.SessionOptions()
|
|
30
|
+
options.intra_op_num_threads = threads
|
|
31
|
+
options.inter_op_num_threads = 1
|
|
32
|
+
options.log_severity_level = 3
|
|
33
|
+
self._session = ort.InferenceSession(
|
|
34
|
+
str(model), sess_options=options, providers=["CPUExecutionProvider"]
|
|
35
|
+
)
|
|
36
|
+
self._sr = np.asarray(SAMPLE_RATE, dtype=np.int64)
|
|
37
|
+
self._restore_frames = math.ceil(restore_s * SAMPLE_RATE / FRAME - 1e-9) if restore_s > 0 else 0
|
|
38
|
+
self.restores = 0
|
|
39
|
+
self.reset()
|
|
40
|
+
|
|
41
|
+
def reset(self) -> None:
|
|
42
|
+
"""Start a new stream: zero state and context, forget the speech snapshot."""
|
|
43
|
+
self._context = np.zeros((1, 64), dtype=np.float32)
|
|
44
|
+
self._state = np.zeros((2, 1, 128), dtype=np.float32)
|
|
45
|
+
self._snapshot = None
|
|
46
|
+
self._quiet = 0
|
|
47
|
+
|
|
48
|
+
def __call__(self, frame: np.ndarray) -> float:
|
|
49
|
+
audio = np.asarray(frame, dtype=np.float32).reshape(1, FRAME)
|
|
50
|
+
p, self._state = self._session.run(
|
|
51
|
+
None,
|
|
52
|
+
{"input": np.concatenate((self._context, audio), axis=1), "sr": self._sr, "state": self._state},
|
|
53
|
+
)
|
|
54
|
+
self._context = audio[:, -64:].copy()
|
|
55
|
+
p = float(np.asarray(p).reshape(-1)[0])
|
|
56
|
+
if self._restore_frames: # standalone rule; inside VAD, VAD decides both (see vad.py)
|
|
57
|
+
if p >= 0.9:
|
|
58
|
+
self.snapshot()
|
|
59
|
+
self._quiet = self._quiet + 1 if p < 0.1 else 0
|
|
60
|
+
if self._quiet >= self._restore_frames:
|
|
61
|
+
self.restore()
|
|
62
|
+
return p
|
|
63
|
+
|
|
64
|
+
def snapshot(self) -> None:
|
|
65
|
+
"""Remember the current state as the restore point (fresh arrays each frame)."""
|
|
66
|
+
self._snapshot = (self._state, self._context)
|
|
67
|
+
|
|
68
|
+
def restore(self) -> None:
|
|
69
|
+
"""Put the state back to the last confident speech (zeros before any); affects the next frame."""
|
|
70
|
+
self._quiet = 0
|
|
71
|
+
self.restores += 1
|
|
72
|
+
if self._snapshot is None:
|
|
73
|
+
self._context = np.zeros((1, 64), dtype=np.float32)
|
|
74
|
+
self._state = np.zeros((2, 1, 128), dtype=np.float32)
|
|
75
|
+
else:
|
|
76
|
+
self._state, self._context = (a.copy() for a in self._snapshot)
|