- Rust 100%
- ein Crate, drei Binaries: voice-selftest, voice-ha-pipeline, voice-attach-usb - Wyoming-Framing selbst implementiert (event.py als Vorlage) - echter Rundlauf Piper->Whisper grün, 4 Tests offline |
||
|---|---|---|
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
voice-stack-rs
Rust-Port der drei Python-Helfer aus voice-stack. Die Dienste selbst
(Whisper, Piper, openWakeWord, Satellit) bleiben der Compose-Stack — portiert sind die Skripte,
die sie steuern und prüfen.
| alt | neu | was |
|---|---|---|
selftest.py |
voice-selftest |
Piper spricht, Whisper hört zu (Wyoming über TCP) |
ha_pipeline.py |
voice-ha-pipeline |
Assist-Pipeline in Home Assistant anlegen (WebSocket) |
attach_usb.py |
voice-attach-usb |
QuadCast nach Re-Enumeration zurück an VM 9 hängen (TrueNAS) |
Bauen & installieren
cargo build --release
install -m755 target/release/{voice-selftest,voice-ha-pipeline,voice-attach-usb} ~/bin/
Benutzen
voice-selftest # schreibt selftest.wav neben das CWD
voice-selftest /tmp/laut.wav # Zielpfad optional
export HASS_URL=https://homeassistant.example HASS_TOKEN=...
voice-ha-pipeline
export TN_HOST=<truenas-host>
voice-attach-usb
HASS_URL/HASS_TOKEN und TN_HOST sind Pflicht wie im Original; fehlt TN_HOST, endet
voice-attach-usb mit Exit 2 und dem Hinweis export TN_HOST=<hostname-oder-ip> auf stderr —
kein Traceback.
Wyoming-Protokoll
Das Original nutzte das Python-Paket wyoming; hier ist das Framing selbst implementiert
(src/lib.rs), nach wyoming/event.py: eine JSON-Zeile mit type, version (1.2) und den
Längen, danach Datenblock, danach Payload. audio-chunk trägt das PCM als Payload, nicht als
Base64 im JSON.
Abweichungen
- Ein Crate, drei Binaries statt drei Skripte: der Wyoming-Layer wäre sonst dreimal da.
voice-attach-usbliest den TrueNAS-Token weiterhin über~/bin/vault http truenas …und nimmt dafür das letzte JSON-String-Literal der Ausgabe (wie der Python-Regex). Kein Geheimnis landet in der Shell.ok=in der Ausgabe vonvoice-attach-usbdruckt den JSON-Werttruestatt PythonsTrue. Kosmetisch, gleiche Information.- Der WAV-Schreiber ist eigene Handarbeit (44-Byte-Header, beliebige Sample-Breite), weil
wavenur in Python existiert;voice-selftestparst die Datei vor dem Hochladen an Whisper zurück, damit ein kaputter Header sofort auffällt.
Tests
cargo test # 4 Tests, kein Dienst nötig
cargo test -- --ignored # echter Rundlauf gegen den laufenden Compose-Stack
events_survive_a_round_trip_over_tcp— echtes TCP, drei Events hin, eins zurück, Felder und Payload geprüftan_empty_data_map_is_sent_without_a_data_blob—audio-stophat keinendata_lengthwav_bytes_round_trips_through_the_parserattach_usb_without_tn_host_exits_2_and_says_how_to_fix_it(Port vontest_attach_usb.py)
Gemessen (2026-11-05, dieser Host)
$ voice-selftest
TTS ok: 225792 Bytes PCM, 22050 Hz, 16 bit, 1 ch -> selftest.wav
STT : " Hallo, Emarik, das ist Pandy. Die Sprachausgabe Leoft lokal auf deinem Server."
$ docker exec -w /tmp wyoming-satellite python3 selftest.py # Original
TTS ok: 214016 Bytes PCM, 22050 Hz, 16 bit, 1 ch -> selftest.wav
STT : ' Hallo Emerik, das ist Pandy, die Sprachausgabe Leof Lokal auf deinem Server.'
Byte-Gleichheit ist hier kein Kriterium: Piper synthetisiert pro Lauf unterschiedlich lang
(zwei Läufe des Originals: 208896 und 209920 Bytes). Verglichen wird der Rundlauf — Format,
RIFF/WAVE, Transkript.