OpenAI-kompatibler TTS-Dienst (Rust-Port von tts-phonon, ptts statt Python-Shim)
Find a file
Hermes 79789f3727 feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim)
- axum + ptts, ein Binary statt Shim/venv/numpy
- HTTP-Vertrag 1:1 aus test_server.py, 6 Tests ohne Modell-Download
- PHONON_QUANT neu, Body-Limit 1 MiB statt unbegrenzt
2026-11-05 16:27:23 +01:00
src feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00
tests feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00
.gitignore feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00
Cargo.lock feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00
Cargo.toml feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00
README.md feat: Rust-Port von tts-phonon (ptts-Crate statt Python-Shim) 2026-11-05 16:27:23 +01:00

tts-phonon-rs

Rust-Port von tts-phonon: OpenAI-kompatibler TTS-Dienst über Phonon — POST /v1/audio/speech liefert 24 kHz mono 16-bit WAV.

Warum überhaupt portiert

Das Original war ein Python-Shim mit dem Argument „ptts gibt es nur als Python-Binding". Das stimmt nicht: ptts ist ein Rust-Crate (cargo add ptts --features hf,hub). Damit fallen Shim, venv und numpy weg — ein Binary, ein Prozess, dieselben Bytes.

Bauen

cargo build --release          # target/release/phonon-tts
cargo test                     # HTTP-Vertrag, ohne Modell-Download
cargo test -- --ignored        # echter Modelllauf (lädt ~320 MB nach ~/.cache/huggingface)

Laufen

./target/release/phonon-tts              # 0.0.0.0:8882
./target/release/phonon-tts --selftest   # zwei deutsche Sätze, WAV-Header prüfen

Umgebungsvariablen wie im Original: 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).

Neu: PHONON_QUANT (q8 = Default wie im Original, f32 für volle Gewichte).

curl -s localhost:8882/health
curl -s -X POST localhost:8882/v1/audio/speech \
  -H 'content-type: application/json' \
  -d '{"input":"Der Deckenventilator ist jetzt an."}' -o out.wav

Vertrag (identisch zum Original)

  • GET /health → {"ok":true,"voice","lang","sr","device","quant","load_ms"}
  • POST /v1/audio/speech → audio/wav, 16-bit PCM, mono, sample_rate des Checkpoints
  • alles andere → 404 {"error":"not found"}
  • kaputtes JSON → 400 {"error":"bad json"}
  • Body kein Objekt → 400 {"error":"body must be a JSON object"}
  • input fehlt/leer/kein String → 400 {"error":"empty input"}
  • unbekannter Stimmname → still auf die Default-Stimme zurückfallen (Clients schicken feste Namen)
  • Synthese-Fehler → 500 {"error": "..."}

Abweichungen

  • ptts als Crate statt Python-Binding — derselbe Kern, dieselben Gewichte, kein Shim.
  • PHONON_THREADS wirkt nur indirekt: ptts dimensioniert seinen eigenen Pool über xn; der Wert wird gelesen, aber nicht mehr gesetzt (die Python-Bindung hatte dafür einen eigenen Setter). Stattdessen begrenzt ein Mutex die Synthese auf eine Anfrage gleichzeitig — genau das Verhalten, das das Original dokumentiert hat („requests queue, one at a time").
  • Body-Limit 1 MiB — das Original las unbegrenzt viel in den Speicher. Ein Satz passt immer, ein Versehen kostet jetzt eine 413 statt RAM.
  • rewrites="default" entfällt: die Python-Bindung aktivierte darüber die Text-Rewrites des Modells. Der Rust-Pfad nutzt die Normalisierung der Checkpoint-Config. Hörbar erst bei Sonderzeichen-Ketten; nicht Teil des HTTP-Vertrags.
  • --selftest prüft zusätzlich bits/channels explizit, nicht nur nframes.

Tests

cargo test — 6 Tests, kein Modell, kein Netz:

  • health_reports_the_model, speech_returns_24k_mono_16bit_wav
  • missing_or_blank_input_is_400_json, non_object_body_is_400_json
  • unknown_voice_falls_back_to_default, other_paths_and_methods_are_404_json

Sie sind die 1:1-Übersetzung der test_server.py-Fälle des Originals; das Fake-Engine-Muster ersetzt dort das Laden von 320 MB Gewichten für eine 400-Antwort.

cargo test -- --ignored lädt das echte Modell und prüft WAV-Header und Dauer.