Audited README, BEDIENUNGSANLEITUNG and CLAUDE.md against what the repo and the running stack actually do. Findings, all fixed: - CLAUDE.md claimed the repo holds "only deployment config (docker-compose.yml, .env, .gitignore)". It has not been that for a while: services/ is our own source (TTS/STT servers + systemd units), scripts/ is tooling, and prompts/podcast/ + config/content_core.yaml are overlays bind-mounted over the image. Those overlays are the fragile part, so the section now says so and points at smoke_test.sh. - CLAUDE.md still described the image as a moving tag and argued against patching app code "because it would rot against pull_policy: always" — both obsolete since the digest pin. - README listed neither yt-dlp (needed by add_video_source.py) nor smoke_test.sh in the verification step, and section 2.3 still told you to `docker compose pull` as if the tag moved. - BEDIENUNGSANLEITUNG had nothing at all about updates — the very thing a user has to do periodically. New section 8 covers check_updates / update_stack / smoke_test and, importantly, why updating is not automatic (DB migration is one-way without a backup; the local adaptations can break silently without crashing). - Renumbered the ad-hoc "3a. Updates" in README into a real section 4, and the BEDIENUNGSANLEITUNG sections after the new 8 accordingly. Verified mechanically: every scripts//services//config//prompts/ path named in the docs exists, and all internal and cross-document anchors resolve (0 dead links — one was already broken before this change: README pointed at BEDIENUNGSANLEITUNG.md#stimmklonung instead of #5-stimmklonung). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
311 lines
14 KiB
Markdown
311 lines
14 KiB
Markdown
# Open Notebook (lokales Deployment)
|
||
|
||
Selbst gehostetes [Open Notebook](https://github.com/lfnovo/open-notebook) per Docker Compose —
|
||
KI-gestützter Notiz-/Recherche-Assistent mit Chat, Quellenverwaltung und Podcast-Erstellung.
|
||
|
||
**Nur zwei KI-Provider:**
|
||
- **Ollama** (lokal, auf dem Host, GPU 1+2 — RTX 3090) für Chat, Embedding, Tools
|
||
- **OpenRouter** (Cloud, einziger externer Provider) als Alternative/Ergänzung
|
||
|
||
Text-to-Speech und Speech-to-Text laufen ebenfalls lokal: [Chatterbox TTS](https://github.com/resemble-ai/chatterbox)
|
||
(mit Stimmklonung) und [faster-whisper](https://github.com/SYSTRAN/faster-whisper), beide über
|
||
schlanke selbstgeschriebene OpenAI-kompatible HTTP-Wrapper in `services/`.
|
||
|
||
Ausführliche Bedienung (Chat-Modi, Podcasts, Stimmen, Troubleshooting): siehe
|
||
[`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md). Architektur- und Betriebsdetails für
|
||
Weiterentwicklung: siehe [`CLAUDE.md`](CLAUDE.md).
|
||
|
||
---
|
||
|
||
## Architektur
|
||
|
||
| Komponente | Läuft wo | Zweck |
|
||
|---|---|---|
|
||
| `surrealdb` | Docker | Datenbank (nur `127.0.0.1:8000`) |
|
||
| `open_notebook` | Docker | Web-UI (`:8502`) + REST-API (`:5055`) |
|
||
| Ollama | Host (systemd) | Chat-/Embedding-Modelle, GPU 1+2 |
|
||
| `services/tts_server.py` | Host (systemd-User-Dienst) | Chatterbox TTS, GPU 2, Port `8901` |
|
||
| `services/stt_server.py` | Host (systemd-User-Dienst) | faster-whisper STT, GPU 2, Port `8902` |
|
||
|
||
Der `open_notebook`-Container erreicht Ollama und die TTS/STT-Wrapper über
|
||
`host.docker.internal` (Linux-Route via `extra_hosts: host-gateway`).
|
||
|
||
---
|
||
|
||
## 1. Voraussetzungen
|
||
|
||
- **Docker** + Docker Compose (Plugin), Daemon läuft
|
||
- **Ollama** installiert und als Dienst aktiv (`systemctl status ollama`), erreichbar auf `11434`
|
||
- Empfohlene systemd-Umgebung für Multi-GPU-Setups mit dedizierter Desktop-GPU:
|
||
`CUDA_VISIBLE_DEVICES=1,2` (GPU 0 ausschließen), `OLLAMA_KEEP_ALIVE=-1`,
|
||
`OLLAMA_CONTEXT_LENGTH=131072`
|
||
- Benötigte Modelle: `qwen3.5:27b`, `nomic-embed-text` (weitere optional, siehe
|
||
`scripts/setup_models.sh`) — mit `ollama pull <modell>` laden
|
||
- **Python-Umgebungen für TTS/STT** (bereits vorhandene Installationen dieses Systems):
|
||
- Conda-Env `chatterbox` mit [chatterbox-tts](https://github.com/resemble-ai/chatterbox)
|
||
(`~/miniforge3/envs/chatterbox`, siehe `~/chatterbox-tts-cli/`)
|
||
- System-Python mit `faster-whisper`, `fastapi`, `uvicorn`
|
||
- **OPENROUTER_API_KEY** als Umgebungsvariable im Environment verfügbar (für die Secret-Generierung
|
||
in Schritt 2)
|
||
- **`ufw`** (falls als Firewall aktiv) — Standardrichtlinie „deny incoming“ wird vorausgesetzt
|
||
- **`openssl`**, **`ffmpeg`**, **`curl`**, **`python3`**
|
||
- **`yt-dlp`** — nur für `scripts/add_video_source.py` (Videos ohne Untertitel); der übrige Stack
|
||
läuft auch ohne
|
||
|
||
---
|
||
|
||
## 2. Installation
|
||
|
||
### 2.1 Repository
|
||
|
||
```bash
|
||
cd ~/open-notebook # bzw. Zielverzeichnis
|
||
git status # falls schon vorhanden: prüfen statt neu klonen
|
||
```
|
||
|
||
### 2.2 Secrets erzeugen (`.env`)
|
||
|
||
```bash
|
||
ENC_KEY=$(openssl rand -base64 32)
|
||
DB_PASS=$(openssl rand -base64 24 | tr -d '/+=')
|
||
cat > .env <<EOF
|
||
OPEN_NOTEBOOK_ENCRYPTION_KEY=${ENC_KEY}
|
||
SURREAL_USER=root
|
||
SURREAL_PASSWORD=${DB_PASS}
|
||
OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||
EOF
|
||
chmod 600 .env
|
||
```
|
||
|
||
`.env` wird nicht committed (siehe `.gitignore`). `.env.example` ist das Template.
|
||
|
||
### 2.3 Container starten
|
||
|
||
```bash
|
||
docker compose up -d
|
||
docker compose ps # beide Container sollten "Up" sein
|
||
```
|
||
|
||
Die Images sind in `docker-compose.yml` auf einen **Digest gepinnt** (`pull_policy: missing`) —
|
||
`up -d` lädt sie beim ersten Mal und danach nie wieder unbemerkt eine andere Version. Aktualisiert
|
||
wird bewusst, siehe [Abschnitt 4](#4-updates).
|
||
|
||
Web-UI: `http://localhost:8502` · REST-API: `http://localhost:5055` (beide nur `127.0.0.1`).
|
||
|
||
### 2.4 Firewall öffnen für TTS/STT
|
||
|
||
Die TTS/STT-Wrapper laufen auf dem Host; der Docker-Container erreicht sie über die
|
||
Docker-Bridge, was bei aktiver `ufw`-Firewall (Standard: deny incoming) explizit erlaubt werden muss —
|
||
analog zur bestehenden Regel für Ollama (`11434/tcp`):
|
||
|
||
```bash
|
||
sudo ufw allow 8901/tcp comment 'open-notebook TTS (chatterbox)'
|
||
sudo ufw allow 8902/tcp comment 'open-notebook STT (faster-whisper)'
|
||
```
|
||
|
||
Ohne diesen Schritt bleiben TTS/STT vom Container aus unerreichbar (Timeout), obwohl sie lokal
|
||
laufen und funktionieren.
|
||
|
||
### 2.5 TTS/STT-Server einrichten
|
||
|
||
```bash
|
||
./scripts/start_services.sh
|
||
```
|
||
|
||
Installiert `services/systemd/*.service` nach `~/.config/systemd/user/`, aktiviert `linger`
|
||
(damit die Dienste auch ohne aktive Login-Session laufen) und startet beide. Sie laufen auf
|
||
GPU 2 — nicht GPU 0/1, um die Desktop-GPU und Ollamas Chat-Modell nicht zu verdrängen.
|
||
|
||
Das Skript ist idempotent und **überlebt Reboots**: die Dienste starten automatisch mit. Status
|
||
und Logs:
|
||
|
||
```bash
|
||
systemctl --user status open-notebook-tts open-notebook-stt
|
||
journalctl --user -u open-notebook-tts -f
|
||
```
|
||
|
||
> Der erste Start dauert bis zu einer Minute, weil whisper `large-v3` geladen wird.
|
||
|
||
Für Stimmklonung mindestens eine Referenz-WAV in `services/voices/default.wav` ablegen (z. B.
|
||
Symlink auf eine eigene Sprachaufnahme, 10–30s, WAV) — siehe
|
||
[`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md#5-stimmklonung).
|
||
|
||
### 2.6 KI-Provider und Modelle registrieren
|
||
|
||
```bash
|
||
./scripts/setup_models.sh
|
||
```
|
||
|
||
Richtet automatisch ein:
|
||
- Ollama-Credential (Chat + Embedding) mit `num_ctx=98304` — **wichtig**, siehe Warnkasten unten
|
||
- Kuratierte Ollama-Modelle: `qwen3.5:27b` (Standard für Chat/Tools/großer Kontext),
|
||
`nomic-embed-text` (Standard-Embedding), `qwen3-coder-30b-128k` (optional für Code)
|
||
- OpenRouter-API-Key aus `.env`/Environment in die verschlüsselte Datenbank migriert
|
||
- Kuratierte OpenRouter-Modelle (Claude Sonnet 5, Qwen3-Max, Gemini 3.5 Flash)
|
||
- Lokale TTS/STT-Wrapper als `openai_compatible`-Credentials + Standardmodelle
|
||
|
||
> **Warum `num_ctx` explizit gesetzt werden muss:** Ohne diese Einstellung begrenzt die
|
||
> zugrunde liegende Bibliothek (`esperanto`) jede Ollama-Chat-Anfrage stillschweigend auf
|
||
> 8192 Tokens Kontext — unabhängig vom eigentlichen Modell-Limit. Größere Chat-Kontexte
|
||
> (z. B. ganze Bücher im „Volltext"-Modus) werden dann abgeschnitten, und weil `qwen3.5:27b`
|
||
> ein „denkendes" Modell ist, kommt dabei oft eine **leere Antwort statt eines Fehlers**
|
||
> heraus. `98304` ist der größte Wert, der auf dieser Hardware (RTX 3090, 24 GB) noch
|
||
> vollständig auf der GPU bleibt (`ollama ps` → `100% GPU`). Details:
|
||
> [`CLAUDE.md`](CLAUDE.md).
|
||
|
||
### 2.7 Verifikation
|
||
|
||
```bash
|
||
curl -s http://127.0.0.1:5055/api/models/providers | python3 -m json.tool
|
||
```
|
||
|
||
Erwartung: `"available": ["openrouter", "ollama"]`. TTS/STT-Gesundheit:
|
||
|
||
```bash
|
||
curl -s http://127.0.0.1:8901/health # Chatterbox TTS
|
||
curl -s http://127.0.0.1:8902/health # faster-whisper STT
|
||
```
|
||
|
||
Gründlicher — prüft API, TTS, STT, YouTube-Extraktion und alle lokalen Anpassungen auf einmal:
|
||
|
||
```bash
|
||
./scripts/smoke_test.sh
|
||
```
|
||
|
||
UI öffnen (`http://localhost:8502`), ein Notebook anlegen, eine Quelle hinzufügen und chatten —
|
||
siehe [`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md) für die weitere Bedienung.
|
||
|
||
---
|
||
|
||
## 3. Alltägliche Befehle
|
||
|
||
```bash
|
||
docker compose up -d # starten
|
||
docker compose down # stoppen (Daten bleiben erhalten)
|
||
docker compose logs -f open_notebook # Logs verfolgen
|
||
docker compose ps # Status
|
||
|
||
systemctl --user status open-notebook-tts open-notebook-stt # TTS/STT-Status
|
||
systemctl --user restart open-notebook-tts # z.B. nach neuer Stimme
|
||
journalctl --user -u open-notebook-tts -f # TTS-Logs
|
||
./scripts/start_services.sh # Units neu installieren + starten (idempotent)
|
||
|
||
./scripts/prune_podcast_data.py # Podcast-Datenmüll anzeigen (Trockenlauf)
|
||
./scripts/prune_podcast_data.py --yes # ... und löschen
|
||
|
||
./scripts/add_video_source.py <url> # Video als Quelle (auch ohne Untertitel)
|
||
|
||
./scripts/check_updates.sh # gibt es ein neues Release? (ändert nichts)
|
||
./scripts/update_stack.sh # Update mit Backup, Rauchtest und Rollback
|
||
./scripts/smoke_test.sh # tut der Stack noch, worauf wir uns verlassen?
|
||
```
|
||
|
||
TTS/STT starten nach einem Reboot von selbst — `start_services.sh` ist nur für die
|
||
Ersteinrichtung bzw. nach Änderungen an den Unit-Dateien nötig.
|
||
|
||
---
|
||
|
||
## 4. Updates
|
||
|
||
Die Images sind **auf einen Digest gepinnt** — es läuft immer genau der Stand, der im Repo steht.
|
||
Weder ein `docker compose up -d` noch ein Reboot tauscht die Anwendung unbemerkt aus.
|
||
|
||
Beim Sitzungsstart (oder wann immer Sie mögen):
|
||
|
||
```bash
|
||
./scripts/check_updates.sh # zeigt nur: läuft X, verfügbar ist Y
|
||
```
|
||
|
||
Wenn ein neues Release da ist:
|
||
|
||
```bash
|
||
./scripts/update_stack.sh # Backup -> Update -> Rauchtest -> bei Fehler Rollback
|
||
```
|
||
|
||
Warum nicht automatisch das Neueste bei jedem Start? Ein Update **migriert die Datenbank**, und
|
||
eine ältere Anwendung kann eine migrierte Datenbank in der Regel nicht mehr lesen — ein
|
||
misslungenes Update ohne Backup wäre eine Einbahnstraße. Zudem hängen die lokalen Anpassungen
|
||
dieses Projekts (Podcast-Vorlagen, content-core-Config, Env-Variablen) an Interna der Anwendung
|
||
und können still brechen. Genau das prüft der Rauchtest; schlägt er fehl, rollt das Skript
|
||
Daten und Compose-Datei automatisch zurück.
|
||
|
||
Hinweis: `v1-latest` folgt den **Releases**, nicht dem `main`-Branch — der Repository-Stand ist
|
||
typischerweise Wochen voraus, aber unveröffentlicht. Wer den will, müsste selbst bauen.
|
||
|
||
---
|
||
|
||
## 5. Verzeichnisstruktur
|
||
|
||
```
|
||
open-notebook/
|
||
├── docker-compose.yml # SurrealDB + open_notebook
|
||
├── .env # Secrets (gitignored)
|
||
├── .env.example # Template
|
||
├── surreal_data/ # DB-Daten (gitignored)
|
||
├── notebook_data/ # Notebook-Dateien/Uploads (gitignored)
|
||
├── config/
|
||
│ └── content_core.yaml # content-core (u.a. deutsche YouTube-Transkripte)
|
||
├── services/
|
||
│ ├── tts_server.py # Chatterbox-Wrapper (OpenAI-API-kompatibel)
|
||
│ ├── stt_server.py # faster-whisper-Wrapper
|
||
│ ├── systemd/ # Unit-Dateien für die beiden Wrapper
|
||
│ ├── voices/ # Referenz-WAVs fürs Voice-Cloning (gitignored)
|
||
│ └── logs/ # (gitignored, Altlast der früheren nohup-Variante)
|
||
├── backups/ # Sicherungen vor Image-Updates (gitignored)
|
||
├── scripts/
|
||
│ ├── setup_models.sh # Provider/Modelle einrichten (idempotent)
|
||
│ ├── start_services.sh # TTS/STT als systemd-User-Dienste installieren+starten
|
||
│ ├── prune_podcast_data.py # verwaiste Podcast-Ordner + Zwischenclips aufräumen
|
||
│ ├── add_video_source.py # Video als Quelle (Untertitel oder Tonspur+Whisper)
|
||
│ ├── check_updates.sh # neues Release verfügbar? (ändert nichts)
|
||
│ ├── update_stack.sh # Update mit Backup/Rauchtest/Rollback
|
||
│ └── smoke_test.sh # prüft die Annahmen, auf denen die Anpassungen beruhen
|
||
├── README.md # diese Datei
|
||
├── BEDIENUNGSANLEITUNG.md # ausführliche Bedienungsanleitung (Deutsch)
|
||
└── CLAUDE.md # technische Architektur-Doku für Weiterentwicklung
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Bekannte Stolpersteine
|
||
|
||
Kurzreferenz — Details jeweils in [`CLAUDE.md`](CLAUDE.md):
|
||
|
||
- **Embedding-Modell muss klein sein.** `qwen3-embedding` (ein zweckentfremdetes 8B-LLM,
|
||
~13 GB geladen) konkurriert mit dem Chat-Modell um GPU-Speicher und fällt dann teilweise auf
|
||
die CPU zurück — einzelne Embeddings dauern dann 40+ Sekunden und reißen Timeouts.
|
||
`nomic-embed-text` (274 MB) ist der Standard hier.
|
||
- **`num_ctx` beim Ollama-Chat-Credential ist Pflicht** (siehe Warnkasten in 2.6) — sonst
|
||
leere Chat-Antworten bei größerem Kontext, ohne sichtbaren Fehler.
|
||
- **Neue Host-Ports brauchen eine `ufw`-Regel**, sonst kann der Container sie nicht erreichen
|
||
(Timeout, keine Fehlermeldung in Open Notebook selbst).
|
||
- **YouTube-Transkripte brauchen `config/content_core.yaml`.** `content-core` sucht Untertitel
|
||
standardmäßig nur in `en`/`es`/`pt` — deutsche Videos landen sonst als **leere Quelle** im
|
||
Notebook (Titel da, Inhalt leer, Fehler nur im Log). Die eingehängte Config setzt Deutsch an
|
||
erste Stelle. Achtung: `CCORE_CONFIG_PATH` ersetzt die Konfiguration komplett (kein Merge) —
|
||
die Datei muss vollständig bleiben, siehe Kopfkommentar darin.
|
||
- **Videos ohne Untertitel** gehen über `./scripts/add_video_source.py <url>` (yt-dlp lädt die
|
||
Tonspur, der lokale Whisper-Server transkribiert sie). Dafür ist
|
||
`OPENAI_COMPATIBLE_BASE_URL_STT` in `docker-compose.yml` zwingend: content-core reicht dem
|
||
STT-Modell sonst keine `base_url` durch, fällt auf OpenAI zurück und scheitert mit
|
||
„OpenAI API key not found" — das ist **kein** fehlender Key, sondern eine fehlende URL. Keinen
|
||
`OPENAI_API_KEY` in den Container legen: das schickte Audio in die Cloud, obwohl Whisper lokal
|
||
auf GPU 2 bereitsteht.
|
||
- **Episoden löschen räumt nicht auf.** Der Mülleimer-Button (bzw.
|
||
`DELETE /api/podcasts/episodes/{id}`) entfernt nur die finale MP3 und den DB-Eintrag — der
|
||
Ordner `notebook_data/podcasts/episodes/<uuid>/` mit den Einzelclips, `outline.json` und
|
||
`transcript.json` bleibt liegen. Das ist eine Lücke im Upstream-Code, kein lokales
|
||
Konfigurationsproblem. Abhilfe: `./scripts/prune_podcast_data.py`.
|
||
- **GPU 2 teilen sich drei Prozesse** (TTS, STT und der separate `chatterbox-tts`-MCP-Dienst auf
|
||
Port 9999) — es bleiben nur ~12 GB Arbeitsspeicher für die Sprachsynthese. Deshalb serialisiert
|
||
`tts_server.py` seine Generierung intern und `TTS_BATCH_SIZE=1` steht in `docker-compose.yml`:
|
||
ohne beides schickt der Podcast-Generator 5 Clips gleichzeitig, die parallel auf der GPU laufen
|
||
und sie mit `CUDA out of memory` sprengen (sichtbar als `HTTP 500` beim Vertonen).
|
||
- **Podcast-Sprache** wird über das `language`-Feld des Episode-Profils gesteuert, aber nur
|
||
dank der gepatchten Prompt-Vorlagen unter `prompts/podcast/` (per Bind-Mount in
|
||
`docker-compose.yml` eingehängt) — die Original-Vorlagen im Image ignorieren `language`, was
|
||
englische Podcasts (von der deutsch-fixierten TTS mit Akzent vorgelesen) verursachte. Die
|
||
Vorlagen enthalten zusätzlich `/no_think`, damit `qwen3.5:27b` bei langem Quellinhalt nicht
|
||
durch überlange Reasoning-Blöcke ungültiges JSON liefert. Änderst du die Vorlagen, danach
|
||
`docker compose up -d --force-recreate open_notebook`. Details:
|
||
[`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md#11-troubleshooting) und [`CLAUDE.md`](CLAUDE.md).
|