diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 8343da4..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -*.onnx filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/workflows/build_and_publish_to_pypi.yml b/.github/workflows/build_and_publish_to_pypi.yml index e6f642a..bee4a13 100755 --- a/.github/workflows/build_and_publish_to_pypi.yml +++ b/.github/workflows/build_and_publish_to_pypi.yml @@ -13,8 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - with: - lfs: true - name: Set up Python 3.8 uses: actions/setup-python@v3 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6a13242..ffc7c50 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,6 +8,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: jobs: unit_tests_linux: @@ -18,8 +19,6 @@ jobs: steps: - uses: actions/checkout@v3 - with: - lfs: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: @@ -42,8 +41,6 @@ jobs: steps: - uses: actions/checkout@v3 - with: - lfs: true - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index eef6e33..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -recursive-include openwakeword *.onnx -recursive-include openwakeword *.tflite \ No newline at end of file diff --git a/README.md b/README.md index 0612791..31c436f 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,20 @@ Many thanks to [TeaPoly](https://github.com/TeaPoly/speexdsp-ns-python) for thei # Usage -For quick local testing, clone this repository and use the included [example script](examples/detect_from_microphone.py) to try streaming detection from a local microphone. **Important note!** The model files are stored in this repo using [git-lfs](https://git-lfs.com/); make sure it is installed on your system and if needed use `git-lfs fetch --all` to make sure the the models download correctly. +For quick local testing, clone this repository and use the included [example script](examples/detect_from_microphone.py) to try streaming detection from a local microphone. You can individually download pre-trained models from current and past [releases](https://github.com/dscripka/openWakeWord/releases/), or you can download them using Python (see below). Adding openWakeWord to your own Python code requires just a few lines: ```python +import openwakeword from openwakeword.model import Model -# Instantiate the model +# One-time download of all pre-trained models (or only select models) +openwakeword.utils.download_models() + +# Instantiate the model(s) model = Model( - wakeword_models=["path/to/model.onnx"], # can also leave this argument empty to load all of the included pre-trained models + wakeword_models=["path/to/model.tflite"], # can also leave this argument empty to load all of the included pre-trained models ) # Get audio data containing 16-bit 16khz PCM audio data from a file, microphone, network stream, etc. diff --git a/openwakeword/__init__.py b/openwakeword/__init__.py index d49e9e2..6ad8f3f 100755 --- a/openwakeword/__init__.py +++ b/openwakeword/__init__.py @@ -5,24 +5,48 @@ from openwakeword.custom_verifier_model import train_custom_verifier __all__ = ['Model', 'VAD', 'train_custom_verifier'] -models = { +FEATURE_MODELS = { + "embedding": { + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/embedding_model.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/embedding_model.tflite" + }, + "melspectrogram": { + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/melspectrogram.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/melspectrogram.tflite" + } +} + +VAD_MODELS = { + "silero_vad": { + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/silero_vad.onnx"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/silero_vad.onnx" + } +} + +MODELS = { "alexa": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/alexa_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/alexa_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/alexa_v0.1.tflite" }, "hey_mycroft": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_mycroft_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_mycroft_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/hey_mycroft_v0.1.tflite" }, "hey_jarvis": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_jarvis_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_jarvis_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/hey_jarvis_v0.1.tflite" }, "hey_rhasspy": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_rhasspy_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/hey_rhasspy_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/hey_rhasspy_v0.1.tflite" }, "timer": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/timer_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/timer_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/timer_v0.1.tflite" }, "weather": { - "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/weather_v0.1.tflite") + "model_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources/models/weather_v0.1.tflite"), + "download_url": "https://github.com/dscripka/openWakeWord/releases/download/v0.5.1/weather_v0.1.tflite" } } @@ -40,6 +64,6 @@ model_class_mappings = { def get_pretrained_model_paths(inference_framework="tflite"): if inference_framework == "tflite": - return [models[i]["model_path"] for i in models.keys()] + return [MODELS[i]["model_path"] for i in MODELS.keys()] elif inference_framework == "onnx": - return [models[i]["model_path"].replace(".tflite", ".onnx") for i in models.keys()] + return [MODELS[i]["model_path"].replace(".tflite", ".onnx") for i in MODELS.keys()] diff --git a/openwakeword/model.py b/openwakeword/model.py index 46f603a..6ae820c 100755 --- a/openwakeword/model.py +++ b/openwakeword/model.py @@ -67,7 +67,7 @@ class Model(): with VAD scores above the threshold will be returned. The default value (0), disables voice activity detection entirely. custom_verifier_models (dict): A dictionary of paths to custom verifier models, where - the keys are the model names (corresponding to the openwakeword.models + the keys are the model names (corresponding to the openwakeword.MODELS attribute) and the values are the filepaths of the custom verifier models. custom_verifier_threshold (float): The score threshold to use a custom verifier model. If the score @@ -85,7 +85,7 @@ class Model(): wakeword_model_names = [] if wakeword_models == []: wakeword_models = pretrained_model_paths - wakeword_model_names = list(openwakeword.models.keys()) + wakeword_model_names = list(openwakeword.MODELS.keys()) elif len(wakeword_models) >= 1: for ndx, i in enumerate(wakeword_models): if os.path.exists(i): diff --git a/openwakeword/resources/models/alexa_v0.1.onnx b/openwakeword/resources/models/alexa_v0.1.onnx deleted file mode 100644 index f52240e..0000000 --- a/openwakeword/resources/models/alexa_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6ff566a01d12670e8d9e3c59da32651db1575d17272a601b7f8a39283dfbae3e -size 854246 diff --git a/openwakeword/resources/models/alexa_v0.1.tflite b/openwakeword/resources/models/alexa_v0.1.tflite deleted file mode 100644 index 5d516e2..0000000 --- a/openwakeword/resources/models/alexa_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7333a317a790070a7f3432b81d9439c779481cc4ebd67c73da7174ea3cf48397 -size 855312 diff --git a/openwakeword/resources/models/embedding_model.onnx b/openwakeword/resources/models/embedding_model.onnx deleted file mode 100644 index 2c928ee..0000000 --- a/openwakeword/resources/models/embedding_model.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70d164290c1d095d1d4ee149bc5e00543250a7316b59f31d056cff7bd3075c1f -size 1326578 diff --git a/openwakeword/resources/models/embedding_model.tflite b/openwakeword/resources/models/embedding_model.tflite deleted file mode 100644 index 52a5336..0000000 --- a/openwakeword/resources/models/embedding_model.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c0aea21eb84a4ce90a08c870da41b7a7173b45269e6a3207c71d67c40f3a59d8 -size 1330312 diff --git a/openwakeword/resources/models/hey_jarvis_v0.1.onnx b/openwakeword/resources/models/hey_jarvis_v0.1.onnx deleted file mode 100644 index a45f1de..0000000 --- a/openwakeword/resources/models/hey_jarvis_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:94a13cfe60075b132f6a472e7e462e8123ee70861bc3fb58434a73712ee0d2cb -size 1271370 diff --git a/openwakeword/resources/models/hey_jarvis_v0.1.tflite b/openwakeword/resources/models/hey_jarvis_v0.1.tflite deleted file mode 100644 index d155242..0000000 --- a/openwakeword/resources/models/hey_jarvis_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14bff778604985e1b5c19f0f7bbe477a69cf281d8db34b232b3b972411f710e2 -size 1278912 diff --git a/openwakeword/resources/models/hey_mycroft_v0.1.onnx b/openwakeword/resources/models/hey_mycroft_v0.1.onnx deleted file mode 100644 index b9952b3..0000000 --- a/openwakeword/resources/models/hey_mycroft_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2a311e8fa1338de89c31b3b46dc4dffd4af2f9a8d6ddead48893c2d301b1f18 -size 857691 diff --git a/openwakeword/resources/models/hey_mycroft_v0.1.tflite b/openwakeword/resources/models/hey_mycroft_v0.1.tflite deleted file mode 100644 index 53b373c..0000000 --- a/openwakeword/resources/models/hey_mycroft_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf9e43136afd3ca323698820a6e32a47f885ef4c30a3b8b577ec71688a9d64d8 -size 860300 diff --git a/openwakeword/resources/models/hey_rhasspy_v0.1.onnx b/openwakeword/resources/models/hey_rhasspy_v0.1.onnx deleted file mode 100644 index dea9d9d..0000000 --- a/openwakeword/resources/models/hey_rhasspy_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a9b3ed3be2910e35780e097905aa9f35a9c10038df47914cf2b3ec4d670f6ea -size 204081 diff --git a/openwakeword/resources/models/hey_rhasspy_v0.1.tflite b/openwakeword/resources/models/hey_rhasspy_v0.1.tflite deleted file mode 100644 index 4fb9b6c..0000000 --- a/openwakeword/resources/models/hey_rhasspy_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01d2526b45068f565aa3849d6ec2b7abae099154fc1b496f9ef20de9ef241fe9 -size 416140 diff --git a/openwakeword/resources/models/melspectrogram.onnx b/openwakeword/resources/models/melspectrogram.onnx deleted file mode 100644 index be0643d..0000000 --- a/openwakeword/resources/models/melspectrogram.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba2b0e0f8b7b875369a2c89cb13360ff53bac436f2895cced9f479fa65eb176f -size 1087958 diff --git a/openwakeword/resources/models/melspectrogram.tflite b/openwakeword/resources/models/melspectrogram.tflite deleted file mode 100644 index c0f0ab8..0000000 --- a/openwakeword/resources/models/melspectrogram.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96fa0adccb6e8cf95cb14465409a1a2898ee4a96a85bb9ed3c7eb0e68bf163e8 -size 1092516 diff --git a/openwakeword/resources/models/silero_vad.onnx b/openwakeword/resources/models/silero_vad.onnx deleted file mode 100755 index 664012e..0000000 --- a/openwakeword/resources/models/silero_vad.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a35ebf52fd3ce5f1469b2a36158dba761bc47b973ea3382b3186ca15b1f5af28 -size 1807522 diff --git a/openwakeword/resources/models/timer_v0.1.onnx b/openwakeword/resources/models/timer_v0.1.onnx deleted file mode 100644 index 5603f7d..0000000 --- a/openwakeword/resources/models/timer_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:371e44535470a29248b3b8f1bbbbaf2525c86417fd8f75c67fcf02ae0b9626df -size 1742475 diff --git a/openwakeword/resources/models/timer_v0.1.tflite b/openwakeword/resources/models/timer_v0.1.tflite deleted file mode 100644 index 11a7d50..0000000 --- a/openwakeword/resources/models/timer_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21d5b0267e97df64870b7aca312e2043ebed248d365698926a115a3694ff9626 -size 1743316 diff --git a/openwakeword/resources/models/weather_v0.1.onnx b/openwakeword/resources/models/weather_v0.1.onnx deleted file mode 100644 index 6c5599e..0000000 --- a/openwakeword/resources/models/weather_v0.1.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8441da8e746899e8d969528d5bad5651cdd563079c05962788f77753041f60e7 -size 1149158 diff --git a/openwakeword/resources/models/weather_v0.1.tflite b/openwakeword/resources/models/weather_v0.1.tflite deleted file mode 100644 index 95dab6e..0000000 --- a/openwakeword/resources/models/weather_v0.1.tflite +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4178991c7aeb76670f5a56559eb4129a6f3ae6207886db8bd8094fea7d362c3f -size 1150224 diff --git a/openwakeword/utils.py b/openwakeword/utils.py index 3993483..5f7a926 100644 --- a/openwakeword/utils.py +++ b/openwakeword/utils.py @@ -25,6 +25,7 @@ from tqdm import tqdm import openwakeword from numpy.lib.format import open_memmap from typing import Union, List, Callable, Deque +import requests # Base class for computing audio features using Google's speech_embedding @@ -590,6 +591,79 @@ def compute_features_from_generator(generator, n_total, clip_duration, output_fi trim_mmap(output_file) +# Function to download files from a URL with a progress bar +def download_file(url, target_directory, file_size=None): + """A simple function to download a file from a URL with a progress bar using only the requests library""" + local_filename = url.split('/')[-1] + + with requests.get(url, stream=True) as r: + if file_size is not None: + progress_bar = tqdm(total=file_size, unit='iB', unit_scale=True, desc=f"{local_filename}") + else: + total_size = int(r.headers.get('content-length', 0)) + progress_bar = tqdm(total=total_size, unit='iB', unit_scale=True, desc=f"{local_filename}") + + with open(os.path.join(target_directory, local_filename), 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + progress_bar.update(len(chunk)) + + progress_bar.close() + + +# Function to download models from GitHub release assets +def download_models( + model_names: List[str] = [], + target_directory: str = os.path.join(pathlib.Path(__file__).parent.resolve(), "resources", "models") + ): + """ + Download the specified models from the release assets in the openWakeWord GitHub repository. + Uses the official urls in the MODELS dictionary in openwakeword/__init__.py. + + Args: + model_names (List[str]): The names of the models to download (e.g., hey_jarvis_v0.1). Both ONNX and + tflite models will be downloaded. If not provided (the default), + the latest versions of all models will be downloaded. + target_directory (str): The directory to save the models to. Defaults to the install location + of openWakeWord (i.e., the `resources/models` directory). + Returns: + None + """ + if not isinstance(model_names, list): + raise ValueError("The model_names argument must be a list of strings") + + # Always download melspectrogram and embedding models, if they don't already exist + if not os.path.exists(target_directory): + os.makedirs(target_directory) + for feature_model in openwakeword.FEATURE_MODELS.values(): + if not os.path.exists(os.path.join(target_directory, feature_model["download_url"].split("/")[-1])): + download_file(feature_model["download_url"], target_directory) + download_file(feature_model["download_url"].replace(".tflite", ".onnx"), target_directory) + + # Always download VAD models, if they don't already exist + for vad_model in openwakeword.VAD_MODELS.values(): + if not os.path.exists(os.path.join(target_directory, vad_model["download_url"].split("/")[-1])): + download_file(vad_model["download_url"], target_directory) + + # Get all model urls + official_model_urls = [i["download_url"] for i in openwakeword.MODELS.values()] + official_model_names = [i["download_url"].split("/")[-1] for i in openwakeword.MODELS.values()] + + if model_names != []: + for model_name in model_names: + url = [i for i, j in zip(official_model_urls, official_model_names) if model_name in j] + if url != []: + if not os.path.exists(os.path.join(target_directory, url[0].split("/")[-1])): + download_file(url[0], target_directory) + download_file(url[0].replace(".tflite", ".onnx"), target_directory) + else: + print(official_model_urls) + for official_model_url in official_model_urls: + if not os.path.exists(os.path.join(target_directory, official_model_url.split("/")[-1])): + download_file(official_model_url, target_directory) + download_file(official_model_url.replace(".tflite", ".onnx"), target_directory) + + # Handle deprecated arguments and naming (thanks to https://stackoverflow.com/a/74564394) def re_arg(kwarg_map): def decorator(func): diff --git a/setup.py b/setup.py index eeb5e2e..6210b55 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,8 @@ setuptools.setup( 'tflite-runtime>=2.8.0,<3; platform_system == "Linux"', 'tqdm>=4.0,<5.0', 'scipy>=1.3,<2', - 'scikit-learn>=1,<2' + 'scikit-learn>=1,<2', + 'requests>=2.0,<3', ], extras_require={ 'test': [ @@ -44,7 +45,8 @@ setuptools.setup( 'types-requests', 'types-PyYAML', 'mock>=5.1,<6', - 'types-mock>=5.1,<6' + 'types-mock>=5.1,<6', + 'types-requests>=2.0,<3' ], 'full': [ 'mutagen>=1.46.0,<2', diff --git a/tests/test_custom_verifier_model.py b/tests/test_custom_verifier_model.py index d5665e5..53f02ce 100644 --- a/tests/test_custom_verifier_model.py +++ b/tests/test_custom_verifier_model.py @@ -34,6 +34,9 @@ import scipy.io.wavfile import tempfile import pytest +# Download models needed for tests +openwakeword.utils.download_models(model_names=["alexa_v0.1", "hey_mycroft_v0.1"]) + # Tests class TestModels: diff --git a/tests/test_models.py b/tests/test_models.py index c38ecb8..e728065 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,6 +40,9 @@ import pickle import tempfile import mock +# Download models needed for tests +openwakeword.utils.download_models() + # Tests class TestModels: