docs: add project documentation (architecture, API, interfaces, config, deployment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 12:02:47 +02:00
parent 21712c972d
commit 2db89bc33a
6 changed files with 475 additions and 0 deletions

11
doc/README.md Normal file
View File

@@ -0,0 +1,11 @@
# VoiceLog Projektdokumentation
> Zentrales Verzeichnis für Architektur, Schnittstellen und Betrieb.
| Dokument | Inhalt |
|---|---|
| [architecture.md](architecture.md) | Gesamtarchitektur, Datenmodell, Hintergrundverarbeitung |
| [api.md](api.md) | Alle HTTP-Endpunkte der Anwendung |
| [interfaces.md](interfaces.md) | Externe Schnittstellen (Transcription-API, Ollama) |
| [configuration.md](configuration.md) | Umgebungsvariablen und Laufzeiteinstellungen |
| [deployment.md](deployment.md) | Build, Deploy, Betrieb mit Podman |

100
doc/api.md Normal file
View File

@@ -0,0 +1,100 @@
# HTTP-API
Alle Endpunkte werden von `app.py` bereitgestellt. HTML-Seiten geben `text/html` zurück; Daten-Endpunkte geben JSON zurück.
## PWA / System
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/` | Upload-Seite |
| GET | `/manifest.webmanifest` | PWA-Manifest |
| GET | `/icon.svg` | App-Icon |
| GET | `/sw.js` | Service Worker |
| GET | `/healthz` | Statuscheck (JSON): API_BASE, OLLAMA_BASE_URL, Modell, DB-Pfad |
## Einstellungen
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/settings` | Einstellungsseite |
| POST | `/settings` | Einstellungen speichern |
| GET | `/settings/models` | Verfügbare Ollama-Modelle laden (JSON) |
### `GET /settings/models` Response
```json
{ "ok": true, "models": ["llama3.2:3b", "qwen3.5:9b", ...] }
{ "ok": false, "error": "...", "models": [] }
```
## Projekte
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | `/projects` | Projekt anlegen (Form: `name`) |
| POST | `/projects/update` | Projekt umbenennen (Form: `id`, `name`) |
| POST | `/projects/{id}/delete` | Projekt löschen (Dokumente → Default) |
| POST | `/projects/create-api` | Projekt anlegen, gibt JSON zurück (für JS-Upload-Flow) |
## Upload
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | `/upload` | Audio-/Videodatei hochladen (Form: `project_id`, `file`); legt Upload-Job an |
## Library / Dokumente
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/library` | Dokumentenliste (Query: `project_id`, `q_title`, `q_content`) |
| GET | `/document/{id}` | Dokument anzeigen |
| GET | `/document/{id}/download.md` | Dokument als Markdown herunterladen |
| POST | `/document/{id}/edit` | Inhalt bearbeiten (Form: `content_md`) |
| POST | `/document/{id}/rename` | Umbenennen (Form: `title`) |
| POST | `/document/{id}/move` | Projekt wechseln (Form: `project_id`) |
| POST | `/document/{id}/delete` | Dokument löschen |
| POST | `/documents/bulk-move` | Mehrere Dokumente verschieben (Form: `ids[]`, `project_id`) |
| POST | `/documents/bulk-delete` | Mehrere Dokumente löschen (Form: `ids[]`) |
## Prompts & Projekte
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/prompts` | Prompts- und Projektverwaltung |
| GET | `/prompts/{id}/preview` | Prompt-Vorschau (JSON: `{html}`) |
| POST | `/prompts/add` | Prompt anlegen (Form: `name`, `prompt`) |
| POST | `/prompts/update` | Prompt speichern inkl. KI-Einstellungen (siehe unten) |
| POST | `/prompts/{id}/delete` | Prompt löschen |
### `POST /prompts/update` Felder
| Feld | Typ | Beschreibung |
|---|---|---|
| `id` | int | Prompt-ID |
| `name` | str | Name |
| `prompt` | str | Prompttext |
| `llm_use_default` | int | 1 = globale KI-Einstellungen, 0 = promptspezifisch |
| `llm_model` | str? | Modellname |
| `llm_think` | str? | `"true"` / `"false"` |
| `llm_num_ctx` | str? | Kontextgröße oder `"auto"` |
| `llm_num_predict` | str? | Max. Tokens |
| `llm_repeat_penalty` | str? | |
| `llm_repeat_last_n` | str? | |
## Hintergrundjobs
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/jobs` | Job-Übersicht (HTML) |
| GET | `/jobs/data` | Job-Liste als JSON (Query: `limit=200`) |
| GET | `/jobs/{id}/debug-data` | Detail-Daten eines Jobs (JSON) |
| POST | `/jobs/{id}/cancel` | Job abbrechen (JSON-Response) |
| POST | `/jobs/{id}/cancel-form` | Job abbrechen (Redirect) |
| POST | `/jobs/{id}/delete` | Job löschen (JSON-Response) |
| POST | `/jobs/{id}/delete-form` | Job löschen (Redirect) |
## Prompt ausführen
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/run` | Formular: Prompt auf Dokument(e) anwenden |
| POST | `/run` | Analyse-Job(s) starten |
| POST | `/run/project` | Analyse-Job für alle Transkripte eines Projekts starten |

124
doc/architecture.md Normal file
View File

@@ -0,0 +1,124 @@
# Architektur
## Überblick
VoiceLog ist eine Single-File-FastAPI-Anwendung (`app.py`, ~2000 Zeilen). Es gibt keine Templates, kein separates Frontend-Build-System und keine weiteren Module. HTML wird serverseitig als Python-f-Strings erzeugt; Bootstrap 5.3 (CDN) übernimmt das Layout.
```
Browser
FastAPI (app.py, Port 8094)
├── SQLite (DB_PATH, Standard: /data/ui.db)
├── ThreadPoolExecutor (2 Workers)
│ ├── Upload-Job → Transcription-API (API_BASE)
│ └── Analyse-Job → Ollama (OLLAMA_BASE_URL)
└── Dateisystem (JOB_DIR, Standard: /data/jobs/)
```
Traefik sitzt als Reverse-Proxy davor und terminiert TLS (`https://voicelog.aquantico.lan`).
---
## Datenmodell (SQLite)
### `projects`
| Spalte | Typ | Beschreibung |
|---|---|---|
| `id` | INTEGER PK | |
| `name` | TEXT UNIQUE | Projektname |
| `created_at` | TEXT | ISO-8601-UTC |
### `prompts`
| Spalte | Typ | Beschreibung |
|---|---|---|
| `id` | INTEGER PK | |
| `name` | TEXT UNIQUE | Anzeigename |
| `prompt` | TEXT | Prompttext für das LLM |
| `created_at` | TEXT | |
| `updated_at` | TEXT | |
| `llm_use_default` | INTEGER | 1 = globale KI-Einstellungen nutzen (Standard) |
| `llm_model` | TEXT | Überschreibt globales Modell wenn `llm_use_default=0` |
| `llm_think` | TEXT | `"true"` / `"false"` |
| `llm_num_ctx` | TEXT | Kontextgröße oder `"auto"` |
| `llm_num_predict` | INTEGER | Max. Ausgabe-Tokens |
| `llm_repeat_penalty` | REAL | |
| `llm_repeat_last_n` | INTEGER | |
### `documents`
| Spalte | Typ | Beschreibung |
|---|---|---|
| `id` | INTEGER PK | |
| `project_id` | INTEGER FK | → projects |
| `kind` | TEXT | `transcript` oder `analysis` |
| `title` | TEXT | |
| `content_md` | TEXT | Inhalt (Markdown) |
| `source_document_id` | INTEGER FK | Bei `analysis`: Quell-Transkript |
| `prompt_id` | INTEGER FK | Bei `analysis`: verwendeter Prompt |
| `raw_json` | TEXT | Rohantwort der externen API als JSON |
| `created_at` | TEXT | |
### `jobs`
| Spalte | Typ | Beschreibung |
|---|---|---|
| `id` | INTEGER PK | |
| `kind` | TEXT | `upload` oder `analysis` |
| `status` | TEXT | `queued``running``done` / `error` / `cancelled` |
| `project_id` | INTEGER | |
| `document_id` | INTEGER | Quell-Dokument (bei `analysis`) |
| `prompt_id` | INTEGER | |
| `title` | TEXT | Anzeigename |
| `file_path` | TEXT | Temporärer Pfad der Audiodatei (bei `upload`) |
| `error` | TEXT | Fehlermeldung bei Status `error` |
| `result_document_id` | INTEGER | Erzeugtes Dokument nach `done` |
| `user_prompt` | TEXT | Zusatzinformation vom Nutzer |
| `llm_prompt` | TEXT | Vollständiger LLM-Prompt inkl. `[num_ctx=…]`-Header |
| `llm_response` | TEXT | Rohausgabe des LLM |
| `llm_thinking` | TEXT | Thinking-Chain-of-Thought-Ausgabe |
| `created_at` | TEXT | |
| `started_at` | TEXT | |
| `finished_at` | TEXT | |
### `settings`
Einfache Key-Value-Tabelle für persistente Laufzeiteinstellungen.
| Spalte | Typ |
|---|---|
| `key` | TEXT PK |
| `value` | TEXT |
Bekannte Keys: `ollama_model`, `ollama_think`, `ollama_num_ctx`, `ollama_num_predict`, `ollama_repeat_penalty`, `ollama_repeat_last_n`.
---
## Hintergrundverarbeitung
Jobs werden über `enqueue_job()` in die DB geschrieben und sofort an einen `ThreadPoolExecutor(max_workers=2)` übergeben.
### Upload-Job (`_process_upload_job`)
1. Liest temporäre Audiodatei aus `JOB_DIR`
2. Sendet sie als `multipart/form-data` an `{API_BASE}/transcribe-diarize`
3. Speichert `formatted_text` aus der Antwort als `transcript`-Dokument
4. Löscht die temporäre Datei
### Analyse-Job (`_process_analysis_job`)
1. Lädt Dokument und Prompt aus der DB
2. Prüft `llm_use_default` am Prompt: globale oder promptspezifische KI-Einstellungen
3. Berechnet `num_ctx` dynamisch über `_estimate_num_ctx()` (außer bei manuellem Override)
4. Streamt Anfrage an `{OLLAMA_BASE_URL}/api/generate`
5. Puffert `thinking`- und `response`-Chunks in `_JOB_STREAMS` (für Live-Anzeige)
6. Speichert Ergebnis als `analysis`-Dokument
Beide Worker prüfen nach jedem externen Call, ob der Job zwischenzeitlich auf `cancelled` gesetzt wurde.
---
## Datenbankmigrationen
Werden inline in `init_db()` mit `ALTER TABLE … ADD COLUMN` in `try/except`-Blöcken ausgeführt. Die DB wird beim Start automatisch angelegt und migriert.
---
## PWA
Die App liefert `/manifest.webmanifest`, `/icon.svg` und `/sw.js` direkt aus Route-Handlern aus und ist damit als Progressive Web App installierbar.

42
doc/configuration.md Normal file
View File

@@ -0,0 +1,42 @@
# Konfiguration
## Umgebungsvariablen
Werden beim Start ausgewertet und dienen als Fallback-Werte, wenn keine DB-Einstellung vorhanden ist.
| Variable | Standard | Beschreibung |
|---|---|---|
| `API_BASE` | `http://gx10.aquantico.lan:8093` | URL der Transcription-API |
| `OLLAMA_BASE_URL` | `http://gx10.aquantico.lan:11434` | URL der Ollama-Instanz |
| `OLLAMA_MODEL` | `qwen3.5:9b` | Standard-LLM-Modell (Fallback) |
| `OLLAMA_NUM_PREDICT` | `16384` | Max. Ausgabe-Tokens (Fallback) |
| `OLLAMA_THINK` | `true` | Extended Thinking aktivieren (Fallback) |
| `DB_PATH` | `/data/ui.db` | Pfad zur SQLite-Datenbank |
| `JOB_DIR` | `/data/jobs` | Verzeichnis für temporäre Upload-Dateien |
## Laufzeiteinstellungen (DB)
Überschreiben die Umgebungsvariablen. Werden über die Einstellungsseite (`/settings`) gesetzt und in der `settings`-Tabelle gespeichert.
| Key | Beschreibung | Mögliche Werte |
|---|---|---|
| `ollama_model` | Aktives LLM-Modell | z.B. `qwen3.5:9b`, `llama3.2:3b` |
| `ollama_think` | Extended Thinking | `true` / `false` |
| `ollama_num_ctx` | Kontextfenstergröße | `auto`, `4096`, `8192`, `16384`, `32768`, `65536`, `131072` |
| `ollama_num_predict` | Max. Ausgabe-Tokens | Integer, z.B. `16384` |
| `ollama_repeat_penalty` | Wiederholungsstrafe | Float, z.B. `1.15` |
| `ollama_repeat_last_n` | Repeat-Fenster (Tokens) | Integer, z.B. `128` |
## Promptspezifische KI-Einstellungen
Jeder Prompt kann eigene LLM-Parameter haben (Spalten `llm_*` in der `prompts`-Tabelle). Solange `llm_use_default=1` (Standard), werden die globalen DB-Einstellungen verwendet.
## `num_ctx`-Automatik
Bei `ollama_num_ctx = "auto"` (Standard) wird die Kontextgröße dynamisch berechnet:
```python
needed = len(prompt) // 3 + 2048 # grobe Token-Schätzung + Antwortpuffer
# wählt kleinstes passendes aus: 4096, 8192, 16384, 32768, 65536
# Fallback: 65536
```

94
doc/deployment.md Normal file
View File

@@ -0,0 +1,94 @@
# Deployment
## Voraussetzungen
- Podman + podman-compose
- Externes Podman-Netzwerk `traefik` (Traefik-Instanz läuft im selben Netzwerk)
- Traefik konfiguriert für Entrypoint `websecure` (HTTPS)
## Erstkonfiguration
```bash
cp .env.example .env # Umgebungsvariablen anpassen
```
## Build & Start
```bash
sudo podman compose up -d --build
```
Die App ist danach erreichbar unter:
- `https://voicelog.aquantico.lan` (via Traefik)
- `http://127.0.0.1:8094` (direkt)
## Neustart nach Code-Änderungen
```bash
sudo podman rm -f diarization-ui
sudo podman compose up -d --build
```
## Image in Registry publizieren
```bash
podman build -t registry.aquantico.lan/claw/diarization-ui:latest .
podman push registry.aquantico.lan/claw/diarization-ui:latest
```
## Datenpersistenz
Alle persistenten Daten liegen im Podman-Volume `diarization-ui_diarization_ui_data`, gemountet unter `/data` im Container:
```
/data/ui.db — SQLite-Datenbank
/data/jobs/ — Temporäre Upload-Dateien (werden nach Verarbeitung gelöscht)
```
> **Achtung:** Beim Neuerstellen des Containers kein `podman volume rm` ausführen — sonst gehen alle Daten verloren. Nur `podman rm -f diarization-ui` reicht.
## Daten-Backup
```bash
sudo podman cp diarization-ui:/data/ui.db ./ui.db.backup
```
## Volumes prüfen
```bash
sudo podman volume ls | grep diarization
```
Das korrekte Volume heißt `diarization-ui_diarization_ui_data` (nicht `voicelog_diarization_ui_data`, das ist ein veraltetes Volume aus einer früheren Benennung).
## Logs
```bash
sudo podman logs -f diarization-ui
```
## docker-compose.yml Struktur
```yaml
services:
diarization-ui:
networks: [traefik]
labels:
- traefik.enable=true
- traefik.http.routers.openwebui.rule=Host(`voicelog.aquantico.lan`)
- traefik.http.routers.openwebui.entrypoints=websecure
- traefik.http.routers.openwebui.tls=true
- traefik.http.services.openwebui.loadbalancer.server.port=8094
networks:
traefik:
external: true
```
## Lokale Entwicklung (ohne Docker/Podman)
```bash
pip install -r requirements.txt
API_BASE=http://... OLLAMA_BASE_URL=http://... DB_PATH=./ui.db \
uvicorn app:app --host 0.0.0.0 --port 8094 --reload
```

104
doc/interfaces.md Normal file
View File

@@ -0,0 +1,104 @@
# Externe Schnittstellen
## Transcription-API (`API_BASE`)
Wird für Audio-/Videotranskription mit Diarisierung verwendet.
**Standard-URL:** `http://gx10.aquantico.lan:8093`
### `POST /transcribe-diarize`
Upload der Mediendatei als `multipart/form-data`.
**Request:**
```
Content-Type: multipart/form-data
field: file (binary, application/octet-stream)
```
Unterstützte Formate: alle gängigen Audio- und Videoformate (MP3, WAV, M4A, OGG, MP4, MKV, MOV, …). Die API extrahiert den Audiotrack selbst.
**Response (JSON):**
```json
{
"formatted_text": "Speaker 1: Hallo...\nSpeaker 2: ...",
...
}
```
Das Feld `formatted_text` wird als Markdown-Inhalt des `transcript`-Dokuments gespeichert. Die vollständige JSON-Antwort landet in `documents.raw_json`.
**Timeout:** 1800 s (30 Minuten, für lange Aufnahmen)
---
## Ollama (`OLLAMA_BASE_URL`)
Wird für die LLM-Analyse von Transkripten verwendet.
**Standard-URL:** `http://gx10.aquantico.lan:11434`
### `POST /api/generate`
**Request (JSON):**
```json
{
"model": "qwen3.5:9b",
"prompt": "...",
"stream": true,
"think": true,
"options": {
"num_ctx": 16384,
"num_predict": 16384,
"repeat_penalty": 1.15,
"repeat_last_n": 128
}
}
```
- `stream: true` — Antwort wird zeilenweise als JSON-Objekte gestreamt
- `think` — aktiviert Extended Thinking / Chain-of-Thought (modellabhängig)
- `num_ctx` — wird dynamisch berechnet (`_estimate_num_ctx`) oder aus den Einstellungen gelesen
- Alle `options`-Parameter sind über die Einstellungsseite oder pro Prompt konfigurierbar
**Response (NDJSON-Stream):**
```json
{"response": "Teil der Antwort", "thinking": "...", "done": false}
...
{"response": "", "done": true, "total_duration": ...}
```
Chunks werden in `_JOB_STREAMS[job_id]` gepuffert für die Live-Anzeige auf der Jobs-Seite.
**Timeout:** 1200 s
### `GET /api/tags`
Wird von `GET /settings/models` aufgerufen, um verfügbare Modelle zu listen.
**Response (JSON):**
```json
{
"models": [
{ "name": "qwen3.5:9b", ... },
...
]
}
```
**Timeout:** 5 s
---
## KI-Einstellungen (Prioritäten)
Beim Analyse-Job gelten folgende Prioritäten für die LLM-Parameter:
```
Prompt-spezifische Einstellungen (llm_use_default=0)
→ Werte aus prompts.llm_* Spalten
Globale Einstellungen (llm_use_default=1, Standard)
→ Werte aus der settings-Tabelle
Fallback
→ Umgebungsvariablen (OLLAMA_MODEL, OLLAMA_THINK, …)
```