diff --git a/README.md b/README.md index 9be9a3c..fbc28ec 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,42 @@ # Voice-Agent -Self-hosted Transkriptions- und Protokollsystem für Sitzungsaufzeichnungen — vollständig lokal, DSGVO-konform. +Self-hosted Transkriptions- und Protokollsystem für Sprachaufzeichnungen — vollständig lokal, DSGVO-konform. > Vollständiges Lastenheft: siehe [`need.md`](need.md) ## Architektur ``` -Benutzer +Benutzer (Browser / Handy-PWA) │ - ▼ HTTP (Upload) -┌─────────────────────────────┐ ┌──────────────────────────────┐ -│ LXC-Container │ HTTP │ Mac (Apple Silicon) │ -│ - FastAPI + HTML/JS UI │ ──────► │ - FastAPI Worker │ -│ - SQLite Job-Store │ │ - lightning-whisper-mlx │ -│ - Result-Verzeichnis │ ◄────── │ - Ollama (llama3.1:8b) │ -│ - Nginx Reverse Proxy │ │ - python-docx │ -└─────────────────────────────┘ └──────────────────────────────┘ + ▼ HTTPS (selbst-signiert, Mikrofon-fähig) +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ LXC-Container (Linux) │ HTTP │ Mac (Apple Silicon) │ +│ - FastAPI + HTML/JS UI │ ──────► │ - FastAPI Worker │ +│ - SQLite Job-Store + Auth │ │ - lightning-whisper-mlx │ +│ - Profile (YAML, ≥17) │ │ - Ollama (Standard 8B) │ +│ - Result-/Upload-Verz. │ ◄────── │ - python-docx + reportlab │ +│ - Nginx HTTPS-Reverse-Proxy │ │ - Per-Job Log-Ringbuffer │ +└──────────────────────────────┘ └──────────────────────────────┘ ``` -- **LXC**: keine AI-Verarbeitung — nur Uploads, Job-Tracking, Auslieferung. -- **Mac**: Stateless API. Erhält Audio/Text, gibt Transkripte / Zusammenfassungen / Protokolle / DOCX zurück. +- **LXC**: keine AI-Verarbeitung — nur Uploads, Job-Tracking, Login, Auslieferung. +- **Mac**: Stateless API. Erhält Audio/Text, gibt Transkripte / Zusammenfassungen / Protokolle / DOCX / PDF zurück. -## MVP-Funktionsumfang (v0.1) +## Funktionsumfang (aktueller Stand) -- [x] Audio-Upload (MP3, WAV, M4A, MP4, OGG, FLAC, max. 500 MB) -- [x] Whisper-Transkription auf dem Mac -- [x] Ollama-Zusammenfassung (Beschlüsse, Aufgaben, Teilnehmer) -- [x] Strukturiertes Sitzungsprotokoll -- [x] DOCX-Export -- [ ] Speaker-Diarization (Prio 2) -- [ ] Auth / Benutzerverwaltung (Prio 2) -- [ ] PDF-Export (Prio 2) +- [x] Audio-Upload (MP3, WAV, M4A, MP4, OGG, FLAC, WEBM — max. 500 MB) +- [x] Mobile-First-Layout + PWA mit Mikrofon-Aufnahme (Start / Pause / Stop) +- [x] Whisper-Transkription auf dem Mac (MLX, Apple Neural Engine) +- [x] Ollama-Zusammenfassung +- [x] Strukturiertes Protokoll je Profil +- [x] **17 Verarbeitungs-Profile** (Sitzung, Diktat, Pflegedoku, IT-Ticket, Sales-Call, …) +- [x] **DOCX- und PDF-Export** +- [x] **Benutzer-Verwaltung** (bcrypt, Cookie-Sessions, Per-User-Job-Isolation, Admin-Rolle) +- [x] **HTTPS via selbst-signiertem Zertifikat** (Mikrofon-Zugriff am Handy) +- [x] **Idempotente Pipeline + Retry** — bei „Server disconnected" automatischer 1× Retry, Transkript wird beim manuellen Erneut-Versuchen weiterverwendet +- [x] **Diagnose-Panel pro Job** — Live-Heartbeat während laufender Mac-Calls + Mac-Worker-Log direkt in der UI +- [ ] Speaker-Diarization (Prio 2 — siehe `mac-worker/README.md`) --- @@ -40,12 +45,12 @@ Benutzer Voraussetzungen: - **Mac** (Apple Silicon empfohlen, M1/M2/M3/M4) mit min. 16 GB RAM, besser 32 GB. -- **Proxmox-Host** für den LXC-Container. +- **Proxmox-Host** (oder beliebiger Linux-Host) für den LXC-Container. - **Netzwerk**: Mac und LXC im selben LAN, der LXC muss den Mac auf Port 8080 erreichen. ### Schritt 1 — LXC-Container provisionieren (Proxmox) -Entweder mit dem Toolkit-Skill `/proxmox-lxc` oder manuell ein Container mit: +Container mit: - Debian 13 (Trixie) oder Ubuntu 24.04 - 2 vCPU, 2 GB RAM, 20 GB Disk reichen @@ -71,15 +76,17 @@ cd /var/www/voice-agent Das Skript installiert idempotent: -1. apt-Pakete (`git python3-venv nginx ffmpeg ...`) +1. apt-Pakete (`git python3-venv nginx ffmpeg openssl …`) 2. Deploy-User `deploy`, klont/aktualisiert Repo nach `/var/www/voice-agent` 3. Python-venv + Dependencies in `lxc-frontend/.venv` -4. `.env` aus Vorlage anlegen (falls nicht vorhanden) -5. Verzeichnisse `/var/lib/voice-agent/{uploads,results}` -6. systemd-Service `voice-agent.service` (autostart, restart on-failure) -7. Nginx-Reverse-Proxy auf Port 80 → `127.0.0.1:8000` +4. `.env` aus Vorlage anlegen, `SESSION_SECRET` automatisch erzeugen +5. Verzeichnisse `/var/lib/voice-agent/{uploads,results}` + SQLite-DB +6. systemd-Service `voice-agent.service` (autostart, restart-on-failure) +7. Selbst-signiertes TLS-Zertifikat mit SAN (Hostname + IP) +8. Nginx HTTPS-Reverse-Proxy + HTTP→HTTPS Redirect +9. CLI-Wrapper `/usr/local/bin/voice-agent-admin` -Erneutes Ausführen aktualisiert auf den neuesten `main`. +Erneutes Ausführen aktualisiert auf den neuesten `main` (idempotent, kein Datenverlust — die SQLite-DB liegt unter `/var/lib/voice-agent/`, nicht im App-Verzeichnis). ### Schritt 3 — Mac-Worker einrichten @@ -90,24 +97,13 @@ Auf dem Mac (Details in [`mac-worker/README.md`](mac-worker/README.md)): git clone https://git.cynfo.net/christian/voice-agent.git ~/voice-agent cd ~/voice-agent/mac-worker -# Python-Umgebung -python3.11 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -cp .env.example .env - -# Ollama + Modell +# Ollama + Modell (einmalig) brew install ollama launchctl setenv OLLAMA_HOST "0.0.0.0:11434" brew services restart ollama ollama pull llama3.1:8b -# Worker starten (Vordergrund — für Test) -uvicorn app.main:app --host 0.0.0.0 --port 8080 - -# Für Dauerbetrieb: als LaunchAgent registrieren -# (startet bei Login, restart nach Crash, Logs in ~/Library/Logs/) +# Worker als LaunchAgent installieren — macht venv + Dependencies + Autostart in einem Rutsch ./deploy/install-launchagent.sh ``` @@ -128,9 +124,29 @@ sudo nano /var/www/voice-agent/lxc-frontend/.env sudo systemctl restart voice-agent ``` -### Schritt 5 — Benutzen +### Schritt 5 — Erst-Anmeldung + Benutzen -Browser auf `http:///` → Audio hochladen → fertig. +Browser auf `https:///` — beim allerersten Aufruf legt die UI den initialen Admin an (Username + Passwort wählen). Bestehende Jobs werden diesem Konto zugeordnet. + +Audio hochladen oder direkt in der UI aufnehmen → fertig. + +--- + +## Update / Neudeployment + +**LXC** — einfach nochmal: + +```bash +sudo /var/www/voice-agent/deploy/install.sh +``` + +**Mac** — ein Befehl macht git-pull + pip-install + Worker-Restart + Health-Check: + +```bash +~/voice-agent/mac-worker/deploy/update.sh +``` + +(Falls `update.sh` aus irgendeinem Grund nicht läuft, ist `./deploy/install-launchagent.sh restart` der manuelle Fallback.) --- @@ -147,25 +163,61 @@ sudo voice-agent-admin list sudo voice-agent-admin create maria 'StartPasswort123' sudo voice-agent-admin create chefin 'Geheim12345' --admin sudo voice-agent-admin delete maria + +# Passwort prüfen ohne Browser (Debug) +sudo voice-agent-admin verify admin 'NeuesPasswort123' ``` Vollständige Befehlsreferenz: [`lxc-frontend/README.md`](lxc-frontend/README.md#benutzer--und-passwort-verwaltung-per-cli). -## Update / Neudeployment +--- -Auf dem LXC einfach nochmal: +## Profile -```bash -sudo /var/www/voice-agent/deploy/install.sh -``` +Jeder Job läuft mit einem **Verarbeitungs-Profil**, das die Prompts für Summary, Protokoll und das Layout des Exports steuert. Standard ist `meeting`. Das Standard-Profil je User wird im Web-UI im Header gewählt und bleibt gespeichert. -Auf dem Mac: +Mitgelieferte Profile (alle in `mac-worker/profiles/*.yaml`): -```bash -cd ~/voice-agent && git pull -cd mac-worker && source .venv/bin/activate && pip install -r requirements.txt -# Worker neu starten -``` +| Profil | Zweck | +|---|---| +| `meeting` | Klassisches Sitzungsprotokoll | +| `phone-call` | Telefonat-Notiz mit Beteiligten/Verabredungen | +| `memo` | Kurze Sprachnotiz / Diktat | +| `interview` | Interview mit Sprecherspuren | +| `one-on-one` | 1:1-Gespräch (Mitarbeiter / Kunde) | +| `brainstorm` | Ideen-Sammlung mit Cluster-Vorschlägen | +| `service-ticket` | IT-Service-Ticket aus Anruf-Mitschnitt | +| `incident-report` | Incident-Doku (was, wann, Auswirkung, Status) | +| `postmortem` | Post-Mortem mit Lessons-Learned | +| `site-survey` | Vor-Ort-Aufnahme (Leitungsverlauf, Hardware, Zustand) | +| `sales-call` | Sales-Gespräch mit Pain-Points / Next-Steps | +| `schichtuebergabe` | Schichtübergabe Pflege | +| `pflegedoku` | Pflegedokumentations-Eintrag | +| `sturzbericht` | Sturzbericht (Pflege) | +| `visite` | Arzt-Visite | +| `angehoerigengespraech` | Angehörigen-Gespräch (Pflege) | +| `biografie` | Biografie-Eintrag (Pflege) | + +Eigene Profile: einfach eine weitere YAML-Datei in `mac-worker/profiles/` ablegen (Beispiel siehe `meeting.yaml`) und Mac-Worker neu starten. + +--- + +## Diagnose / Troubleshooting + +Wenn ein Job hängt oder fehlschlägt: + +- **In der UI**: bei FAILED-Jobs und langlaufenden Stages erscheint **„Diagnose ▾"**. Aufklappen zeigt: + - aktuelle Stage und wie lange sie schon läuft + - letzter Heartbeat (alle 3 s während Mac-Calls) + - die letzten 200 Mac-Worker-Log-Zeilen für genau diesen Job +- **Auf dem LXC**: + - `systemctl status voice-agent` / `journalctl -u voice-agent -f` + - `curl -sk https://localhost/api/mac/health` +- **Auf dem Mac**: + - `~/voice-agent/mac-worker/deploy/install-launchagent.sh status` + - `tail -f ~/Library/Logs/voice-agent-worker.{out,err}.log` + +Wenn der Mac mitten in `summarize` die Verbindung abreissen lässt, retried der LXC einmal automatisch (3 s Backoff) — ein vorgelagerter Preload-Aufruf hat zudem das Ollama-Modell schon ins RAM geladen, damit es nicht in der eigentlichen Generierung neu lädt. --- @@ -181,7 +233,7 @@ cd mac-worker && source .venv/bin/activate && pip install -r requirements.txt | `faster` | `large-v3` | Intel-Mac / Linux / CUDA | | `mock` | – | Tests ohne Modell-Download | -Standard ist `mlx` + `large-v3`. Für lange Sitzungen lohnt sich ein Test mit `distil-large-v3`. Alternativen (whisper.cpp + CoreML, WhisperX mit Diarization, Voxtral) sind in [`mac-worker/README.md`](mac-worker/README.md#alternative-transkriptions-engines) dokumentiert. +Standard ist `mlx` + `large-v3`. Für lange Sitzungen lohnt sich `distil-large-v3`. Alternativen (whisper.cpp + CoreML, WhisperX mit Diarization, Voxtral) sind in [`mac-worker/README.md`](mac-worker/README.md#alternative-transkriptions-engines) dokumentiert. ### Zusammenfassung (Ollama) @@ -198,7 +250,7 @@ Empfohlene Alternativen je nach Hardware (alle via `ollama pull ` und `OLL | `gpt-oss:20b` | 20 B | ~14 GB | Sehr gute strukturierte Ausgabe (OpenAI Open-Weight) | | `llama3.3:70b` | 70 B | ~48 GB | Top-Qualität, nur auf Macs mit 64 GB+ | -Für Sitzungsprotokolle (deutsch, viel JSON-Struktur) ist **`qwen2.5:14b`** oder **`mistral-small:24b`** der beste Sweet-Spot auf einem Mac mit 32 GB. +Für deutsche Protokolle mit viel JSON-Struktur ist **`qwen2.5:14b`** oder **`mistral-small:24b`** der beste Sweet-Spot auf einem Mac mit 32 GB. Detaillierte Bewertung siehe [`mac-worker/README.md`](mac-worker/README.md#ollama-modell-wählen). @@ -214,9 +266,11 @@ voice-agent/ │ └── .env.example ├── mac-worker/ # FastAPI AI-Worker (läuft auf dem Mac) │ ├── app/ +│ ├── profiles/ # YAML-Profile (Prompts + Layout) +│ ├── deploy/ # install-launchagent.sh, update.sh, plist-Template │ ├── requirements.txt │ └── .env.example -├── deploy/ # systemd / nginx / install.sh +├── deploy/ # systemd / nginx / install.sh (LXC) ├── config/ # Toolkit-Konfiguration (project.env) ├── .claude/ # KI-Agenten Skills ├── need.md # Lastenheft @@ -226,6 +280,8 @@ voice-agent/ ## Sicherheit - Vollständig lokaler Betrieb. Keine Cloud-Calls. -- HTTPS via Reverse-Proxy ist Aufgabe der Infrastruktur (Let's Encrypt o. Ä.). +- HTTPS mit selbst-signiertem Cert (für Mikrofon-Zugriff am Handy zwingend nötig). +- Web-UI ist Cookie-Session-basiert (bcrypt cost 12, signiert mit `SESSION_SECRET`). - Mac-Worker hat **keine** Auth — Betrieb nur im internen Netz. - Uploads / Ergebnisse unter `/var/lib/voice-agent/` — bei Bedarf Backup einplanen. +- `.env`-Dateien (LXC + Mac) sind chmod 600, enthalten Session-Secret + ggf. Tokens. diff --git a/deploy/install.sh b/deploy/install.sh index a285c04..8c66dbb 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -52,9 +52,10 @@ elif ! grep -q "^SESSION_SECRET=" "$APP_DIR/lxc-frontend/.env"; then echo " SESSION_SECRET nachgetragen." fi -# ALLOWED_EXTS sicherstellen, dass 'webm' für Browser-Aufnahmen drin ist +# ALLOWED_EXTS sicherstellen, dass 'webm' für Browser-Aufnahmen drin ist. +# Wortgrenzen-Check verhindert dabei wiederholtes Anhängen. if grep -q "^ALLOWED_EXTS=" "$APP_DIR/lxc-frontend/.env"; then - if ! grep -E "^ALLOWED_EXTS=.*\bwebm\b" "$APP_DIR/lxc-frontend/.env" >/dev/null; then + if ! grep -Eq "^ALLOWED_EXTS=([^,]*,)*webm(,|$)" "$APP_DIR/lxc-frontend/.env"; then sed -i 's|^ALLOWED_EXTS=\(.*\)$|ALLOWED_EXTS=\1,webm|' "$APP_DIR/lxc-frontend/.env" echo " ALLOWED_EXTS um 'webm' ergänzt (für Mikrofon-Aufnahmen)." fi @@ -111,23 +112,40 @@ exec sudo -u deploy /var/www/voice-agent/lxc-frontend/.venv/bin/python \ WRAPPER chmod 0755 /usr/local/bin/voice-agent-admin +HOST_IP_NOW="$(hostname -I 2>/dev/null | awk '{print $1}')" + echo -echo "Fertig. Health-Check:" -echo " curl -sk https://localhost/health" +echo "════════════════════════════════════════════════════════════════════" +echo " Voice-Agent LXC-Installation FERTIG" +echo "════════════════════════════════════════════════════════════════════" echo -echo "Aufruf im Browser: https:/// (HTTP wird auf HTTPS umgeleitet)" -echo "Beim ersten Aufruf wird der initiale Admin angelegt." +echo "Web-UI: https://${HOST_IP_NOW}/ (HTTP wird auf HTTPS umgeleitet)" +echo "Health: curl -sk https://localhost/health" echo -echo "── Mikrofon-Aufnahme von Handys ──────────────────────────────────────" -echo "Damit iOS/Android das Mikrofon freigibt, MUSS die Seite über HTTPS laufen." -echo "Das selbst-signierte Zertifikat einmalig auf jedem Gerät installieren:" +echo "── Erst-Inbetriebnahme ──────────────────────────────────────────────" +echo " 1) MAC_API_URL in $APP_DIR/lxc-frontend/.env auf die IP des Mac-Workers setzen" +echo " und 'sudo systemctl restart voice-agent' ausführen." +echo " 2) Browser öffnen — beim allerersten Aufruf wird der initiale Admin angelegt." echo -echo " iPhone: Safari öffnen, http:///ssl/voice-agent.crt aufrufen" -echo " → Profil herunterladen → Einstellungen → Profil installieren" -echo " → Allgemein → Info → Zertifikatsvertrauen → 'voice-agent' aktivieren" +echo "── Benutzer / Passwort ──────────────────────────────────────────────" +echo " voice-agent-admin list # alle User" +echo " voice-agent-admin create maria 'StartPasswort123' # neuer User" +echo " voice-agent-admin reset admin 'NeuesPasswort123' # Passwort zurücksetzen" echo -echo " Android: Browser öffnen, http:///ssl/voice-agent.crt aufrufen" -echo " → Cert installieren als 'VPN/Apps' (Variiert je nach Hersteller)" +echo "── Mikrofon-Aufnahme von Handys (PWA) ───────────────────────────────" +echo " iOS/Android brauchen HTTPS. Selbst-signiertes Cert einmalig installieren:" +echo " iPhone: Safari → http://${HOST_IP_NOW}/ssl/voice-agent.crt" +echo " → Profil installieren → Einstellungen → Allgemein → Info" +echo " → Zertifikatsvertrauen → 'voice-agent' aktivieren" +echo " Android: Browser → http://${HOST_IP_NOW}/ssl/voice-agent.crt installieren" echo -echo "Falls noch nicht geschehen: MAC_API_URL in $APP_DIR/lxc-frontend/.env setzen" -echo "und Service neu starten: sudo systemctl restart voice-agent" +echo "── Update auf neueste Version ───────────────────────────────────────" +echo " sudo $APP_DIR/deploy/install.sh # idempotent — pullt + restartet" +echo +echo "── Diagnose / Troubleshooting ───────────────────────────────────────" +echo " Service-Status: systemctl status voice-agent" +echo " Live-Log: journalctl -u voice-agent -f" +echo " Mac erreichbar? curl -sk https://localhost/api/mac/health" +echo " Pro Job in der UI: Button 'Diagnose ▾' öffnet Mac-Worker-Log live" +echo + diff --git a/lxc-frontend/README.md b/lxc-frontend/README.md index d334a75..42f0b08 100644 --- a/lxc-frontend/README.md +++ b/lxc-frontend/README.md @@ -33,9 +33,10 @@ uvicorn app.main:app --reload --port 8000 | GET / POST / DELETE | `/api/admin/users[...]` | User-Verwaltung (admin) | | POST | `/api/jobs` | multipart: `audio`, `title`, `profile` → Job anlegen | | GET | `/api/jobs` | Liste aller Jobs (Owner-gefiltert; Admin sieht alle) | -| GET | `/api/jobs/{id}` | Job-Detail | +| GET | `/api/jobs/{id}` | Job-Detail (inkl. `step_started_at`, `last_heartbeat_at`) | +| GET | `/api/jobs/{id}/diag` | Diagnose-Bündel: Stage-Sekunden + Mac-Worker-Log | | POST | `/api/jobs/{id}/retry` | Verarbeitung neu anstoßen (Transkript bleibt) | -| GET | `/api/jobs/{id}/download/{kind}` | `kind` ∈ `docx`, `transcript`, `summary`, `protocol` | +| GET | `/api/jobs/{id}/download/{kind}` | `kind` ∈ `pdf`, `docx`, `transcript`, `summary`, `protocol` | ## Benutzer- und Passwort-Verwaltung per CLI @@ -80,10 +81,18 @@ Danach im Browser direkt einloggen. ## Datenfluss ``` -Upload → SQLite-Job → BackgroundTask +Upload → SQLite-Job → BackgroundTask (idempotent) → Mac: /api/transcribe (multipart) - → Mac: /api/summarize (JSON) - → Mac: /api/protocol (JSON) + → Mac: /api/preload (Modell ins RAM ziehen, vor Summarize) + → Mac: /api/summarize (JSON) + → Mac: /api/protocol (JSON) → Mac: /api/export/docx (JSON) → DOCX + → Mac: /api/export/pdf (JSON) → PDF → Ergebnisse landen in /var/lib/voice-agent/results/{job_id}/ ``` + +Während laufender Mac-Calls pingt ein Heartbeat alle 3 s `last_heartbeat_at`. +Bei `RemoteProtocolError` / `ConnectError` retried der MacClient einmal automatisch +mit 3 s Backoff. Jeder Mac-Call trägt einen `X-Job-Id`-Header — der Mac-Worker +sammelt damit pro Job einen Log-Ringbuffer (200 Zeilen, 1 h TTL), den +`/api/jobs/{id}/diag` für die UI bündelt. diff --git a/mac-worker/README.md b/mac-worker/README.md index 6196d77..5677662 100644 --- a/mac-worker/README.md +++ b/mac-worker/README.md @@ -2,19 +2,14 @@ Stateless FastAPI-Service auf dem Mac. Macht die schwere Arbeit: Whisper-Transkription und Ollama-Zusammenfassung. Wird vom LXC-Frontend per HTTP angesprochen. -## Setup (Apple Silicon) +## Voraussetzungen -```bash -cd mac-worker -python3.11 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -cp .env.example .env -# Bei Bedarf .env anpassen -``` +- Apple Silicon (M1/M2/M3/M4), 16 GB RAM minimum +- macOS mit Xcode-CLI-Tools (`xcode-select --install`) +- Homebrew +- Python 3.11+ (`brew install python@3.11` — falls nicht vorhanden) -## Ollama vorbereiten +## Schritt 1 — Ollama vorbereiten ```bash # Ollama installieren, falls noch nicht geschehen @@ -28,47 +23,40 @@ launchctl setenv OLLAMA_HOST "0.0.0.0:11434" brew services restart ollama ``` -## Worker starten — Vordergrund (Test) - -```bash -uvicorn app.main:app --host 0.0.0.0 --port 8080 -``` - -Health-Check vom LXC: -```bash -curl http://:8080/health -``` - -## Worker als Dienst (LaunchAgent — empfohlen für Dauerbetrieb) - -Der Worker soll dauerhaft laufen, nach Login automatisch starten und bei Crash automatisch neu starten. Dafür ein **LaunchAgent** (kein LaunchDaemon — MLX/Metal-GPU-Zugriff braucht User-Context). - -**Installation** (einmalig, nachdem `.venv` und `.env` eingerichtet sind): +## Schritt 2 — Repo + LaunchAgent installieren ```bash +# Repo holen +git clone https://git.cynfo.net/christian/voice-agent.git ~/voice-agent cd ~/voice-agent/mac-worker + +# Vollständige Installation in einem Befehl: ./deploy/install-launchagent.sh ``` -Das Skript: +Das Skript macht **alles in einem Rutsch**, idempotent: -1. prüft, ob `.venv/bin/uvicorn` existiert -2. legt `.env` aus Vorlage an, falls fehlt -3. generiert `~/Library/LaunchAgents/com.voiceagent.worker.plist` mit den passenden Pfaden -4. lädt den Agent (`launchctl load -w …`) -5. testet `http://127.0.0.1:8080/health` +1. legt `.venv` an (wenn fehlt) und installiert `requirements.txt` +2. legt `.env` aus Vorlage an (wenn fehlt) +3. generiert `~/Library/LaunchAgents/com.voiceagent.worker.plist` mit den richtigen Pfaden +4. registriert den Agent via modernem `launchctl bootstrap` +5. wartet auf `http://127.0.0.1:8080/health` und gibt die Status-Antwort aus **Verwaltung:** | Aktion | Befehl | |---|---| -| Status + Logs anzeigen | `./deploy/install-launchagent.sh status` | +| Status + letzte Logs | `./deploy/install-launchagent.sh status` | +| Worker neu starten (kein Reinstall) | `./deploy/install-launchagent.sh restart` | +| **Update** (git-pull + pip + restart + Health) | `./deploy/update.sh` | +| Vollständige Neuinstallation | `./deploy/install-launchagent.sh` | | Entfernen | `./deploy/install-launchagent.sh uninstall` | -| Update (neue Version pullen) | `git pull && pip install -r requirements.txt && ./deploy/install-launchagent.sh` | -| Stoppen | `launchctl unload -w ~/Library/LaunchAgents/com.voiceagent.worker.plist` | -| Starten | `launchctl load -w ~/Library/LaunchAgents/com.voiceagent.worker.plist` | | Live-Logs | `tail -f ~/Library/Logs/voice-agent-worker.{out,err}.log` | +> **Hinweis (modernes macOS):** `launchctl unload -w …` wirft auf neueren Versionen +> oft `Input/output error 5`. Die Skripte nutzen daher `launchctl bootout` / +> `bootstrap` / `kickstart -k` — das funktioniert zuverlässig. + **Eigenschaften des Agents:** - `RunAtLoad` — startet beim Login @@ -81,15 +69,31 @@ Das Skript: - Auto-Login aktivieren (System-Settings → Users), oder - in einen LaunchDaemon umbauen — dann fällt allerdings die Apple-Neural-Engine-Beschleunigung weg, MLX läuft nur auf der CPU. +## Worker im Vordergrund starten (für Entwicklung) + +```bash +cd ~/voice-agent/mac-worker +source .venv/bin/activate +uvicorn app.main:app --host 0.0.0.0 --port 8080 +``` + ## API-Endpunkte | Methode | Pfad | Zweck | |---|---|---| | GET | `/health` | Status + Ollama-Reachability | +| GET | `/api/profiles` | Verfügbare YAML-Profile auflisten | | POST | `/api/transcribe` | multipart: `audio`, `language` → Transkript-JSON | -| POST | `/api/summarize` | JSON: `transcript`, `title` → Summary-JSON | -| POST | `/api/protocol` | JSON: `transcript`, `summary`, `title` → Protokoll-JSON | -| POST | `/api/export/docx` | JSON: Protokoll → DOCX-Binary | +| POST | `/api/summarize` | JSON: `transcript`, `title`, `profile` → Summary-JSON | +| POST | `/api/protocol` | JSON: `transcript`, `summary`, `title`, `profile` → Protokoll-JSON | +| POST | `/api/export/docx` | JSON: `data`, `profile` → DOCX-Binary | +| POST | `/api/export/pdf` | JSON: `data`, `profile` → PDF-Binary | +| POST | `/api/preload` | JSON: `profile` → forciert Ollama-Modell-Load (`keep_alive 30 m`) | +| GET | `/api/jobs/{id}/log` | Letzte 200 Logzeilen für genau diesen Job | + +Alle relevanten Calls dürfen einen Header `X-Job-Id: ` mitschicken — der +Mac-Worker schreibt dann jede Logzeile dieses Requests in einen per-Job +Ringbuffer, den `/api/jobs/{id}/log` für die LXC-UI ausliefert. ## Firewall am Mac diff --git a/mac-worker/deploy/install-launchagent.sh b/mac-worker/deploy/install-launchagent.sh index acb8e6b..f2496ff 100755 --- a/mac-worker/deploy/install-launchagent.sh +++ b/mac-worker/deploy/install-launchagent.sh @@ -1,103 +1,189 @@ #!/usr/bin/env bash -# Installiert den Voice-Agent Mac-Worker als LaunchAgent. -# Idempotent — kann zum Update wiederholt laufen. +# Voice-Agent Mac-Worker — vollständiger Installer. # -# Nutzung: -# ./install-launchagent.sh # installieren / aktualisieren -# ./install-launchagent.sh uninstall # entfernen -# ./install-launchagent.sh status # Status + letzte Logs +# Macht alles, was nötig ist, damit der Worker dauerhaft läuft: +# - Python-venv anlegen + Dependencies installieren (idempotent) +# - .env aus Vorlage anlegen (falls fehlt) +# - LaunchAgent registrieren / aktualisieren / starten +# - Health-Check +# +# Kann beliebig oft erneut laufen (z. B. nach git pull). +# +# Befehle: +# ./install-launchagent.sh # installieren / aktualisieren +# ./install-launchagent.sh uninstall # entfernen +# ./install-launchagent.sh status # Status + letzte Logs +# ./install-launchagent.sh restart # Worker neu starten (kein Reinstall) set -euo pipefail LABEL="com.voiceagent.worker" PLIST_DST="$HOME/Library/LaunchAgents/${LABEL}.plist" LOG_DIR="$HOME/Library/Logs" +DOMAIN="gui/$(id -u)" +SERVICE_TARGET="${DOMAIN}/${LABEL}" -# Verzeichnis dieses Skripts → Repo-Root/mac-worker SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORKDIR="$(cd "$SCRIPT_DIR/.." && pwd)" TEMPLATE="$SCRIPT_DIR/com.voiceagent.worker.plist.template" cmd="${1:-install}" +# Bevorzugter Python-Interpreter (venv-Anlage). Reihenfolge: 3.11, 3.12, 3 +pick_python() { + for p in python3.11 python3.12 python3; do + if command -v "$p" >/dev/null 2>&1; then + echo "$p" + return 0 + fi + done + echo ""; return 1 +} + +ensure_venv() { + local py + py="$(pick_python)" + if [ -z "$py" ]; then + echo "FEHLER: kein python3 gefunden. Bitte 'brew install python@3.11' ausführen." >&2 + exit 1 + fi + if [ ! -x "$WORKDIR/.venv/bin/python" ]; then + echo "[venv] Erzeuge $WORKDIR/.venv mit $py" + "$py" -m venv "$WORKDIR/.venv" + fi + echo "[venv] Pip aktualisieren + Dependencies installieren" + "$WORKDIR/.venv/bin/pip" install --upgrade pip >/dev/null + "$WORKDIR/.venv/bin/pip" install -r "$WORKDIR/requirements.txt" +} + +ensure_env() { + if [ ! -f "$WORKDIR/.env" ]; then + echo "[env] Lege $WORKDIR/.env aus .env.example an (bitte ggf. anpassen)." + cp "$WORKDIR/.env.example" "$WORKDIR/.env" + fi +} + +write_plist() { + mkdir -p "$LOG_DIR" "$HOME/Library/LaunchAgents" + sed \ + -e "s|__WORKDIR__|$WORKDIR|g" \ + -e "s|__HOME__|$HOME|g" \ + "$TEMPLATE" > "$PLIST_DST" +} + +# Modernes launchctl: bootout/bootstrap statt unload/load +# (auf neueren macOS-Versionen wirft "launchctl unload -w" Input/output error 5). +service_unload() { + launchctl bootout "$SERVICE_TARGET" 2>/dev/null || true +} + +service_load() { + launchctl bootstrap "$DOMAIN" "$PLIST_DST" +} + +service_kickstart() { + # -k = Prozess hart killen + neu starten (greift auch wenn schon läuft). + launchctl kickstart -k "$SERVICE_TARGET" +} + uninstall() { if [ -f "$PLIST_DST" ]; then echo "[*] Stoppe und entlade LaunchAgent…" - launchctl unload "$PLIST_DST" 2>/dev/null || true + service_unload rm -f "$PLIST_DST" - echo "[✓] $PLIST_DST entfernt." + echo "[OK] $PLIST_DST entfernt." else echo "[i] Kein LaunchAgent installiert ($PLIST_DST nicht vorhanden)." fi } show_status() { - echo "── launchctl list | grep $LABEL ─────────────────────────" - launchctl list | grep "$LABEL" || echo "(nicht geladen)" + echo "── launchctl print $SERVICE_TARGET ─────────────────────" + launchctl print "$SERVICE_TARGET" 2>/dev/null | head -20 || echo "(nicht geladen)" echo echo "── Health-Check ─────────────────────────────────────────" - curl -fsS http://127.0.0.1:8080/health 2>/dev/null && echo || echo "(nicht erreichbar)" + if curl -fsS http://127.0.0.1:8080/health 2>/dev/null; then + echo + else + echo "(nicht erreichbar)" + fi echo echo "── stderr.log (letzte 30 Zeilen) ────────────────────────" tail -n 30 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || echo "(keine Logs)" } -install_agent() { - # Vorbedingungen prüfen - if [ ! -x "$WORKDIR/.venv/bin/uvicorn" ]; then - echo "FEHLER: $WORKDIR/.venv/bin/uvicorn nicht gefunden." >&2 - echo "Bitte zuerst die Python-Umgebung einrichten:" >&2 - echo " cd $WORKDIR && python3.11 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" >&2 +restart_only() { + if [ ! -f "$PLIST_DST" ]; then + echo "[!] Kein LaunchAgent installiert — bitte zuerst './install-launchagent.sh' ausführen." >&2 exit 1 fi - - if [ ! -f "$WORKDIR/.env" ]; then - echo "WARNUNG: $WORKDIR/.env fehlt — kopiere aus .env.example." - cp "$WORKDIR/.env.example" "$WORKDIR/.env" - fi - - mkdir -p "$LOG_DIR" "$HOME/Library/LaunchAgents" - - echo "[1/4] Generiere plist mit WORKDIR=$WORKDIR" - sed \ - -e "s|__WORKDIR__|$WORKDIR|g" \ - -e "s|__HOME__|$HOME|g" \ - "$TEMPLATE" > "$PLIST_DST" - - echo "[2/4] Falls bereits geladen: vorher entladen" - launchctl unload "$PLIST_DST" 2>/dev/null || true - - echo "[3/4] Lade LaunchAgent" - launchctl load -w "$PLIST_DST" - - echo "[4/4] Warte 2 s und prüfe Health-Endpoint" + echo "[*] kickstart -k $SERVICE_TARGET" + service_kickstart sleep 2 - if curl -fsS http://127.0.0.1:8080/health >/dev/null; then - echo - echo "[✓] Voice-Agent Mac-Worker läuft." - curl -s http://127.0.0.1:8080/health | python3 -m json.tool 2>/dev/null || true + if curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1; then + echo "[OK] Worker läuft wieder." else - echo - echo "[!] Health-Check fehlgeschlagen. Logs:" - echo " $LOG_DIR/voice-agent-worker.err.log" - echo " $LOG_DIR/voice-agent-worker.out.log" + echo "[!] Health-Check schlägt nach Restart fehl — siehe $LOG_DIR/voice-agent-worker.err.log" >&2 tail -n 20 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || true exit 1 fi +} - echo - echo "── Verwaltung ───────────────────────────────────────────" - echo " Stoppen / Starten: launchctl unload|load -w $PLIST_DST" - echo " Status: $0 status" - echo " Entfernen: $0 uninstall" - echo " Logs (live): tail -f $LOG_DIR/voice-agent-worker.{out,err}.log" +install_agent() { + echo "[1/6] venv + Dependencies" + ensure_venv + + echo "[2/6] .env" + ensure_env + + echo "[3/6] plist generieren ($PLIST_DST)" + write_plist + + echo "[4/6] LaunchAgent (bootout falls vorhanden, dann bootstrap)" + service_unload + service_load + + echo "[5/6] Health-Check (max. 8 s warten)" + local ok=0 + for _ in 1 2 3 4 5 6 7 8; do + sleep 1 + if curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1; then + ok=1 + break + fi + done + if [ "$ok" -ne 1 ]; then + echo + echo "[!] Health-Check fehlgeschlagen. Letzte Log-Zeilen:" >&2 + tail -n 30 "$LOG_DIR/voice-agent-worker.err.log" 2>/dev/null || true + echo + echo " Plist: $PLIST_DST" + echo " Logs: $LOG_DIR/voice-agent-worker.{out,err}.log" + exit 1 + fi + + echo "[6/6] Health-Antwort:" + curl -s http://127.0.0.1:8080/health | python3 -m json.tool 2>/dev/null || true + + cat <&2 + echo "Nutzung: $0 [install|uninstall|status|restart]" >&2 exit 1 ;; esac diff --git a/mac-worker/deploy/update.sh b/mac-worker/deploy/update.sh new file mode 100755 index 0000000..a8d28a8 --- /dev/null +++ b/mac-worker/deploy/update.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Voice-Agent Mac-Worker — Ein-Befehl-Update. +# +# Macht: git pull (im Repo-Root) + pip install -r requirements.txt + Worker-Restart. +# Idempotent. Beendet sich mit Health-Check. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKDIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_DIR="$(cd "$WORKDIR/.." && pwd)" +LABEL="com.voiceagent.worker" +SERVICE_TARGET="gui/$(id -u)/${LABEL}" + +echo "[1/4] git pull ($REPO_DIR)" +git -C "$REPO_DIR" pull --ff-only + +echo "[2/4] pip install (venv: $WORKDIR/.venv)" +if [ ! -x "$WORKDIR/.venv/bin/pip" ]; then + echo "FEHLER: $WORKDIR/.venv fehlt. Bitte zuerst $SCRIPT_DIR/install-launchagent.sh ausführen." >&2 + exit 1 +fi +"$WORKDIR/.venv/bin/pip" install -r "$WORKDIR/requirements.txt" + +echo "[3/4] Worker neu starten (kickstart -k $SERVICE_TARGET)" +if ! launchctl print "$SERVICE_TARGET" >/dev/null 2>&1; then + echo "FEHLER: LaunchAgent ist nicht geladen. Bitte $SCRIPT_DIR/install-launchagent.sh ausführen." >&2 + exit 1 +fi +launchctl kickstart -k "$SERVICE_TARGET" + +echo "[4/4] Health-Check (max. 8 s warten)" +for _ in 1 2 3 4 5 6 7 8; do + sleep 1 + if curl -fsS http://127.0.0.1:8080/health >/dev/null 2>&1; then + echo "[OK] Update durch, Worker läuft." + curl -s http://127.0.0.1:8080/health | python3 -m json.tool 2>/dev/null || true + exit 0 + fi +done + +echo "[!] Health-Check schlägt nach Update fehl." >&2 +tail -n 30 "$HOME/Library/Logs/voice-agent-worker.err.log" 2>/dev/null || true +exit 1