No description
Find a file
Pandi 2d22266284 fix(server): a non-string input killed the request thread instead of answering 400
Any valid JSON body that was not an object with a string input crashed the
handler: an integer input hit str.strip, a list body hit .get, an integer
voice hit .lower. The client got RemoteDisconnected, not an error code.
do_POST now rejects a non-object body and a non-string or blank input with
a 400 JSON response, and falls back to the default voice for a non-string
one. Adds the first tests for the service (5, stdlib unittest): health, a
real 24 kHz mono 16-bit WAV from a synthesis request, both 4xx paths and the
documented unknown-voice fallback.
2026-11-04 10:24:13 +01:00
.gitignore Add Phonon TTS service: OpenAI-compatible /v1/audio/speech over ptts 2026-11-04 09:11:56 +01:00
README.md fix(server): a non-string input killed the request thread instead of answering 400 2026-11-04 10:24:13 +01:00
requirements.txt Add Phonon TTS service: OpenAI-compatible /v1/audio/speech over ptts 2026-11-04 09:11:56 +01:00
server.py fix(server): a non-string input killed the request thread instead of answering 400 2026-11-04 10:24:13 +01:00
test_server.py fix(server): a non-string input killed the request thread instead of answering 400 2026-11-04 10:24:13 +01:00

phonon-tts

OpenAI-compatible TTS shim over Phonon (ptts): POST /v1/audio/speech returns 24 kHz mono 16-bit WAV. One Python file — stdlib http.server, numpy, ptts; no PyTorch.

Why

ptts ships as a Rust core with Python bindings only, so OpenAI-shaped clients (pandi-call, Open WebUI) need an HTTP wrapper instead of a binary — one URL change.

Run

cd ~/coding/projects/tts-phonon
python3 -m venv venv
venv/bin/pip install -r requirements.txt
venv/bin/python server.py            # serves on 0.0.0.0:8882
venv/bin/python server.py --selftest # synth two German sentences, assert valid WAV

As a user service (this is how it actually runs):

systemctl --user enable --now phonon-tts.service
journalctl --user -u phonon-tts -f

Tests

venv/bin/python -m unittest -v test_server   # HTTP-Vertrag: /health, WAV-Header, 4xx-Pfade, Voice-Fallback

Spaet das Modell (~1 s Ladezeit, ~100–300 ms pro Satz), ein eigener Server auf 127.0.0.1:0 — der laufende Dienst auf :8882 bleibt unberuehrt. Keine Audio-Qualitaetspruefung.

Usage

curl -s http://127.0.0.1:8882/health
# {"ok":true,"voice":"Henrik","lang":"de","sr":24000,"device":"cpu","quant":"q8_0","load_ms":867}

curl -s -X POST http://127.0.0.1:8882/v1/audio/speech \
  -H 'Content-Type: application/json' \
  -d '{"input":"Der Deckenventilator ist jetzt an, auf einundzwanzig Prozent.","voice":"Henrik"}' \
  -o out.wav

Environment: PHONON_MODEL (default hf://empyrealworks/Phonon-40M-multilingual), PHONON_VOICE (Henrik), PHONON_LANG (de), PHONON_PORT (8882), PHONON_BIND (0.0.0.0), PHONON_THREADS (4).

Auth

None. No token, no key — it accepts every request it can reach. PHONON_BIND defaults to 0.0.0.0, i.e. all interfaces, so this belongs on localhost or a trusted LAN only. Set PHONON_BIND=127.0.0.1 if it should not leave the host.

Known limits

  • Synthesis is behind one global lock: requests queue, one at a time. THREADS is ptts' compute width, not concurrency.
  • response_format is ignored; the response is always WAV, whatever the client asked.
  • Unknown voice names are silently replaced by the default voice instead of erroring (clients hardcode names like martin).
  • No /v1/models, no streaming, no speed, no input length cap.
  • Weights download from Hugging Face on first start (~/.cache/huggingface), the repo carries none. Load 0.8–0.9 s, RSS ~320 MB, 80–220 ms per request for 1.4–3.8 s audio.