Metadata-Version: 2.4
Name: wyoming-piper
Version: 2.5.1
Summary: Wyoming Server for Piper
Author-email: Michael Hansen <mike@rhasspy.org>
License: MIT
Project-URL: Homepage, http://github.com/rhasspy/wyoming-piper
Keywords: rhasspy,wyoming,piper,tts
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: wyoming<2,>=1.10.2
Requires-Dist: regex>=2024.11.6
Requires-Dist: piper-tts<2,>=1.8.0
Requires-Dist: sentence-stream<2,>=1.2.1
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: flake8; extra == "dev"
Requires-Dist: isort; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: pylint; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: wyoming-piper[web]; extra == "dev"
Requires-Dist: scipy<2,>=1.10; extra == "dev"
Requires-Dist: numpy<3,>=1.26; extra == "dev"
Requires-Dist: python-speech-features<1,>=0.6; extra == "dev"
Provides-Extra: http
Requires-Dist: wyoming[http]; extra == "http"
Provides-Extra: zeroconf
Requires-Dist: wyoming[zeroconf]; extra == "zeroconf"
Provides-Extra: zh
Requires-Dist: piper-tts[zh]; extra == "zh"
Provides-Extra: ja
Requires-Dist: piper-tts[ja]; extra == "ja"
Provides-Extra: th
Requires-Dist: piper-tts[th]; extra == "th"
Requires-Dist: requests; extra == "th"
Provides-Extra: omnivoice-deps
Requires-Dist: torch; extra == "omnivoice-deps"
Requires-Dist: torchaudio; extra == "omnivoice-deps"
Requires-Dist: transformers; extra == "omnivoice-deps"
Requires-Dist: accelerate; extra == "omnivoice-deps"
Requires-Dist: pydub; extra == "omnivoice-deps"
Requires-Dist: soundfile; extra == "omnivoice-deps"
Requires-Dist: audioop-lts; python_version >= "3.13" and extra == "omnivoice-deps"
Requires-Dist: onnxruntime; extra == "omnivoice-deps"
Requires-Dist: huggingface_hub; extra == "omnivoice-deps"
Requires-Dist: numpy; extra == "omnivoice-deps"
Provides-Extra: omnivoice
Requires-Dist: wyoming-piper[omnivoice-deps]; extra == "omnivoice"
Requires-Dist: omnivoice; extra == "omnivoice"
Provides-Extra: web
Requires-Dist: flask<4,>=3; extra == "web"
Dynamic: license-file

# Wyoming Piper

