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.
@@ -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,8 @@
1
+ """RealtimeVAD: fast speech-end detection for low-latency voice assistants."""
2
+
3
+ from .iterator import VADIterator
4
+ from .vad import VAD, Event, Segment
5
+
6
+ __version__ = "0.0.4"
7
+
8
+ __all__ = ["VAD", "VADIterator", "Event", "Segment", "__version__"]
@@ -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.
@@ -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)