astra-vision/README.md
Jeuner 6efb67f1a6 docs: document module separation, tool overview, and RSS extensibility
Ergänzt die README um:
- Architektur-Abschnitt mit dem tatsächlich verifizierten Import-Graphen
  (comfyui.py/rss.py/articles.py/documents.py/feeds.py sind gegenseitig
  unabhängig, tools.py ist der einzige Integrationspunkt, triggers.py
  ruft Handler per context.tools-Introspektion ohne tools.py zu
  importieren)
- Werkzeuge-Überblickstabelle (was, wodurch ausgelöst, gemessene
  Zuverlässigkeit)
- Kurzanleitung, wie man RSS-Feeds/Themen in feeds.py ergänzt
- Sicherheitshinweis: anders als die Basisversion macht dieser Fork
  bewusst ausgehende Verbindungen (ComfyUI, RSS-Feeds, beliebige
  Artikel-URLs über read_article — auch potenziell modellvorgeschlagene,
  da die URL nicht gegen eine Allowlist geprüft wird)

NICHT gepusht — weiterhin nur lokal, wie angewiesen.

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

208 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# astra-vision
**Fork von [astra-local-voice](https://github.com/Jeuners/astra-local-voice).**
Gleicher lokaler Sprachagent, plus Bilderzeugung (ComfyUI) und Bild/PDF-Upload
mit Vision (siehe unten). Die schlanke Basisversion ohne diese Werkzeuge lebt
im Hauptrepo — dorthin zurück, falls du nur den reinen Sprachagenten willst.
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.
## Architektur
Jede Fähigkeit ist ein eigenständiges, gegenseitig unabhängiges Modul —
verifiziert am Import-Graphen, nicht nur behauptet:
```
astra/comfyui.py — nur httpx, kein astra-Import
astra/rss.py — nur feedparser+httpx, importiert astra.feeds (Daten)
astra/articles.py — nur httpx+trafilatura, kein astra-Import
astra/documents.py — nur pypdf, kein astra-Import
astra/feeds.py — reine Daten, kein einziger Import
```
Keines dieser fünf Module kennt Pipecat, LLM-Tools oder ein anderes der
fünf. **`astra/tools.py`** ist die einzige Stelle, die sie zu
LLM-aufrufbaren `FunctionSchema`s verdrahtet (Beschreibung + Handler +
UI-Benachrichtigungen). **`astra/triggers.py`** ruft dieselben Handler
bei Bedarf direkt auf — per Introspektion über `context.tools`, ohne
`tools.py` zu importieren — wenn ein einfacher Keyword-Treffer
zuverlässiger ist als die LLM-Entscheidung (siehe unten).
### Werkzeuge im Überblick
| Werkzeug | Macht | Ausgelöst durch | Live gemessene Zuverlässigkeit |
|---|---|---|---|
| `generate_image` | Bild via ComfyUI erzeugen | LLM-Entscheidung | ~100 % |
| `read_news` | Schlagzeilen aus konfiguriertem RSS-Thema | Keyword-Trigger (`triggers.py`) | ~100 % (Trigger) / ~1520 % (reine LLM-Entscheidung) |
| `read_article` | Vollen Artikeltext zu einer Nummer/URL laden | Keyword-Trigger + LLM-Fallback | Trigger 2/2 live; LLM-Fallback ~65 % |
## Bilder erzeugen und Dokumente lesen
Zwei zusätzliche, sauber getrennte Fähigkeiten, unabhängig von STT/LLM/TTS:
- **`astra/comfyui.py`** — reiner async HTTP-Client für einen lokalen
[ComfyUI](https://github.com/comfyanonymous/ComfyUI)-Server
(`z-image-turbo`-Workflow). Kennt nichts von Pipecat.
- **`astra/documents.py`** — PDF-Textextraktion (`pypdf`), keine
Netzwerkzugriffe.
- **`astra/tools.py`** — verdrahtet `generate_image` als natives
Ollama-Tool. Qwen 3.5 entscheidet selbst, wann es aufgerufen wird
(`ollama show qwen3.5` listet `tools` als unterstützte Fähigkeit); das
generierte Bild landet im laufenden Gespräch als `/api/media/<id>` und
wird per WebRTC-Datenkanal ans UI gemeldet.
Bilder (PNG/JPEG/WebP) und PDFs lassen sich während eines laufenden
Gesprächs über den Button „Bild oder PDF hinzufügen“ hochladen
(`POST /api/upload`). Ein PDF wird als Text in den Gesprächskontext
eingefügt, ein Bild als Base64 mit Qwens nativer Vision-Fähigkeit — beides
nur für die Dauer der Session, nichts wird auf Disk geschrieben.
## Nachrichten aus deutschen RSS-Feeds
- **`astra/feeds.py`** — kuratierte Feed-Liste, nach Thema gruppiert
(`tech`, `nachrichten`, `wirtschaft`, `hilden` — lokal für Hilden via
RP ONLINE). Reine Daten, editierbar: neues Thema = neuer Dict-Key mit
`{"name": ..., "url": ...}`-Tupeln, neuer Feed = eine Zeile in einem
bestehenden Thema. `TOPICS` wird automatisch aus den Keys abgeleitet,
taucht dadurch sofort im `read_news`-Schema (`enum`) und in
`triggers.py`'s Themen-Erkennung auf — keine weitere Codeänderung nötig,
solange der neue Themenname selbsterklärend ist (sonst zusätzlich einen
Alias in `_TOPIC_ALIASES` in `astra/triggers.py` ergänzen).
- **`astra/rss.py`** — async Feed-Client (`feedparser`), holt konfigurierte
Feeds eines Themas parallel ab, überspringt nicht erreichbare Feeds statt
komplett zu scheitern.
- Tool `read_news` in `astra/tools.py`: Astra ruft es auf, wenn nach
aktuellen Nachrichten gefragt wird, und fasst die Schlagzeilen mündlich
zusammen statt sie roh vorzulesen.
- **`astra/articles.py`** — lädt eine Artikel-URL und extrahiert den reinen
Fließtext (`trafilatura`), ohne Navigation/Werbung/Boilerplate.
- Tool `read_article` in `astra/tools.py`: Astra ruft es auf, wenn der
Nutzer zu einer schon genannten Schlagzeile mehr wissen will, und liest
den vollen Artikeltext statt nur der RSS-Kurzbeschreibung.
Tool-Verhalten steht bewusst ausschließlich in der jeweiligen
`FunctionSchema.description` (siehe `astra/tools.py`), nicht im
`SYSTEM_PROMPT` — eine Quelle der Wahrheit pro Werkzeug statt duplizierter
Regeln in einem wachsenden globalen Prompt. Gemessener Preis davon: mit
drei gleichzeitig verfügbaren Tools (Bild + Nachrichten + Artikel) ruft
das lokale 9,7B-Modell `read_news` nur noch in ca. 1520 % der Fälle
tatsächlich auf (vorher mit zwei Tools ca. 2050 %, mit Tool-Regeln
zusätzlich im System-Prompt ca. 6075 %) und erfindet sonst Schlagzeilen.
`read_article` ist bei einer konkreten Nachfrage zu einer schon genannten
Schlagzeile brauchbarer (~65 %, vermutlich weil der Kontext dort weniger
mehrdeutig ist). Bildgenerierung bleibt bei ~100 % zuverlässig. Bekannte
Grenze eines kleinen lokalen Modells bei Tool-Konkurrenz, kein Bug — die
saubere Trennung war eine bewusste Architekturentscheidung.
**`astra/triggers.py`** umgeht diese Grenze gezielt für `read_news`: ein
einfacher Keyword-Check (`nachrichten`/`news` + optional ein Themen-Alias
wie `hilden`/`technik`/`wirtschaft`) im transkribierten Nutzertext ruft
den `read_news`-Handler direkt auf — derselbe Handler, dieselbe UI,
nur ohne die unzuverlässige LLM-Entscheidung dazwischen. Das Ergebnis
landet als Tool-Roundtrip im Kontext, damit die nächste LLM-Antwort es
kennt. Live getestet: 2/2 zuverlässig, wo die reine LLM-Entscheidung nur
~1520 % erreichte.
Schlagzeilen werden nummeriert angezeigt (1., 2., 3. …), und derselbe
Mechanismus kennt einen zweiten Trigger: `detect_article_reference()`
erkennt Formulierungen wie "Artikel 2", "Artikel Nummer drei" oder
"zweiter Artikel" im Nutzertext, löst die Nummer gegen die zuletzt
gezeigte Liste auf und ruft `read_article` direkt mit dem passenden Link
auf — ganz ohne dass das Modell selbst den richtigen Link kennen oder
sich für das Werkzeug entscheiden muss. Live getestet über einen echten
Zwei-Turn-Dialog ("News Hilden" → "Hole mir Detail zu Artikel zwei"):
korrekt aufgelöst, echter Artikeltext abgerufen.
`generate_image` bleibt bewusst ein reines LLM-Tool, weil ein Bildwunsch
zu variabel für ein Keyword-Muster ist und ohnehin zuverlässig
funktioniert.
## 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` |
| `ASTRA_COMFYUI_URL` | `http://100.125.107.123:8000` |
## 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
- Anders als die Basisversion macht dieser Fork bewusst ausgehende
Verbindungen: zu ComfyUI (`ASTRA_COMFYUI_URL`), zu den konfigurierten
RSS-Feeds (`astra/feeds.py`) und zu beliebigen Artikel-URLs, die
`read_article` aufruft (aus einem `read_news`-Ergebnis oder — bei
LLM-Tool-Aufruf statt Trigger — theoretisch vom Modell selbst
vorgeschlagen). Alle drei sind auf ihre jeweilige eigene HTTP-Anfrage
begrenzt, keins davon hat sonstigen Datei- oder Netzwerkzugriff.
---
Erstellt von Astra (Grunddeploy), gecheckt, dokumentiert und bewertet durch Claude.