浪人
DE|EN
Jarvis lernt Piper - Sprachausgabe lokal im Homelab
tech

Jarvis lernt Piper - Sprachausgabe lokal im Homelab

Zurück zum Blog
6 Min. Lesezeit

Jarvis lernt Piper - Sprachausgabe lokal im Homelab

Zwei Voice-Assistant-Lautsprecher, einer in der Küche, einer im Schlafzimmer, beide hören auf „Hey Jarvis“. Es sind zwei Echo Dots der 2. Generation, die mit EchoMuse geflasht sind und jetzt ohne Alexa als Sprachsatelliten für Home Assistant arbeiten. Bis jetzt lief allerdings die komplette Sprachverarbeitung über Home Assistant Cloud von Nabu Casa: Die Spracherkennung (STT) hat die Cloud erledigt, die Sprachausgabe (TTS) ebenfalls, mit der österreichischen Stimme „Jonas“. Dazwischen saß der eingebaute Assistent von Home Assistant, ganz ohne Sprachmodell.

Das ist bequem, hängt aber an Internet und Abo. Mein Ziel ist, die Pipeline Schritt für Schritt ins eigene Homelab zu holen. Den Anfang macht die Sprachausgabe mit Piper. Die Spracherkennung kommt später, denn bei Whisper und Co. will ich mir die Modellfrage erst in Ruhe ansehen.

Schritt 1: Aufräumen vor dem Umbau

Bevor ich etwas umbaue, wollte ich wissen, was Assist eigentlich darf. Die Bestandsaufnahme war ernüchternd: Von zehn Entitäten, die für Assist freigegeben waren, zeigten sieben auf Rollläden, die es gar nicht mehr gab. Unterm Strich konnte Assist also genau drei Lichter steuern. Die toten Freigaben habe ich entfernt.

assist-freigaben

Dazu kam ein dritter Lautsprecher, ein M5Stack Atom Echo mit ESPHome, der seit Wochen nicht mehr erreichbar war. Er sollte weg, und genau dabei bin ich gestolpert: ESPHome-Geräte lassen sich nicht über das Geräteregister löschen. Der Weg führt über den Integrationseintrag des Geräts unter ESPHome, dort verschwindet es dann samt Entitäten.

Ein paar Entscheidungen habe ich bewusst getroffen:

  • Heizung und Vorhänge bleiben ohne Sprachfreigabe.
  • Schlafzimmer: Statt der großen Lichtgruppe mit Effekt- und Nachttischlichtern ist nur noch das Hauptlicht freigegeben.
  • Aliasse: Die Hauptlichter hören jetzt nur noch auf „Wohnzimmer“ und „Schlafzimmer“.

Getestet habe ich das mit einer reinen Abfrage wie „Ist Wohnzimmer an?“. So sieht man, ob Assist die richtige Entität findet, ohne dass dabei etwas geschaltet wird.

Freigabe vs. eigene Sprachbefehle

Die Freigabe in den Assist-Einstellungen bestimmt nur, was die eingebauten Befehle sehen und steuern dürfen, also Sätze wie „Licht an“ oder „Rollläden runter“.

Automationen mit Sprach-Auslöser funktionieren anders. Eigene Sätze wie „Code Red“ prüft Home Assistant zuerst, und die zugehörige Automation darf schalten, was immer in ihr steht. Eine Freigabe braucht sie dafür nicht.

Daraus folgt: Die Freigabe ist kein Sicherheitsmechanismus. Was per Sprache auslösbar ist, kann jeder in Hörweite auslösen. Ich überlege mir deshalb bei jedem Sprachbefehl, ob ich damit leben kann, dass ihn auch ein Besuch oder der Fernseher sagt.

Schritt 2: Piper im Homelab

Mein Homelab-Server ist kein Kraftpaket für KI-Workloads: Intel i5-1145G7 mit 4 Kernen und 8 Threads, 62 GB RAM, keine dedizierte GPU. Darauf laufen bereits rund 30 Docker-Container. Für Sprachsynthese mit Piper sollte das reichen, ob es tatsächlich reicht, zeigt Schritt 3.

Piper läuft als einzelner Container rhasspy/wyoming-piper per Docker Compose:

services:
  piper:
    image: rhasspy/wyoming-piper:latest
    container_name: piper
    command: --voice de_DE-thorsten-medium
    ports:
      - "10200:10200"
    volumes:
      - ./piper-data:/data
    restart: unless-stopped

