Zum Inhalt

/A1 Anleitung 24 Minuten Lesezeit

Lokalen Sprachassistenten bauen wie SpaceTrotter

Von Michael Mackedanz, gelernter Elektroinstallateur. Alles in dieser Anleitung läuft bei mir im eigenen Homelab.

Vorschaubild des YouTube-Videos zu dieser Anleitung ▶ Video auf YouTube ansehen

Ein Sprachassistent, der auf meinem Windows-PC zuhört, versteht und antwortet. Erkennung und Stimme laufen lokal, das Gehirn ist Claude Code oder ein lokales Modell. Hier steht, aus welchen Teilen er besteht, in welcher Reihenfolge du ihn mit Claude Code aufbaust und wo ich hängen geblieben bin.

Das ist eine Bauanleitung, kein Download. Es gibt kein Repository und kein fertiges Paket. Du baust deinen eigenen Assistenten in deiner eigenen Claude-Code-Sitzung, und diese Anleitung gibt dir die Reihenfolge, die Komponenten, die Prompts und die Messwerte, an denen du siehst, ob du auf dem richtigen Weg bist.

Was SpaceTrotter kann

SpaceTrotter ist ein schlichtes Fenster mit einer Punktkugel, einer Zustandszeile und dem Gesprächsverlauf. Ich drücke die Leertaste oder sage „SpaceTrotter, …“, stelle meine Frage und bekomme die Antwort gesprochen, Satz für Satz, während der Rest noch entsteht. Nach jeder Antwort hört er wieder zu, nach 9 Sekunden Stille ist das Gespräch vorbei.

Dazu kommen Werkzeuge, die ich nach und nach freigeschaltet habe: Uhrzeit, Programme starten, Webseiten öffnen, Lautstärke, Fenster, Notizen in meinem Obsidian-Vault suchen und lesen, Home Assistant lesen und Licht schalten, in Nextcloud Entwürfe, Termine und Notizen anlegen, eine Websuche und ein Morgen-Briefing auf Zuruf. Mit dem lokalen Modell kann er sich auch meine Bildschirme ansehen.

Der Name ist der meiner Star Citizen Organisation und gleichzeitig das Weckwort.

Warum ein Neubau

Ich hatte schon einen Assistenten, Jarvis. Der läuft in einem Container auf meinem Proxmox-Host, nutzt die Spracherkennung des Browsers (die läuft beim Browser-Anbieter) und eine Online-Stimme. Im Alltag habe ich ihn fast nur noch als Vorleser für die Antworten von Claude Code genutzt.

Ein Video eines anderen Bastlers hat mich dann auf die Idee gebracht, Erkennung und Stimme komplett lokal zu machen: eine Sprachpausen-Erkennung statt fester Pausenzeiten, ein lokales Erkennungsmodell, eine lokale Stimme und ein Weckwort. Ich habe Jarvis dafür nicht umgebaut, sondern daneben neu angefangen. Jarvis blieb unverändert, bis der Neubau lief. Das würde ich wieder so machen: Du hast immer einen funktionierenden Stand und kannst den neuen in Ruhe kaputt machen.

Was du brauchst

Hardware

Gebaut und gemessen habe ich alles auf meinem Gaming-PC:

TeilBei mir
BetriebssystemWindows 11 Pro, direkt, ohne WSL
ProzessorAMD Ryzen 9 7950X3D (die Spracherkennung läuft hier mit 4 Threads)
GrafikkarteNVIDIA GeForce RTX 4080 Super, 16 GB
Arbeitsspeicher63 GB laut Windows
Mikrofonbeliebig, die Auswahl im Fenster kennt alle Eingänge

Wie viel Grafikspeicher du brauchst, hängt am Gehirn. Mit Claude als Gehirn belegt nur die Stimme die Karte, bei mir gemessen 2,0 GB. Mit dem lokalen Modell Gemma 4 12B kommen je nach Kontextlänge 8,6 bis 9,8 GB dazu, die Karte war dann mit allem zusammen bei rund 15 von 16 GB. Welche kleinste Karte noch reicht, habe ich nicht getestet. Ich habe nur mit einer NVIDIA-Karte und CUDA gebaut.

SpaceTrotter muss bei mir nicht neben einem Spiel laufen. Spiele teilen sich dieselben 16 GB, das würde nicht passen.

Software

Die Versionen sind die, mit denen es bei mir läuft. Neuere gehen wahrscheinlich auch, getestet habe ich sie nicht.

BausteinVersion bei mirWofür
Python3.13.15 in einer eigenen venvder Sprachdienst
PyTorch2.8.0+cu128Stimme auf der Grafikkarte
omnivoice0.2.1Stimme
sherpa-onnx2.0.1 (CPU)Spracherkennung
FastAPI und uvicorn0.162.0 und 0.54.0der lokale Webdienst
numpy und soundfile2.5.4 und 0.14.0Audio rein und raus
@ricky0123/vad-web0.0.31Sprechpausen-Erkennung im Browser
onnxruntime-web1.22.0rechnet Silero im Browser
Claude Code2.1.269 beim Baudas Gehirn (und dein Bauwerkzeug)
llama.cpp llama-serverBuild b12020, Windows, CUDA 12.4lokales Modell, optional
mcp (Python)2.3.0eigene Werkzeuge, optional
Microsoft Edgeaktuelldas Fenster als App

