Backup-Mirror von github.com/Jeuners/astra-vision
Find a file
Jeuner f8963ebc79 fix: 400 from Ollama after a tool call, and silent interruptions
Pipecat stores a tool call's assistant message OpenAI-style, with
function.arguments as a JSON string. Ollama's native /api/chat wants an
object there and rejects the follow-up request with 400 ("Value looks
like object, but can't find closing '}' symbol") — reproduced verbatim
against real Ollama, then fixed with astra/core.py::normalize_tool_calls,
applied in build_request. Verified end-to-end with a real browser test
(synthesized speech as the fake mic): "erzeuge ein Bild von einer Katze
im Weltraum" now completes the full round trip — tool call, ComfyUI
image, and Astra's follow-up reply — with no error.

Also: an assistant turn interrupted before producing any text showed
nothing at all in the transcript. astra/server.py's assistant_turn
handler now notifies on interruption even with empty content, so an
early interruption (e.g. mic picking up noise during the slightly
longer vision-augmented responses) is visible instead of silent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
2026-09-07 16:29:48 +02:00
astra fix: 400 from Ollama after a tool call, and silent interruptions 2026-09-07 16:29:48 +02:00
scripts feat: initial commit of astra local voice agent 2026-09-07 13:39:51 +02:00
tests fix: 400 from Ollama after a tool call, and silent interruptions 2026-09-07 16:29:48 +02:00
web fix: show uploaded images inline instead of just the filename 2026-09-07 16:12:23 +02:00
.gitignore feat: initial commit of astra local voice agent 2026-09-07 13:39:51 +02:00
pyproject.toml feat: image generation via ComfyUI and PDF/image upload 2026-09-07 15:56:18 +02:00
README.md feat: image generation via ComfyUI and PDF/image upload 2026-09-07 15:56:18 +02:00
uv.lock feat: image generation via ComfyUI and PDF/image upload 2026-09-07 15:56:18 +02:00

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-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.

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-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.

Starten

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

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:

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.