# 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 ` 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`** --- ## 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 < 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#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 ``` 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 # Video als Quelle (auch ohne Untertitel) ``` 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. 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) ├── 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 ├── README.md # diese Datei ├── BEDIENUNGSANLEITUNG.md # ausführliche Bedienungsanleitung (Deutsch) └── CLAUDE.md # technische Architektur-Doku für Weiterentwicklung ``` --- ## 5. 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 ` (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//` 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#10-troubleshooting) und [`CLAUDE.md`](CLAUDE.md).