Claude Code installierst du über npm, es braucht also Node.js. Neuere Claude-Code-Versionen verlangen laut npm Node 22. Jede Frage an SpaceTrotter geht vom Kontingent deines Claude-Plans ab, genau wie deine normale Arbeit in Claude Code.

Modelle

ModellAufgabeGrößeLizenz
Silero VAD v5erkennt, ob gerade jemand sprichtrund 2 MBMIT
Parakeet TDT 0.6B v3, als int8-Fassung für sherpa-onnxSprache zu TextEncoder 622 MBCC BY 4.0
OmniVoiceText zu SpracheGewichte 3,27 GBCode Apache 2.0, Gewichte CC-BY-NC
Gemma 4 12B, offizielle QAT-q4_0-GGUF von Googlelokales Gehirn, optional6,5 GB, Bildbaustein 175 MBApache 2.0
Qwen3-TTS 1.7B (VoiceDesign und Base)Alternative StimmeBase-Modell 4,3 GB DownloadApache 2.0

Offizielle Seiten: Silero VAD, vad-web, Parakeet-Modellkarte, Parakeet für sherpa-onnx, OmniVoice auf GitHub und auf Hugging Face, llama.cpp, Gemma 4 12B QAT GGUF, Qwen3-TTS.

Zu OmniVoice gleich vorweg, weil es wichtig ist: Die Modellgewichte stehen unter CC-BY-NC. Das heißt nicht-kommerziell. Für einen Assistenten, der bei dir zu Hause privat läuft, ist das in Ordnung. Sobald du damit Geld verdienst, Videos für einen Kanal mit Geschäftsbezug vertonst oder den Assistenten in einem Firmenumfeld einsetzt, ist OmniVoice raus. Das ist meine Einschätzung, keine Rechtsberatung. Mehr dazu weiter unten bei der Stimme.

Die Architektur auf einen Blick

Alles läuft auf einem Rechner. Ein kleiner Python-Dienst hört nur auf 127.0.0.1, das Fenster ist eine Webseite, die Edge als App-Fenster öffnet.

Mikrofon
  → Silero VAD im Browser (erkennt Anfang und Ende eines Satzes)
  → 16-kHz-Audio an den Dienst: /stt → Parakeet auf der CPU → Text
  → Text an /ask → Gehirn: Claude Code (Dauerprozess) oder llama-server mit Gemma
  → Antwort kommt Stück für Stück zurück
  → Browser schneidet fertige Sätze ab → /tts → OmniVoice auf der GPU → WAV
  → Lautsprecher

Die Ohren: Silero VAD läuft im Browser über vad-web. Es merkt, wann du anfängst und aufhörst zu sprechen, und liefert den Satz als 16-kHz-Audio. Genau das Format will Parakeet. Parakeet läuft über sherpa-onnx auf dem Prozessor und braucht keine Grafikkarte. Auf der CPU war es bei mir schnell genug: 0,1 bis 0,4 Sekunden pro Satz.

Das Gehirn: Entweder Claude Code als Dauerprozess im Hintergrund, angesprochen über stream-json, oder ein lokales Modell über llama-server aus llama.cpp. Im Fenster schaltest du zwischen „Claude“ und „Lokal“ um. Das lokale Modell startet nur, solange „Lokal“ gewählt ist, sonst bleibt der Grafikspeicher frei.

Die Stimme: OmniVoice auf der Grafikkarte, immer mit demselben geklonten Stimmprofil, damit jeder Satz gleich klingt. Ein Aufruf liefert einen ganzen Satz als Audio, das passt zum satzweisen Sprechen.

Das Fenster: eine einzelne HTML-Seite mit der Kugel, die auf die Lautstärke der Stimme reagiert, dem Verlauf und ein paar Paneelen für Modell, Kontingent, Audio und Steuerung.

Das Weckwort: kein eigenes Weckwort-Modell. Ist das Weckwort an, hört Silero dauernd mit, jeder Sprachabschnitt geht lokal an Parakeet, und nur wenn der erkannte Text mit dem Namen anfängt, reagiert SpaceTrotter. Alles andere wird verworfen, nichts verlässt den PC.

Von der leeren Claude-Code-Sitzung zur Oberfläche

Ich baue so etwas mit Claude Code. Die Prompts unten sind Beispiele, die du an deine Lage anpasst: anderer Ordner, anderer Name, andere Werkzeuge. Arbeite die Schritte nacheinander ab und geh erst weiter, wenn der Schritt davor läuft. Sonst suchst du später einen Fehler in fünf Teilen gleichzeitig.

Ein Grundsatz vorweg, der mir viel Ärger erspart hat: Erst messen, dann bauen. Bevor irgendein Dienst entsteht, lässt du Claude jedes Modell einzeln in einem kleinen Testskript laufen und Zeit und Grafikspeicher messen.

Schritt 1: Ordner und Plan

Ziel: ein eigener Ordner, eine eigene Python-Umgebung, und Claude kennt das Vorhaben.

Leg einen leeren Ordner an, zum Beispiel C:\assistent\, und starte darin eine neue Claude-Code-Sitzung.

