diff --git a/.apps.yml b/.apps.yml index c19d2040c..3150dfadd 100644 --- a/.apps.yml +++ b/.apps.yml @@ -54,6 +54,10 @@ apps: repository: home-assistant/app-openwakeword target: openwakeword image: ghcr.io/home-assistant/app-openwakeword + piper: + repository: home-assistant/app-piper + target: piper + image: ghcr.io/home-assistant/app-piper rpc_shutdown: repository: home-assistant/app-rpc-shutdown target: rpc_shutdown diff --git a/.yamllint b/.yamllint index 44844fbc7..9b0f4cac4 100644 --- a/.yamllint +++ b/.yamllint @@ -16,6 +16,7 @@ ignore: | /nginx_proxy/config.yaml /openthread_border_router/config.yaml /openwakeword/config.yaml + /piper/config.yaml /rpc_shutdown/config.yaml /samba/config.yaml /silabs_flasher/config.yaml diff --git a/piper/Dockerfile b/piper/Dockerfile deleted file mode 100644 index 3162aebc6..000000000 --- a/piper/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -ARG BUILD_FROM -FROM ${BUILD_FROM} - -# Set shell -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -# Install Piper -WORKDIR /usr/src -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 \ - python3 \ - python3-pip \ - \ - # 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 ORT_DISABLE_TELEMETRY=1 -WORKDIR / -COPY rootfs / - -# 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 deleted file mode 100644 index f86a0a223..000000000 --- a/piper/build.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -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.5.2 - UV_VERSION: 0.12.12 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/piper b/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/dependencies.d/piper deleted file mode 100644 index e69de29bb..000000000 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/run b/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/run deleted file mode 100755 index 17bf8e4c1..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/run +++ /dev/null @@ -1,25 +0,0 @@ -#!/command/with-contenv bashio -# vim: ft=bash -# shellcheck shell=bash -# ============================================================================== -# Sends discovery information to Home Assistant. -# ============================================================================== -declare config - -# Wait for Piper to become available -bash -c \ - "until - echo '{ \"type\": \"describe\" }' - > /dev/tcp/localhost/10200; do sleep 0.5; - done" > /dev/null 2>&1 || true; - -config=$(\ - bashio::var.json \ - uri "tcp://$(hostname):10200" \ -) - -if bashio::discovery "wyoming" "${config}" > /dev/null; then - bashio::log.info "Successfully send discovery information to Home Assistant." -else - bashio::log.error "Discovery message to Home Assistant failed!" -fi \ No newline at end of file diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/type b/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/type deleted file mode 100644 index 3d92b15f2..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/type +++ /dev/null @@ -1 +0,0 @@ -oneshot \ No newline at end of file diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/up b/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/up deleted file mode 100755 index 31b0a0205..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/discovery/up +++ /dev/null @@ -1 +0,0 @@ -/etc/s6-overlay/s6-rc.d/discovery/run \ No newline at end of file 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 deleted file mode 100644 index e69de29bb..000000000 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run b/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run deleted file mode 100755 index 43524e7aa..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/run +++ /dev/null @@ -1,142 +0,0 @@ -#!/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 deleted file mode 100644 index 3d92b15f2..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/type +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 6c31c4929..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/packs/up +++ /dev/null @@ -1 +0,0 @@ -/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/base b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/dependencies.d/base deleted file mode 100644 index e69de29bb..000000000 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 deleted file mode 100644 index e69de29bb..000000000 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/finish b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/finish deleted file mode 100755 index 8558b9a95..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/finish +++ /dev/null @@ -1,27 +0,0 @@ -#!/command/with-contenv bashio -# vim: ft=bash -# shellcheck shell=bash -# ============================================================================== -# Take down the S6 supervision tree when service fails -# s6-overlay docs: https://github.com/just-containers/s6-overlay -# ============================================================================== -# shellcheck disable=SC2155 -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/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run deleted file mode 100755 index 75f910114..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run +++ /dev/null @@ -1,103 +0,0 @@ -#!/command/with-contenv bashio -# vim: ft=bash -# shellcheck shell=bash -# ============================================================================== -# 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 '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 - if bashio::jq.exists "${options}" ".${old_key}"; then - bashio::log.info "Removing ${old_key}" - bashio::addon.option "${old_key}" - 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 \ - --sentence-silence "$(bashio::config 'sentence_silence')" \ - --data-dir /data \ - --data-dir /share/piper \ - --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/piper/type b/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/type deleted file mode 100644 index 1780f9f44..000000000 --- a/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/type +++ /dev/null @@ -1 +0,0 @@ -longrun \ No newline at end of file diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery b/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/discovery deleted file mode 100644 index e69de29bb..000000000 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 deleted file mode 100644 index e69de29bb..000000000 diff --git a/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/piper b/piper/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/piper deleted file mode 100644 index e69de29bb..000000000