Skip to content

Commit 35d9fe3

Browse files
committed
tests for self_confirm functionality
1 parent 506c795 commit 35d9fe3

File tree

2 files changed

+181
-6
lines changed

2 files changed

+181
-6
lines changed

openwakeword/model.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ def onnx_predict(onnx_model, x):
215215
self.vad = openwakeword.VAD()
216216

217217
# If self-confirm is enabled, load another copy of the model to use for confirmation
218+
self.self_confirm_enabled = self_confirm
218219
if self_confirm is True:
219220
self.confirmation_model = Model(
220221
wakeword_models=wakeword_models,
@@ -406,7 +407,7 @@ def self_confirm(self, last_n_seconds: float = 1.5):
406407
test-time augmentation that can significantly reduce false detections, but significantly increases
407408
computational cost of running the model when used. The confirmation model uses the same audio
408409
pre-processer as the main model, but runs on the last `last_n_seconds` seconds of audio
409-
to get, essentially, a second opinion on whether a wake-word/phrase was detected. The slight shift in
410+
to get, essentially, a second opinion on whether a wake-word/phrase was detected. The slight shift in
410411
features that results from using a different segment of audio is more likely to avoid a spurious false detection
411412
than a true detection, so this can be a very effective way to reduce false detections.
412413
@@ -424,13 +425,14 @@ def self_confirm(self, last_n_seconds: float = 1.5):
424425
score from the confirmation model over the last `last_n_seconds` seconds of audio.
425426
"""
426427
# Check for self-confirm functionality
427-
if not self.self_confirm:
428+
if self.self_confirm_enabled is False:
428429
raise ValueError("The self-confirm functionality is not enabled for this model instance!")
429-
430+
430431
# Check for at least two cores
431-
if os.cpu_count() < 2:
432+
cpu_count = os.cpu_count()
433+
if cpu_count is None or cpu_count < 2:
432434
raise ValueError("The self-confirm functionality requires at least two CPU cores, as it uses threading.")
433-
435+
434436
# Get the last n seconds of audio from the audio buffer of the main model, and get the features
435437
# with the self-confirmation model preprocessor
436438
n_samples = int(last_n_seconds*16000)
@@ -449,7 +451,7 @@ def self_confirm(self, last_n_seconds: float = 1.5):
449451
predictions.append(self.confirmation_model.predict(audio_data[i:i+step_size]))
450452

451453
predictions_dict = {}
452-
for mdl in self.confirmation_model.models.keys():
454+
for mdl in predictions[0].keys():
453455
predictions_per_model = [p[mdl] for p in predictions]
454456
predictions_dict[mdl] = np.max(predictions_per_model)
455457

tests/test_self_confirm.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright 2022 David Scripka. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
# Imports
17+
import openwakeword
18+
import os
19+
import numpy as np
20+
import pytest
21+
22+
23+
# Tests
24+
class TestSelfConfirm:
25+
def test_self_confirm_basic_functionality(self):
26+
"""Test that self_confirm returns properly formatted predictions_dict"""
27+
# Initialize model with self_confirm enabled
28+
owwModel = openwakeword.Model(
29+
wakeword_models=[os.path.join("openwakeword", "resources", "models", "alexa_v0.1.onnx")],
30+
inference_framework="onnx",
31+
self_confirm=True
32+
)
33+
34+
# Feed in ~10 seconds of random data to fill the audio buffer (10 seconds * 16000 Hz = 160000 samples)
35+
# Process in chunks of 1280 samples (80 ms)
36+
chunk_size = 1280
37+
n_samples = 160000 # 10 seconds of audio
38+
39+
for i in range(0, n_samples, chunk_size):
40+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
41+
owwModel.predict(random_audio)
42+
43+
# Run the self-confirm function
44+
predictions_dict = owwModel.self_confirm(last_n_seconds=1.5)
45+
46+
# Verify predictions_dict is properly formed
47+
assert isinstance(predictions_dict, dict), "predictions_dict should be a dictionary"
48+
49+
# Check that it has the expected model keys
50+
expected_models = list(owwModel.models.keys())
51+
assert len(predictions_dict) == len(expected_models), f"predictions_dict should have {len(expected_models)} key(s)"
52+
53+
for model_name in expected_models:
54+
assert model_name in predictions_dict, f"predictions_dict should contain key '{model_name}'"
55+
56+
# Check that values are between 0 and 1
57+
score = predictions_dict[model_name]
58+
assert isinstance(score, (float, np.floating)), f"Score for {model_name} should be a float"
59+
assert 0 <= score <= 1, f"Score for {model_name} should be between 0 and 1, got {score}"
60+
61+
def test_self_confirm_with_multiple_models(self):
62+
"""Test self_confirm with multiple models loaded"""
63+
owwModel = openwakeword.Model(
64+
wakeword_models=["alexa", "hey mycroft"],
65+
inference_framework="onnx",
66+
self_confirm=True
67+
)
68+
69+
# Feed in ~10 seconds of random data
70+
chunk_size = 1280
71+
n_samples = 160000
72+
73+
for i in range(0, n_samples, chunk_size):
74+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
75+
owwModel.predict(random_audio)
76+
77+
# Run self-confirm
78+
predictions_dict = owwModel.self_confirm(last_n_seconds=1.5)
79+
80+
# Verify all models have predictions
81+
assert len(predictions_dict) >= 2, "predictions_dict should have at least 2 models"
82+
83+
for model_name, score in predictions_dict.items():
84+
assert 0 <= score <= 1, f"Score for {model_name} should be between 0 and 1"
85+
86+
def test_self_confirm_without_enable_flag(self):
87+
"""Test that self_confirm raises ValueError when not enabled"""
88+
# Initialize model WITHOUT self_confirm enabled
89+
owwModel = openwakeword.Model(
90+
wakeword_models=[os.path.join("openwakeword", "resources", "models", "alexa_v0.1.onnx")],
91+
inference_framework="onnx",
92+
self_confirm=False
93+
)
94+
95+
# Feed in some random data
96+
chunk_size = 1280
97+
n_samples = 160000
98+
99+
for i in range(0, n_samples, chunk_size):
100+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
101+
owwModel.predict(random_audio)
102+
103+
# Attempting to call self_confirm should raise ValueError
104+
with pytest.raises(ValueError, match="self-confirm functionality is not enabled"):
105+
owwModel.self_confirm(last_n_seconds=1.5)
106+
107+
def test_self_confirm_insufficient_audio_data(self):
108+
"""Test that self_confirm raises ValueError when insufficient audio data"""
109+
owwModel = openwakeword.Model(
110+
wakeword_models=[os.path.join("openwakeword", "resources", "models", "alexa_v0.1.onnx")],
111+
inference_framework="onnx",
112+
self_confirm=True
113+
)
114+
115+
# Feed in only a small amount of data (less than required for self_confirm)
116+
chunk_size = 1280
117+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
118+
owwModel.predict(random_audio)
119+
120+
# Attempting to call self_confirm should raise ValueError
121+
with pytest.raises(ValueError, match="Not enough audio data"):
122+
owwModel.self_confirm(last_n_seconds=1.5)
123+
124+
def test_self_confirm_with_tflite_models(self):
125+
"""Test self_confirm with tflite inference framework"""
126+
owwModel = openwakeword.Model(
127+
wakeword_models=[os.path.join("openwakeword", "resources", "models", "alexa_v0.1.tflite")],
128+
inference_framework="tflite",
129+
self_confirm=True
130+
)
131+
132+
# Feed in ~10 seconds of random data
133+
chunk_size = 1280
134+
n_samples = 160000
135+
136+
for i in range(0, n_samples, chunk_size):
137+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
138+
owwModel.predict(random_audio)
139+
140+
# Run self-confirm
141+
predictions_dict = owwModel.self_confirm(last_n_seconds=1.5)
142+
143+
# Verify predictions_dict is properly formed
144+
assert isinstance(predictions_dict, dict)
145+
for model_name, score in predictions_dict.items():
146+
assert 0 <= score <= 1, f"Score for {model_name} should be between 0 and 1"
147+
148+
def test_self_confirm_multiclass_model(self):
149+
"""Test self_confirm with a multiclass model"""
150+
owwModel = openwakeword.Model(
151+
wakeword_models=["timer"],
152+
inference_framework="onnx",
153+
self_confirm=True
154+
)
155+
156+
# Feed in ~10 seconds of random data
157+
chunk_size = 1280
158+
n_samples = 160000
159+
160+
for i in range(0, n_samples, chunk_size):
161+
random_audio = np.random.randint(-1000, 1000, chunk_size).astype(np.int16)
162+
owwModel.predict(random_audio)
163+
164+
# Run self-confirm
165+
predictions_dict = owwModel.self_confirm(last_n_seconds=1.5)
166+
167+
# Verify predictions_dict is properly formed
168+
assert isinstance(predictions_dict, dict)
169+
assert len(predictions_dict) > 0, "predictions_dict should not be empty"
170+
171+
for model_name, score in predictions_dict.items():
172+
assert isinstance(score, (float, np.floating)), f"Score for {model_name} should be a float"
173+
assert 0 <= score <= 1, f"Score for {model_name} should be between 0 and 1, got {score}"

0 commit comments

Comments
 (0)