Lokale KI

Claude Code antwortet jetzt laut

Claude Code kann bereits zuhören. Ihm das Sprechen beizubringen, machte aus einer Idee in einer Zeile eine Reihe kleiner, echter Entscheidungen: welche Stimme, wie man sie gattert, wie man sie unterbricht und wie man sie ausliefert, ohne das Modell eines anderen mitzuliefern.

Ramazan Yavuz
Ramazan Yavuz ·

Claude Code hat einen /voice-Modus. Man hält eine Taste, spricht, es transkribiert. Sprache hinein. Was es nicht hat, ist Sprache hinaus: die Antworten kommen weiterhin als Text an, den man lesen muss. Diese Asymmetrie ist die ganze Idee hinter claude-can-speak. Das Modell sollte zurücksprechen können, aus eigener Initiative bei den Dingen, die es wert sind, gehört zu werden, oder bei jeder Antwort, wenn man eine durchgehende Erzählung will. Einschalten und es wird ein Gespräch in beide Richtungen. Ausschalten und es ist wieder still. Der Rest der Arbeit bestand darin, diesen Satz wahr zu machen, ohne dass etwas angeklebt wirkt, und eine der Entscheidungen, die ich im ersten Anlauf falsch traf, war die Annahme, der vorhandene /voice-Schalter könne beide Richtungen tragen. Das konnte er nicht, und das ist der lehrreichste Teil der Geschichte.


Die erste echte Entscheidung war die Stimme, und sie war weniger offensichtlich, als ich erwartet hatte. Mein Instinkt war, zur natürlichsten, modernsten neuronalen Text-to-Speech zu greifen, die ich lokal laufen lassen konnte. Davon gibt es mehrere. Ehrlich formuliert geht es nicht um "KI-Stimme gegen Roboterstimme", denn alle ernstzunehmenden lokalen Optionen sind heute neuronal. Die eigentliche Achse ist Natürlichkeit gegen Latenz auf einer CPU, und das ist hier wichtig, weil dieses Ding nach jeder Antwort spricht. Ein Modell, das vier Sekunden pro Satz braucht, ist unbrauchbar, wenn es ständig feuert.

Ich habe mir drei angesehen. Kokoro, ein Modell mit 82 Millionen Parametern, ist das natürlichste der kleinen und läuft schnell auf einer CPU. Piper ist eine Spur weniger natürlich, dafür mehrsprachig über gut dreißig Sprachen und sehr schnell. XTTS Version zwei ist das natürlichste von allen und kann Stimmen klonen, braucht aber auf einer CPU mehrere Sekunden pro Antwort, und seine Lizenz ist nicht-kommerziell, was ein Problem für etwas ist, das ich offen veröffentlichen will. XTTS verlor in beiden Punkten.

Es blieb eine echte Spannung zwischen Kokoro und Piper, und ich machte den Fehler anzunehmen, Kokoro könne die Sprachen abdecken, die ich wollte. Kann es nicht. Ich wollte Englisch plus Deutsch, idealerweise Türkisch, damit sich dasselbe Setup für künftige Projekte wiederverwenden ließe. Kokoros eigene Stimmliste entscheidet die Sache: amerikanisches und britisches Englisch, Japanisch, Mandarin, Spanisch, Französisch, Hindi, Italienisch, brasilianisches Portugiesisch. Kein Deutsch. Kein Türkisch. Das vor dem Bauen zu prüfen, nicht danach, bewahrte mich davor, die falsche Engine auszuliefern.


Also baute ich beide, eine als Standard und die andere einen Schalter entfernt. Piper ist der mehrsprachige Pfad: Deutsch über die Thorsten-Stimme, Türkisch über die dfki-Stimme, und der Rest seines Katalogs per Namen verfügbar. Kokoro ist der Standard, denn für den Hauptanwendungsfall, englische Sprachausgabe, klingt es schlicht besser. Das habe ich per Ohr bestätigt, nicht per Datenblatt. Ich synthetisierte denselben Satz durch jede infrage kommende weibliche Stimme, die Piper-Optionen und die Kokoro-Optionen, und spielte sie direkt nacheinander ab. Kokoros af_heart gewann klar. Ein Hörtest ist mehr wert als ein Benchmark, wenn das Maß lautet "klingt das wie ein Mensch".