Wir bauen in diesem Ordner einen lokalen Sprachassistenten für Windows 11, ohne WSL.
Bausteine: Silero VAD im Browser (@ricky0123/vad-web), Parakeet TDT 0.6B v3 als
int8-Modell über sherpa-onnx auf der CPU für die Spracherkennung, OmniVoice auf
meiner NVIDIA-Karte für die Stimme, als Gehirn zuerst Claude Code als Dauerprozess.
Später kommt optional ein lokales Modell über llama.cpp dazu.
Leg eine eigene venv an, installiere noch nichts Großes und schreib mir zuerst einen
Plan mit den Schritten, den Downloads und ihren Größen. Alles bleibt in diesem Ordner.

Läuft, wenn: Es gibt eine venv, und du hast einen Plan, den du verstehst.

Schritt 2: Stimme messen

Ziel: OmniVoice spricht deutsche Sätze, und du kennst Zeit pro Satz und Grafikspeicher.

Installiere PyTorch mit CUDA und omnivoice in die venv. Die Modelle sollen unter
modelle\ landen (HF_HOME). Schreib ein Testskript, das fünf deutsche Sätze mit
language "de" in fp16 erzeugt, einmal mit 8, 16 und 32 Diffusionsschritten.
Miss Zeit pro Satz, Ladezeit und Spitze im Grafikspeicher und leg die WAV-Dateien
in getrennte Ordner, damit ich sie anhören kann.

Bei mir auf der RTX 4080 Super: 32 Schritte rund 1,25 Sekunden pro Satz, 16 Schritte rund 0,6, 8 Schritte rund 0,33. Grafikspeicher jeweils 2,0 GB, Laden rund 3 Sekunden. Die Zeit hing kaum von der Satzlänge ab.

Läuft, wenn: Du hörst deutsche Sätze und hast deine eigenen Zahlen.

Schritt 3: Spracherkennung messen und gegenprüfen

Ziel: Parakeet schreibt deutsche Sprache zuverlässig mit, und zwar auf der CPU.

Lade das Parakeet-Modell sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8 und installiere
sherpa-onnx für die CPU. Schreib ein Testskript mit 4 Threads, das die WAV-Dateien
aus dem Stimmtest zurück in Text schreibt. Zeig mir Originalsatz und erkannten Text
nebeneinander und miss die Zeit pro Satz.

Diese Gegenprobe, Stimme rein und Erkennung raus, ist das nützlichste Testwerkzeug im ganzen Projekt. Sie zeigt dir Fehler in beiden Richtungen, ohne dass du selbst ins Mikrofon sprechen musst. Bei mir kamen Zahlen sauber als Ziffern zurück („21 Grad“, „23.55 Uhr“). Bei einzelnen Stimmen kam „ohne Fehler“ als „ohne Feh“ an. Ob das an der Stimme oder an der Erkennung liegt, klärt nur Hören.

Läuft, wenn: Die Sätze kommen fast fehlerfrei zurück, in Zehntelsekunden.

Schritt 4: Eine feste Stimme festlegen

Ziel: Jeder Satz klingt nach derselben Person.

OmniVoice erzeugt ohne Vorgabe eine Zufallsstimme. Du kannst eine Stimme per Beschreibung erzeugen und daraus dann ein Stimmprofil klonen. Laut Projekt soll die Vorlage 3 bis 10 Sekunden lang sein.

Erzeuge mit OmniVoice mehrere Kandidaten-Stimmen per instruct, zum Beispiel
"Male, Young Adult, Low Pitch", mit 32 Schritten, je zwei deutsche Sätze.
Leg die Kandidaten zum Anhören ab. Aus dem Kandidaten, den ich auswähle, erstellst
du ein Stimmprofil als Datei, das der Dienst später einmal lädt.

Zwei Erfahrungen: Meine erste Vorlage entstand mit der groben Angabe „male“ und 8 Schritten. Der Klon daraus klang durchgehend so, als würde er Geheimnisse erzählen, leicht behaucht. Mit einer genaueren Beschreibung und 32 Schritten für die Vorlage war das weg. Und die Zahl der Schritte im Betrieb entscheidest du nach Gehör. Ich hatte mich zuerst für 8 entschieden, im Gespräch klangen lange Sätze damit aber seltsam. Heute laufen 32 Schritte, rund 1,3 Sekunden pro Satz. Der erste Ton kommt damit etwa eine Sekunde später.

Läuft, wenn: Du ein Stimmprofil hast, das dir gefällt.

Schritt 5: Der Dienst

Ziel: ein Webdienst, der beide Modelle einmal lädt und Sprache rein, Text raus und Text rein, Sprache raus kann.

Bau einen FastAPI-Dienst app\server.py, der nur auf 127.0.0.1 hört (Port frei
wählbar). Beim Start lädt er Parakeet und OmniVoice mit dem Stimmprofil einmal.
Endpunkte: /stt nimmt 16-kHz-Float32-Audio und gibt Text zurück, /tts nimmt Text und
gibt WAV zurück, /health meldet den Zustand. Nur Anfragen von diesem PC und von der
eigenen Seite zulassen, alles andere 403. Jeder Aufruf schreibt Dauer und Text ins Log.

Läuft, wenn: /health antwortet und ein Test-Aufruf von /tts eine hörbare WAV liefert.

Schritt 6: Claude Code als Gehirn

Ziel: Der Dienst hält einen Claude-Code-Prozess offen und reicht die Antwort Stück für Stück weiter.

