diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py new file mode 100644 index 0000000..f049269 --- /dev/null +++ b/benchmark/benchmark.py @@ -0,0 +1,50 @@ +# Copyright 2022 David Scripka. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Imports +import openwakeword +import numpy as np +from pathlib import Path +from collections import defaultdict + +# Define benchmark to assess inference speed of models at different audio chunk sizes +# Smaller chunk sizes may increase model performance, at the cost of inference efficiency +def run_benchmark(): + # Load models + model_paths = [str(i) for i in Path("openwakeword/resources/models").glob("*.onnx") \ + if "embedding" not in str(i) and "melspectrogram" not in str(i)] + M = openwakeword.Model( + wakeword_model_paths=model_paths, + input_sizes=[16] + ) + + # Create random data to use for benchmarking + clip = np.random.random(16000*10).astype(np.float32) + + # Run the benchmark + step_size = 1280 + preprocessing_times = [] + model_times = defaultdict(list) + for i in range(0, clip.shape[0]-step_size, step_size): + pred, timing_dict = M.predict(clip[i:i+step_size], timing=True) + preprocessing_times.append(timing_dict["preprocessor"]) + for mdl_name in M.models.keys(): + model_times[mdl_name].append(timing_dict["models"][mdl_name]) + + print(f"Average of {np.mean(preprocessing_times)} for audio preprocessing with a frame size of {step_size/16000} seconds") + for mdl_name in M.models.keys(): + print(f"Average of {np.mean(model_times[mdl_name])} for model \"{mdl_name}\"", "\n\n") + +if __name__ == "__main__": + run_benchmark() \ No newline at end of file diff --git a/openwakeword/detect.py b/openwakeword/detect.py index 51afdf6..0f33b68 100644 --- a/openwakeword/detect.py +++ b/openwakeword/detect.py @@ -18,6 +18,7 @@ import numpy as np import os from openwakeword.utils import AudioFeatures from typing import List +import time class Model(): def __init__(self, wakeword_model_paths: List[str], input_sizes: List[int], **kwargs): @@ -36,16 +37,40 @@ class Model(): # Create AudioFeatures object self.preprocessor = AudioFeatures(**kwargs) - def predict(self, x): + def predict(self, x, timing=False): """Predict with all of the wakeword models on the input audio frames""" + # Get audio features + if timing: + timing_dict = {} + timing_dict["models"] = {} + feature_start = time.time() + self.preprocessor(x) + + if timing: + feature_end = time.time() + timing_dict["preprocessor"] = feature_end - feature_start + + # Get predictions from model(s) predictions = {} for mdl in self.models.keys(): input_name = self.models[mdl].get_inputs()[0].name + + if timing: + model_start = time.time() + predictions[mdl] = self.models[mdl].run( None, {input_name: self.preprocessor.get_features(self.model_inputs[mdl])} )[0][0][0] - return predictions + + if timing: + model_end = time.time() + timing_dict["models"][mdl] = model_end - model_start + + if timing: + return predictions, timing_dict + else: + return predictions \ No newline at end of file diff --git a/openwakeword/utils.py b/openwakeword/utils.py index e932646..4dc012b 100644 --- a/openwakeword/utils.py +++ b/openwakeword/utils.py @@ -12,17 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. + # Imports import os import onnxruntime as ort import numpy as np +import pathlib from collections import deque # Base class for computing audio features using Google's speech_embedding model (https://tfhub.dev/google/speech_embedding/1) class AudioFeatures(): def __init__(self, - melspec_onnx_model_path=os.path.join("openwakeword", "resources", "models", "melspectrogram.onnx"), - embedding_onnx_model_path=os.path.join("openwakeword", "resources", "models", "embedding_model.onnx"), + melspec_onnx_model_path=os.path.join(pathlib.Path(__file__).parent.resolve(), "resources", "models", "melspectrogram.onnx"), + embedding_onnx_model_path=os.path.join(pathlib.Path(__file__).parent.resolve(), "resources", "models", "embedding_model.onnx"), sr=16000, ncpu=1 ): @@ -80,22 +82,17 @@ class AudioFeatures(): self.melspectrogram_buffer = self.melspectrogram_buffer[-self.melspectrogram_max_len:, :] def _streaming_features(self, x): - assert len(x) == 1280*3 + if len(x) != 1280: + raise ValueError(f"You must provide input samples in frames of 1280 samples @ 1600khz. Received a frame of {len(x)} samples.") self._streaming_melspectrogram(x) - if self.melspectrogram_buffer.shape[0] < 76: - pass - for i in range(-3,0,1): - start = -76+8*i - end = start + 76 - x = self.melspectrogram_buffer[start:end].astype(np.float32)[None,:,:,None] - if x.shape[1] != 76: - continue + x = self.melspectrogram_buffer[-76:].astype(np.float32)[None,:,:,None] + if x.shape[1] == 76: self.feature_buffer = np.vstack((self.feature_buffer, self.embedding_model.run(None, {'input_1': x})[0].squeeze())) if self.feature_buffer.shape[0] > self.feature_buffer_max_len: self.feature_buffer = self.feature_buffer[-self.feature_buffer_max_len:, :] - def get_features(self, x, n_feature_frames=16): + def get_features(self, n_feature_frames=16): return self.feature_buffer[-n_feature_frames:, :][None,].astype(np.float32) def __call__(self, x): diff --git a/tests/test_models.py b/tests/test_models.py index 096af2c..afafe54 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,6 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# Copyright 2022 David Scripka. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Imports import openwakeword @@ -30,7 +44,7 @@ class TestModels: input_sizes=[16] ) - step_size = 1280*3 + step_size = 1280 predictions = [] for i in range(0, hey_jane_clip.shape[0]-step_size, step_size): predictions.append(model.predict(hey_jane_clip[i:i+step_size])["hey_jane"])