Beide Engines laufen in einem einzigen Docker-Container, sodass sie nie die Python-Umgebung des Hosts berühren. Der Container ist persistent: er startet einmal und bleibt warm, sodass die Kosten für das Importieren der Modell-Laufzeit einmal bezahlt werden, nicht bei jeder Antwort. Der Host reicht ihm Text, er reicht eine WAV zurück, und der Host spielt sie ab. Nur Audio überquert die Grenze. Warm kommt ein Piper-Satz in etwa einer Sekunde zurück und ein Kokoro-Satz in etwas über zwei, beides in Ordnung für etwas, das spricht, während man weiterarbeitet.


Die Verdrahtung in Claude Code ist ein Stop-Hook. Wenn eine Antwort fertig ist, führt Claude Code den Hook aus und übergibt ihm die fertige Nachricht. Der Hook prüft, ob der Feuerwehrschlauch an ist, und falls nicht, beendet er sich still und nichts spricht. Ist er an, entfernt er Markdown und verwirft eingerahmte Code-Blöcke, denn einen Code-Block laut vorzulesen ist nicht anzuhören, und schickt dann den bereinigten Text an den Container und spielt das Ergebnis ab. Der Hook kehrt sofort zurück und das Sprechen geschieht in einem abgekoppelten Hintergrundprozess, sodass er deinen nächsten Zug nie verzögert. Was diese Prüfung "ist der Feuerwehrschlauch an" liest, entpuppte sich als die eine Entscheidung, die ich zweimal treffen musste, und darauf komme ich zurück.

Eine Feinheit hat mich hier erwischt. Die WAV-Encoder müssen rückwärts springen, um den Datei-Header zu schreiben, und eine Pipe auf die Standardausgabe ist nicht spulbar, also produzierte die erste Version eine abgeschnittene, nicht abspielbare Datei. Die Lösung ist, in eine temporäre Datei innerhalb des Containers zu synthetisieren und diese dann hinauszustreamen. Im Nachhinein offensichtlich, unsichtbar, bis man darauf stößt.


Jede Antwort laut vorzulesen ist ein grobes Werkzeug, und mittendrin wurde mir klar, dass es nicht der einzige Modus sein sollte. Also gibt es einen zweiten: eine Claude-Code-Skill namens speak, die dem Modell eine bewusste "sag das laut"-Fähigkeit gibt. Statt alles zu erzählen, kann Claude gezielt nur das aussprechen, was hörenswert ist: ein gesprochenes "der Build ist fertig und die Tests sind durch", wenn man weggegangen ist, ein Hinweis, dass ein Deploy bestätigt werden muss, ein kurzer Zuruf, um den man gebeten hat. Die Beschreibung der Skill sagt dem Modell, wann es sie nutzen soll und, wichtiger noch, wann nicht: nie für Routine-Antworten, nie für Code oder Dateipfade, nur für Dinge, die es wirklich wert sind, die Ohren zu unterbrechen. Die beiden Modi sind unabhängig. Man kann den Feuerwehrschlauch laufen lassen, die bewusste Skill, beide oder keinen.

Nach jeder Antwort zu sprechen verlangt auch eine Möglichkeit zu stoppen. Eine lange Antwort, die man nicht zu Ende hören will, ist schlimmer als gar kein Ton. Es gibt drei Wege zu unterbrechen: einen Stop-Befehl ausführen, oder einfach die nächste Nachricht senden, die ein zweiter Hook als Signal nimmt, um die vorherige Antwort verstummen zu lassen, oder einfach eine neue Antwort die alte ablösen lassen. Der Mechanismus darunter erfasst die Prozessgruppe des Sprechens in dem Moment, in dem es startet, noch bevor die Synthese fertig ist, sodass eine Unterbrechung greift, ob das Modell noch synthetisiert oder schon mitten im Satz ist. Diese Reihenfolge richtig hinzubekommen war der Unterschied zwischen einem Stop-Knopf, der funktioniert, und einem, der nur manchmal funktioniert.


