Skip to main content

Syscon TTS

Offline neural text-to-speech for the PlantStar APU.

Turns alert text into a voice WAV file with no cloud service, no per-request licensing, and no internet connection at runtime. It replaces the APU's previous VoiceText integration.

  • Engine: Piper — fast, CPU-only neural TTS. No GPU required.
  • Interfaces: a Python library (what the APU uses), a CLI, and an optional HTTP server.
  • Voices: eight voices across five languages out of the box — including every locale the APU's UI offers — configured in a single JSON manifest, and extensible per deployment by dropping model files into the voices directory.
  • Licensing: the shipped catalogue only contains voices whose upstream license permits use in a product you sell. See Voices and licensing.
pip install syscon-tts
syscon-tts download-voices      # one-time, needs internet
syscon-tts doctor               # confirm the install

Platform support

Piper ships Linux-only binary wheels. Rather than making that a hard install failure everywhere else, the dependency is platform-gated, so the same pip install syscon-tts works on every platform and gives you what that platform can actually do:

Platform Synthesis Replay cached audio Library / CLI / manifest
Linux x86-64, Python 3.9–3.11 (APU, customer machines) yes yes yes
Windows, macOS (developer machines) no yes yes
Linux, Python 3.12+ no yes yes

Developer machines can therefore install the package, browse voices, run the test suite, and exercise the whole alert pipeline against WAVs generated on a Linux box — they just can't generate new audio. Attempting to do so raises SynthesisUnavailableError, which names the platform and explains why.

Run syscon-tts doctor any time to see which mode you're in.

On Linux with Python 3.12+, Piper is skipped rather than failing the install. That is a deliberate trade — doctor reports it as a problem, so make it part of provisioning rather than discovering it at the first alert.


Using it from the APU

The only contract the rest of the APU depends on is the side effect: a WAV exists at MEDIA_ROOT/public_alert_sounds/<file_name>.wav. Everything downstream — the websocket push, /media/ serving, check_for_file_delete — is agnostic about how the file got there. So the integration is a rewrite of call_tts() and nothing else.

from syscon_tts import AlertSynthesizer

# Build once and keep it. Each instance caches loaded voice models in memory;
# constructing one per alert reintroduces a multi-second cold load every time.
# Settings are passed field by field, so no environment plumbing is needed.
synthesizer = AlertSynthesizer(
    alerts_dir=Path(settings.MEDIA_ROOT, "public_alert_sounds"),
)

result = synthesizer.ensure(
    message,
    file_name=file_name,     # the APU's get_valid_filename(message)[:50]
    language=locale,         # optional: "es-mx", "zh-hans", ...
)
# result.path    -> the WAV on disk
# result.cached  -> True if it already existed (no synthesis happened)

Two things the APU must do around this call, both covered in DEPLOY.md: run it in a thread (synthesis is a seconds-long CPU burn and call_tts is invoked from the Tornado IO loop), and apply the APU's own root:www-data ownership to result.path afterwards.

ensure() is cache first: if the WAV is already there it returns immediately without touching Piper. Only a cache miss synthesizes.

The package derives file names with the exact algorithm the APU already uses (django.utils.text.get_valid_filename(text)[:50]), verified byte-for-byte including Unicode and the empty-name error cases — so both sides always agree on the path. Passing file_name explicitly is still recommended, so the APU stays the single source of truth.

Full integration recipe, including the IS_LIVE gate, file ownership and the threading requirement: DEPLOY.md.


Voices and licensing

ID Language Voice Upstream license
en_us_kristin en_US Kristin — female (default) public domain
en_us_john en_US John — male public domain
es_mx_ald es_MX Ald — Mexican Spanish Unlicense
es_mx_ald_x_low es_MX Ald — smallest model Unlicense
es_es_davefx es_ES DaveFX — male (Castilian) CC0
zh_cn_huayan zh_CN Huayan — Mandarin unknown — needs review
fr_fr_siwis fr_FR Siwis — female CC BY 4.0 (attribution)
de_de_thorsten de_DE Thorsten — male CC0

Piper's catalogue mixes public-domain voices with non-commercial ones, and PlantStar ships to paying customers, so licenses are tracked per voice. list-voices shows them, doctor flags anything installed that still needs a review, and a bulk download-voices skips those voices unless you pass --accept-license.