Erweitere den Dienst um einen Claude-Code-Dauerprozess: claude -p mit stream-json
für Ein- und Ausgabe und --include-partial-messages. Erste Ausbaustufe: keine
Werkzeuge, kein MCP, und die Benutzer-Hooks dürfen nicht laufen. Der Prozess arbeitet
in einem eigenen leeren Ordner app\workspace. Neuer Endpunkt /ask gibt die Antwort als
NDJSON zurück, Textstück für Textstück. Nach 10 Minuten ohne Frage beginnt ein frisches
Gespräch. Systemtext: Antworten werden vorgelesen, also kurz, auf Deutsch, kein
Markdown, keine Listen, keine Emojis, Zahlen immer als Ziffern.

Die Startzeile sah in der ersten Ausbaustufe im Kern so aus:

claude -p --input-format stream-json --output-format stream-json --verbose
  --include-partial-messages --setting-sources project --strict-mcp-config
  --tools "" --no-session-persistence --model haiku --system-prompt "..."

--setting-sources project in einem leeren Ordner sorgt dafür, dass deine eigenen Einstellungen und Hooks aus Claude Code nicht mitlaufen. Warum das wichtig ist, steht bei den Stolperstellen. Am Ende jedes Textblocks schickt der Dienst einen Absatz hinterher. So wird ein Satz wie „Ich schau kurz nach.“ sofort gesprochen, auch wenn danach ein Werkzeug läuft.

Läuft, wenn: eine getippte Frage an /ask nach gut einer Sekunde die ersten Textstücke liefert. Mit Haiku waren das bei mir 1,1 bis 1,5 Sekunden.

Schritt 7: Das Fenster

Ziel: Leertaste drücken, sprechen, Antwort hören.

Bau app\static\index.html: Silero VAD v5 über @ricky0123/vad-web 0.0.31 mit
onnxruntime-web 1.22.0, alle Dateien lokal unter static\vendor, nichts aus dem Netz.
Leertaste startet das Gespräch. Am Satzende geht das Audio an /stt, der Text an /ask.
Die Antwort wird satzweise an /tts geschickt und der Reihe nach abgespielt, immer
nur einen Satz im Voraus. Stücke unter 20 Zeichen warten auf den nächsten Satz,
Abkürzungen wie "z. B." und Ordnungszahlen trennen nicht. Nach jeder Antwort wieder
zuhören, nach 9 s Stille Schluss. Leertaste während der Antwort unterbricht.
Oberfläche: dunkles Fenster, Punktkugel, die auf die Stimme reagiert, Zustandszeile
(Bereit, Hört zu, Versteht, Denkt nach, Spricht) und Verlauf.

Die Einstellungen für die Sprechpausen-Erkennung, mit denen ich zufrieden bin:

redemptionMs: 700, minSpeechMs: 250, preSpeechPadMs: 300

Die Kugel reagiert wirklich auf die Stimme, weil das Audio über einen AnalyserNode im Browser läuft. Mit einer Online-Stimme des Browsers geht das nicht, die gibt kein Audiosignal heraus.

Läuft, wenn: Du im Browser unter http://127.0.0.1:<Port> eine Frage sprichst und die Antwort hörst. In meiner Messung kam der erste Ton rund 2 Sekunden nach dem Abschicken einer getippten Frage, damals mit 8 Schritten für die Stimme.

Schritt 8: Starten, beenden, Fensterplatz

Ziel: Ein Klick startet alles, das X am Fenster beendet alles.

Schreib ein PowerShell-Skript "Assistent starten.ps1": Läuft der Dienst nicht, starte
ihn unsichtbar über WMI (Win32_Process Create mit wscript und einer kleinen .vbs),
damit er nicht mit dem Skript endet. Warte, bis /health antwortet, dann öffne Edge
mit --app als eigenes Fenster. Fehler als Meldungsfenster. Dazu eine Verknüpfung auf
dem Desktop, PowerShell versteckt.
Der Dienst soll sich selbst beenden, wenn 30 s lang kein Fenster mehr verbunden ist:
Jede offene Seite hält dafür einen Server-Sent-Events-Strom offen. Nach dem Start
wartet er bis zu 5 Minuten auf das erste Fenster, F5 muss er überstehen.

Danach bei Bedarf:

Edge merkt sich die Lage des App-Fensters nicht zuverlässig. Merk dir im Dienst alle
3 s Lage, Größe und Maximiert des Fensters per GetWindowPlacement in einer JSON-Datei
und setz ein neu geöffnetes Fenster per SetWindowPlacement dorthin zurück. DPI-bewusst,
ich habe unterschiedlich skalierte Monitore. Fehlt der gemerkte Monitor, Fenster
dort lassen, wo Windows es hinlegt.

Läuft, wenn: Ein Doppelklick das Fenster öffnet, das X nach spätestens einer halben Minute den Dienst samt Grafikspeicher freigibt und das Fenster beim nächsten Start am alten Platz steht.

Schritt 9: Weckwort

Ziel: Der Assistent reagiert auf seinen Namen am Satzanfang, ohne eigenes Weckwort-Modell.

Baue ein Weckwort ein, Taste W schaltet es um. Mit Weckwort hört Silero dauernd zu,
jeder Sprachabschnitt geht an /stt. Nur wenn der Text mit dem Namen beginnt
(optional nach "Hey", "Hallo", "Okay"), reagiert der Assistent. Steht danach eine
Frage, geht sie direkt raus. Kommt nur der Name, antwortet er "Ja?" und hört zu.
Alles andere wird verworfen und nur klein als "überhört" angezeigt. Nach dem Gespräch
wartet er wieder auf den Namen.
Bevor du das Muster schreibst: Erzeuge mit der Stimme 40 Sätze, die mit dem Namen
beginnen, lass Parakeet sie zurückschreiben und zeig mir alle Schreibweisen.

