From abb73d50046ac8431a7c281dadb5e7486af2acae Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 14 Sep 2026 03:01:34 -0500 Subject: [PATCH 1/2] Bump piper to 2.5.2 (#4821) * Install packs dynamically * Bump piper to 2.5.2 and restrict the web UI to ingress Passes --web-server-allow 172.30.32.2, new in wyoming-piper 2.5.2, so the voice management UI answers only Home Assistant's ingress proxy. It has to bind 0.0.0.0 because ingress connects to the container's address rather than loopback, which otherwise left it reachable by every other app on the same Docker network. Also: - Discard the uv pack cache when the interpreter or wyoming-piper moves to a new series, so it cannot grow without bound across updates. Patch releases keep it. - Stop a failed pack download from aborting the packs service. bashio runs with errexit, so one failed download skipped every remaining pack, and because the Piper service depends on that oneshot it could have stopped the app from starting at all. - Describe OmniVoice the same way in the docs, changelog and translations: it runs on other architectures with a warning rather than being amd64 only. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- piper/CHANGELOG.md | 37 +++++ piper/DOCS.md | 86 ++++++++++- piper/Dockerfile | 48 +++--- piper/build.yaml | 3 +- piper/config.yaml | 30 +++- .../s6-rc.d/packs/dependencies.d/base | 0 piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run | 142 ++++++++++++++++++ .../rootfs/etc/s6-overlay/s6-rc.d/packs/type | 1 + piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up | 1 + .../s6-rc.d/piper/dependencies.d/packs | 0 piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run | 84 +++++++++-- .../s6-overlay/s6-rc.d/user/contents.d/packs | 0 piper/translations/en.yaml | 27 +++- 13 files changed, 427 insertions(+), 32 deletions(-) create mode 100644 piper/rootfs/etc/s6-overlay/s6-rc.d/packs/dependencies.d/base create mode 100755 piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run create mode 100644 piper/rootfs/etc/s6-overlay/s6-rc.d/packs/type create mode 100644 piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up create mode 100644 piper/rootfs/etc/s6-overlay/s6-rc.d/piper/dependencies.d/packs create mode 100644 piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/packs 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: >- From 6ec353eb1b7ef86ea7b10b61fafcda474d881137 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Mon, 14 Sep 2026 13:15:28 +0200 Subject: [PATCH 2/2] Bump Z-Wave JS to 1.8.0 (#4822) --- zwave_js/CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++++ zwave_js/build.yaml | 2 +- zwave_js/config.yaml | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) 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