Voices deliberately not catalogued: en_US-ryan, en_US-hfc_female, en_US-hfc_male and zh_CN-xiao_ya (CC BY-NC-SA — non-commercial); en_US-amy, en_GB-alan, en_US-lessac (license stated only as "see dataset URL"); zh_CN-chaowen (CC0 dataset, but fine-tuned from the non-commercial xiao_ya). Any of them can still be used by a site that has cleared it — see "Per-deployment voices" below.

Mandarin is unresolved. zh_CN-huayan is the only Mandarin voice Piper 1.2 can run whose license is not explicitly non-commercial, and its model card states the dataset license as Unknown. It needs legal sign-off before it goes to a customer. It serves both zh-Hans and zh-Hant sites — spoken Mandarin is the same, only the script differs — but spot-check Traditional text before promising it.

Choosing a voice by language

The APU knows the site's locale; it does not need a voice-id table.

synthesizer.ensure(message, language="es-mx")   # -> es_mx_ald
synthesizer.ensure(message, language="zh-hant") # -> zh_cn_huayan

en/en-us, es/es-mx, and zh/zh-hans/zh-hant/zh-Hant-TW all normalize to the catalogue's spelling. Exact locale matches win over same-language ones (a es-MX site prefers Mexican Spanish but still speaks with the Castilian voice if that is what is installed), and a language nothing can serve falls back to the default voice — on a plant floor, an announcement in the wrong accent beats silence. Per-language defaults are configurable:

SYSCON_TTS_DEFAULT_VOICES=es-mx=es_mx_ald,zh-hans=zh_cn_huayan

One utterance is rendered by one voice, so a message mixing two languages is spoken by whichever voice was selected. Split the message instead.

Per-deployment voices

Every site provisions its own voice set. Two ways to add one without a new release of this package:

  1. Drop the files in. Any .onnx + .onnx.json pair in the voices directory that no catalogue entry claims is discovered automatically, with its language read from the Piper config. This covers models from https://huggingface.co/rhasspy/piper-voices (~40 languages) and custom-trained ones alike.
  2. Layer a manifest. Point SYSCON_TTS_EXTRA_MANIFEST at a JSON file of the same shape as the bundled one; new ids are added, reused ids replace the bundled entry. Use this when the voice should also be downloadable.

Fetch only what a site will actually speak:

syscon-tts download-voices --language es-mx

Voice ids are a durable contract — the APU stores the selected id in its settings table — so a released id is never renamed, only added to.


CLI

syscon-tts doctor                        # platform, Piper, model + license status
syscon-tts download-voices               # fetch the catalogue (needs internet)
syscon-tts download-voices --language es-mx   # or just one site's language
syscon-tts download-voices en_us_kristin      # or one named voice
syscon-tts list-voices                   # catalogue, install state, licenses
syscon-tts list-voices -l zh-hans        # only what serves this locale

syscon-tts speak -v en_us_kristin -o hello.wav "Hello from PlantStar"
syscon-tts speak -l es-mx -o aviso.wav "Presion de cavidad excedida en prensa 4."
syscon-tts speak -v de_de_thorsten -f mp3 -o ansage.mp3 --text-file notice.txt

syscon-tts alert "Press 4 cavity pressure exceeded."   # cache-first, as the APU does
syscon-tts serve --port 5002                           # needs [server] extra

alert exits 2 when the audio is absent and this machine cannot synthesize — distinguishable from 1 for ordinary errors.


Configuration

All optional; every one has a working default.

Variable Default Purpose
SYSCON_TTS_MANIFEST bundled data/voices.json Voice catalogue
SYSCON_TTS_EXTRA_MANIFEST — Second manifest layered over the first
SYSCON_TTS_VOICES_DIR <data dir>/voices Where .onnx models live
SYSCON_TTS_ALERTS_DIR <data dir>/alerts Where alert WAVs are written
SYSCON_TTS_DEFAULT_VOICE en_us_kristin Voice used when none is given
SYSCON_TTS_DEFAULT_VOICES — Per-language defaults, es-mx=es_mx_ald,...
SYSCON_TTS_MAX_LOADED_VOICES 3 Voice models kept resident in memory
SYSCON_TTS_FILE_MODE 0644 Mode for generated audio
SYSCON_TTS_DIR_MODE 0755 Mode for directories this package creates
SYSCON_TTS_HOST 127.0.0.1 HTTP server bind host
SYSCON_TTS_PORT 5002 HTTP server bind port
SYSCON_TTS_MAX_CHARS 20000 Max text length per request

