diff --git a/README.md b/README.md index e69de29..bb10b71 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,31 @@ +# openWakeWord + +openWakeWord is a fully open-source wakeword library that can be used to create voice-enabled applications and interfaces. It comes with a (growing!) set of pre-trained models, and new models can be easily trained as well for different wake words & phrases. The overall goal of the library is to provide a simple framework for wakeword/phrase detection, while also providing pre-built models (and the ability to train new models) that perform well enough to be useable in the real-word. + +More specifically, openWakeWord aims to: + +1) Be fast & accurate *enough* for real-world usage. The models can easily run in real-time using ~x% of a single core on a Raspberry Pi3 (see the see the [Performance & Evaluation]() section for more details), but are likely too big for less powerful systems or microcontrollers. The models should have false-accept and false-reject rates of that are below the annoyance threshold for the average user. This is obviously subjective, by a false-accept rate of <0.5 per hour and a false-reject rate of <5% seems reasonable in practice. + +2) Have a simple interface for model inference. Models process a stream of audio data in 80 ms frames, and return a prediction for each frame indicated whether the wake word/phrase has been detected. + +3) Have a shared feature extraction backbone for all models so that many separate models can be run with minimal additional resource requirements. See the [Model Architecture]() section for more details. + +4) Require *little to no manual data collection* to train new models. The included models (see the [Pre-trained Models]() section for more details) were all trained with *100% synthetic* speech generated from text-to-speech models. Training new models is a simple as generating new clips for the target wake word/phrase and training a small head-model on top of of the frozen common backbone. See the [Training New Models]() section for more details. + +# Pre-Trained Models + +# Model Architecture + +# Performance and Evaluation + +- Mention 0.5/hour false accept rate for near continuous speech (e.g., dinner party corpus) +- False-reject rate of 5% means that the chanced of missing two activations is only 0.25%. E.g., if a user on average intentially speaks a wake word/phrase 20 times per day, they would expect to have to try two times once per day, and try three times only once every 20 days (assuming the failed activations aren't correlated and the environmental conditions are such that an activation is expected). + + +# Training New Models + +# Language Support + +Currently, openWakeWord only supports English, primarily because the pre-trained text-to-speech models used to generate training data are all english. It's likely that speech-to-text models trained on other languages would also work well, but non-english models & datasets are less commonly available. + +Future release roadmaps may have non-english support. In particular, [Mycroft.AIs Mimic 3](https://github.com/MycroftAI/mimic3-voices) TTS engine could work well. \ No newline at end of file diff --git a/docs/models/alexa.md b/docs/models/alexa.md new file mode 100755 index 0000000..e69de29 diff --git a/examples/detect_from_microphone.py b/examples/detect_from_microphone.py index 79a928a..9c7513e 100644 --- a/examples/detect_from_microphone.py +++ b/examples/detect_from_microphone.py @@ -17,7 +17,7 @@ import os import plotext as plt import sounddevice import numpy as np -from openwakeword.detect import Model +from openwakeword.model import Model # Get microphone stream mic_stream = sounddevice.InputStream( diff --git a/openwakeword/__init__.py b/openwakeword/__init__.py index ba32899..cc109d6 100644 --- a/openwakeword/__init__.py +++ b/openwakeword/__init__.py @@ -1 +1 @@ -from openwakeword.detect import Model \ No newline at end of file +from openwakeword.model import Model \ No newline at end of file diff --git a/openwakeword/metrics.py b/openwakeword/metrics.py new file mode 100644 index 0000000..ee7bb98 --- /dev/null +++ b/openwakeword/metrics.py @@ -0,0 +1,66 @@ +# 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. + +## Define metric utility functions specific to the wakeword detection use-case + +from time import time +import matplotlib.pyplot as plt +import re +from tqdm import tqdm +import numpy as np + +def generate_roc_curve_fprs(scores, n_points=25, time_per_prediction=.08, **kwargs): + """ + Generates the false positive rate (fpr) per hour for the given predictions + over a range of score thresholds. Assumes that all predictions should be less than the threshold, + else the prediction is a false positive. + + Args: + predictions (List): A list of predicted scores, between 0 and 1 + labels (List): A list of ground-truth labels + thresholds (List[float]): A list of threshold values to plot curves for + time_per_prediction: The time (in seconds) that each prediction represents + + Returns: + list: A list of fprs per hour + """ + + # Determine total time + total_hours = time_per_prediction*len(scores)/3600 # convert to hours + + # Calculate true positive rate + fprs = [] + for threshold in tqdm(np.linspace(0.05,0.95,num=n_points)): + # Remove repeated predictions from data to not overcount false positives + bin_pred = ''.join(["1" if i else "0" for i in np.array(scores) >= threshold]) + bin_pred = re.sub("1(0){1,5}1", "1", bin_pred) + bin_pred = re.sub("0(1){1,50}0", "1", bin_pred) + fprs.append(len(re.findall('1', bin_pred))/total_hours) + + return fprs + +def generate_roc_curve_tprs(scores, n_points=25): + """ + Generates the true positive rate (true accept rate) for the given predictions + over a range score thresholds. Assumes that all predictions are supposed to be equal to 1. + + Args: + scores (list): A list of scores for each prediction + """ + + tprs = [] + for threshold in tqdm(np.linspace(0.05,0.95,num=n_points)): + tprs.append(sum(scores >= threshold)/len(scores)) + + return tprs diff --git a/openwakeword/detect.py b/openwakeword/model.py similarity index 100% rename from openwakeword/detect.py rename to openwakeword/model.py diff --git a/openwakeword/utils.py b/openwakeword/utils.py index 29dae36..84c0b83 100644 --- a/openwakeword/utils.py +++ b/openwakeword/utils.py @@ -20,6 +20,9 @@ import numpy as np import pathlib from collections import deque from multiprocessing.pool import ThreadPool +from multiprocessing import Process, Queue +import time +import openwakeword # Base class for computing audio features using Google's speech_embedding model (https://tfhub.dev/google/speech_embedding/1) class AudioFeatures(): @@ -257,3 +260,57 @@ class AudioFeatures(): def __call__(self, x): self._streaming_features(x) + + +# Bulk prediction function +def bulk_predict(file_paths, wakeword_model_paths, ncpu=1): + """ + Bulk predict on the provided input files in parallel using multiprocessing using the specified model. + + Args: + input_paths (List[str]): The list of input file to predict + wakeword_model_path (List(str)): The paths to the wakeword ONNX model files + ncpu (int): How many processes to create (up to max of available CPUs) + + Returns: + dict: A dictionary containing the predictions for each file, with the filepath as the key + """ + + # Create openWakeWord model objects + n_batches = len(file_paths)//ncpu + remainder = len(file_paths) % ncpu + chunks = [file_paths[i:i+n_batches] for i in range(0, len(file_paths)-remainder, n_batches)] + for i in range(1, remainder+1): + chunks[i-1].append(file_paths[-1*i]) + + # Create jobs + ps = [] + mdls = [] + q = Queue() + for chunk in chunks: + oww = openwakeword.Model( + wakeword_model_paths=wakeword_model_paths, + input_sizes=[16] + ) + mdls.append(oww) + def f(clips): + results = [] + for clip in clips: + results.append({clip: mdls[-1].predict_clip(clip)}) + q.put(results) + + ps.append(Process(target=f, args=(chunk,))) + + # Submit jobs + for p in ps: + p.start() + + # Collection results + results = [] + for p in ps: + while q.empty(): + time.sleep(1) + results.extend(q.get()) + + # Consolidate results and return + return {list(i.keys())[0]:list(i.values())[0] for i in results} \ No newline at end of file diff --git a/setup.py b/setup.py index 6b4ca1e..0a7d0c8 100644 --- a/setup.py +++ b/setup.py @@ -6,9 +6,12 @@ with open("README.md", "r", encoding="utf-8") as fh: setuptools.setup( name="openwakeword", version="0.0.1", + install_requires=[ + 'onnxruntime>=1.10.0,<2' + ], author="David Scripka", author_email="david.scripka@gmail.com", - description="An open-source audio wake word (or phrase) detection framework with a focus on accuracy and customizability", + description="An open-source audio wake word (or phrase) detection framework with a focus on performance and simplicity", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/pypa/sampleproject",