Angefangen hat es mit einem Telefon. Für einen kleinen Betrieb hatte ich einen KI-Empfang gebaut, der Anrufe annimmt, Fragen beantwortet und Termine vorschlägt. Die Spracherkennung (Parakeet), die Stimme (ein geklontes Qwen3-TTS-Modell) und das Gehirn (Claude über Claude Code) laufen auf meinem Mac, nicht in einem Cloud-Dienst für Sprache. Irgendwann kam die naheliegende Frage: Warum nicht dieselbe Technik für mich selbst, auf dieser Website?
Die Idee
Ich wollte keinen aufdringlichen Chat-Popup, sondern etwas Unauffälliges: ein kleines rundes Symbol unten rechts auf jeder Seite, in der Sprache der Seite (DE, EN, IT). Ein Klick öffnet ein Fenster mit einem animierten Porträt von mir, das atmet, blinzelt und beim Sprechen den Mund bewegt. Der Assistent beantwortet Fragen zu mir und zum Inhalt der Seite, und wer möchte, kann ihn direkt im Browser anrufen. Er sagt von Anfang an, dass er eine KI ist, und er erfindet nichts: Was nicht auf der Seite steht, weiß er nicht, und dann verweist er auf das Kontaktformular.
So funktioniert es
Browser (Widget)
│ wss://daniele-michelin.com/assistent/ws
▼
nginx auf dem Server ──► mein Mac zu Hause
├─ Spracherkennung + Stimme (lokal)
└─ Gehirn (Claude über Claude Code)
▲
└── Einstellungen: der Mac holt sie jede Minute von der Website (Token)
Der Browser spricht nie direkt mit meinem Mac. Er redet mit meiner eigenen Domain, und nginx reicht die Verbindung weiter:
location = /assistent/ws {
proxy_pass http://MEIN-MAC:5070/ospite/ws$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Dadurch bleibt die Content-Security-Policy der Seite streng (connect-src 'self'), und die Adresse meines Zuhauses sieht kein Besucher.
Alle Einstellungen leben in einer geschützten Admin-Seite auf der Website: Widget, Tonfall, Wissen, Stimme, Limits, Termine, Mail. Der Mac holt sie jede Minute mit einem Token ab. Ich wollte keinen Kanal, über den die Website etwas auf meinem Rechner ändern kann; der Mac fragt, nicht umgekehrt.
Wenn der Mac aus ist
Ein Mac zu Hause ist kein Rechenzentrum. Ist er aus oder belegt, schaltet das Widget auf reinen Text-Chat um, den die Website selbst beantwortet. Dahinter steckt bewusst kein KI-Modell und kein Drittanbieter: Die Antworten sind aus den öffentlichen Inhalten der Seite geschrieben und werden über Stichwörter ausgewählt. Das ist weniger flexibel, aber ehrlich und ohne Datenabfluss. Eigene Frage-Antwort-Paare lege ich im Admin an.
Termine und Nachrichten
Der Assistent kann ein Gespräch mit mir vereinbaren. Die freien Zeiten ergeben sich aus meinen Zeitfenstern, abzüglich der Termine, die schon gebucht sind, und der Einträge in meinem Nextcloud-Kalender (per CalDAV, nur lesend für „belegt“; gebuchte Termine schreibe ich dort hinein). Dazu kommen ein ICS-Feed zum Abonnieren und Bestätigungs-Mails. Nachrichten für mich laufen genau wie beim Kontaktformular: gleicher Empfänger, gleiches Mail-Design, gleicher Versand.
Hier habe ich einen Fehler gefunden, der mich beschäftigt hat. Beim Test sagte der Assistent „Termin bestätigt“, obwohl nichts gebucht war: Das Modell hatte die Aktion nicht ausgelöst und fand trotzdem die passenden Worte. Seitdem entscheidet das System, nicht das Modell. Eine Buchung wartet in einem Zwischenzustand, das System spricht die Zusammenfassung selbst, und erst nach einem ausdrücklichen „Ja“ wird gebucht. Den Ausgang meldet ebenfalls das System: „Erledigt“, „Der Termin ist gerade vergeben worden“ oder ein Fehler. Man sollte einem Sprachmodell nie die Meldung überlassen, ob eine Aktion geklappt hat.
Kleinigkeiten, die den Unterschied machen
- Sagt 30 Sekunden niemand etwas, legt der Assistent auf und gibt die Leitung frei. Die Chat-Funktion bleibt: Die nächste Frage verbindet automatisch neu, ohne erneute Begrüßung und mit dem bisherigen Verlauf als Kontext.
- Es spricht immer nur eine Person gleichzeitig mit dem Sprachmodell; der Rest sieht eine freundliche „gleich wieder“-Meldung oder bekommt den Text-Modus.
- Bei der Stimme gilt: Geklont wird nur, wer zugestimmt hat. Der Assistent spricht jetzt mit meiner eigenen geklonten Stimme (eine 16-sekündige deutsche Aufnahme, die auch für Italienisch und Englisch funktioniert), und ich habe sie selbst aufgenommen.
Datenschutz
Der Inhalt der Gespräche wird nicht gespeichert; in den Protokollen stehen höchstens Uhrzeit und Länge einer Nachricht. Das Audio verlässt meinen Mac nicht. Der erkannte Text geht heute an das Sprachmodell (Claude), das ist der Preis für die Qualität. Die IP-Adresse eines Besuchers liegt nur im Arbeitsspeicher, für ein Tageslimit. Termin-Daten (Name, Mail, Anliegen, Zeit) liegen in einer nicht öffentlichen Datei und werden 90 Tage nach dem Termin automatisch gelöscht. Auf der Datenschutz-Seite erscheint der Abschnitt zum Assistenten nur, wenn die Funktion eingeschaltet ist, und die Absätze zu Terminen und Nachrichten nur, wenn sie aktiv sind.
Was schiefging
- Mikrofon geht, Ton nicht. Ich hatte den Audio-Kontext erst nach einer Netzwerkabfrage angelegt. Safari und manchmal auch Chrome lassen einen so erzeugten Kontext „suspendiert“. Das Mikrofon funktionierte, der Assistent verstand mich, aber ich hörte nichts. Er muss synchron im Klick-Handler entstehen.
- Ports 5060 und 5061. Das sind SIP-Ports, und
fetchsowie Browser lehnen sie mit „bad port“ ab. Ich hatte sie nur gewählt, weil sie frei waren. - Cache. Mein
main.jsist ein Jahr lang unveränderlich gecacht. Nach der Änderung sah ich den Assistenten als Einziger nicht. Jetzt hängt die Version des Widgets an einer kleinen Konfigurationsdatei, die bei jedem Seitenaufruf frisch gelesen wird. - Lizenz. Das Sprachsystem, aus dem ich gestartet bin, ist ein Fork eines Projekts ohne Lizenz. Ich darf es deshalb nicht veröffentlichen, und mein Fork bleibt privat.
Der Code ist offen
Aus diesem Grund enthält das öffentliche Repository nur meinen eigenen Code: das Widget, die PHP-Endpunkte und die Admin-Seite, die Konfiguration für nginx, die Logik der Engine (Limits, der vom System gesteuerte Buchungsablauf) und eine kleine reine Text-Referenz-Engine mit austauschbarem Gehirn, standardmäßig ein lokales Ollama. Die Sprach-Pipeline selbst (Erkennung und geklonte Stimme) ist nicht enthalten; das Protokoll, das sie spricht, ist dokumentiert, sodass sich jede Sprach-Software anbinden lässt. Es steht unter MIT-Lizenz, mit englischer Dokumentation, Screenshots, einem Architekturdiagramm und einer Ein-Befehl-Demo: github.com/Mikweb2025-design/ai-site-assistant.
Was ich noch erreichen will: das Gehirn lokal
Heute geht der erkannte Text an Claude. Mein Ziel ist, dass gar nichts mehr den Mac verlässt: ein Sprachmodell, das nur lokal läuft. Ehrlicher Stand: Kleine lokale Modelle waren in meinen früheren Tests für ein Gespräch in Telefon-Geschwindigkeit zu langsam und zu schwach. Jetzt will ich neuere kleine Modelle auf Apple Silicon (MLX) ausprobieren und das Gehirn hinter einer OpenAI-kompatiblen Schnittstelle austauschbar machen. Im Repository steht eine Ollama-/OpenAI-kompatible Option deshalb auf der Roadmap. Wenn ich ein Modell finde, das schnell genug antwortet und bei den Fakten bleibt, schreibe ich darüber.
Eine erste Runde habe ich schon mit der Referenz-Engine und dem echten Text dieser Seite als Wissen gemacht. Ein Modell mit 2 Milliarden Parametern antwortete aufgewärmt in 2 bis 6 Sekunden, beantwortete eine deutsche Frage richtig und lehnte eine themenfremde korrekt ab. Die 4B- und 12B-Modelle, die ich installiert hatte, brauchten mit dem langen Prompt über 30 Sekunden pro Antwort, viel zu langsam. Die Richtung ist also klar: kürzere Prompts (nur der passende Wissensausschnitt statt der ganzen Seite), Streaming Satz für Satz und ein winziger Klassifizierer vorweg für Begrüßungen. Die Zahlen und das Skript, um sie auf dem eigenen Rechner zu wiederholen, liegen im Repository (docs/roadmap.md).