The platform data dir is /var/lib/syscon-tts on Linux when writable (otherwise ~/.local/share/syscon-tts), %LOCALAPPDATA%\syscon-tts on Windows, and ~/Library/Application Support/syscon-tts on macOS.

An embedded caller that already knows its paths should skip the environment entirely — every setting is a constructor keyword:

AlertSynthesizer(alerts_dir=..., default_voices={"zh_CN": "zh_cn_huayan"})

Where the files go on an APU

What Where Why
Voice models /var/lib/syscon-tts/voices (the default — no config needed) Large, immutable, re-downloadable, and per-machine
Generated alert WAVs MEDIA_ROOT/public_alert_sounds Has to be web-served; ephemeral, deleted after playback

Voice models deliberately do not live under MEDIA_ROOT: the APU's backup_plantstar copies the entire media tree into every backup, which is then compressed, encrypted and shipped off-box. Voice models would add hundreds of megabytes to every backup at every site, to preserve files that a single syscon-tts download-voices reproduces. Back up the selection — the voice ids a site uses — not the models.

/var/lib/syscon-tts also survives sideload_code (which only writes under website/) and pip install -U, and cannot leak into git.


Optional HTTP server

pip install 'syscon-tts[server]'
syscon-tts serve --port 5002
Method Path Description
GET /health Liveness + whether this host can synthesize
GET /voices Voice profiles, licenses and install state (?language=es-mx)
POST /synthesize Render text → audio bytes
curl -s -X POST http://localhost:5002/synthesize \
  -H 'Content-Type: application/json' \
  -d '{"text":"Machine 4 cycle complete.","voice":"en_us_john"}' \
  -o alert.wav

Status codes: 404 unknown voice · 501 this host can never synthesize (no Piper) · 503 voice model not downloaded · 413 text too long · 400 bad input.

The APU does not need this — it imports the library in-process, which keeps FastAPI and uvicorn out of the Django environment entirely.


Development

git clone https://github.com/SYSCON-International/syscon_tts.git
cd syscon_tts
python -m venv .venv && . .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest

The full test suite passes on Windows and macOS without Piper — synthesis is mocked throughout, so no voice models are needed. See CONTRIBUTING.md.


Documentation

  • DEPLOY.md — provisioning an APU, the call_tts() replacement, air-gapped installs, tuning.
  • docs/ARCHITECTURE.md — module layout, design decisions, operations, troubleshooting.

Performance

Piper runs on CPU, so throughput depends on the host. Measure on the target:

python scripts/benchmark.py            # per-voice cold load, memory, RTF, latency

It reports the real-time factor (RTF = synthesis time ÷ audio seconds) for short/medium/long messages. Record results with docs/bench-results-template.md and use them to pick quality tiers — switching a voice to its low-quality model in the manifest is the main lever on a weak CPU.

Expect roughly 50–120 MB resident per loaded medium voice; models stay in memory after first use to avoid reload latency.

License

MIT — see LICENSE.

Metadata

Release files for syscon-tts 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for syscon-tts 0.0.2
File Size Uploaded
syscon_tts-0.0.2.tar.gz 52.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for syscon-tts 0.0.2
File Interpreter ABI Platform
syscon_tts-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 90.7 kB

Release files / syscon_tts-0.0.2.tar.gz

Download URL syscon_tts-0.0.2.tar.gz
Size 52.4 kB
Tags Source
SHA-256 checksum
How to use checksums
225ff4ee2a14740af47bb7409de9b2ab0d27a9aa90797bdb39ed0e4086bc9aa7
BLAKE2b-256 checksum
How to use checksums
67b62a2dcb2df98d5947a98c48200dd462a5cfc0f17a55b63b3605ed0a6f31ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / syscon_tts-0.0.2-py3-none-any.whl

Download URL syscon_tts-0.0.2-py3-none-any.whl
Size 38.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ef4ba63db26eb0a1088a921512e539f7c3a236cd5e0dc896984fefe5ce067f0
BLAKE2b-256 checksum
How to use checksums
3d783ef4cf9704197168ecc131279f025d61f1cad41f0fa83a40eb294bae4d73
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.4

2 release files

0.0.3

2 release files

This release

0.0.2 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page