Im Volume landen die Stimmen. Die mit --voice gesetzte Stimme ist nur die Standardstimme des Containers. Welche Stimme tatsächlich spricht, entscheidet Home Assistant in der Pipeline. Fordert HA eine andere Stimme an, lädt der Container sie beim ersten Aufruf selbst nach.

Einen Reverse Proxy oder Hostnamen habe ich bewusst nicht eingerichtet. Wyoming ist eine einfache TCP-Verbindung und kein Web-Protokoll, Home Assistant verbindet sich direkt per IP und Port. Ein Name wäre nur eine zusätzliche Fehlerquelle über DNS, ohne erkennbaren Nutzen.

In Home Assistant war die Einbindung dann schnell erledigt: Wyoming-Integration hinzufügen, IP und Port 10200 eintragen, fertig. Danach gibt es die Entität tts.piper.

Eine zweite Pipeline statt Umbau

Die bestehende Cloud-Pipeline habe ich nicht angefasst, sondern kopiert. Die neue Pipeline „Lokal“ nutzt für die Spracherkennung weiter die Cloud, für die Sprachausgabe Piper. STT und TTS sind innerhalb einer Pipeline unabhängig voneinander einstellbar, deshalb lässt sich die Umstellung gut in Etappen machen.

pipeline-lokal

Welche Pipeline ein Lautsprecher verwendet, wird pro Gerät über dessen Auswahl „Assistent“ festgelegt. Ich habe zuerst nur einen Lautsprecher umgestellt, um direkt vergleichen zu können, und den zweiten erst später nachgezogen.

lautsprecher-assistent-auswahl

Eine Anmerkung zur Stimme: Eine österreichische Piper-Stimme gibt es nicht. Ich bin bei „thorsten“ gelandet, aber das ist Geschmackssache. In den Pipeline-Einstellungen lassen sich die Stimmen bequem probehören.

Schritt 3: Messen statt Bauchgefühl

Nach dem A/B-Vergleich hatte ich ein Gefühl, aber keine Zahlen. Und der erste naheliegende Weg, an Zahlen zu kommen, hat sich als Sackgasse erwiesen.

Drei Mess-Fallen

Die Pipeline-Protokolle täuschen. In den Debug-Ansichten der Pipeline zeigt HA für TTS nur 2-7 ms. Dort wird aber nur ein Link zur Sprachausgabe erzeugt. Die eigentliche Synthese passiert erst, wenn der Lautsprecher diesen Link abruft. Für TTS-Vergleiche sind die Protokolle daher wertlos.

Der Cache greift trotz cache: false. Bei meinen Tests hat HA identische Sätze trotzdem aus dem Zwischenspeicher geliefert, in rund 0,05 s. Die Lösung: jeden Durchgang mit einer anderen Anzahl Leerzeichen am Satzende eindeutig machen. Gleiche Sprache, anderer Cache-Schlüssel.

Stimmwechsel kostet Ladezeit. Wechselt man zwischen zwei Piper-Stimmen, lädt Piper das Modell jedes Mal neu. Ich habe deshalb pro Stimme in eigenen Blöcken mit Aufwärmrunde gemessen. Im Betrieb heißt das: eine Stimme festlegen und dabei bleiben.

Gemessen habe ich über die REST-API von Home Assistant. Ein kleines Python-Skript fordert über /api/tts_get_url die Sprachausgabe an und lädt die WAV-Datei danach komplett herunter. Gemessen wird die Zeit vom Auftrag bis zur fertigen Datei. Das ist das, was der Lautsprecher am Ende auch erlebt. Der Kern, gekürzt:

def run(entity, lang, voice, text):
    t0 = time.perf_counter()
    # 1. HA legt nur einen Link zur Sprachausgabe an
    path = json.load(req("POST", "/api/tts_get_url", {
        "engine_id": entity, "message": text, "language": lang,
        "options": {"voice": voice},
        "cache": False,  # wird von HA ignoriert, siehe uniq()
    }))["path"]
    # 2. Erst der Abruf löst die eigentliche Synthese aus
    data = req("GET", path).read()
    return time.perf_counter() - t0

pad = 0
def uniq(s):
    # gleiche Sprache, aber neuer Text für den Cache
    global pad
    pad += 1
    return s + " " * pad