Die letzte Entscheidung betraf, was ausgeliefert wird, und sie entpuppte sich als verkappte Lizenzentscheidung. Der saubere Instinkt war: die Modelle gar nicht erst bündeln. Pipers Stimmen tragen einen Flickenteppich aus Lizenzen je Stimme, sein Phonemizer ist GPL, und die Bedingungen von Kokoros Stimmpaket sind nicht ausbuchstabiert. Diesen Mix innerhalb meines eigenen MIT-Pakets weiterzuverteilen wäre ein Schlamassel. Sie nicht mitzuliefern ist zugleich einfacher und korrekter: das Paket enthält nur meinen eigenen Code, und jedes Modell wird beim ersten Gebrauch geladen, direkt aus seiner offiziellen Quelle, unter seiner eigenen Lizenz, in einen lokalen Cache. Ich verteile nichts weiter. Eine kurze Datei mit Drittanbieter-Hinweisen listet jede Engine und jedes Modell mit Lizenz und Herkunft.

Die Distribution folgte derselben Logik, das Ding seiner Natur anzupassen. Mein üblicher Standard ist ein Debian-Paket, aber dies ist eine generische Claude-Code-Erweiterung, kein Linux-Systemwerkzeug, und ein .deb würde es ohne guten Grund auf Debian und Ubuntu einschränken. Es ist überwiegend Claude-Konfiguration je Nutzer plus ein kleines CLI. npm passt dazu weit besser und erreicht macOS, Arch, Fedora und WSL gleichermaßen. Also wird es als npm install -g claude-can-speak ausgeliefert, mit Docker als Laufzeit-Voraussetzung, die der Installer prüft und erklärt, statt als Paketabhängigkeit.


Der lehrreichste Fehler tauchte nach dem Release auf, und es war meine eigene clevere Idee, die zurückbiss. Der ursprüngliche Plan war auf dem Papier elegant: den Feuerwehrschlauch, der alles ausspricht, an Claude Codes eingebautes /voice zu koppeln, sodass ein Schalter beide Richtungen steuern würde, man spricht zu ihm und es spricht zurück. So habe ich es ausgeliefert. Dann schaltete ich /voice aus, und die Antworten wurden weiter vorgelesen. Die Annahme war an zwei Stellen zugleich falsch. Erstens ist /voice Sprache hinein, der Diktier-Schalter, und sein Live-Zustand ist nichts, was ein Hook zuverlässig aus der Einstellungsdatei lesen kann; das Feld, das ich las, bedeutete "Diktat ist konfiguriert", was dauerhaft wahr war. Zweitens feuert ein Hook am Ende einer Antwort bei jeder Antwort, unabhängig vom Voice-Zustand, sodass es gar nichts gab, das es wirklich gattert. Das Feature schien nur deshalb zu funktionieren, weil das Feld, das ich las, zufällig wahr war.

Die Lösung war, aufzuhören clever zu sein und dem Werkzeug seinen eigenen ehrlichen Schalter zu geben. Der Feuerwehrschlauch liest jetzt eine einzige Zustandsdatei, die claude-can-speak on und claude-can-speak off schreiben, standardmäßig aus. Es ist überhaupt nicht mehr an /voice gekoppelt, denn die beiden Dinge waren nie wirklich dasselbe Anliegen: das eine ist, wie man Prompts diktiert, das andere, ob man Antworten vorgelesen haben will. Sie zu vermengen erzeugte einen Schalter, der sich nicht ausschalten ließ. Die Lektion ist eine alte, die ich regelmäßig neu lerne: ein Feature, das davon abhängt, den internen Zustand eines anderen Systems zurückzuentwickeln, ist ein Feature, das nur darauf wartet, kaputtzugehen, und "es läuft gerade auf meiner Maschine" ist nicht dasselbe wie "es funktioniert". Das mit einem Trace zu prüfen, also zu beobachten, ob der Hook überhaupt feuerte, machte aus einem verwirrenden "warum spricht es noch" eine einzeilige Grundursache.