Der zweite Teil des Prompts ist der wichtige. Parakeet schreibt Namen, die es nicht kennt, sehr unterschiedlich. Das Muster am Satzanfang baust du aus den Schreibweisen, die wirklich herauskommen. Bei mir sieht es so aus:

sp[ae][iy]?[cs]e?[ds]?[\s-]*[dt]?[rt]r?[oa]t+(er|a)

Teste danach gezielt Sätze, auf die er nicht anspringen darf. Bei mir waren das „Space Station“, „Speise trotzdem“ und „Ich spiele mit Space Trotter“ (Name nicht am Anfang).

Läuft, wenn: „SpaceTrotter, wie spät ist es?“ im Raum zuverlässig ankommt und normale Gespräche ignoriert werden.

Schritt 10: Lokales Gehirn (optional)

Ziel: ein Schalter „Claude | Lokal“, und lokal antwortet Gemma 4 12B auf der Grafikkarte.

Bau einen Schalter Claude/Lokal ein. Lokal startet der Dienst llama-server aus
llama.cpp (portable Windows-Fassung mit CUDA) mit Gemma 4 12B als GGUF auf einem
eigenen Port, nur auf 127.0.0.1, und spricht ihn über die OpenAI-kompatible
Schnittstelle mit Streaming an. llama-server läuft nur, solange Lokal gewählt ist,
beim Wechsel zurück wird er beendet. Das Gespräch hält der Dienst im Speicher,
10 Minuten Leerlauf heißt neues Gespräch. Die Downloads lade ich selbst: Schreib mir
dafür ein Skript, das nur von GitHub (ggml-org) und Hugging Face (google) lädt und
Vorhandenes überspringt.

Die Downloads hat bei mir die Sicherheitsprüfung von Claude Code blockiert. Das ist in Ordnung so: Claude schreibt das Skript, du liest es und startest es selbst.

Die Startzeile für llama-server bei mir:

llama-server -m gemma-4-12b-it-qat-q4_0.gguf --host 127.0.0.1 --port <Port>
  -ngl 99 -c 32768 --jinja

Mit dem Bildbaustein kommen --mmproj mmproj-gemma-4-12b-it-qat-q4_0.gguf -ub 1280 dazu. In jeder Anfrage schicke ich außerdem "chat_template_kwargs": {"enable_thinking": false} mit. Warum, steht bei den Stolperstellen.

Läuft, wenn: der Schalter auf „Lokal“ nach wenigen Sekunden „bereit“ meldet und die erste Antwort in rund einer Sekunde kommt.

Schritt 11: Werkzeuge

Ziel: Der Assistent kann etwas tun, aber nur das, was du ausdrücklich freigibst.

Fang klein an. Bei mir war die erste Ausbaustufe reines Gespräch, ohne Zugriff auf PC, Mails, Home Assistant oder Internet. Die Rechte kamen danach einzeln dazu.

Bau einen eigenen MCP-Server app\pc_tools.py (stdio, Python-Paket mcp) mit harmlosen
Werkzeugen: Uhrzeit, Programme im Startmenü suchen und starten, Webseite öffnen,
Ordner öffnen, Lautstärke, Medien, Fenster auflisten und nach vorne holen.
Claude Code bekommt ihn über eine eigene MCP-Konfiguration mit --strict-mcp-config.
Starte Claude mit --permission-mode default und --permission-prompt-tool stdio:
Was in einer Freigabeliste steht, läuft durch. Was ich bestätigen soll, erscheint als
Karte im Fenster mit Ausführen und Ablehnen, nach 5 Minuten ohne Klick abgelehnt.
Alles andere lehnt der Dienst ab.

Wie das mit Freigaben und Sperren im Einzelnen aussieht, steht im nächsten Abschnitt.

Läuft, wenn: „Wie spät ist es?“ über das Werkzeug beantwortet wird und ein nicht freigegebenes Werkzeug abgelehnt wird.

Schritt 12: Was danach kam

Diese Teile sind nicht nötig, haben sich bei mir aber bewährt. Jeder ist ein eigener, kleiner Auftrag an Claude:

  • HUD-Paneele links: Zustand, Gesprächsgröße in Token, Modellwahl Haiku, Sonnet oder Opus (ein Wechsel startet Claude neu), Kontingent für 5 Stunden und Woche aus dem rate_limit_event von Claude Code, Lautstärke bis 150 %, Mikro stumm, Texteingabe.
  • Mikrofonauswahl mit Rückfall auf den Windows-Standard, wenn das gemerkte Gerät abgesteckt ist. Die Gerätenamen gibt der Browser erst nach der ersten Mikrofonfreigabe heraus.
  • Briefing per Sprache: „Briefing“, „Guten Morgen“ oder „Was steht heute an?“ (nur kurze Sätze, damit „erklär mir, was ein Briefing ist“ nicht auslöst). Der Dienst sammelt Wetter, Termine, ungelesene Mails und offene Punkte selbst und gleichzeitig, nur lesend. Das Modell fasst nur noch zusammen. So kann das lokale Modell es genauso gut wie Claude.
  • Claude Code vorlesen: Ein Stop-Hook in deiner normalen Claude-Code-Einrichtung schickt jede Antwort an einen Endpunkt des Dienstes, die Seite liest sie vor, wenn ein Schalter an ist und gerade kein Gespräch läuft. Markdown wird dafür bereinigt, Codeblöcke werden als „Codeblock ausgelassen“ angesagt.
  • Grafikkarte abgeben: Ein Knopf beendet SpaceTrotter ganz und gibt die Karte an ComfyUI. Schließe ich ComfyUI, kommt er von selbst zurück.