Getestet habe ich fünf Sätze von kurz bis lang, typische Antworten aus dem Alltag. Jeder Satz lief drei Durchgänge pro Stimme, in der Tabelle steht der Median.

Satz Cloud „Jonas“ Piper thorsten-high Piper thorsten-medium
„Licht ist an.“ 0,18 s 0,59 s 0,18 s
„Es ist 15 Uhr.“ 0,20 s 0,75 s 0,18 s
Timer-Restzeit (1 Satz) 0,28 s 2,11 s 0,42 s
PV-Leistung + Speicher (langer Satz) 0,34 s 4,15 s 0,68 s
Auto-Akku (2 Sätze) 0,53 s 4,28 s 0,90 s

Zeit vom Auftrag bis zur fertigen Sprachdatei, Median aus 3 Durchgängen. Nach einem Container-Neustart dauert die erste Antwort rund 2 s, weil Piper das Modell lädt. Danach liegt sie wieder bei 0,16-0,21 s.

Die Tabelle ist eindeutig:

  • thorsten-high ist auf einer CPU ohne GPU zu langsam. Bei längeren Sätzen sind es über 4 Sekunden, einzelne Ausreißer lagen bei bis zu 10,8 s. Für eine Sprachantwort ist das keine Option.
  • thorsten-medium ist bei kurzen Antworten gleichauf mit der Cloud und bleibt auch beim längsten Satz unter einer Sekunde. Den Qualitätsunterschied zu „high“ höre ich kaum.
  • Die Cloud ist trotzdem messbar am schnellsten, vor allem bei langen Sätzen. Das gehört zur Ehrlichkeit dazu.

Ergebnis

Beide Lautsprecher nutzen jetzt die Pipeline „Lokal“ mit Piper „thorsten-medium“, und sie ist als Standard-Pipeline gesetzt. Die alte Cloud-Pipeline bleibt als Rückfallebene bestehen. Falls Piper einmal hakt, schalte ich pro Lautsprecher mit einem Klick zurück.

Schneller ist die Sprachausgabe dadurch nicht geworden. Der Gewinn liegt woanders: Die Antworten entstehen im eigenen Netz, unabhängig von Internetverbindung und Abo, bei praktisch gleicher Reaktionszeit. Im Alltag merke ich zwischen „Licht ist an“ aus der Cloud und „Licht ist an“ von Piper keinen Unterschied, außer dass Jonas österreichisch klingt und Thorsten nicht.

Komplett lokal ist das Setup noch nicht: Die Spracherkennung läuft weiter über die Cloud.

Was ich gelernt habe

  • Erst aufräumen, dann umbauen. Sieben von zehn Freigaben zeigten ins Leere. Ohne die Bestandsaufnahme hätte ich später Fehler gesucht, die mit Piper nichts zu tun haben.
  • Freigabe ist keine Zugriffskontrolle. Sie steuert die eingebauten Befehle, nicht die eigenen Sprach-Automationen.
  • HA-Protokolle messen nicht, was man glaubt. Für TTS zeigen sie nur das Anlegen des Links. Wer vergleichen will, muss die fertige Datei abholen und die Zeit selbst stoppen.
  • Cache und Modellwechsel verfälschen Messungen. Eindeutige Texte und Messblöcke pro Stimme sind Pflicht.
  • Auf einer CPU ohne GPU ist „medium“ die richtige Wahl. „high“ klingt minimal besser, kostet aber ein Vielfaches an Zeit.
  • Etappen funktionieren. Weil STT und TTS in der Pipeline getrennt sind, ließ sich die Umstellung ohne Risiko und mit Rückfallebene machen.

Fazit und Ausblick

Die Sprachausgabe läuft jetzt lokal, und zwar ohne spürbaren Nachteil im Alltag. Der Aufwand war überschaubar: ein Container, eine Integration, eine kopierte Pipeline. Am meisten Zeit haben das Aufräumen und das saubere Messen gekostet, und genau diese Zeit war gut investiert.

Als Nächstes will ich auch die Spracherkennung lokal versuchen. Die Frage ist, welches STT-Modell auf einer CPU ohne GPU kurze deutsche Befehle mit österreichischem Akzent zuverlässig versteht. Dafür werde ich mir verschiedene Whisper-Größen, deutsch-optimierte Varianten und Alternativen ansehen, und zwar wieder mit Messwerten statt Bauchgefühl.