diff --git a/snowboy/CHANGELOG.md b/snowboy/CHANGELOG.md new file mode 100644 index 0000000..c6dc197 --- /dev/null +++ b/snowboy/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +- Initial release diff --git a/snowboy/DOCS.md b/snowboy/DOCS.md new file mode 100644 index 0000000..7e0f5d1 --- /dev/null +++ b/snowboy/DOCS.md @@ -0,0 +1,93 @@ +# Home Assistant Add-on: snowboy + +## Installation + +Follow these steps to get the add-on installed on your system: + +1. Navigate in your Home Assistant frontend to **Settings** -> **Add-ons** -> **Add-on store**. +2. Add the store https://github.com/rhasspy/hassio-addons +2. Find the "snowboy" add-on and click it. +3. Click on the "INSTALL" button. + +## How to use + +After this add-on is installed and running, it will be automatically discovered +by the Wyoming integration in Home Assistant. To finish the setup, +click the following my button: + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=wyoming) + +Alternatively, you can install the Wyoming integration manually, see the +[Wyoming integration documentation](https://www.home-assistant.io/integrations/wyoming/) +for more information. + +## Configuration + +### Option: `sensitivity` + +Activation threshold (0-1), where higher means fewer activations. + +### Option: `debug_logging` + +Enable debug logging. Useful for seeing satellite connections and each wake word detection in the logs. + +## Custom Wake Words + +This add-on will train custom wake words on start-up from WAV audio samples placed in `/share/snowboy/train//` + +To get started, first record 3 samples of your wake word: + +```sh +arecord -r 16000 -c 1 -f S16_LE -t wav -d 3 sample1.wav +arecord -r 16000 -c 1 -f S16_LE -t wav -d 3 sample2.wav +arecord -r 16000 -c 1 -f S16_LE -t wav -d 3 sample3.wav +``` + +Ideally, this should be recorded on the same device you plan to use for wake word recognition (same microphone, etc). + +After your 3 samples are recorded, you will need to copy them to your Home Assistant server. You can use the [Samba add-on](https://www.home-assistant.io/common-tasks/supervised/#installing-and-using-the-samba-add-on) to do this. + +Copy the WAV files to `/share/snowboy/train//` where `` is either `en` for English or `zh` for Chinese (other languages are not supported). `` should be the name of your wake word, such as `hey_computer` (spaces in the same are not recommended). + +Your directory structure should look like this after copying the samples: + +- `/share/snowboy/train/` + - `en/` + - `hey_computer/` + - `sample1.wav` + - `sample2.wav` + - `sample3.wav` + +Restart the add-on and check the log for a message that your wake word was trained. Enable debug logging in the add-on configuration for more information. + +After training, your wake word model (`.pmdl`) will be next to your samples: + +- `/share/snowboy/train/` + - `en/` + - `hey_computer/` + - `hey_computer.pmdl` + - `sample1.wav` + - `sample2.wav` + - `sample3.wav` + +Copy your wake word model (e.g., `hey_computer.pmdl`) to `/share/snowboy` to start using it immediately. + +If you'd like to retrain, delete the `.pmdl` file next to your samples and restart the add-on. You will need to copy the new model to `/share/snowboy` again after training. + +## Support + +Got questions? + +You have several options to get them answered: + +- The [Home Assistant Discord Chat Server][discord]. +- The Home Assistant [Community Forum][forum]. +- Join the [Reddit subreddit][reddit] in [/r/homeassistant][reddit] + +In case you've found an bug, please [open an issue on our GitHub][issue]. + +[discord]: https://discord.gg/c5DvZ4e +[forum]: https://community.home-assistant.io +[issue]: https://github.com/home-assistant/addons/issues +[reddit]: https://reddit.com/r/homeassistant +[repository]: https://github.com/rhasspy/hassio-addons diff --git a/snowboy/Dockerfile b/snowboy/Dockerfile new file mode 100644 index 0000000..2f96191 --- /dev/null +++ b/snowboy/Dockerfile @@ -0,0 +1,45 @@ +ARG BUILD_FROM +FROM ${BUILD_FROM} +ARG BUILD_ARCH + +# Set shell +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install snowboy +WORKDIR /usr/src +ARG WYOMING_SNOWBOY_VERSION +ARG SNOWMAN_ENROLL_VERSION +ENV PIP_BREAK_SYSTEM_PACKAGES=1 + +RUN \ + apt-get update \ + && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python3-dev \ + build-essential \ + swig \ + libatlas-base-dev \ + curl \ + && pip3 install --no-cache-dir -U \ + setuptools \ + wheel \ + && pip3 install --no-cache-dir \ + "wyoming-snowboy @ https://github.com/rhasspy/wyoming-snowboy/archive/refs/tags/v${WYOMING_SNOWBOY_VERSION}.tar.gz" \ + && curl --location --output - \ + "https://github.com/rhasspy/snowman-enroll/releases/download/v${SNOWMAN_ENROLL_VERSION}/snowman_enroll-${BUILD_ARCH}.tar.gz" | \ + tar -xzf - \ + && apt-get remove --yes build-essential swig \ + && apt-get autoclean \ + && apt-get purge \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR / +COPY src/train.py /usr/src/ +COPY rootfs / + +HEALTHCHECK --start-period=10m \ + CMD echo '{ "type": "describe" }' \ + | nc -w 1 localhost 10400 \ + | grep -iq "snowboy" \ + || exit 1 diff --git a/snowboy/README.md b/snowboy/README.md new file mode 100644 index 0000000..5756a7d --- /dev/null +++ b/snowboy/README.md @@ -0,0 +1,17 @@ +# Home Assistant Add-on: snowboy + +![Supports aarch64 Architecture][aarch64-shield] ![Supports amd64 Architecture][amd64-shield] ![Supports armv7 Architecture][armv7-shield] + +Home Assistant add-on that uses [snowboy](https://github.com/Kitt-AI/snowboy) for wake word detection and [snowman](https://github.com/Thalhammer/snowman/) for custom wake word training. + +See the [documentation](DOCS.md) for how to train a custom wake word. + +Part of the [Year of Voice](https://www.home-assistant.io/blog/2022/12/20/year-of-voice/). + +Requires Home Assistant 2023.9 or later. + +[aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg +[amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg +[armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg +[armhf-shield]: https://img.shields.io/badge/armhf-no-red.svg +[i386-shield]: https://img.shields.io/badge/i386-no-red.svg diff --git a/snowboy/build.yaml b/snowboy/build.yaml new file mode 100644 index 0000000..09209de --- /dev/null +++ b/snowboy/build.yaml @@ -0,0 +1,10 @@ +--- +build_from: + amd64: ghcr.io/home-assistant/amd64-base-debian:bookworm + aarch64: ghcr.io/home-assistant/aarch64-base-debian:bookworm +codenotary: + signer: notary@home-assistant.io + base_image: notary@home-assistant.io +args: + WYOMING_SNOWBOY_VERSION: 1.0.0 + SNOWMAN_ENROLL_VERSION: 1.0.0 diff --git a/snowboy/config.yaml b/snowboy/config.yaml new file mode 100644 index 0000000..b25c1fe --- /dev/null +++ b/snowboy/config.yaml @@ -0,0 +1,24 @@ +--- +version: 1.0.0-7 +slug: snowboy +name: snowboy +description: snowboy wake word detection using the Wyoming protocol +url: https://github.com/rhasspy/hassio-addons/tree/master/snowboy +arch: + - amd64 + - aarch64 + - armv7 +init: false +discovery: + - wyoming +map: + - share:rw +options: + sensitivity: 0.5 + debug_logging: false +schema: + sensitivity: float + debug_logging: bool +ports: + "10400/tcp": null +homeassistant: 2023.9.0 diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/snowboy b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/snowboy new file mode 100644 index 0000000..e69de29 diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/run b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/run new file mode 100755 index 0000000..049657a --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/run @@ -0,0 +1,24 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Sends discovery information to Home Assistant. +# ============================================================================== +declare config + +# Wait for snowboy to become available +bash -c \ + "until + echo '{ \"type\": \"describe\" }' + > /dev/tcp/localhost/10400; do sleep 0.5; + done" > /dev/null 2>&1 || true; + +config=$(\ + bashio::var.json \ + uri "tcp://$(hostname):10400" \ +) + +if bashio::discovery "wyoming" "${config}" > /dev/null; then + bashio::log.info "Successfully sent discovery information to Home Assistant." +else + bashio::log.error "Discovery message to Home Assistant failed!" +fi diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/type b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/type new file mode 100644 index 0000000..3d92b15 --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/type @@ -0,0 +1 @@ +oneshot \ No newline at end of file diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/up b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/up new file mode 100755 index 0000000..31b0a02 --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/discovery/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/discovery/run \ No newline at end of file diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/dependencies.d/base b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/dependencies.d/base new file mode 100644 index 0000000..e69de29 diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/finish b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/finish new file mode 100755 index 0000000..2911a30 --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/finish @@ -0,0 +1,26 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Take down the S6 supervision tree when service fails +# s6-overlay docs: https://github.com/just-containers/s6-overlay +# ============================================================================== +declare exit_code +readonly exit_code_container=$( /run/s6-linux-init-container-results/exitcode + fi + [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/run b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/run new file mode 100755 index 0000000..b6aa5cf --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/run @@ -0,0 +1,37 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Start snowboy service +# ============================================================================== +flags=() + +if bashio::config.true 'debug_logging'; then + flags+=('--debug') +fi + +train_dir='/share/snowboy/train' +# Train models in a directory with the structure: +# / +# / +# sample1.wav +# ... +# +# When trained, a .pmdl file with the same name as the directory will be +# present. +if [ -n "${train_dir}" ]; then + train_flags=() + if bashio::config.true 'debug_logging'; then + train_flags+=('--debug') + fi + + pushd /usr/src + python3 train.py \ + --train-dir "${train_dir}" \ + --snowman-dir . "${train_flags[@]}" + popd +fi + +exec python3 -m wyoming_snowboy \ + --uri 'tcp://0.0.0.0:10400' \ + --custom-model-dir /share/snowboy \ + --sensitivity "$(bashio::config 'sensitivity')" ${flags[@]} diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/type b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/snowboy/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery new file mode 100644 index 0000000..e69de29 diff --git a/snowboy/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/snowboy b/snowboy/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/snowboy new file mode 100644 index 0000000..e69de29 diff --git a/snowboy/src/train.py b/snowboy/src/train.py new file mode 100644 index 0000000..3525426 --- /dev/null +++ b/snowboy/src/train.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +import argparse +import logging +import subprocess +from pathlib import Path + +_LOGGER = logging.getLogger() + + +def main() -> None: + """Main entry point""" + parser = argparse.ArgumentParser() + parser.add_argument( + "--train-dir", + help="Path to directory with / structure and WAV samples", + ) + parser.add_argument( + "--snowman-dir", + help="Path to directory with snowman enroll binary and resources", + ) + parser.add_argument( + "--debug", action="store_true", help="Print DEBUG messages to the console" + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + _LOGGER.debug(args) + + train_dir = Path(args.train_dir) + if not train_dir.is_dir(): + _LOGGER.debug("Training directory does not exist: %s", train_dir) + return + + snowman_dir = Path(args.snowman_dir) + for lang_dir in train_dir.iterdir(): + if not lang_dir.is_dir(): + continue + + lang = lang_dir.name + for ww_dir in lang_dir.iterdir(): + if not ww_dir.is_dir(): + continue + + wav_files = list(ww_dir.glob("*.wav")) + if not wav_files: + # No WAV files + _LOGGER.debug("No WAV files in %s, skipping", ww_dir) + continue + + ww_name = ww_dir.name + ww_model = ww_dir / f"{ww_name}.pmdl" + if ww_model.exists() and (ww_model.stat().st_size > 0): + # Already trained + _LOGGER.debug("Found %s, skipping %s", ww_model, ww_dir) + continue + + # WAV -> .pmdl + enroll = [ + str((snowman_dir / "enroll").absolute()), + "--language", + lang, + "--output", + str(ww_model), + ] + + for wav_path in wav_files: + enroll.extend(["--recording", str(wav_path)]) + + _LOGGER.debug(enroll) + subprocess.check_call(enroll) + _LOGGER.info("Trained %s", ww_model) + + +if __name__ == "__main__": + main() diff --git a/snowboy/translations/en.yaml b/snowboy/translations/en.yaml new file mode 100644 index 0000000..670f091 --- /dev/null +++ b/snowboy/translations/en.yaml @@ -0,0 +1,12 @@ +--- +configuration: + sensitivity: + name: Sensitivity + description: >- + Activation threshold (0-1), where higher means fewer activations. + debug_logging: + name: Debug logging + description: >- + Enable debug logging. Useful for seeing each wake word detection in the logs. +network: + 10400/tcp: porcupine1 Wyoming Protocol