Werkzeuge und Grenzen

Drei Stufen haben sich bewährt: frei, nur nach Klick, gesperrt. Frei ist alles, was nichts kaputt machen kann. Nach Klick ist alles, was etwas verschickt, löscht oder teilt, also Mail senden, Dateien löschen, Freigaben. Gesperrt ist alles, was im Haus sicherheitsrelevant ist, dazu Neustarts von Home Assistant und Backups.

Das lokale Modell bekommt bei mir weniger als Claude. Es darf zum Beispiel in Nextcloud nur anlegen und vorbereiten: Mail-Entwürfe, Termine ohne Teilnehmer, neue Notizen. Senden, ändern oder löschen kann es nicht, dafür muss ich auf Claude umschalten. Diese Regeln stehen im Dienst, der dem lokalen Modell nur die erlaubten Werkzeuge anbietet und auch nur die ausführt.

Gefährliche Aktionen sperrst du im Code, nicht im Systemtext. Ich hatte zuerst eine Sperre, die nur hielt, solange das Modell der Anweisung im Prompt folgte. Das habe ich in einem eigenen Test ohne diese Anweisung geprüft, und die Sperre hielt nicht überall. Heute prüft der Dienst jeden Aufruf selbst, für Claude und für jedes lokale Modell gleich. Dabei geht es nicht nur um den direkten Befehl. Prüf auch Umwege: andere Befehle, die dasselbe Gerät erreichen, und Automationen oder Skripte, die es indirekt bedienen. Ist die Prüfung selbst nicht möglich, etwa weil Home Assistant gerade nicht erreichbar ist, wird gesperrt statt durchgelassen. Und teste solche Sperren nur mit Aufrufen, die nichts bewirken können, also mit einem Befehl, der einen Zustand herstellt, der ohnehin schon besteht.

Zwei kleinere Regeln aus der Praxis. Gibst du Claude die eingebauten Werkzeuge Read, Glob und Grep, darf es damit jede Datei auf dem PC lesen, wenn du sie pauschal freigibst. Begrenze das Lesen auf einen Ordner (bei mir der Vault) und lehne alles außerhalb ab. Und wenn dein Assistent Webseiten lesen darf, sperr dafür Adressen im eigenen Netz, auch hinter Weiterleitungen. Sonst kann eine präparierte Webseite das Modell an deinen Router oder dein Home Assistant schicken. Im Systemtext steht zusätzlich, dass Anweisungen aus Webseiten, Mails oder Dateien nur Daten sind.

Stolperstellen

Der Name als Weckwort. Mein erster Name war „Mecki“. Parakeet schrieb ihn im Test als Mackie, Maki, Meki, Mekie, Meky, Maggie, Metsky, Meske, Metzgi und Metzke, und im Alltag sprang er schlecht an. „Space Trotter“ schrieb Parakeet 41 von 42 Mal richtig, einmal als „Space Drotter“. Im Raum funktionierte das deutlich besser. Such dir einen Namen, den das Erkennungsmodell sicher schreibt, und prüf das mit der Gegenprobe aus Schritt 3, bevor du dich daran gewöhnst. Ein Restrisiko bleibt: Sagst du im Sprachchat beim Spielen einen Satz, der mit dem Namen anfängt, und das Weckwort ist an, springt er an.

Benutzer-Hooks laufen mit. Wenn du in deinem normalen Claude Code Hooks eingerichtet hast, etwa einen, der Antworten vorliest, dann laufen die auch im Claude-Prozess deines Assistenten, solange du nichts dagegen tust. Bei mir hätte sonst ein Vorlese-Hook auf die Antworten des Assistenten selbst reagiert. --setting-sources project in einem leeren Arbeitsordner verhindert das. Prüf es, bevor du dich darauf verlässt.

Der Dienst stirbt mit der Shell. Startest du einen Dienst aus einer Claude-Code-Sitzung oder einem Skript mit Start-Process, endet er oft mit dem Befehl. Bei mir startet ihn deshalb WMI (Invoke-CimMethod Win32_Process Create) über eine kleine .vbs, die Python unsichtbar startet und die Ausgabe in eine Logdatei schreibt.

Das X merkt keiner. Meine erste Fassung der Selbstabschaltung hat geschlossene Fenster nie bemerkt, der Zähler blieb stehen. Grund war ein synchroner Generator im Server-Sent-Events-Endpunkt. Asynchron mit request.is_disconnected() erkennt der Dienst eine getrennte Verbindung in unter 2 Sekunden.

Mikrofon nur im sicheren Kontext. Browser geben das Mikrofon nur auf HTTPS oder auf localhost bzw. 127.0.0.1 frei. Auf einer HTTP-Adresse im Heimnetz läuft die Spracheingabe nicht, das kenne ich von Jarvis. Solange Fenster und Dienst auf demselben PC laufen, ist 127.0.0.1 die einfache Lösung. Außerdem gibt der Browser Ton erst nach der ersten Eingabe auf der Seite frei. Bei SpaceTrotter ist das die Leertaste, also kein Problem.

