diff --git a/piper/CHANGELOG.md b/piper/CHANGELOG.md index 408f2bebb..a44e8a729 100644 --- a/piper/CHANGELOG.md +++ b/piper/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 2.5.2 + +- Upgrade to `wyoming-piper` 2.5.2 +- Add Japanese (OpenJTalk) and Thai (TLTK) support behind the new + `enable_japanese` and `enable_thai` options, and the Thai voice they enable + (`th_TH-tsync2-medium`) +- Japanese and Thai voices are hidden from Home Assistant until their option is + turned on. They used to be offered and then produce silence +- A voice that fails to synthesize now reports why instead of returning empty + audio +- An interrupted voice download is retried on the next start instead of leaving + a truncated file that stayed broken until it was deleted by hand +- Voice models are included in backups again. They were skipped because they + were only re-downloadable copies, but voices can now be uploaded and deleted + from the web interface, so which ones are installed is your choice and an + uploaded voice exists nowhere else. Large re-downloadable caches are still + skipped +- Add 1 new Estonian voice (`et_EE-news-medium`) +- Japanese, Thai and OmniVoice are downloaded when their option is turned on + rather than shipped in the app image, which keeps the image at roughly its + previous size instead of growing to ~2 GB. The download is cached in `/data` + and reused on later starts +- Add a voice management web interface, available through the app's "Open Web + UI" button (ingress). Upload and delete custom Piper voices, and upload + OmniVoice cloning voices, without needing file access to `/share`. Only Home + Assistant's ingress proxy is allowed to reach it +- Add a `backend` option to switch between `piper` (default) and the + experimental `omnivoice` backend. OmniVoice is higher quality and supports + voice cloning, but is much slower and really wants a desktop or server CPU; + it runs elsewhere with a warning, but expect it to be too slow to be useful +- Add an `omnivoice_steps` option to trade OmniVoice quality for speed +- Custom OmniVoice voices are stored in `/data/omnivoice_voices`, kept separate + from the Piper voice models +- Replace the health check with a Wyoming Describe/Info round trip, which also + works with the `omnivoice` backend +- Exclude the OmniVoice model and the HuggingFace cache from backups + ## 2.3.4 - Disable ONNX Runtime telemetry: onnxruntime 1.29.0 (pulled in by the 2.3.3 diff --git a/piper/DOCS.md b/piper/DOCS.md index 6d4736912..8d3a3ab7a 100644 --- a/piper/DOCS.md +++ b/piper/DOCS.md @@ -22,6 +22,46 @@ for more information. ## Configuration +### Option: `backend` + +The text-to-speech engine to use. + +- `piper` (the default) is fast, runs well on a Raspberry Pi, and uses the + voices listed under the `voice` option below. +- `omnivoice` is **experimental**. It is considerably higher quality, supports + many more languages, and can clone a voice from a short recording, but it is + much slower and really needs a desktop or server CPU. It will run on + `aarch64`, with a warning, but expect it to be impractically slow there. + +OmniVoice is not part of the app image. Selecting it downloads it, along with a +model of several GB, the first time the app starts, so it can take a long time +before the app becomes available in Home Assistant. Later starts reuse the +download. + +The two backends have separate voices. Switching backends changes the list of +voices the app advertises, so reload the Wyoming integration for Piper +afterwards. + +### Option: `enable_japanese` / `enable_thai` + +Japanese and Thai voices need an extra phonemizer that is not part of the app +image, because it is large and most installs do not need it. Turning the option +on downloads it the first time the app starts (around 350 MB for Japanese, +390 MB for Thai), so that start takes noticeably longer than usual. Later starts +reuse the download. + +While an option is off, its voices are not offered to Home Assistant at all, so +they will not appear in the Wyoming integration's voice list. Reload that +integration after turning one on. If the app's own `voice` option is set to a +Japanese or Thai voice without its option turned on, the app stops at startup +and says which one to enable. + +### Option: `omnivoice_steps` + +Number of decode steps used by the `omnivoice` backend. Fewer steps are faster, +more steps sound better; the default of 32 is a safe choice, and values as low +as 10 still sound clean. Ignored by the `piper` backend. + ### Option: `voice` [Listen to voice samples](https://rhasspy.github.io/piper-samples/) @@ -71,11 +111,55 @@ Download the list of new voices automatically every time the app starts. You mus Print DEBUG level messages to the app's log. +## Web Interface + +The app has a small web interface for managing voices, reachable with the "Open +Web UI" button on the app page. It is served through Home Assistant ingress, so +using it means going through Home Assistant and being an administrator. Its port +is not published, and it only answers Home Assistant's ingress proxy — other +apps on the same machine are refused. + +It has two sections: + +- **Piper** — upload and delete custom Piper voices (a `.onnx` model plus + its `.onnx.json` config). +- **OmniVoice** — upload a reference recording (a WAV file) and its transcript + to create a cloning voice. + +The app picks up added and removed voices on its own, but Home Assistant caches +the voice list, so **reload the Wyoming integration for Piper** for a new voice +to show up there. + +Voices in `/share/piper` are listed but cannot be deleted from the web +interface, because that directory is mounted read-only. Delete those files +directly instead. + +Because voices can be added and removed here, the set you end up with is your +own and is included in the app's backups — including uploads, which exist +nowhere else. Only the large re-downloadable caches are left out: the OmniVoice +model weights and the downloads for the Japanese and Thai options. + ## Custom Voices -Add custom voice files to the `/share/piper` directory. Each custom voice must include a model file (`.onnx`) and config file (`.onnx.json`). +Add custom voice files to the `/share/piper` directory, or upload them through +the web interface. Each custom voice must include a model file (`.onnx`) +and config file (`.onnx.json`). See the [training guide](https://github.com/rhasspy/piper/blob/master/TRAINING.md) for details on how to train and export a custom voice. +## Custom OmniVoice Voices + +The `omnivoice` backend uses its own voices, which are not Piper voice models. +They live in `/data/omnivoice_voices` inside the app, organized as +`//`, and are managed through the **OmniVoice** section of +the web interface. Each voice is a reference recording (`ref.wav`) plus its +transcript (`ref.txt`), which OmniVoice clones. + +A `default` voice is always available for every supported language and uses +OmniVoice's built-in speaker, so cloning voices are optional. + +These voices are included in app backups. The downloaded OmniVoice model is not, +since it is several GB and is re-downloaded when needed. + ## Support Got questions? diff --git a/piper/Dockerfile b/piper/Dockerfile index 39fbe1662..3162aebc6 100644 --- a/piper/Dockerfile +++ b/piper/Dockerfile @@ -6,35 +6,49 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install Piper WORKDIR /usr/src -ARG BUILD_ARCH ARG WYOMING_PIPER_VERSION +ARG UV_VERSION + +ENV WYOMING_PIPER_VERSION=${WYOMING_PIPER_VERSION} + +# There is no virtual environment in this image +ENV UV_SYSTEM_PYTHON=true + +# A unique approach is taken in this image to dependency management. Different +# voices (th, ja) and backends (omnivoice) require large Python dependencies. +# Rather than making everyone pay the disk space for them, a new "pack" s6 +# dependency installs them on startup when enabled. +# +# Importantly, the uv cache is stored in /data and because uv caches wheels +# uncompressed already, the relevant files can be symlinked into the container's +# filesystem with virtually no overhead. This only works if we target the system +# Python because the build time and runtime dependencies need to be layered. +# Hence, there is no virtual environment anymore. + RUN \ apt-get update \ && apt-get install -y --no-install-recommends \ - netcat-traditional \ python3 \ python3-pip \ - python3-venv \ \ - && python3 -m venv .venv \ - && .venv/bin/pip3 install --no-cache-dir -U \ - setuptools \ - wheel \ - && .venv/bin/pip3 install --no-cache-dir \ - --extra-index-url https://www.piwheels.org/simple \ - --extra-index-url https://download.pytorch.org/whl/cpu \ - "wyoming-piper[zeroconf,zh] @ https://github.com/rhasspy/wyoming-piper/archive/refs/tags/v${WYOMING_PIPER_VERSION}.tar.gz" \ + # Install into the system interpreter rather than a venv + && rm -f /usr/lib/python3*/EXTERNALLY-MANAGED \ + # pip is only here to bootstrap uv + && pip3 install --no-cache-dir "uv==${UV_VERSION}" \ + \ + # --no-cache: the runtime cache lives in /data and must not be seeded with + # build-time downloads baked into the image. + && uv pip install --no-cache \ + "wyoming-piper[zeroconf,zh,web] @ https://github.com/rhasspy/wyoming-piper/archive/refs/tags/v${WYOMING_PIPER_VERSION}.tar.gz" \ \ && rm -rf /var/lib/apt/lists/* -ENV PATH="/usr/src/.venv/bin:$PATH" - ENV ORT_DISABLE_TELEMETRY=1 WORKDIR / COPY rootfs / -HEALTHCHECK --start-period=10m \ - CMD echo '{ "type": "describe" }' \ - | nc -w 1 localhost 10200 \ - | grep -q "piper" \ +# A Describe/Info round trip rather than a bare port check +HEALTHCHECK --start-period=30m --interval=30s --timeout=20s --retries=3 \ + CMD /usr/bin/python3 -m wyoming_piper.health_check \ + --uri tcp://127.0.0.1:10200 \ || exit 1 diff --git a/piper/build.yaml b/piper/build.yaml index 7964371ba..f86a0a223 100644 --- a/piper/build.yaml +++ b/piper/build.yaml @@ -3,4 +3,5 @@ build_from: amd64: ghcr.io/home-assistant/amd64-base-debian:trixie aarch64: ghcr.io/home-assistant/aarch64-base-debian:trixie args: - WYOMING_PIPER_VERSION: 2.3.1 + WYOMING_PIPER_VERSION: 2.5.2 + UV_VERSION: 0.12.12 diff --git a/piper/config.yaml b/piper/config.yaml index 55789e57d..0da7732cd 100644 --- a/piper/config.yaml +++ b/piper/config.yaml @@ -1,5 +1,5 @@ --- -version: 2.3.4 +version: 2.5.2 slug: piper name: Piper description: Text-to-speech with Piper @@ -10,27 +10,51 @@ arch: init: false discovery: - wyoming +# Voice models ("*.onnx") used to be excluded, back when /data held nothing but +# re-downloadable copies of catalog voices. The web interface now uploads and +# deletes voices there, so which models are present is the user's own choice and +# has to survive a backup -- an uploaded voice exists nowhere else. Only the +# genuinely disposable caches below are skipped. backup_exclude: - - "*.onnx" + # OmniVoice weights, 632 MB. The 1.5 MB graph beside them is kept, but the + # backend needs both files, so a restore without the weights just downloads + # them again. + - "*.onnx.data" + # HuggingFace cache (HF_HOME is /data, so models land in /data/hub). + - "*/hub" + # Wheel cache for the optional packs, rebuilt on demand. + - "*/uv-cache" map: - share +ingress: true +ingress_port: 8099 +panel_icon: mdi:account-voice +panel_title: Piper voices options: + backend: piper voice: en_US-lessac-medium speaker: 0 length_scale: 1.0 noise_scale: 0.667 noise_w: 0.333 sentence_silence: 0.0 + omnivoice_steps: 32 + enable_japanese: false + enable_thai: false debug_logging: false update_voices: true schema: + backend: list(piper|omnivoice) voice: | - list(ar_JO-kareem-low|ar_JO-kareem-medium|bg_BG-dimitar-medium|bn_BD-google-medium|ca_ES-upc_ona-medium|ca_ES-upc_ona-x_low|ca_ES-upc_pau-x_low|ca-upc_ona-x-low|ca-upc_pau-x-low|cs_CZ-jirka-low|cs_CZ-jirka-medium|cs_CZ-kasandra-medium|cy_GB-bu_tts-medium|cy_GB-gwryw_gogleddol-medium|da_DK-talesyntese-medium|da-nst_talesyntese-medium|de_DE-eva_k-x_low|de_DE-karlsson-low|de_DE-kerstin-low|de_DE-mls-medium|de_DE-pavoque-low|de_DE-ramona-low|de_DE-thorsten_emotional-medium|de_DE-thorsten-high|de_DE-thorsten-low|de_DE-thorsten-medium|de-eva_k-x-low|de-karlsson-low|de-kerstin-low|de-pavoque-low|de-ramona-low|de-thorsten-low|el_GR-joy-medium|el-gr-rapunzelina-low|el_GR-rapunzelina-low|el_GR-rapunzelina-medium|en-gb-alan-low|en_GB-alan-low|en_GB-alan-medium|en_GB-alba-medium|en_GB-aru-medium|en_GB-cori-high|en_GB-cori-medium|en_GB-jenny_dioco-medium|en_GB-northern_english_male-medium|en_GB-semaine-medium|en-gb-southern_english_female-low|en_GB-southern_english_female-low|en_GB-vctk-medium|en-us-amy-low|en_US-amy-low|en_US-amy-medium|en_US-arctic-medium|en_US-bryce-medium|en-us-danny-low|en_US-danny-low|en_US-hfc_female-medium|en_US-hfc_male-medium|en_US-joe-medium|en_US-john-medium|en-us-kathleen-low|en_US-kathleen-low|en_US-kristin-medium|en_US-kusal-medium|en_US-l2arctic-medium|en_US-lessac-high|en-us-lessac-low|en_US-lessac-low|en-us-lessac-medium|en_US-lessac-medium|en-us-libritts-high|en_US-libritts-high|en_US-libritts_r-medium|en_US-ljspeech-high|en_US-ljspeech-medium|en_US-mike-medium|en_US-norman-medium|en_US-reza_ibrahim-medium|en-us-ryan-high|en_US-ryan-high|en-us-ryan-low|en_US-ryan-low|en-us-ryan-medium|en_US-ryan-medium|en_US-sam-medium|es_AR-daniela-high|es-carlfm-x-low|es_ES-carlfm-x_low|es_ES-davefx-medium|es_ES-mls_10246-low|es_ES-mls_9972-low|es_ES-sharvard-medium|es-mls_10246-low|es-mls_9972-low|es_MX-ald-medium|es_MX-ald-x_low|es_MX-claude-high|eu_ES-antton-medium|eu_ES-maider-medium|fa_IR-amir-medium|fa_IR-ganji_adabi-medium|fa_IR-ganji-medium|fa_IR-gyro-medium|fa_IR-reza_ibrahim-medium|fi_FI-harri-low|fi_FI-harri-medium|fi-harri-low|fr_FR-gilles-low|fr_FR-mls_1840-low|fr_FR-mls-medium|fr_FR-siwis-low|fr_FR-siwis-medium|fr_FR-tom-medium|fr_FR-upmc-medium|fr-gilles-low|fr-mls_1840-low|fr-siwis-low|fr-siwis-medium|he_IL-saspeech-medium|hi_IN-pratham-medium|hi_IN-priyamvada-medium|hi_IN-rohan-medium|hu_HU-anna-medium|hu_HU-berta-medium|hu_HU-imre-medium|hy_AM-gor-medium|id_ID-news_tts-medium|is-bui-medium|is_IS-bui-medium|is_IS-salka-medium|is_IS-steinn-medium|is_IS-ugla-medium|is-salka-medium|is-steinn-medium|is-ugla-medium|it_IT-paola-medium|it_IT-riccardo-x_low|it-riccardo_fasol-x-low|it_IT-serena-high|it_IT-serena-medium|ja_JA-hi_fi_captain-medium|ka_GE-natia-medium|kk-iseke-x-low|kk-issai-high|kk_KZ-iseke-x_low|kk_KZ-issai-high|kk_KZ-raya-x_low|kk-raya-x-low|ko_KR-kss-medium|ku_TR-berfin_renas-medium|lb_LU-marylux-medium|lv_LV-aivars-medium|ml_IN-arjun-medium|ml_IN-meera-medium|mr_IN-google-medium|ne-google-medium|ne-google-x-low|ne_NP-chitwan-medium|ne_NP-google-medium|ne_NP-google-x_low|nl_BE-nathalie-medium|nl_BE-nathalie-x_low|nl_BE-rdh-medium|nl_BE-rdh-x_low|nl-mls_5809-low|nl-mls_7432-low|nl-nathalie-x-low|nl_NL-alex-medium|nl_NL-mls_5809-low|nl_NL-mls_7432-low|nl_NL-mls-medium|nl_NL-pim-medium|nl_NL-ronnie-medium|nl-rdh-medium|nl-rdh-x-low|no_NO-nvcc-medium|no_NO-talesyntese-medium|no-talesyntese-medium|pl-mls_6892-low|pl_PL-bass-high|pl_PL-darkman-medium|pl_PL-gosia-medium|pl_PL-mc_speech-medium|pl_PL-mls_6892-low|pt_BR-cadu-medium|pt-br-edresson-low|pt_BR-edresson-low|pt_BR-faber-medium|pt_BR-jeff-medium|pt_PT-tugão-medium|ro_RO-mihai-medium|ru-irinia-medium|ru_RU-denis-medium|ru_RU-dmitri-medium|ru_RU-irina-medium|ru_RU-ruslan-medium|sk_SK-lili-medium|sl_SI-artur-medium|sq_AL-edon-medium|sr_RS-serbski_institut-medium|sv_SE-alma-medium|sv_SE-lisa-medium|sv_SE-nst-medium|sw_CD-lanfrica-medium|te_IN-maya-medium|te_IN-padmavathi-medium|te_IN-venkatesh-medium|tr_TR-dfki-medium|uk-lada-x-low|uk_UA-lada-x_low|uk_UA-mykyta-high|uk_UA-oleksa-high|uk_UA-tetiana-high|uk_UA-ukrainian_tts-medium|ur_PK-aegis_female-medium|ur_PK-fasih-medium|vi-25hours-single-low|vi-vivos-x-low|vi_VN-25hours_single-low|vi_VN-vais1000-medium|vi_VN-vivos-x_low|zh_CN-chaowen-medium|zh_CN-huayan-medium|zh-cn-huayan-x-low|zh_CN-huayan-x_low|zh_CN-xiao_ya-medium) + list(ar_JO-kareem-low|ar_JO-kareem-medium|bg_BG-dimitar-medium|bn_BD-google-medium|ca_ES-upc_ona-medium|ca_ES-upc_ona-x_low|ca_ES-upc_pau-x_low|ca-upc_ona-x-low|ca-upc_pau-x-low|cs_CZ-jirka-low|cs_CZ-jirka-medium|cs_CZ-kasandra-medium|cy_GB-bu_tts-medium|cy_GB-gwryw_gogleddol-medium|da_DK-talesyntese-medium|da-nst_talesyntese-medium|de_DE-eva_k-x_low|de_DE-karlsson-low|de_DE-kerstin-low|de_DE-mls-medium|de_DE-pavoque-low|de_DE-ramona-low|de_DE-thorsten_emotional-medium|de_DE-thorsten-high|de_DE-thorsten-low|de_DE-thorsten-medium|de-eva_k-x-low|de-karlsson-low|de-kerstin-low|de-pavoque-low|de-ramona-low|de-thorsten-low|el_GR-joy-medium|el-gr-rapunzelina-low|el_GR-rapunzelina-low|el_GR-rapunzelina-medium|en-gb-alan-low|en_GB-alan-low|en_GB-alan-medium|en_GB-alba-medium|en_GB-aru-medium|en_GB-cori-high|en_GB-cori-medium|en_GB-jenny_dioco-medium|en_GB-northern_english_male-medium|en_GB-semaine-medium|en-gb-southern_english_female-low|en_GB-southern_english_female-low|en_GB-vctk-medium|en-us-amy-low|en_US-amy-low|en_US-amy-medium|en_US-arctic-medium|en_US-bryce-medium|en-us-danny-low|en_US-danny-low|en_US-hfc_female-medium|en_US-hfc_male-medium|en_US-joe-medium|en_US-john-medium|en-us-kathleen-low|en_US-kathleen-low|en_US-kristin-medium|en_US-kusal-medium|en_US-l2arctic-medium|en_US-lessac-high|en-us-lessac-low|en_US-lessac-low|en-us-lessac-medium|en_US-lessac-medium|en-us-libritts-high|en_US-libritts-high|en_US-libritts_r-medium|en_US-ljspeech-high|en_US-ljspeech-medium|en_US-mike-medium|en_US-norman-medium|en_US-reza_ibrahim-medium|en-us-ryan-high|en_US-ryan-high|en-us-ryan-low|en_US-ryan-low|en-us-ryan-medium|en_US-ryan-medium|en_US-sam-medium|es_AR-daniela-high|es-carlfm-x-low|es_ES-carlfm-x_low|es_ES-davefx-medium|es_ES-mls_10246-low|es_ES-mls_9972-low|es_ES-sharvard-medium|es-mls_10246-low|es-mls_9972-low|es_MX-ald-medium|es_MX-ald-x_low|es_MX-claude-high|et_EE-news-medium|eu_ES-antton-medium|eu_ES-maider-medium|fa_IR-amir-medium|fa_IR-ganji_adabi-medium|fa_IR-ganji-medium|fa_IR-gyro-medium|fa_IR-reza_ibrahim-medium|fi_FI-harri-low|fi_FI-harri-medium|fi-harri-low|fr_FR-gilles-low|fr_FR-mls_1840-low|fr_FR-mls-medium|fr_FR-siwis-low|fr_FR-siwis-medium|fr_FR-tom-medium|fr_FR-upmc-medium|fr-gilles-low|fr-mls_1840-low|fr-siwis-low|fr-siwis-medium|he_IL-saspeech-medium|hi_IN-pratham-medium|hi_IN-priyamvada-medium|hi_IN-rohan-medium|hu_HU-anna-medium|hu_HU-berta-medium|hu_HU-imre-medium|hy_AM-gor-medium|id_ID-news_tts-medium|is-bui-medium|is_IS-bui-medium|is_IS-salka-medium|is_IS-steinn-medium|is_IS-ugla-medium|is-salka-medium|is-steinn-medium|is-ugla-medium|it_IT-paola-medium|it_IT-riccardo-x_low|it-riccardo_fasol-x-low|it_IT-serena-high|it_IT-serena-medium|ja_JA-hi_fi_captain-medium|ka_GE-natia-medium|kk-iseke-x-low|kk-issai-high|kk_KZ-iseke-x_low|kk_KZ-issai-high|kk_KZ-raya-x_low|kk-raya-x-low|ko_KR-kss-medium|ku_TR-berfin_renas-medium|lb_LU-marylux-medium|lv_LV-aivars-medium|ml_IN-arjun-medium|ml_IN-meera-medium|mr_IN-google-medium|ne-google-medium|ne-google-x-low|ne_NP-chitwan-medium|ne_NP-google-medium|ne_NP-google-x_low|nl_BE-nathalie-medium|nl_BE-nathalie-x_low|nl_BE-rdh-medium|nl_BE-rdh-x_low|nl-mls_5809-low|nl-mls_7432-low|nl-nathalie-x-low|nl_NL-alex-medium|nl_NL-mls_5809-low|nl_NL-mls_7432-low|nl_NL-mls-medium|nl_NL-pim-medium|nl_NL-ronnie-medium|nl-rdh-medium|nl-rdh-x-low|no_NO-nvcc-medium|no_NO-talesyntese-medium|no-talesyntese-medium|pl-mls_6892-low|pl_PL-bass-high|pl_PL-darkman-medium|pl_PL-gosia-medium|pl_PL-mc_speech-medium|pl_PL-mls_6892-low|pt_BR-cadu-medium|pt-br-edresson-low|pt_BR-edresson-low|pt_BR-faber-medium|pt_BR-jeff-medium|pt_PT-tugão-medium|ro_RO-mihai-medium|ru-irinia-medium|ru_RU-denis-medium|ru_RU-dmitri-medium|ru_RU-irina-medium|ru_RU-ruslan-medium|sk_SK-lili-medium|sl_SI-artur-medium|sq_AL-edon-medium|sr_RS-serbski_institut-medium|sv_SE-alma-medium|sv_SE-lisa-medium|sv_SE-nst-medium|sw_CD-lanfrica-medium|te_IN-maya-medium|te_IN-padmavathi-medium|te_IN-venkatesh-medium|th_TH-tsync2-medium|tr_TR-dfki-medium|uk-lada-x-low|uk_UA-lada-x_low|uk_UA-mykyta-high|uk_UA-oleksa-high|uk_UA-tetiana-high|uk_UA-ukrainian_tts-medium|ur_PK-aegis_female-medium|ur_PK-fasih-medium|vi-25hours-single-low|vi-vivos-x-low|vi_VN-25hours_single-low|vi_VN-vais1000-medium|vi_VN-vivos-x_low|zh_CN-chaowen-medium|zh_CN-huayan-medium|zh-cn-huayan-x-low|zh_CN-huayan-x_low|zh_CN-xiao_ya-medium) speaker: int length_scale: float noise_scale: float noise_w: float sentence_silence: float + omnivoice_steps: int(1,) + enable_japanese: bool + enable_thai: bool debug_logging: bool update_voices: bool ports: diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/dependencies.d/base b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run new file mode 100755 index 000000000..43524e7aa --- /dev/null +++ b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run @@ -0,0 +1,142 @@ +#!/command/with-contenv bashio +# vim: ft=bash +# shellcheck shell=bash +# ============================================================================== +# Install the optional feature packs selected in the app configuration. +# +# Japanese, Thai and OmniVoice are not part of the image: together they are +# ~1.5 GB, for options most installs never turn on. They are installed here +# instead, into the container's own site-packages -- which is thrown away with +# the container -- from a uv cache kept in /data. +# +# uv symlinks the files out of that cache rather than copying them, so the +# installed tree is a few MB of links and only the cache takes real space. And +# because the tree is rebuilt on every start against the interpreter that is +# actually running, there is nothing to keep in sync: a base image with a new +# Python simply misses in the cache and downloads again. +# +# A pack that fails to install is not fatal here. Whether it was actually +# needed depends on the backend and voice, so the Piper service decides that +# and fails with a message naming the option to turn on. +# ============================================================================== + +export UV_CACHE_DIR='/data/uv-cache' +export UV_SYSTEM_PYTHON=true + +# /data and the container filesystem are different mounts, so uv's default +# hardlink mode silently degrades to full copies and every pack would cost its +# size twice -- once in the cache, once installed. Symlinks span mounts. +export UV_LINK_MODE=symlink + +# Discard the cache when either the interpreter or wyoming-piper moves to a new +# series, so it cannot grow without bound across app updates. A new Python minor +# changes the ABI tag and strands every cached wheel; a new wyoming-piper minor +# can move dependency floors far enough that the cached resolution is dead +# weight. Patch releases change neither, so 2.5.1 -> 2.5.2 keeps the cache and +# 2.5.x -> 2.6.x does not. +readonly cache_series_file="${UV_CACHE_DIR}/.series" +# Defaulted because bashio runs with "set -o nounset": the version comes from +# the image environment and is always set in practice, but an unset one must +# not abort the service before a single pack is installed. +piper_version="${WYOMING_PIPER_VERSION:-}" +cache_series="python=$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" +cache_series+=" wyoming-piper=${piper_version%.*}" + +if [[ -f "${cache_series_file}" ]] \ + && [[ "$(< "${cache_series_file}")" != "${cache_series}" ]]; then + bashio::log.info \ + "Discarding pack cache: $(< "${cache_series_file}") -> ${cache_series}" + rm -rf "${UV_CACHE_DIR}" +fi + +mkdir -p "${UV_CACHE_DIR}" +printf '%s\n' "${cache_series}" > "${cache_series_file}" + +# Try the cache before the network, so a restart costs no index round trip and +# an installation without internet access still comes back up. Falls back to a +# normal online install whenever anything is missing from the cache. +install_pack() { + local name="${1}" + shift + + if uv pip install --quiet --offline "$@" > /dev/null 2>&1; then + bashio::log.info "${name}: restored from cache" + return 0 + fi + + bashio::log.info "${name}: downloading, this can take several minutes..." + if uv pip install --quiet "$@"; then + bashio::log.info "${name}: installed" + return 0 + fi + + bashio::log.warning "${name}: install failed" + return 1 +} + +# The three OmniVoice steps cannot be one requirement: torch has to come from +# the CPU index (PyPI's default wheels carry ~2.7 GB of CUDA libraries that are +# never loaded), and the omnivoice package has to be installed without its +# dependencies -- gradio, librosa, webdataset and tensorboardx are only used by +# its demo and training paths, which this backend never imports. The +# omnivoice-deps extra pins what it does import. +install_omnivoice() { + install_pack 'OmniVoice (PyTorch, CPU)' \ + --index-url https://download.pytorch.org/whl/cpu \ + torch \ + torchaudio || return 1 + + install_pack 'OmniVoice (dependencies)' \ + "wyoming-piper[omnivoice-deps] @ https://github.com/rhasspy/wyoming-piper/archive/refs/tags/v${piper_version}.tar.gz" \ + || return 1 + + install_pack 'OmniVoice' --no-deps omnivoice || return 1 +} + +packs_selected='false' + +# "|| true" throughout: bashio runs with "set -o errexit", so without it a +# single failed download would abort this service, skip every remaining pack, +# and -- because the Piper service depends on this one -- could stop the app +# from starting at all. Whether a missing pack actually matters depends on the +# backend and voice, which the Piper service decides. +if bashio::config.true 'enable_japanese'; then + packs_selected='true' + install_pack 'Japanese (OpenJTalk)' 'piper-tts[ja]' || true +fi + +if bashio::config.true 'enable_thai'; then + packs_selected='true' + # tltk imports requests without declaring it. wyoming-piper's own "th" + # extra works around that, but this installs piper-tts[th] directly to + # avoid re-fetching wyoming-piper, so the workaround is repeated here. + install_pack 'Thai (TLTK)' 'piper-tts[th]' requests || true +fi + +if [[ "$(bashio::config 'backend')" == 'omnivoice' ]]; then + packs_selected='true' + + if [[ "$(uname -m)" != 'x86_64' ]]; then + bashio::log.warning \ + "OmniVoice is being installed on $(uname -m). It is designed for a" \ + "desktop or server CPU and will be very slow on this hardware." + fi + + install_omnivoice || true +fi + +# Report what /data holds. On Home Assistant OS it cannot be browsed without a +# host shell, so the log is the only place this is visible. +if bashio::var.true "${packs_selected}"; then + bashio::log.info "Pack cache: $(du -sh "${UV_CACHE_DIR}" | cut -f1) in ${UV_CACHE_DIR}" +fi + +if bashio::config.true 'debug_logging'; then + while read -r entry; do + bashio::log.info "/data: ${entry}" + done < <(du -sh /data/* 2>/dev/null) +fi + +# Never block Piper from starting: the service checks for what it actually +# needs and reports a usable error itself. +exit 0 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/type b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/type new file mode 100644 index 000000000..3d92b15f2 --- /dev/null +++ b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/type @@ -0,0 +1 @@ +oneshot \ No newline at end of file diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up new file mode 100644 index 000000000..6c31c4929 --- /dev/null +++ b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/packs/run \ No newline at end of file diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/dependencies.d/packs b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/dependencies.d/packs new file mode 100644 index 000000000..e69de29bb diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run index fa59930ab..75f910114 100755 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run +++ b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run @@ -4,15 +4,78 @@ # ============================================================================== # Start Piper service # ============================================================================== + +# Reference recordings for OmniVoice cloning voices, uploaded through the web +# UI. Kept separate from the Piper voice models, which are .onnx pairs +# sitting directly in /data (and /share/piper); an OmniVoice voice is a +# // directory instead. +readonly omnivoice_voices_dir='/data/omnivoice_voices' + +# Ingress proxies to the container's address rather than to loopback, so the web +# UI has to bind every interface -- which also puts it in reach of every other +# app on the same Docker network. It has no authentication of its own, so the +# peer address is restricted to the ingress proxy, whose address is fixed. The +# port is deliberately not published in config.yaml either. +readonly web_server_port=8099 +readonly ingress_proxy='172.30.32.2' + +# True if a module the "packs" service installs is importable. +have_module() { + python3 -c \ + "import importlib.util, sys; sys.exit(importlib.util.find_spec('${1}') is None)" +} + flags=() -if bashio::config.true 'update_voices'; then - flags+=('--update-voices') -fi if bashio::config.true 'debug_logging'; then flags+=('--debug') fi +mkdir -p "${omnivoice_voices_dir}" + +backend="$(bashio::config 'backend')" +if [[ "${backend}" == 'omnivoice' ]]; then + if ! have_module 'omnivoice'; then + # One argument: bashio::exit.nok only logs its first. + bashio::exit.nok \ + "The OmniVoice backend is not installed. Check the log above for a failed download, or set 'backend' back to 'piper'." + fi + + flags+=( + '--backend' 'omnivoice' + '--omnivoice-steps' "$(bashio::config 'omnivoice_steps')" + ) +else + # Japanese and Thai need a phonemizer that is only installed when its + # option is on. Checked here so the failure names the option, instead of + # surfacing as silence the first time something is spoken. Only the default + # voice can be checked -- Home Assistant may request any voice at runtime. + voice="$(bashio::config 'voice')" + case "${voice}" in + ja_*|ja-*) + have_module 'pyopenjtalk' || bashio::exit.nok \ + "The Japanese voice '${voice}' needs the 'enable_japanese' option to be turned on." + ;; + th_*|th-*) + have_module 'tltk' || bashio::exit.nok \ + "The Thai voice '${voice}' needs the 'enable_thai' option to be turned on." + ;; + esac + + # Piper-only synthesis settings; the omnivoice backend ignores them. + flags+=( + '--voice' "${voice}" + '--speaker' "$(bashio::config 'speaker')" + '--length-scale' "$(bashio::config 'length_scale')" + '--noise-scale' "$(bashio::config 'noise_scale')" + '--noise-w' "$(bashio::config 'noise_w')" + ) + + if bashio::config.true 'update_voices'; then + flags+=('--update-voices') + fi +fi + # Delete old options options=$(bashio::addon.options) for old_key in 'max_piper_procs' 'streaming'; do @@ -22,16 +85,19 @@ for old_key in 'max_piper_procs' 'streaming'; do fi done +# --omnivoice-ref-dir is passed for both backends so the web UI can manage +# cloning voices even while Piper is the active backend. +# # shellcheck disable=SC2068 exec python3 -m wyoming_piper \ --uri 'tcp://0.0.0.0:10200' \ --zeroconf \ - --length-scale "$(bashio::config 'length_scale')" \ - --noise-scale "$(bashio::config 'noise_scale')" \ - --noise-w "$(bashio::config 'noise_w')" \ --sentence-silence "$(bashio::config 'sentence_silence')" \ - --speaker "$(bashio::config 'speaker')" \ - --voice "$(bashio::config 'voice')" \ --data-dir /data \ --data-dir /share/piper \ - --download-dir /data ${flags[@]} + --download-dir /data \ + --omnivoice-ref-dir "${omnivoice_voices_dir}" \ + --web-server \ + --web-server-host '0.0.0.0' \ + --web-server-port "${web_server_port}" \ + --web-server-allow "${ingress_proxy}" ${flags[@]} diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/packs b/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/packs new file mode 100644 index 000000000..e69de29bb diff --git a/piper/translations/en.yaml b/piper/translations/en.yaml index 527ddeb3f..074e864d0 100644 --- a/piper/translations/en.yaml +++ b/piper/translations/en.yaml @@ -1,10 +1,19 @@ --- configuration: + backend: + name: Backend + description: >- + The text-to-speech engine to use. `piper` is fast and runs on any + hardware. `omnivoice` is experimental: much higher quality, supports + voice cloning, but it is much slower and really wants a desktop or + server CPU. It will run on other hardware, but expect it to be too slow + to be useful there. voice: name: Voice description: >- The Piper voice to use. Voices are named according to the following - scheme: `--`. + scheme: `--`. Ignored when the backend is + `omnivoice`, which picks its voice per request. speaker: name: Speaker description: >- @@ -31,6 +40,22 @@ configuration: name: Sentence silence description: >- Adds the given number of seconds of silence after every sentence. + omnivoice_steps: + name: OmniVoice steps + description: >- + Number of decode steps for the `omnivoice` backend. Fewer steps are + faster, more steps sound better. Only used when the backend is + `omnivoice`. + enable_japanese: + name: Japanese support + description: >- + Download the Japanese phonemizer, needed for Japanese voices. Around + 350 MB is downloaded the first time this is turned on. + enable_thai: + name: Thai support + description: >- + Download the Thai phonemizer, needed for Thai voices. Around 390 MB is + downloaded the first time this is turned on. update_voices: name: Update voices description: >- diff --git a/zwave_js/CHANGELOG.md b/zwave_js/CHANGELOG.md index e2cae9e32..ccc151098 100644 --- a/zwave_js/CHANGELOG.md +++ b/zwave_js/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## 1.8.0 + +### Z-Wave JS 15.27.1...15.29.0 + +#### Features + +- Queued commands that are redundant are now deduplicated automatically + +#### Bugfixes + +- Fixed an issue preventing controller firmware updates from starting properly +- Work around an issue where some controllers on Z-Wave SDK 8.0.0 that had migrated from an old 500 series controller would behave like sleeping nodes +- Prevent SONOFF controllers from entering the bootloader during startup on Windows +- Correct signal-to-noise margin units from dBm to dB +- Fixed how some commands were handled internally to improve spec compliance +- Fields in incoming commands that are unexpected for the version supported by the end device are now ignored +- Fixed an issue where spurious "unknown" notification sensor entities could be created +- Fixed an issue where the regular background RSSI measurements could prevent refreshing the status of some legacy devices in some scenarios + +#### Config file changes + +- Fix malformed notifications from HomeSeer FS100 devices +- Add labels to several devices' endpoints +- Correct external sensor resistance options for Heatit Z-TRM7 +- Add fingerprint `0x0c02:0x3002` to Fibargroup FGSD-002 +- Update Zooz ZEN32 for firmware 3.30 +- Add Aeotec ZWA056 Water Sensor 8 +- Add Aeotec ZWA065 Siren 8 +- Add Aeotec ZWA057 aërQ Temperature and Humidity Sensor 8 +- Add Aeotec ZWA055 Door / Window Sensor 8 +- Update Inovelli VZW32-SN P50 and P107 for firmware 2.4 +- Add Zooz ZEN12 800LR Motion Dimmer +- Add Zooz ZEN37 parameter 10 for firmware 2.40 +- Add an Off option for Heatit Z-TRM7 standby display brightness +- Update Zooz configurations for new firmware parameters +- Add Simon 100 Battery Master Roller Blind +- Update Inovelli VZW32-SN P111 to match the public documentation + +### Z-Wave JS UI 11.22.3...11.24.0 + +#### Features + +- Add a speed column to the control panel +- Add environment metadata to debug capture sessions + +### Detailed changelogs + +- [Z-Wave JS UI 11.24.0](https://github.com/zwave-js/zwave-js-ui/releases/tag/v11.24.0) +- [Z-Wave JS UI 11.23.0](https://github.com/zwave-js/zwave-js-ui/releases/tag/v11.23.0) +- [Z-Wave JS 15.29.0](https://github.com/zwave-js/zwave-js/releases/tag/v15.29.0) +- [Z-Wave JS 15.28.0](https://github.com/zwave-js/zwave-js/releases/tag/v15.28.0) + ## 1.7.1 ### Z-Wave JS 15.27.0...15.27.1 diff --git a/zwave_js/build.yaml b/zwave_js/build.yaml index 8945ce6d8..ebfaaaac7 100644 --- a/zwave_js/build.yaml +++ b/zwave_js/build.yaml @@ -3,4 +3,4 @@ build_from: aarch64: ghcr.io/home-assistant/aarch64-base:3.23 amd64: ghcr.io/home-assistant/amd64-base:3.23 args: - ZWAVEJS_UI_VERSION: 11.22.3 + ZWAVEJS_UI_VERSION: 11.24.0 diff --git a/zwave_js/config.yaml b/zwave_js/config.yaml index 78f7dd9c6..58bddd34f 100644 --- a/zwave_js/config.yaml +++ b/zwave_js/config.yaml @@ -1,5 +1,5 @@ --- -version: 1.7.1 +version: 1.8.0 slug: zwave_js name: Z-Wave JS description: Control a Z-Wave network with Home Assistant Z-Wave JS