astra-local-voice/README.md
Jeuner 991f0d1eb5 feat: add voice picker UI and optional Tailnet exposure
Stimmen werden jetzt lazy pro Name geladen und gecacht statt einer
fest verdrahteten Default-Stimme; das UI bekommt ein Dropdown mit
allen 26 deutschen Pocket-TTS-Stimmen (/api/voices), Auswahl wird im
Browser gemerkt und ist während eines laufenden Gesprächs gesperrt.

Zusätzlich ein optionaler ASTRA_TAILNET_HOST, damit die App über
`tailscale serve` auch von einem anderen Gerät im selben Tailnet
erreichbar ist, ohne die Loopback-only-Härtung für alle anderen
Hosts aufzuweichen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
2026-09-07 15:15:13 +02:00

85 lines
2.9 KiB
Markdown

# astra
Ein lokaler deutscher Sprachagent für Apple Silicon. Kein Cloud-Anruf, kein
Tracking, kein gespeichertes Audio — Spracherkennung, Sprachmodell und
Sprachausgabe laufen alle auf deinem Mac.
## Wie es funktioniert
Drei lokale Modelle, verbunden über eine [Pipecat](https://github.com/pipecat-ai/pipecat)-Pipeline:
| Stufe | Modell | Wo |
|---|---|---|
| Spracherkennung | Nemotron ASR (streaming) | MLX, on-device |
| Sprachmodell | Qwen 3.5 | über natives Ollama `/api/chat` |
| Sprachausgabe | Pocket TTS, 26 deutsche Stimmen wählbar | MLX, on-device |
Der Browser spricht per WebRTC direkt mit einem FastAPI-Server auf
`localhost:7860`. Der Server ist bewusst nur lokal erreichbar: Host- und
Origin-Prüfung auf jedem Request, strikte Content-Security-Policy, keine
offenen Ports nach außen.
Die Stimme lässt sich im UI per Dropdown wählen (`/api/voices` listet alle
26, Auswahl wird im Browser gemerkt). Jede Stimme wird beim ersten Gebrauch
lazy geladen und danach für die Laufzeit des Prozesses gecacht.
## Starten
```bash
uv sync
uv run python -m astra.prepare # lädt & prüft alle drei Modelle einmalig
uv run python -m astra.server # startet auf http://localhost:7860
```
Ollama muss separat laufen (`ollama serve`) und `qwen3.5:latest` muss
gezogen sein. Die Seite öffnen, Mikrofon erlauben, sprechen.
## Konfiguration
Über Umgebungsvariablen, siehe `astra/core.py::Settings`:
| Variable | Default |
|---|---|
| `ASTRA_MODEL` | `qwen3.5:latest` |
| `ASTRA_OLLAMA_URL` | `http://127.0.0.1:11434` |
| `ASTRA_STT_MODEL` | `mlx-community/nemotron-3.5-asr-streaming-0.6b-8bit` |
| `ASTRA_TTS_LANGUAGE` | `german` |
| `ASTRA_VOICE` | `alba` |
| `ASTRA_PORT` | `7860` |
| `ASTRA_TAILNET_HOST` | *(leer)* — z. B. `minim4-1.tail0f2cb2.ts.net` |
## Tests
```bash
uv run pytest
uv run ruff check .
```
## Im Tailnet freigeben
Standardmäßig nur `localhost` erreichbar. Für Zugriff von einem anderen
Gerät im selben Tailscale-Netz:
```bash
tailscale serve --bg 7860
ASTRA_TAILNET_HOST="$(tailscale status --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))')" \
uv run python -m astra.server
```
Danach ist die Seite unter `https://<tailnet-host>/` erreichbar (Port 443,
implizit — Tailscale terminiert TLS und proxyt auf 7860). Host- und
Origin-Prüfung lassen dann zusätzlich diesen einen Hostnamen durch.
## Sicherheit
- Nur `localhost`/`127.0.0.1` (bzw. der optionale Tailnet-Host) erreichbar,
alle anderen Hosts bekommen 403
- POST-Requests werden gegen den erwarteten Origin geprüft
- `think` ist im Ollama-Request hart auf `false` gesetzt — die Pipeline
wirft, falls das Modell trotzdem Denkausgabe liefert
- Kein Audio, keine Transkripte werden auf Disk geschrieben; der
Gesprächsverlauf lebt nur im Speicher der laufenden Session
---
Erstellt von Astra (Grunddeploy), gecheckt, dokumentiert und bewertet durch Claude.