- Python 86.1%
- HTML 13.9%
| Action | ||
| admin | ||
| display | ||
| llm | ||
| Screen | ||
| server | ||
| Voice | ||
| .gitignore | ||
| chat_mode.py | ||
| main.py | ||
| mcp_config.json | ||
| README.md | ||
| requirements.txt | ||
| test_cancel.py | ||
| test_context_budget.py | ||
| test_context_budget_live.py | ||
| test_notify.py | ||
| test_skills_history.py | ||
| test_triggers.py | ||
| test_view_library.py | ||
OpenVoiceAssistant
Ein lokal laufender, Open-Source-Sprachassistent für Raspberry Pi / PC. Komplett offline-fähig: Wake-Word, Spracherkennung, LLM, TTS und Display laufen lokal – alle sensiblen Daten bleiben auf dem Gerät.
AI Nutzung
Dieser Code ist nahezu vollständig von Qwen3.8 27b generiert, da es sich unter anderem um einen Test der bisherigen Fähigkeiten lokal lauffähiger Modelle handelt.
Was kann der Assistent?
Sprache: Per Wake-Word (OpenWakeWord) ansprechen – versteht Befehle, fragt nach, antwortet per Sprache (TTS) und ist unterbrechbar (Barge-in).
Verstehen & Handeln (Werkzeuge):
- Sandbox – Shell-Befehle, Dateien lesen/schreiben/suchen, APIs aufrufen (Smarthome, Wetter, Züge …)
- Display – Views, interaktive Seiten/Formulare, Spiele, Dashboards, Timer
- Kamera & Vision – USB-Webcam, RTSP, ONVIF/PTZ; multimodal (sieht Bilder)
- Barcode & Produkt-Suche – Barcode/QR scannen, GTIN/EAN in Open Food Facts + retraceable.io suchen (Produktname, Nährwerte, Zutaten, Vegan/Vegetarian-Analyse)
- Gedächtnis – merkt sich Fakten & Vorlieben (lokal, JSON)
- Skills & Routinen – wiederverwendbare Abläufe, die die KI selbst anlegt
- Timer & Planung – Countdown-Timer + geplante Routinen („weck mich um 8 und zeig mir die Nachrichten", auch täglich)
- Push – ntfy-Benachrichtigungen aufs Handy; alles, was als Benachrichtigung auf dem Display erscheint, wird automatisch mitgepusht (mit Priorität: Timer/Alarme = höchste Prio)
- Tandoor – Rezepte, Meal-Plan, Einkaufsliste, Vorrat, Koch-Log
- Vikunja – Task-Management: To-dos, Projekte, Labels, Kommentare
- Wetter – aktuelles Wetter + Vorhersage (OpenWeatherMap)
- Immich – Foto-/Video-Bibliothek: Slideshow, Personensuche, Memories, Stats
- Traccar – GPS-Tracking: Standort, Akku, Heatmaps, Tagesrouten
- MCP – externe MCP-Server anbinden und deren Tools nutzen
Externe Trigger: Webhook (POST /api/webhook) für Person-Erkennung,
neue RSS-Einträge, n8n, Home Assistant … – triggert den Assistenten ohne Wake-Word.
Architektur (Kurz)
Mikrofon → Wake-Word (OpenWakeWord) → ASR (faster-whisper) → LLM (OpenAI-kompatibel)
│ Tool-Loop (Harness)
TTS (OmniVoice / Piper) ◄───────────────────────────────────────────────────┘
Display (Flask, :5050) ◄── Views / Kamera / Bilder ──► Sandbox (OpenTerminal)
Komponenten:
main.py– Einstieg, Voice-Loop, Display-/Webhook-TriggerVoice/– Wake-Word, Aufnahme (Pre-Roll), ASR, TTSllm/– Harness (Tool-Loop), Tools, Skills, Timer, Gedächtnis, Sandbox-Clientdisplay/– Flask-Server, Views, Action-Bridge, Webhookserver/– optionaler Voice-Server: ASR (Whisper) + TTS (OmniVoice) als HTTP-API
ASR (Whisper) und TTS (OmniVoice) können optional per
ASR_SERVER_URL/TTS_SERVER_URLauf einen Voice-Server (python -m server) ausgelagert werden. Ohne diese Vars läuft alles lokal – der Client bleibt vollständig autark.
Setup
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Konfiguration per Env-Vars in .env (Tabelle unten).
Starten
python main.py
Sage dann das Wake-Word (OpenWakeWord, z.B. „Hey Mycroft").
Standard-Verhalten: --save (WAV-Aufnahmen) und --display (Display-Server)
sind standardmäßig an. Deaktivieren geht mit --no-save bzw. --display_off;
--chat (Terminal-Chat statt Voice) schaltet --save automatisch ab.
Voice-Server (optional: ASR / TTS auslagern)
Whisper (ASR) und OmniVoice (TTS) sind die rechenintensiven Bausteine. Du kannst sie optional auf einen (GPU-)Server auslagern – der Client macht dann Wake-Word, Aufnahme, LLM, Display und Audio-Playback und schickt nur das Audio (ASR) bzw. den Text (TTS) zum Server. Beide Bausteine sind unabhängig schaltbar und optional: Ohne Server-Vars läuft alles lokal, genau wie vorher.
Server starten (auf der GPU-Maschine):
python -m server # Endpoints: GET /health · POST /asr · POST /tts
# oder: python server/main.py
Client konfigurieren (in der .env des Clients):
| Env | Default | Bedeutung |
|---|---|---|
ASR_SERVER_URL |
(leer = lokal) | Base-URL des Voice-Servers; gesetzt → Whisper läuft dort |
TTS_SERVER_URL |
(leer = lokal) | Base-URL des Voice-Servers; gesetzt → OmniVoice läuft dort |
VOICE_SERVER_API_KEY |
(auto) | Bearer-Key; wird vom Server automatisch generiert & gespeichert – Client + Server müssen denselben Wert haben |
Server-seitige Env-Vars: VOICE_SERVER_PORT (8099), VOICE_SERVER_HOST (0.0.0.0),
WHISPER_MODEL, ASR_DEVICE (cuda/cpu), ASR_COMPUTE (float16/int8/float32),
ASR_LANGUAGE, ASR_BEAM, TTS_DEVICE, TTS_DTYPE, TTS_LANGUAGE, TTS_INSTRUCT.
API-Key: Der Server generiert beim ersten Start automatisch einen Key und
speichert ihn unter ~/.assistant/voice_server_api_key (stabil über Neustarts).
Beim Start wird er zusammen mit der Client-Konfiguration ausgegeben – einfach
kopieren. Optional: VOICE_SERVER_API_KEY setzen, um den Key selbst zu bestimmen.
Robustheit: Ist der Server nicht erreichbar, fällt der Client automatisch auf lokalen Whisper zurück (falls installiert) bzw. bei TTS auf Piper/espeak – der Client bleibt also autark und funktioniert auch ohne Server.
Admin-Web-Oberfläche (SSO)
Eine separate Admin-Oberfläche zum Beobachten und Steuern des Assistenten –
mit SSO-Login (kein extra Passwort, du nutzt deinen bestehenden
Identity Provider). Läuft auf eigenem Port (Default :5051), sauber vom
Kiosk-Display (:5050) getrennt.
Starten: Läuft automatisch mit python main.py (einziger Start, als
Background-Thread neben Display & Voice). Deaktivieren: python main.py --admin_off.
Fürs Einzeltesten geht auch standalone:
python main.py # startet Voice + Display (:5050) + Admin (:5051)
python -m admin # nur der Admin-Server, standalone
Anmeldung – zwei Wege (OIDC hat Vorrang):
- SSO / OIDC (empfohlen) – Authentik, Keycloak, Zitadel oder jeder OpenID-Provider. Authorization Code + PKCE, ID-Token wird gegen die JWKS des Providers verifiziert. Kein Client-Secret im Browser.
- Lokaler Pin – nur wenn kein OIDC konfiguriert ist. Einmaliger
ADMIN_PINgenügt. Damit ist die Oberfläche sofort nutzbar.
Was kann sie?
- Monitoring: Live-Status (Zustand/aktuelle View), Health, Transcript, Gedächtnis, Skills & Routinen, Timer, Views, Config (Secrets maskiert).
- Steuerung: Befehl an den Assistenten senden, Routine/Skill ausführen, Timer setzen, Push-Benachrichtigung, Views speichern/löschen.
Steuer-Aktionen laufen über den Display-Server (/api/webhook …) – die
einzige Stelle, die den Assistenten angreift. Read-only-Daten (Transcript,
Gedächtnis, Skills, Config) werden lokal gelesen.
Authentik einrichten (Beispiel):
- In Authentik ein Application (OIDC) anlegen, z. B.
ova-admin. - Redirect URI setzen:
http://<host>:5051/auth/callback(muss exakt stimmen). - Scopes:
openid profile email. Client-Typ public (PKCE) →AUTH_CLIENT_SECRETbleibt leer. - In der
.envdes Assistenten:
ADMIN_PORT=5051
AUTH_ISSUER=https://<authentik>/application/o/ # Issuer (endet auf /o/)
AUTH_CLIENT_ID=ova-admin
AUTH_CLIENT_SECRET= # public client → leer
AUTH_REDIRECT_URI=http://<host>:5051/auth/callback
AUTH_ALLOW_EMAILS=du@example.com # nur du (empfohlen)
# AUTH_ALLOW_SUBS=… # alternativ per sub
# ADMIN_PIN= # nur ohne OIDC
Sicherheits-Hinweis:
AUTH_ALLOW_EMAILS/AUTH_ALLOW_SUBSbegrenzen, wer sich einloggen darf – ohne Allowlist darf jeder IdP-User. Für einen Admin-Zugang die Allowlist setzen.
| Env | Default | Bedeutung |
|---|---|---|
ADMIN_HOST / ADMIN_PORT |
0.0.0.0 / 5051 |
Bind-Adresse des Admin-Servers |
AUTH_ISSUER |
(leer) | OIDC-Issuer; gesetzt + AUTH_CLIENT_ID → SSO aktiv |
AUTH_CLIENT_ID |
(leer) | OIDC-Client-ID |
AUTH_CLIENT_SECRET |
(leer) | nur „confidential"-Clients; public+PKCE → leer |
AUTH_REDIRECT_URI |
(abgeleitet) | muss exakt mit der im IdP registrierten stimmen |
AUTH_ALLOW_EMAILS |
(leer = alle) | Komma-Liste erlaubter E-Mails |
AUTH_ALLOW_SUBS |
(leer) | Komma-Liste erlaubter sub-Werte |
AUTH_SCOPE |
openid profile email |
OIDC-Scopes |
ADMIN_PIN |
(leer) | lokaler Fallback-Pin (nur ohne OIDC) |
ADMIN_SECRET |
(auto) | Cookie-Signing-Secret; leer → wird generiert & gespeichert |
ADMIN_SESSION_HOURS |
12 |
Session-Gültigkeit in Stunden |
Log & Debugging
Der Log ist farbig und übersichtlich: Level, Komponente und das
Action-Label (z.B. Tool-Call, Tool-Result, Antwort) werden farbig getrennt, damit man den Flow im Terminal schnell lesen kann – und jeder
Befehl beginnt mit einem farbigen Trenner (📌 Befehl).
python main.py -v # ausführlicher (DEBUG: Runden, Timing, Tokens)
python main.py --log-file ova.log # zusätzlich in Datei loggen (immer DEBUG, ohne Farben)
python main.py --no-color # Farben aus (z.B. wenn die Ausgabe gepiped wird)
Farben werden automatisch deaktiviert, wenn die Ausgabe kein TTY ist oder
NO_COLOR gesetzt ist; FORCE_COLOR=1 erzwingt sie.
Konfiguration (Env-Vars)
Sensible Werte (API-Keys, Kamera-Zugangsdaten) nur in .env, nie im Repo.
| Env | Default | Bedeutung |
|---|---|---|
LLM_BASE_URL |
http://localhost:11434/v1 |
LLM-Endpoint (Ollama / OpenAI-kompatibel) |
LLM_MODEL |
– | Modellname |
LLM_API_KEY |
ollama |
API-Key |
SANDBOX_URL |
http://localhost:3000 |
OpenTerminal-Sandbox |
SANDBOX_API_KEY |
– | Sandbox-Auth |
DISPLAY_URL |
http://localhost:5050 |
Display-Server |
DISPLAY_DIR |
~/.assistant/display |
Bilder für das Display |
CAMERA_DEVICE |
/dev/video0 |
USB-Webcam |
CAMERA_MAX_WIDTH / CAMERA_QUALITY |
768 / 80 |
Vision-Downscale |
WHISPER_MODEL |
base |
Whisper-Modell (tiny/base/small/medium/large-v3) |
ASR_SERVER_URL |
(leer = lokal) | Whisper auf Voice-Server auslagern (Base-URL) |
TTS_SERVER_URL |
(leer = lokal) | OmniVoice auf Voice-Server auslagern (Base-URL) |
VOICE_SERVER_API_KEY |
(leer = offen) | Bearer-Key für den Voice-Server |
PRE_ROLL_SECONDS |
90 |
Dauer des Kontextes/Pre-Roll in s |
VAD_ENERGY_THRESHOLD |
0.01 |
VAD-Lautstärkeschwelle (RMS); zu niedrig → Aufnahme stoppt nicht (läuft auf 30 s), in lauten Umgebungen höher (0.03–0.05) |
TTS_COOLDOWN_S |
2.0 |
Wake-Word-Cooldown nach TTS in s |
ASSISTANT_NAME |
Tess |
Name des Assistenten |
USER_NAME |
(leer) | Name der Person (Personalisierung) |
NTFY_URL / NTFY_TOPIC |
– | Push-Benachrichtigungen (Server + Topic) |
NTFY_USER / NTFY_PASS |
(leer) | Basic-Auth für den ntfy-Server |
MCP_CONFIG |
– | Pfad zur MCP-Config (JSON) |
RETRACEABLE_API_KEY |
(leer) | API-Key für retraceable.io (Zutaten-Analyse, optional) |
RETRACEABLE_API_URL |
https://retraceable.io |
Base-URL für retraceable.io |
TANDOOR_URL |
(leer) | Tandoor Recipes Base-URL |
TANDOOR_API_TOKEN |
(leer) | Tandoor Bearer-Token |
VIKUNJA_URL |
(leer) | Vikunja Base-URL |
VIKUNJA_API_TOKEN |
(leer) | Vikunja Bearer-Token |
IMMICH_URL |
(leer) | Immich Base-URL |
IMMICH_API_KEY |
(leer) | Immich API-Key |
TRACCAR_URL |
https://traccar.jennes.dev |
Traccar Base-URL |
TRACCAR_TOKEN |
(leer) | Traccar API-Token |
TRACCAR_DEVICE |
(leer) | Zu trackendes Gerät (Name oder ID) |
OPENWEATHER_API_KEY |
(leer) | OpenWeatherMap API-Key |
OPENWEATHER_CITY |
(leer) | Default-Stadt für Wetteranfragen |
Skills & Routinen:
| Env | Default | Bedeutung |
|---|---|---|
SKILL_INDEX_IN_PROMPT |
1 |
Skill-Index im System-Prompt an/aus |
SKILL_INDEX_MAX |
15 |
Max. Skills im Index |
Transcript-Log (Backup/Audit, kein Kontext):
| Env | Default | Bedeutung |
|---|---|---|
HISTORY_LOG_ENABLED |
1 |
Log an/aus |
HISTORY_LOG_PATH |
~/.assistant/history.jsonl |
Log-Datei |
HISTORY_LOG_RETENTION_DAYS |
30 |
Einträge älter als N Tage werden entfernt |
Webhook (externe Trigger):
| Env | Default | Bedeutung |
|---|---|---|
WEBHOOK_TOKEN |
(leer) | Freigabe-Token (Header X-Webhook-Token oder ?token=) |
WEBHOOK_AUTH |
1 |
Fail-closed: ohne gültiges Token → 401/403 |
Webhook nutzen
Externe Ereignisse triggern den Assistenten direkt, ohne Wake-Word:
# Freier Befehl (z.B. aus einer Person-Erkennung):
curl -X POST http://<host>:5050/api/webhook \
-H "X-Webhook-Token: $WEBHOOK_TOKEN" -H "Content-Type: application/json" \
-d '{"command": "Person erkannt: Paketbote", "source": "kamera"}'
# Oder eine Routine (Skill) mit Parametern ausführen:
curl -X POST http://<host>:5050/api/webhook \
-H "X-Webhook-Token: $WEBHOOK_TOKEN" -H "Content-Type: application/json" \
-d '{"routine": "guten_morgen", "params": {"k": "v"}}'
Geplante Routinen
Per Sprache: „Weck mich morgen um 8 und zeig mir die Nachrichten" →
schedule_routine(at=<ISO-Zeit>, routine=<Name>, recurring=true für täglich).
Die Routine ist ein Skill und wird zum Zeitpunkt per skill_run ausgeführt.
Repository-Struktur
OpenVoiceAssistant/
├── main.py # Einstieg, Voice-Loop, Trigger-Behandlung
├── Voice/ # Wake-Word, Aufnahme, ASR, TTS
├── llm/ # Harness, Tools, Skills, Timer, Gedächtnis, Sandbox
│ ├── pipeline.py # Einstiegspunkt (Kontext, Log, TTS)
│ ├── harness.py # Tool-Calling-Loop
│ ├── tools.py # alle Werkzeuge (128 Tools)
│ ├── config.py # Konfiguration (Env-Vars)
│ ├── system_prompt.py # System-Prompt (dynamisch)
│ ├── vikunja.py # Vikunja API-Client (Tasks, Projekte, Labels)
│ ├── tandoor.py # Tandoor API-Client (Rezepte, Meal-Plan, Einkaufsliste)
│ ├── weather.py # Wetter (OpenWeatherMap)
│ ├── immich.py # Immich API-Client (Fotos, Videos, Memories)
│ ├── traccar.py # Traccar API-Client (GPS-Tracking)
│ ├── product_lookup.py # Produkt-Suche (OFF + retraceable.io)
│ ├── camera.py # Kamera-Steuerung (USB, RTSP, ONVIF)
│ ├── skills.py # Skill-System (Routinen)
│ ├── timers.py # Timer & geplante Routinen
│ ├── memory.py # Gedächtnis (JSON)
│ ├── history_log.py # Transcript-Log (JSONL)
│ ├── view_library.py # View-Bibliothek (speichern, laden, kopieren)
│ ├── mcp.py # MCP-Server-Client
│ ├── sandbox.py # OpenTerminal-Client
│ └── tts.py # TTS (OmniVoice / Piper / espeak)
├── display/ # Flask-Server, Views, Action-Bridge, Webhook
├── admin/ # Admin-Web-Oberfläche (SSO/OIDC, `python -m admin`)
├── server/ # optionaler Voice-Server (ASR + TTS als HTTP-API)
├── test_skills_history.py
├── test_triggers.py
├── requirements.txt
└── README.md