[Wyoming protocol](https://github.com/OHF-Voice/wyoming) server for the [Piper](https://github.com/OHF-Voice/piper1-gpl) text to speech system.

## Home Assistant Add-on

[![Show add-on](https://my.home-assistant.io/badges/supervisor_addon.svg)](https://my.home-assistant.io/redirect/supervisor_addon/?addon=core_piper)

[Source](https://github.com/home-assistant/addons/tree/master/piper)

## Local Install

Requires Python 3.10 or later.

Clone the repository and set up Python virtual environment:

``` sh
git clone https://github.com/OHF-Voice/wyoming-piper.git
cd wyoming-piper
script/setup
```

Run a Wyoming server that Home Assistant can connect to:

``` sh
script/run --voice en_US-lessac-medium --uri 'tcp://0.0.0.0:10200' --data-dir /data --download-dir /data 
```

For a demo web server, make sure to install the `http` dependencies first:

``` sh
script/setup --http
```

Then run in a separate terminal:

``` sh
script/run_http --uri 'tcp://localhost:10200'
```

and visit http://localhost:5000 to test.

## Optional phonemizers

Most voices are phonemized with espeak-ng, which is built in. Three languages
need an extra:

| Language | Extra  | Provides                    |
| -------- | ------ | --------------------------- |
| Chinese  | `zh`   | g2pW (`pinyin` voices only) |
| Japanese | `ja`   | OpenJTalk                   |
| Thai     | `th`   | TLTK                        |

``` sh
script/setup --ja --th        # or: pip install '.[ja,th]'
```

Voices that need a missing extra are **not advertised**, since a client would
otherwise offer them and every request would answer with silence. They reappear
once the extra is installed — no restart is needed for the voice list itself,
which is rebuilt on each `Describe`, but the phonemizer has to be importable by
the running process. Passing one as `--voice` is an error at startup.

Chinese is the exception: `zh_CN-huayan-medium` and `zh_CN-huayan-x_low` are
espeak voices and work without the `zh` extra, so catalog voices for `zh` are
always advertised. A *custom* Chinese voice is filtered correctly, because its
config records `phoneme_type`.

## OmniVoice backend (experimental)

An alternative [OmniVoice](https://github.com/k2-fsa/OmniVoice) backend is
available, running a block-wise int4 ONNX export under `onnxruntime`. Install the
extra dependencies and select it with `--backend omnivoice`:

``` sh
script/setup --omnivoice
script/run --backend omnivoice \
    --uri 'tcp://0.0.0.0:10200' --data-dir /data --download-dir /data \
    --omnivoice-ref-dir /data/omnivoice_voices --omnivoice-steps 32
```

Installing with pip takes two steps, because the `omnivoice` package requires
gradio, librosa, webdataset and tensorboardx for its demo and training paths,
which this backend never imports. Skipping them drops 42 packages and ~600 MB:

``` sh
pip install 'wyoming-piper[omnivoice-deps]'
pip install --no-deps omnivoice
```

The `omnivoice` extra installs both in one step instead, at that extra ~600 MB.

**Voices.** Point `--omnivoice-ref-dir` at a directory of voices organized as
`<language>/<voice_name>/`, for example:

```
omnivoice_voices/
  en_US/
    lessac/{ref.wav, ref.txt}
    ryan/{ref.wav, ref.txt}
    narrator/{instruct.txt}
  de_DE/
    thorsten/{ref.wav, ref.txt}
```

Each voice directory is one of two kinds:

- **Cloning** — `ref.wav` + `ref.txt` (the transcript of the recording); the
  voice is cloned from the reference audio.
- **Voice design** — `instruct.txt`; its text is a style instruction (e.g.
  `male, deep, slow`) describing the voice to generate, with no reference audio.
  See [voice design](https://github.com/k2-fsa/OmniVoice/blob/master/docs/voice-design.md) for valid attributes.
  Only used when the directory has no `ref.wav`/`ref.txt`.

A `default` voice is also advertised; requesting it (or an empty/unknown voice
name) uses OmniVoice's built-in speaker for the requested language.

OmniVoice lists 646 language codes, most of them ISO 639-3 only. Advertising all
of them buries the usable ones in Home Assistant's language picker, so `default`
is advertised for the ~128 that have an ISO 639-1 (two-letter) tag, plus
Cantonese, Standard Arabic and Odia. This limits only what is *advertised* —
`--omnivoice-language` and a per-request language still accept any code
OmniVoice knows, so the rest stay reachable.

On first use, each reference is encoded and cached next to `ref.wav` as
`ref.rvq` (regenerated whenever `ref.wav` is newer), so the reference isn't
re-encoded on every request.

The model is a block-wise int4 ONNX graph (see `script/quantize_omnivoice.py` to
reproduce it). If `omnivoice.int4.onnx` (and its `.data`) are found in a
`--data-dir`, that copy is used; otherwise it is downloaded into `--download-dir`
(used as the HuggingFace cache) from the repo set by `--omnivoice-onnx-repo`. Use
`--local-files-only` to run fully offline once the model is cached, and
`--omnivoice-steps` to trade quality for speed — int4 stays clean down to ~10
steps. OmniVoice is compute-heavy and best suited to a desktop/server CPU rather
than low-power devices.

## Voice management web UI

A small Flask web UI can run alongside the Wyoming server to manage custom
voices. It is designed to work as a Home Assistant add-on behind ingress.
Install the extra dependency and enable it with `--web-server`:

``` sh
script/setup --web
script/run --voice en_US-lessac-medium \
    --uri 'tcp://0.0.0.0:10200' --data-dir /data --download-dir /data \
    --web-server --web-server-port 5000
```

Then visit http://localhost:5000. The page has two sections:

- **Piper** — upload and delete custom voices (a `<voice>.onnx` model plus its
  `<voice>.onnx.json` config) stored in `--download-dir`. Some metadata (dataset,
  language, quality, sample rate) is read from each config file.
- **OmniVoice** — upload cloned voices (a reference WAV plus its required
  transcript) into `--omnivoice-ref-dir/<language>/<voice_name>/`, and delete a
  cloned voice's whole directory.

Each section shows a warning when its backend is not the one the server was
started with (via `--backend`), but the UI keeps working. The server picks up
added and removed voices on its own — no restart — but Home Assistant caches the
voice list, so **reload the Piper integration** for a new voice to appear in it.

`--web-server-host` / `--web-server-port` set the bind address (default
`127.0.0.1:5000`). The UI has no authentication and can upload and delete files
under `--download-dir` / `--omnivoice-ref-dir`, so only bind it to an address
reachable from a network you trust.

## Docker Image

``` sh
docker run -it \
    -p 10200:10200 \
    -v /path/to/local/data:/data \
    rhasspy/wyoming-piper \
    --voice en_US-lessac-medium
```

OmniVoice ships as a separate `omnivoice` tag, because it pulls in torch and
transformers. It is built for `linux/amd64` only — OmniVoice needs a
desktop/server CPU:

``` sh
docker run -it \
    -p 10200:10200 \
    -v /path/to/local/data:/data \
    rhasspy/wyoming-piper:omnivoice \
    --backend omnivoice \
    --omnivoice-ref-dir /data/cloned-voices \
    --omnivoice-steps 10  # higher = better quality but slower
```

The voice management web UI is **off by default**. It has no authentication, so
only enable it on a network you trust:

``` sh
docker run -it \
    -p 10200:10200 -p 5000:5000 \
    -v /path/to/local/data:/data \
    rhasspy/wyoming-piper \
    --voice en_US-lessac-medium \
    --web-server --web-server-host 0.0.0.0
```

### NVIDIA GPU image

Build the local GPU image with:

``` sh
docker build -f Dockerfile.gpu -t wyoming-piper:gpu .
```

The image contains CUDA-enabled PyTorch and ONNX Runtime, includes OmniVoice,
and enables `--use-cuda` automatically. Run Piper with the NVIDIA Container
Toolkit and a persistent data directory:

``` sh
docker run --rm -it \
    --gpus all \
    -p 10200:10200 \
    -v /path/to/local/data:/data \
    wyoming-piper:gpu \
    --voice en_US-lessac-medium
```

Set `WYOMING_PIPER_ARGS` to a shell-style argument string when Docker Compose
environment variables are more convenient than `command`. For example:

``` yaml
services:
  piper:
    build:
      context: .
      dockerfile: Dockerfile.gpu
    gpus: all
    ports:
      - "10200:10200"
    volumes:
      - ./data:/data
    environment:
      WYOMING_PIPER_ARGS: >-
        --voice en_US-lessac-medium
```

To run OmniVoice instead, replace the environment value with:

``` yaml
      WYOMING_PIPER_ARGS: >-
        --backend omnivoice
        --omnivoice-ref-dir /data/cloned-voices
        --omnivoice-steps 10
```

The host must have an NVIDIA driver and the NVIDIA Container Toolkit. The GPU
image is currently `linux/amd64` only because its PyTorch base image is amd64.

### Container health check

The image has a health check that asks the server for its info over the Wyoming
protocol, so `docker ps` reports `unhealthy` if the server stops answering. It
assumes the default `tcp://0.0.0.0:10200`; if you override `--uri`, override the
check to match:

``` sh
docker run -it \
    -p 10300:10300 \
    -v /path/to/local/data:/data \
    --health-cmd '/usr/src/.venv/bin/python3 -m wyoming_piper.health_check --uri tcp://127.0.0.1:10300' \
    rhasspy/wyoming-piper \
    --uri tcp://0.0.0.0:10300 \
    --voice en_US-lessac-medium
```

Loading the backend can take minutes on first run, since the model has to be
downloaded before the server starts listening. The check's start period allows
for that, so the container reports `starting` rather than `unhealthy` until then.

[Source](https://github.com/rhasspy/wyoming-addons/tree/master/piper)