Keine Threads im Browser. onnxruntime-web nutzt ohne Cross-Origin-Isolation keine Threads. Bei mir steht deshalb ort.env.wasm.numThreads = 1 vor dem Start von Silero.

Edge vergisst den Fensterplatz. App-Fenster mit --app merken sich Lage und Größe nicht zuverlässig. Das habe ich wie in Schritt 8 im Dienst gelöst. Das Fenster springt beim Start kurz von Edges Platz an den gemerkten.

Zahlen ausschreiben macht Fehler. Mein Systemtext legte anfangs nahe, Zahlen fürs Vorlesen auszuschreiben. Gemma schrieb 17 × 23 daraufhin als „dreihundertneunundvierzig“, richtig ist 391. OmniVoice liest Ziffern, Kommazahlen, Uhrzeiten, Daten und Euro-Beträge korrekt vor, das habe ich mit der Gegenprobe geprüft. Seitdem verlangt der Systemtext für beide Gehirne Ziffern, und die Rechnung stimmt.

Gemma denkt erst auf Englisch. Gemma 4 denkt standardmäßig vor jeder Antwort rund 300 Token lang, das erste Wort kam erst nach 4,5 bis 8 Sekunden. Mit enable_thinking: false kommt es nach 0,1 bis 0,14 Sekunden, ganze kurze Antworten in 0,3 bis 0,9 Sekunden.

Das lokale Modell kennt das Datum nicht. Gemma suchte im Netz nach dem falschen Jahr. Datum und Uhrzeit gehen deshalb bei jeder Anfrage in den Systemtext. Ähnlich: Gemma behauptete einmal „Ich habe weitere Details herausgesucht“, ohne ein Werkzeug aufgerufen zu haben. Der Dienst markiert solche Sätze ohne Werkzeugaufruf heute im Verlauf mit einem Warnhinweis.

Die Werkzeugliste frisst den Kontext. Bei 16.384 Token Kontext belegten die Anfragen schon 12.600 bis 13.900 Token, fast alles Werkzeugbeschreibungen. Ich habe die Parameterbeschreibungen auf 100 Zeichen gekürzt (72 Werkzeuge danach 44.710 statt 58.143 Zeichen) und den Kontext auf 32.768 Token erhöht. Bei Gemma 4 kostet mehr Kontext kaum Grafikspeicher, auf der 4080 Super belegt Gemma mit 32k gemessen 9,83 GB.

Bilder werden gedeckelt. Mit dem Bildbaustein deckelte llama.cpp ein Bildschirmfoto mit der Vorgabe auf 512 statt 1120 Token, kleine Schrift wurde unleserlich. -ub 1280 behebt das.

Ein langsamer MCP-Server bremst den Start. Ich hatte meine Vault-Suche zuerst als eigenen MCP-Server eingebunden. Claude Code wartete beim Start darauf, der Start dauerte 27,9 statt 3,6 Sekunden. Jetzt steckt die Suche als Funktion in meinem eigenen PC-Werkzeug-Server. Miss die Startzeit, wenn du einen MCP-Server dazunimmst.

Teilweise auslagern lohnt nicht. Ich wollte wissen, ob ich die Grafikkarte schonen kann, indem ich einen Teil von Gemma in den Arbeitsspeicher lege. Gemma hat 48 Schichten, gemessen mit einer Antwort von rund 150 Token:

Schichten auf der GPUGrafikspeicherSchreibenAntwort TextAntwort mit Bild
alle 489,5 GB67 Token/s2,3 s1,5 s
36 (75 %)7,4 GB22 Token/s7,6 s6,3 s
24 (50 %)5,5 GB12 Token/s12,3 s9,6 s
12 (25 %)3,6 GB9 Token/s17,4 s9,4 s
0 (nur Prozessor)1,1 GB7,6 Token/s21,9 s12,6 s

Schon ein Viertel ausgelagert macht die Antworten rund dreimal so langsam, bei 2,1 GB Ersparnis. Gemma bleibt bei mir komplett auf der Karte.

Nicht ins laufende System testen. Beim ersten Werkzeugtest habe ich in ein laufendes Gespräch hineingefragt und dabei das Gehirn umgestellt. Seitdem teste ich in eigenen Prozessen und nie über den Endpunkt, den das Fenster benutzt.

Messwerte

Alle Werte von meinem PC mit RTX 4080 Super, gemessen zwischen dem 31.10. und 03.11.2026:

WasWert
Spracherkennung Parakeet, CPU, 4 Threads0,1 bis 0,4 s pro Satz
Stimme OmniVoice, 8 Schritterund 0,33 s pro Satz
Stimme OmniVoice, 32 Schritte (heute im Betrieb)rund 1,25 bis 1,3 s pro Satz
Grafikspeicher Stimme2,0 GB (Spitze im Test)
Claude Haiku, erstes Textstück1,1 bis 1,5 s
erster Ton nach Abschicken einer Textfrage (Stimme mit 8 Schritten)rund 2 s
Gemma 4 12B laden3,6 bis 5 s
Gemma mit 32k Kontext, erste Anfrage mit voller Werkzeugliste3,6 s (10.007 Token Prompt in 2,7 s)
Gemma, folgende Anfragen1,0 bis 2,0 s, 56 bis 57 Token/s
Gemma, Frage mit Bildschirmfotorund 1,5 s bei rund 1.230 Token

