huri-module-vad 0.1.0.dev0__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,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: huri-module-vad
3
+ Version: 0.1.0.dev0
4
+ Summary: HuRI module: Voice activity detection (WebRTC VAD): microphone frames in, speech segments out
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: huri-core[server]<0.5,>=0.4.0.dev0
8
+ Requires-Dist: huri-events<0.2,>=0.1.0.dev0
9
+ Requires-Dist: numpy<2,>=1.26
10
+ Requires-Dist: webrtcvad==2.0.10
11
+ Requires-Dist: setuptools<82
12
+
13
+ # huri-module-vad
14
+
15
+ Voice activity detection (WebRTC VAD): microphone frames in, speech segments out.
16
+
17
+ | Module | Topics | What it does |
18
+ |---|---|---|
19
+ | `vad` | `huri.audio.in` → `huri.voice` | Detects speech in microphone frames (WebRTC VAD) |
20
+
21
+ ```sh
22
+ pip install -e packages/huri-module-vad # from a huri-modules checkout, next to huri-core and huri-events
23
+ HURI_MODULES=vad ... # enable it on the server
24
+ ```
25
+
26
+ A distribution of
27
+ [huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
28
+ `pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
29
+ `huri-events`, which are not on PyPI: without a checkout, install them from git
30
+ in the same pip command, so pip never looks them up on an index:
31
+
32
+ ```sh
33
+ ref=0.1.0 # a huri-modules release tag
34
+ core=0.4.0 # HURI_CORE_REF in that release's versions.env
35
+ pip install \
36
+ "huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
37
+ "huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
38
+ "huri-module-vad @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-vad"
39
+ ```
@@ -0,0 +1,27 @@
1
+ # huri-module-vad
2
+
3
+ Voice activity detection (WebRTC VAD): microphone frames in, speech segments out.
4
+
5
+ | Module | Topics | What it does |
6
+ |---|---|---|
7
+ | `vad` | `huri.audio.in` → `huri.voice` | Detects speech in microphone frames (WebRTC VAD) |
8
+
9
+ ```sh
10
+ pip install -e packages/huri-module-vad # from a huri-modules checkout, next to huri-core and huri-events
11
+ HURI_MODULES=vad ... # enable it on the server
12
+ ```
13
+
14
+ A distribution of
15
+ [huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
16
+ `pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
17
+ `huri-events`, which are not on PyPI: without a checkout, install them from git
18
+ in the same pip command, so pip never looks them up on an index:
19
+
20
+ ```sh
21
+ ref=0.1.0 # a huri-modules release tag
22
+ core=0.4.0 # HURI_CORE_REF in that release's versions.env
23
+ pip install \
24
+ "huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
25
+ "huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
26
+ "huri-module-vad @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-vad"
27
+ ```
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: huri-module-vad
3
+ Version: 0.1.0.dev0
4
+ Summary: HuRI module: Voice activity detection (WebRTC VAD): microphone frames in, speech segments out
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: huri-core[server]<0.5,>=0.4.0.dev0
8
+ Requires-Dist: huri-events<0.2,>=0.1.0.dev0
9
+ Requires-Dist: numpy<2,>=1.26
10
+ Requires-Dist: webrtcvad==2.0.10
11
+ Requires-Dist: setuptools<82
12
+
13
+ # huri-module-vad
14
+
15
+ Voice activity detection (WebRTC VAD): microphone frames in, speech segments out.
16
+
17
+ | Module | Topics | What it does |
18
+ |---|---|---|
19
+ | `vad` | `huri.audio.in` → `huri.voice` | Detects speech in microphone frames (WebRTC VAD) |
20
+
21
+ ```sh
22
+ pip install -e packages/huri-module-vad # from a huri-modules checkout, next to huri-core and huri-events
23
+ HURI_MODULES=vad ... # enable it on the server
24
+ ```
25
+
26
+ A distribution of
27
+ [huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
28
+ `pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
29
+ `huri-events`, which are not on PyPI: without a checkout, install them from git
30
+ in the same pip command, so pip never looks them up on an index:
31
+
32
+ ```sh
33
+ ref=0.1.0 # a huri-modules release tag
34
+ core=0.4.0 # HURI_CORE_REF in that release's versions.env
35
+ pip install \
36
+ "huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
37
+ "huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
38
+ "huri-module-vad @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-vad"
39
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ huri_module_vad.egg-info/PKG-INFO
4
+ huri_module_vad.egg-info/SOURCES.txt
5
+ huri_module_vad.egg-info/dependency_links.txt
6
+ huri_module_vad.egg-info/entry_points.txt
7
+ huri_module_vad.egg-info/requires.txt
8
+ huri_module_vad.egg-info/top_level.txt
9
+ huri_vad/__init__.py
10
+ huri_vad/huri_module.toml
11
+ huri_vad/module.py
12
+ tests/test_vad.py
@@ -0,0 +1,2 @@
1
+ [huri.modules]
2
+ vad = huri_vad.module:VAD
@@ -0,0 +1,5 @@
1
+ huri-core[server]<0.5,>=0.4.0.dev0
2
+ huri-events<0.2,>=0.1.0.dev0
3
+ numpy<2,>=1.26
4
+ webrtcvad==2.0.10
5
+ setuptools<82
File without changes
@@ -0,0 +1,7 @@
1
+ name = "vad"
2
+ summary = "Detects speech in microphone frames (WebRTC VAD)"
3
+ input = { topic = "huri.audio.in", event = "huri_core.core.events:BytesEvent" }
4
+ output = { topic = "huri.voice", event = "huri_events.speech:Voice" }
5
+ events = ["huri-events>=0.1,<0.2"]
6
+ requires = { gpu = false, actuation = false }
7
+ huri_core = ">=0.4.0.dev0,<0.5"
@@ -0,0 +1,69 @@
1
+ from typing import Optional
2
+
3
+ import numpy as np
4
+ import webrtcvad
5
+ from huri_events.speech import Voice
6
+
7
+ from huri_core.core.events import BytesEvent
8
+ from huri_core.core.module import Module
9
+
10
+
11
+ class VAD(Module):
12
+ """VAD Module
13
+
14
+ Detect voice and silence using WebRTC VAD.
15
+
16
+ input: huri.audio.in,
17
+ output: huri.voice
18
+
19
+ :vad_agressiveness: from 0 (low) to 3 (high, can distord audio).
20
+ :silence_duration: how many seconds will a no voice be considered a silence.
21
+ :sample_rate: size of received chunk of audio. Usually 8000, 16000 or 48000.
22
+ :block_duration: size of received chunk of audio (in s).
23
+ Can only be 0.010, 0.020 and 0.030.
24
+ """
25
+
26
+ # Inbound microphone frames travel on their own topic so the TTS-output
27
+ # "audio" topic (consumed by MOV and the client Sender) never collides
28
+ # with mic input — otherwise raw mic bytes get echoed back to the client.
29
+ input_type = "huri.audio.in"
30
+ output_type = "huri.voice"
31
+ input_event = BytesEvent
32
+ output_event = Voice
33
+
34
+ def __init__(
35
+ self,
36
+ vad_agressiveness: int = 3,
37
+ silence_duration: float = 3, # s
38
+ sample_rate: int = 16000,
39
+ block_duration: float = 0.020, # s
40
+ ):
41
+ super().__init__()
42
+
43
+ if block_duration not in [0.010, 0.020, 0.030]:
44
+ raise RuntimeError("block duration must be 0.010, 0.020 or 0.030 s")
45
+
46
+ self.sample_rate: int = sample_rate
47
+ self.block_size: int = int(block_duration * sample_rate) # ms
48
+
49
+ self.silence_frames_size: int = int(silence_duration * sample_rate)
50
+ self.silence_frames_count: int = -1
51
+
52
+ self.vad = webrtcvad.Vad(vad_agressiveness)
53
+
54
+ async def process(self, data: BytesEvent) -> Optional[Voice]:
55
+ if self.vad.is_speech(data.data, self.sample_rate) is True:
56
+ self.silence_frames_count = 0
57
+
58
+ audio_array = np.frombuffer(data.data, dtype=np.int16)
59
+ audio_array_float = audio_array.astype(np.float32) / 32768.0
60
+
61
+ return Voice(audio_array_float)
62
+ else:
63
+ if self.silence_frames_count != -1:
64
+ self.silence_frames_count += self.block_size
65
+ if self.silence_frames_count > self.silence_frames_size:
66
+ self.silence_frames_count = -1 # sent only once
67
+
68
+ return Voice(None)
69
+ return None
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64,<82"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "huri-module-vad"
7
+ version = "0.1.0.dev0"
8
+ description = "HuRI module: Voice activity detection (WebRTC VAD): microphone frames in, speech segments out"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "huri-core[server]>=0.4.0.dev0,<0.5",
13
+ "huri-events>=0.1.0.dev0,<0.2",
14
+ "numpy>=1.26,<2",
15
+ "webrtcvad==2.0.10",
16
+ # webrtcvad imports pkg_resources, removed in setuptools 82
17
+ "setuptools<82",
18
+ ]
19
+
20
+ [project.entry-points."huri.modules"]
21
+ vad = "huri_vad.module:VAD"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["huri_vad*"]
25
+
26
+ [tool.setuptools.package-data]
27
+ huri_vad = ["huri_module.toml", "manifests/*/huri_module.toml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,51 @@
1
+ """VAD's speech/silence logic, with WebRTC's detector scripted."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from huri_events.speech import Voice
6
+ from huri_vad.module import VAD
7
+
8
+ from huri_core.core.events import BytesEvent
9
+ from huri_core.testing import ModuleHarness
10
+
11
+ FRAME = (np.full(320, 16384, dtype=np.int16)).tobytes() # 20 ms at 16 kHz
12
+
13
+
14
+ class Scripted:
15
+ def __init__(self, decisions):
16
+ self.decisions = list(decisions)
17
+
18
+ def is_speech(self, frame, sample_rate):
19
+ return self.decisions.pop(0)
20
+
21
+
22
+ def vad(decisions, silence_duration=0.05):
23
+ harness = ModuleHarness(VAD, silence_duration=silence_duration)
24
+ harness.module.vad = Scripted(decisions)
25
+ return harness
26
+
27
+
28
+ def test_speech_frames_become_normalized_voice():
29
+ (voice,) = vad([True]).run(BytesEvent(FRAME))
30
+ assert voice.data.dtype == np.float32 and voice.data.size == 320
31
+ assert voice.data[0] == pytest.approx(0.5)
32
+
33
+
34
+ def test_end_of_utterance_is_sent_once_after_enough_silence():
35
+ # 0.05 s of silence = more than 2 frames of 20 ms.
36
+ out = vad([True] + [False] * 6).run(*[BytesEvent(FRAME)] * 7)
37
+ assert [v.data is None for v in out] == [False, True]
38
+
39
+
40
+ def test_silence_before_any_speech_sends_nothing():
41
+ assert vad([False] * 5).run(*[BytesEvent(FRAME)] * 5) == []
42
+
43
+
44
+ def test_block_duration_must_be_one_webrtc_accepts():
45
+ with pytest.raises(RuntimeError, match="block duration"):
46
+ ModuleHarness(VAD, block_duration=0.025)
47
+
48
+
49
+ def test_real_detector_runs_on_a_frame():
50
+ out = ModuleHarness(VAD).run(BytesEvent(bytes(640))) # digital silence
51
+ assert out == [] or isinstance(out[0], Voice)