Der ehrliche Schalter behob das Verhalten, ließ aber ein leiseres Problem zurück, das erst auftauchte, als jemand das Werkzeug frisch benutzte. Der Feuerwehrschlauch lebte jetzt hinter einem Terminal-Befehl, claude-can-speak on. Das ist korrekt, aber es ist nicht dort, wo ein Claude-Code-Nutzer hinschaut. Der Instinkt, meiner eingeschlossen, ist, in Claude Code einen Schrägstrich zu tippen und das Menü nach dem Schalter abzusuchen. Dort war nichts, denn das Werkzeug fasst /voice bewusst nicht an, und so war die natürliche Reaktion "ist das überhaupt installiert". Das Feature funktionierte tadellos und fühlte sich kaputt an, einzig weil die Steuerung am falschen Ort war für die Person, die danach greift.

Die Lösung war, den Schalter dorthin zu legen, wo die Hand hingreift. Ein kleiner /ccs-Schrägstrich-Befehl wird jetzt mit dem Werkzeug ausgeliefert und vom Setup installiert, sodass /ccs on, /ccs off und /ccs status von innerhalb Claude Codes funktionieren, direkt neben /voice, wo man sie erwartet hat. Den Terminal-Befehl gibt es weiterhin; der Schrägstrich-Befehl ist nur eine dünne Hülle darüber. Darunter steckte noch eine Feinheit, und es lohnt sich, sie klar auszusprechen, denn es ist die Art von Sache, die sich wie ein Bug liest. Claude Code liest seine Liste der Hooks einmal, beim Start einer Sitzung. Den Feuerwehrschlauch mitten in der Sitzung einzuschalten aktualisiert die Zustandsdatei, die der Hook liest, aber wenn die Sitzung begann, bevor der Hook je registriert war, gibt es in dieser Sitzung keinen Hook, der sie liest. Also startet man beim ersten Mal eine frische Sitzung nach dem Setup, und von da an wird jede Antwort gesprochen. Der eigene Code des Hooks und der An/Aus-Zustand aktualisieren sich beide live; nur die anfängliche Registrierung wird beim Start gelesen. Das in der Dokumentation laut zu benennen war die eigentliche Lösung, denn die Software tat bereits das Richtige und nur die Erklärung fehlte.

Keines dieser Teile ist ein cleverer Algorithmus. Die interessante Arbeit war die Abfolge kleiner, ehrlicher Entscheidungen: Latenz vor roher Natürlichkeit, dann Natürlichkeit vor Breite, sobald die Sprachen geprüft waren, zwei Modi statt einem, eine Unterbrechung, die während der Synthese feuert und nicht nur bei der Wiedergabe, Modelle, die geladen statt ausgeliefert werden, npm statt eines Debian-Pakets, ein expliziter An/Aus-Schalter statt eines cleveren, der von einem anderen Feature geborgt ist, und ein Schrägstrich-Befehl, der diesen Schalter dorthin legt, wo die Hand hingreift. Jede davon kam daher, eine echte Beschränkung ernst zu nehmen, nicht aus einem cleveren Trick. Das Ergebnis ist standardmäßig still, und wenn man es einschaltet, antwortet Claude Code laut, aus eigener Initiative über die Skill oder bei jeder Antwort über den Feuerwehrschlauch. Das ist das ganze Feature, und es sich einfach anfühlen zu lassen, brauchte jede einzelne dieser Entscheidungen, einschließlich der einen, die ich zweimal treffen musste.