Rust-Port der voice-stack-Helfer (Wyoming-Selftest, HA-Pipeline, TrueNAS-USB)
Find a file
Hermes 0b6ead612f feat: Rust-Port der voice-stack-Skripte
- 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
2026-11-05 16:27:24 +01:00
src feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00
tests feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00
.gitignore feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00
Cargo.lock feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00
Cargo.toml feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00
README.md feat: Rust-Port der voice-stack-Skripte 2026-11-05 16:27:24 +01:00

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-usb liest 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 von voice-attach-usb druckt den JSON-Wert true statt Pythons True. Kosmetisch, gleiche Information.
  • Der WAV-Schreiber ist eigene Handarbeit (44-Byte-Header, beliebige Sample-Breite), weil wave nur in Python existiert; voice-selftest parst 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üft
  • an_empty_data_map_is_sent_without_a_data_blob — audio-stop hat keinen data_length
  • wav_bytes_round_trips_through_the_parser
  • attach_usb_without_tn_host_exits_2_and_says_how_to_fix_it (Port von test_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.