Zum Vergleich habe ich das lokale Modell testweise auf einen zweiten Rechner mit ebenfalls 16 GB Grafikspeicher ausgelagert. Es lief, fühlte sich aber deutlich träger an: 40 statt 46 bis 48 Token/s beim Schreiben und rund 1.300 statt rund 3.800 Token/s beim Einlesen. SpaceTrotter ist deshalb auf dem Gaming-PC geblieben.

Die Stimme: OmniVoice oder Qwen3-TTS

OmniVoice ist der Weg, den ich dir für den Assistenten zu Hause empfehle. Es ist schnell, die Stimme ist per Profil gleichbleibend, und Zahlen, Uhrzeiten und Beträge liest es richtig. Laut Paper stecken rund 21.900 Stunden deutsche Sprache im Training.

Der Haken ist die Lizenz. Der Code steht unter Apache 2.0, die Modellgewichte unter CC-BY-NC, laut Modellkarte wegen Einschränkungen aus den Trainingsdaten. Privat zu Hause ist das in Ordnung. Für alles mit Geschäftsbezug, auch ein Video auf einem Kanal, der zu einem Geschäft gehört, ist OmniVoice nach meiner Einschätzung nicht geeignet. Das gilt auch für Stimmen, die du mit OmniVoice erzeugt hast: Meine Vorlage stammt selbst aus OmniVoice, also aus dem NC-Modell. Keine Rechtsberatung, prüf deinen Fall selbst.

Die Alternative ist Qwen3-TTS unter Apache 2.0, also auch kommerziell nutzbar. Deutsch ist eine von zehn Sprachen. Mit dem VoiceDesign-Modell gestaltest du eine Stimme per Textbeschreibung, mit dem Base-Modell klonst du sie. Das musst du auch, denn VoiceDesign erzeugt bei jedem Satz eine leicht andere Stimme. Laut Modellkarte nutzt der Beispielcode flash-attention, das kann unter Windows eine Hürde sein.

Mein Probelauf mit Qwen3-TTS 1.7B lief auf dem Prozessor, weil SpaceTrotter die Grafikkarte belegt hatte:

Qwen3-TTS 1.7BOmniVoice
LizenzApache 2.0Code Apache 2.0, Gewichte CC-BY-NC
wo gerechnetProzessorGrafikkarte
Zeit pro Satz10 bis 26 s (VoiceDesign), 10 bis 19 s (Klon)1,2 bis 1,4 s im laufenden Assistenten, 32 Schritte
Erkennung der Proben durch Parakeetpraktisch fehlerfreifehlerfrei bis auf Einzelfälle

Weil das kein Vergleich unter gleichen Bedingungen war, habe ich den Qwen-Klon später auch auf der Grafikkarte gemessen, mit freier Karte, ohne lokales Modell daneben:

Qwen3-TTS-Klon auf der RTX 4080 SuperRechenzeit im Verhältnis zur SatzlängeGrafikspeicher
1.7B, bf162,4- bis 2,7-mal4,3 GB
1.7B, fp162,5- bis 2,9-mal4,4 GB
0.6B, bf162,4- bis 2,5-mal2,4 GB

Ein Satz von 3 Sekunden brauchte also auch auf der Karte 7 bis 9 Sekunden. Das kleinere Modell war kaum schneller, und die Karte war nur zu gut einem Drittel ausgelastet. Die Bremse ist bei mir nicht die Rechenleistung, sondern die schrittweise Erzeugung in Python, ohne flash-attention unter Windows. Für Echtzeit im Gespräch reicht Qwen damit nicht, für Videos schon. OmniVoice ist in meinem Aufbau deutlich schneller, weil es einen ganzen Satz auf einmal erzeugt.

Zwei Erfahrungen aus den Stimmtests. Klonen macht die Stimme gleichbleibend, aber flacher. Eine per Beschreibung gestaltete Stimme klingt lebendiger, schwankt aber leicht. Und eine lebendigere Stimme ist im Alltag nicht automatisch besser: Ich hatte SpaceTrotter für einen Nachmittag auf eine ausdrucksstärkere Klon-Stimme umgestellt. Bei einem echten Briefing saßen Betonungen falsch, und künstliche Atemgeräusche kamen an unpassenden Stellen. Ich bin zur eher monotonen alten Stimme zurückgekehrt. Wenn ein Klon das Satzende verschluckt, half bei mir, die erzeugte Dauer um 12 % zu verlängern und hinten eine kurze Pause anzuhängen.

Zur KI-Nutzung: Die Stimme, mit der sich SpaceTrotter im Video vorstellt, ist KI-erzeugt, und zwar mit Qwen3-TTS, eben weil das Video zu Mackedanz Lab gehört. Zu Hause spricht er mit OmniVoice. Den Code von SpaceTrotter hat Claude Code nach meinen Vorgaben geschrieben, und diese Anleitung habe ich mit Claude aus meinen Projektnotizen zusammengestellt.

Wenn du Hilfe beim Aufbau willst

Die Anleitung soll reichen, um SpaceTrotter nachzubauen. Wenn du an einer Stelle festhängst oder den Aufbau lieber mit jemandem zusammen angehst, der ihn schon einmal durchgezogen hat, kann ich dir dabei helfen. Schreib mir einfach an kontakt@lab.mackedanz.net.