# Bedienungsanleitung — Open Notebook (lokales Deployment) Diese Anleitung deckt die **tägliche Benutzung** ab. Installation/Neuaufbau: siehe [`README.md`](README.md). Technische Hintergründe und Architektur: siehe [`CLAUDE.md`](CLAUDE.md). --- ## Inhalt 1. [Zugriff](#1-zugriff) 2. [Notebooks und Quellen](#2-notebooks-und-quellen) 3. [Chat: die drei Kontext-Modi](#3-chat-die-drei-kontext-modi) 4. [Podcasts erzeugen](#4-podcasts-erzeugen) 5. [Stimmklonung](#5-stimmklonung) 6. [Modelle und Provider verwalten](#6-modelle-und-provider-verwalten) 7. [Dienste starten/stoppen](#7-dienste-startenstoppen) 8. [Suche über Chunks (Search/Ask)](#8-suche-über-chunks-searchask) 9. [Die API in eigenen Programmen nutzen](#9-die-api-in-eigenen-programmen-nutzen) 10. [Troubleshooting](#10-troubleshooting) --- ## 1. Zugriff - **Web-Oberfläche:** `http://localhost:8502` - **REST-API** (für Automatisierung/Skripte): `http://localhost:5055`, Doku unter `http://localhost:5055/docs` Beide sind nur von diesem Rechner aus erreichbar (`127.0.0.1`), nicht aus dem Netzwerk. --- ## 2. Notebooks und Quellen Ein **Notebook** bündelt Quellen, Notizen, Chats und Podcasts zu einem Thema. Neues Notebook über „+ New Notebook" in der UI anlegen. ### Quellen hinzufügen Unterstützt: hochgeladene Dateien (PDF, EPUB, TXT, DOCX, …), URLs, eingefügter Text. **Wie lang darf eine Datei sein?** Es gibt kein hartes Limit für Upload/Verarbeitung — auch sehr große PDFs/EPUBs werden vollständig extrahiert und in Textabschnitte („Chunks") zerlegt, die für die Suche eingebettet werden. Eine Längenbegrenzung greift nur, wenn eine Quelle im **„Volltext"-Kontextmodus** (siehe Abschnitt 3) oder für eine **Transformation** verwendet wird — dort geht der komplette Text in einem Stück an das Sprachmodell. Praktischer Richtwert bei `qwen3.5:27b` (98.304 Tokens Kontext, siehe Abschnitt 6): grob **180–350 Buchseiten**, abhängig von Textdichte. Wichtig: Die **Datei­größe in MB sagt nichts über die Textmenge aus** — ein gescanntes PDF mit vielen Bildern kann riesig sein, aber nach der Texterkennung kaum Inhalt liefern. ### Verarbeitungsstatus prüfen Nach dem Hochladen läuft die Verarbeitung (Extraktion → Chunking → Embedding) im Hintergrund. Bei großen Dateien kann das ein bis mehrere Minuten dauern. Status ist in der UI an der Quelle sichtbar; per API: `GET /api/sources/{source_id}/status`. --- ## 3. Chat: die drei Kontext-Modi Beim Chatten mit einem Notebook lässt sich pro Quelle einstellen, wie viel davon in den Chat-Kontext einfließt (Buttons/Auswahl in der UI, z. B. als Sammel-Aktion für alle Quellen): | UI-Option | Bedeutung | Geschwindigkeit | |---|---|---| | **Alle aus dem Kontext ausschließen** | Quelle wird ignoriert | – | | **Alle aufnehmen (nur Erkenntnisse)** | Nutzt die generierten „Insights" (Zusammenfassungen) der Quelle | schnell (Sekunden) | | **Alle aufnehmen (vollständiger Inhalt)** | Kompletter Text der Quelle geht an das Sprachmodell | langsam bei langen Dokumenten (bis zu einigen Minuten) | ### Wichtig: „nur Erkenntnisse" braucht vorher eine Transformation Der Modus „nur Erkenntnisse" ist **nur dann nützlich, wenn für die Quelle bereits eine Zusammenfassung („Insight") erzeugt wurde**. Ohne das liefert er praktisch nichts (nur Titel/ Metadaten). Eine Zusammenfassung erzeugen: - In der UI: bei der Quelle eine Transformation anwenden, z. B. **„Simple Summary"** (für die meisten Fälle passend) oder „Dense Summary", „Key Insights", „Table of Contents". - Per API: ```bash curl -X POST http://127.0.0.1:5055/api/sources/{source_id}/insights \ -H "Content-Type: application/json" \ -d '{"transformation_id": ""}' ``` - Bei langen Dokumenten (z. B. ein ganzes Buch) dauert das einmalig **einige Minuten** (die gesamte Quelle wird einmal durchs Sprachmodell geschickt) — danach ist „nur Erkenntnisse" aber dauerhaft schnell nutzbar. ### Faustregel - **Normale Fragen, Überblick, grobe Zusammenhänge** → „nur Erkenntnisse" (nach einmaliger Zusammenfassung) - **Sehr spezifische Detailfrage** (z. B. exaktes Zitat aus einem bestimmten Kapitel), die in der Zusammenfassung fehlen könnte → „vollständiger Inhalt", dafür Geduld bei langen Quellen - **Quelle für die aktuelle Frage irrelevant** → ausschließen (spart Zeit bei mehreren Quellen) Für gezielte Detailsuche über viele/lange Quellen gibt es außerdem eine separate **Such-/Ask-Funktion**, die per Embedding-Ähnlichkeitssuche automatisch nur die relevanten Textstellen findet, statt ganze Dokumente einzubeziehen — meist die schnellste und treffsicherste Option für Detailfragen in langen Werken. --- ## 4. Podcasts erzeugen Open Notebook kann aus Notebook-Inhalten automatisch einen Podcast (Dialog zwischen ein oder mehreren Sprechern) erzeugen — Text via Sprachmodell, Audio via TTS (hier: lokal, Chatterbox). 1. **Episode-Profil** wählen (bestimmt Sprecherzahl/-rollen, Anzahl Segmente, Textmodell): `solo_expert` (1 Sprecher), `tech_discussion` (2 Sprecher), `business_analysis` (3 Sprecher). 2. Notebook/Quelle als Grundlage angeben. 3. Generierung starten — läuft asynchron (Outline → Transkript → Sprachsynthese pro Segment). Bei mehreren Sprechern und langen Transkripten realistisch **mehrere Minuten**, da jedes Dialogsegment einzeln per TTS synthetisiert wird. 4. Fertige Episode in der UI abspielbar/herunterladbar. Alle Episode-/Sprecherprofile sind bereits auf lokale Modelle (`qwen3.5:27b` für Text, Chatterbox für Sprache) umgestellt — keine zusätzlichen Kosten. ### Episoden löschen räumt nur halb auf **Der Mülleimer-Button löscht nicht alles.** Er entfernt die finale MP3 und den Datenbankeintrag — der Ordner der Episode bleibt aber liegen, samt Einzelclips: ``` notebook_data/podcasts/episodes// ├── clips/ ← 1 MP3 pro Dialogsegment BLEIBT LIEGEN ├── outline.json BLEIBT LIEGEN ├── transcript.json BLEIBT LIEGEN └── audio/.mp3 ← nur diese wird gelöscht ``` Das ist eine Lücke im Open-Notebook-Code selbst (der Löschpfad ruft `unlink()` nur auf die eine MP3-Datei auf), nicht an dieser Installation einstellbar. Ebenso hinterlässt jeder fehlgeschlagene oder per Retry wiederholte Lauf einen Ordner, der nie aufgeräumt wird. Zum Aufräumen dient: ```bash ./scripts/prune_podcast_data.py # zeigt nur an, was wegfiele (Trockenlauf) ./scripts/prune_podcast_data.py --yes # löscht wirklich ``` Es entfernt (a) Ordner ohne zugehörige Episode und (b) die `clips/` fertiger Episoden — die Einzelclips sind nach dem Zusammenbau reines Zwischenprodukt, die Episode bleibt vollständig abspielbar. Schutzmechanismen: bei einem laufenden Podcast-Job bricht es ab, und Ordner, die jünger als 60 Minuten sind, bleiben unangetastet (`--min-age`). Mit `--keep-clips` werden nur die verwaisten Ordner gelöscht. --- ## 5. Stimmklonung Die TTS-Wrapper (`services/tts_server.py`) unterstützt Stimmklonung: Referenz-WAV rein, Stimme raus. Aktuell hinterlegte Stimmen: `default` (eigene Stimme), `male_thorsten` und `female_kerstin` (offen lizenzierte, synthetische Piper-Stimmen — bewusst **keine** ungefragt geklonten echten Personen, siehe [`CLAUDE.md`](CLAUDE.md)). ### Eine weitere Stimme hinzufügen 1. Referenz-Aufnahme besorgen: **10–30 Sekunden**, eine Person, saubere Aufnahme, WAV-Format. Rechtlich sauber ist entweder eine eigene Aufnahme, eine Aufnahme mit ausdrücklicher Zustimmung der Person, oder eine offen für diesen Zweck lizenzierte Stimme (z. B. weitere [Piper-Stimmen](https://huggingface.co/rhasspy/piper-voices)). 2. Datei nach `services/voices/.wav` legen (z. B. `services/voices/elena.wav`). 3. TTS-Server neu starten (Stimmen werden nur beim Start eingelesen): ```bash systemctl --user restart open-notebook-tts ``` 4. Prüfen: `curl http://127.0.0.1:8901/health` sollte `` in der `voices`-Liste zeigen. 5. In einem Sprecherprofil (Podcast) den betreffenden Sprecher auf `voice_id: ""` setzen, z. B. per API: ```bash curl -X PUT http://127.0.0.1:5055/api/speaker-profiles/{id} \ -H "Content-Type: application/json" -d '{... "speakers": [{"name": "Elena", ..., "voice_id": "elena"}], ...}' ``` Mit Piper lässt sich auch selbst eine Referenz-Stimme synthetisieren (statt eine echte Aufnahme zu verwenden): ```bash echo "Ein Beispielsatz auf Deutsch." | piper --model .onnx --output_file services/voices/.wav ``` Weitere deutsche Piper-Stimmen: `huggingface.co/rhasspy/piper-voices/tree/main/de/de_DE` (Lizenz jeweils im `MODEL_CARD` prüfen). --- ## 6. Modelle und Provider verwalten Übersicht unter **Settings → AI Providers / Models** in der UI, oder per API: ```bash curl http://127.0.0.1:5055/api/models | python3 -m json.tool # alle registrierten Modelle curl http://127.0.0.1:5055/api/models/defaults | python3 -m json.tool # aktuelle Standardmodelle ``` ### Standardmodelle ändern Sieben Rollen lassen sich unabhängig belegen: Chat, Transformation, großer Kontext, Embedding, Tools, Text-to-Speech, Speech-to-Text. ```bash curl -X PUT http://127.0.0.1:5055/api/models/defaults \ -H "Content-Type: application/json" \ -d '{"default_tools_model": ""}' ``` Der Endpunkt aktualisiert nur die tatsächlich mitgeschickten Felder — nicht angegebene Felder bleiben unverändert, es muss also nicht der komplette Satz aller sieben Modelle mitgeschickt werden. ### Kostenübersicht der aktuell registrierten Modelle | Modell | Provider | Kosten | |---|---|---| | `qwen3.5:27b` | Ollama (lokal) | 0 € (Standard für Chat/Tools/großer Kontext) | | `nomic-embed-text` | Ollama (lokal) | 0 € (Standard-Embedding) | | `qwen3-coder-30b-128k` | Ollama (lokal) | 0 € (optional, für Coding-Aufgaben) | | Chatterbox TTS / faster-whisper | lokal | 0 € | | `anthropic/claude-sonnet-5` | OpenRouter | ~2$/10$ pro Mio. Tokens (Prompt/Antwort) | | `qwen/qwen3-max` | OpenRouter | ~0,78$/3,90$ pro Mio. Tokens — günstigere Cloud-Alternative | | `google/gemini-3.5-flash` | OpenRouter | ~1,50$/9$ pro Mio. Tokens — 1M Tokens Kontext | Weitere OpenRouter-Modelle lassen sich jederzeit über `POST /api/models` registrieren (`provider: "openrouter"`) — aktuelle Preise: `https://openrouter.ai/api/v1/models`. --- ## 7. Dienste starten/stoppen ```bash # Open Notebook (Docker) docker compose up -d docker compose down # TTS/STT (auf dem Host, nicht in Docker — systemd-User-Dienste) systemctl --user status open-notebook-tts open-notebook-stt systemctl --user restart open-notebook-tts open-notebook-stt systemctl --user stop open-notebook-tts open-notebook-stt journalctl --user -u open-notebook-tts -f # Logs ``` TTS/STT starten nach einem Reboot **automatisch** mit. Nur bei der Ersteinrichtung (oder nach Änderungen an `services/systemd/*.service`) einmal `./scripts/start_services.sh` ausführen — das installiert die Unit-Dateien und aktiviert den Autostart. Ollama selbst läuft als systemd-Dienst und muss normalerweise nicht manuell verwaltet werden: ```bash systemctl status ollama ollama ps # aktuell geladene Modelle + GPU/CPU-Verteilung ``` --- ## 8. Suche über Chunks (Search/Ask) Jedes Dokument wird beim Hochladen automatisch in feste, überlappende Textabschnitte ("Chunks", ~400 Tokens) zerlegt und eingebettet — **nicht** kapitelweise, sondern rein größenbasiert (siehe Abschnitt 2). Um gezielt in diesen Chunks zu suchen, gibt es zwei API-Endpunkte (in der UI vermutlich unter einem eigenen Menüpunkt wie „Search"/„Ask" mit Lupen-Symbol, getrennt von „Mit Notebook chatten" — die genaue Beschriftung war ohne Browser-Zugriff nicht zu verifizieren): ### Einfache Suche — schnell, liefert Roh-Textstellen ```bash curl -s -X POST http://127.0.0.1:5055/api/search \ -H "Content-Type: application/json" \ -d '{"query": "Suchbegriff", "type": "vector", "limit": 5}' ``` Liefert die ähnlichsten Chunks per Embedding-Vergleich samt Ähnlichkeits-Score (0–1), ohne KI-Interpretation — am schnellsten, aber du musst die Antwort selbst zusammensetzen. ### „Ask" — durchsuchen und direkt beantworten lassen Die smartere Variante: Die KI zerlegt die Frage selbst in mehrere gezielte Suchanfragen, durchsucht die Chunks für jede getrennt und fasst alles zu einer einzigen, mit Quellenverweisen (`[source:...]`) belegten Antwort zusammen. Getestet mit „Wie kam Wallenstein ums Leben?" — Ergebnis war eine vollständige, korrekt zitierte Antwort in ca. 3 Minuten (mehrere LLM-Aufrufe hintereinander: Suchstrategie → Einzelantworten → Zusammenfassung). ```bash MODEL_ID=$(curl -s http://127.0.0.1:5055/api/models/defaults | python3 -c "import json,sys; print(json.load(sys.stdin)['default_chat_model'])") curl -N -X POST http://127.0.0.1:5055/api/search/ask \ -H "Content-Type: application/json" \ -d "{\"question\": \"Deine Frage\", \"strategy_model\": \"$MODEL_ID\", \"answer_model\": \"$MODEL_ID\", \"final_answer_model\": \"$MODEL_ID\"}" ``` Antwort kommt als Server-Sent-Events-Stream (`data: {...}`-Zeilen, kein einzelnes JSON-Objekt); das letzte `"type": "complete"`-Event enthält die finale Antwort im Feld `final_answer`. **Wann was nutzen:** Für gezielte Detailfragen über lange/viele Quellen ist „Ask" meist treffsicherer und dabei noch schneller als der „vollständiger Inhalt"-Chat-Modus (Abschnitt 3), weil nur die relevanten Chunks statt des ganzen Dokuments verarbeitet werden. --- ## 9. Die API in eigenen Programmen nutzen Die REST-API ist eine ganz normale JSON-über-HTTP-API **ohne Authentifizierung** — kein API-Key nötig, da nur `127.0.0.1` gebunden (jeder lokale Prozess auf diesem Rechner hat Zugriff, aber niemand aus dem Netzwerk). Funktioniert mit jeder Sprache, die HTTP kann. **Bester Startpunkt:** `http://127.0.0.1:5055/docs` — interaktive Swagger-UI mit allen Endpunkten, Schemas und einer „Try it out"-Funktion zum direkten Ausprobieren im Browser. Rohes OpenAPI-Schema: `http://127.0.0.1:5055/openapi.json`. ### Mit curl ```bash # Notebooks auflisten curl -s http://127.0.0.1:5055/api/notebooks | python3 -m json.tool # Neues Notebook anlegen curl -s -X POST http://127.0.0.1:5055/api/notebooks \ -H "Content-Type: application/json" \ -d '{"name": "Mein Notebook", "description": "..."}' # Text-Quelle hinzufügen (multipart, nicht JSON) curl -s -X POST http://127.0.0.1:5055/api/sources \ -F "type=text" -F "notebook_id=notebook:xxx" -F "title=..." \ -F "content=..." -F "embed=true" # Chat-Session erstellen + Nachricht senden curl -s -X POST http://127.0.0.1:5055/api/chat/sessions \ -H "Content-Type: application/json" -d '{"notebook_id": "notebook:xxx"}' curl -s -X POST http://127.0.0.1:5055/api/chat/execute \ -H "Content-Type: application/json" \ -d '{"session_id": "chat_session:xxx", "message": "...", "context": {}}' ``` Weitere Beispiele (Quellen-Insights, Podcast-Generierung, Modell-/Provider-Verwaltung, Chunk-Suche) stehen bereits in den jeweiligen Abschnitten dieser Anleitung. ### Aus Python ```python import requests BASE = "http://127.0.0.1:5055/api" notebooks = requests.get(f"{BASE}/notebooks").json() session = requests.post(f"{BASE}/chat/sessions", json={"notebook_id": notebooks[0]["id"]}).json() answer = requests.post(f"{BASE}/chat/execute", json={ "session_id": session["id"], "message": "Wovon handelt das Buch?", "context": {}, }).json() print(answer["messages"][-1]["content"]) ``` Genauso mit JavaScript (`fetch`), Go, oder jeder anderen Sprache — nichts Open-Notebook- Spezifisches nötig, kein SDK erforderlich. --- ## 10. Troubleshooting ### „Chat gibt keine Antwort" (leere Antwort, kein Fehler) Meist ein zu kleiner Kontext (`num_ctx`) am Ollama-Chat-Credential — siehe README, Abschnitt 2.6. Prüfen: ```bash curl -s http://127.0.0.1:5055/api/credentials | python3 -c " import json,sys for c in json.load(sys.stdin): if c['provider']=='ollama': print(c['name'], c['num_ctx'])" ``` Sollte `98304` (oder höher, dann ggf. auf GPU-Auslastung achten) zeigen, nicht `None`/`null`. ### Quelle hängt beim Verarbeiten / Einbetten fest (Endlos-Retry) Meist Ressourcen-Engpass beim Embedding-Modell (GPU voll, Modell läuft teilweise auf CPU, Timeout). Prüfen: ```bash ollama ps # PROCESSOR-Spalte: sollte "100% GPU" zeigen, nicht "xx%/yy% CPU/GPU" ``` Falls CPU-Anteil: Embedding-Modell ist zu groß für den freien GPU-Speicher. Standardmäßig ist hier das kleine `nomic-embed-text` (274 MB) konfiguriert, das läuft normalerweise problemlos komplett auf der GPU. ### Antwort dauert sehr lange (mehrere Minuten) Meist normal bei „vollständiger Inhalt"-Modus auf langen Dokumenten (siehe Abschnitt 3) — das Sprachmodell muss den kompletten Text lesen. Für Detailfragen auf lange Sicht besser die Such-/Ask-Funktion oder „nur Erkenntnisse" nach einer Zusammenfassung nutzen. ### TTS/STT vom Container aus nicht erreichbar ```bash docker compose exec open_notebook curl -sf http://host.docker.internal:8901/health ``` Bei Timeout: `ufw`-Regeln fehlen (siehe README 2.4) oder die Dienste laufen nicht (`systemctl --user status open-notebook-tts open-notebook-stt`). ### Podcast-Profil zeigt „Einrichtung erforderlich" Das Episode- oder Sprecherprofil verweist auf ein Modell ohne konfiguriertes Credential (z. B. nach einem Reset auf die mitgelieferten OpenAI-Standardprofile). Modell-IDs über `PUT /api/episode-profiles/{id}` (`outline_llm`, `transcript_llm`) bzw. `PUT /api/speaker-profiles/{id}` (`voice_model`) auf registrierte lokale/OpenRouter-Modelle umstellen — siehe `scripts/setup_models.sh` für die IDs der Standardmodelle. ### Podcast wird auf Englisch statt Deutsch erzeugt (oder auf Englisch mit deutschem Akzent vorgelesen) Das ist behoben: Die Podcast-Prompt-Vorlagen (`prompts/podcast/outline.jinja` und `transcript.jinja` in diesem Projekt) wurden um eine explizite Sprachanweisung ergänzt, die das `language`-Feld des Episode-Profils auswertet — sie werden per Bind-Mount aus `docker-compose.yml` in den Container eingehängt. Setzt du am Episode-Profil `language: "de"`, kommt der Podcast auf Deutsch. (Vorher hatte das `language`-Feld keine Wirkung, weil die mitgelieferten Vorlagen es ignorierten — daher englischer Text, der von der deutsch-fixierten TTS mit Akzent vorgelesen wurde.) Falls doch wieder Englisch erscheint, prüfe das `language`-Feld: ```bash curl -s http://127.0.0.1:5055/api/episode-profiles | python3 -c \ "import json,sys; [print(p['name'], p['language']) for p in json.load(sys.stdin)]" ``` Fehlt `de`/`de-DE`, per `PUT /api/episode-profiles/{id}` (vollständiger Body) ergänzen. Zusätzlich sind Briefing und Sprecherbeschreibungen der mitgelieferten Profile auf Deutsch übersetzt (hilft gegen Sprach-Drift, wenn der Quellinhalt englisch ist). **Wichtig:** Änderst du die Vorlagen-Dateien selbst, ist danach ein Container-Neustart nötig (`docker compose up -d --force-recreate open_notebook`), damit der Bind-Mount die neue Version übernimmt. ### Podcast-Erzeugung schlägt mit „Invalid json output" fehl Das Textmodell hat ungültiges JSON geliefert — passierte bei `qwen3.5:27b` (einem „denkenden" Modell), wenn der gesamte Notebook-Inhalt (bei einem ganzen Buch ~70.000 Tokens) in jeden Transkript-Abschnitt gepackt wird: Der Denk-Block frisst dann das Antwort-Budget auf und das JSON wird abgeschnitten. Behoben durch `/no_think` als erste Zeile beider Podcast-Vorlagen (schaltet den Denkmodus ab, ~3× schneller, volles Budget fürs JSON). Falls es dennoch auftritt: kürzeren Quellinhalt verwenden (nicht das ganze Buch) oder die Generierung erneut starten. ### YouTube-Video wird als leere Quelle angelegt Behoben. Symptom war: Quelle hinzufügen scheint zu klappen (der **Titel** erscheint), aber es gibt keinen Inhalt — kein Text, nichts zum Chatten, nichts zum Zusammenfassen. Ursache: Open Notebook holt YouTube-Untertitel über die Bibliothek `content-core`, und die suchte nur nach Transkripten in **Englisch, Spanisch und Portugiesisch**. Ein Video mit ausschließlich deutschem Transkript fiel damit durch; der Fehler stand nur im Container-Log, in der Oberfläche sah die Quelle einfach leer aus. Behoben durch `config/content_core.yaml` (per Bind-Mount eingehängt), die Deutsch an erste Stelle setzt. Falls doch mal ein Video leer bleibt, ist meist **gar kein Untertitel** vorhanden (weder manuell noch automatisch) — das lässt sich so prüfen: ```bash docker compose exec open_notebook /app/.venv/bin/python -c " from youtube_transcript_api import YouTubeTranscriptApi for t in YouTubeTranscriptApi().list('VIDEO_ID'): print(t.language_code, t.is_generated)" ``` Kommt hier nichts zurück, hat das Video keine Untertitel — dann hilft nur, das Video separat herunterzuladen und über die STT-Funktion (Audio-Upload) zu transkribieren. ### Podcast schlägt beim Vertonen fehl („Failed to generate speech") Text und Transkript sind fertig, erst die Sprachausgabe scheitert. Die genaue Fehlermeldung (UI-Episodenliste oder `docker compose logs open_notebook`) unterscheidet **zwei verschiedene Ursachen** — die Verwechslung kostet sonst viel Zeit: | Meldung | Bedeutung | Abhilfe | |---|---|---| | `All connection attempts failed` (`ConnectError`) | Der TTS-Server **läuft nicht** — niemand nimmt den Request an. | `systemctl --user status open-notebook-tts`, ggf. `restart`. | | `OpenAI-compatible TTS endpoint error: HTTP 500` | Der TTS-Server **lebt**, bricht aber bei der Arbeit ab — meist `CUDA out of memory`. | `journalctl --user -u open-notebook-tts -n 50` prüfen. | Der GPU-Speicherfall entstand so: GPU 2 teilen sich drei Prozesse (TTS, STT und der separate `chatterbox-tts`-MCP-Dienst auf Port 9999), es bleiben nur ~12 GB übrig. Der Podcast-Generator schickte aber 5 Clips gleichzeitig, die im TTS-Server echt parallel auf derselben GPU liefen — der Speicherbedarf vervielfachte sich (bis 16,7 GB) und sprengte die Karte. Behoben durch zwei Maßnahmen: `tts_server.py` serialisiert seine Generierung intern (ein Clip zur Zeit — auf einer einzelnen GPU kostet das keinen Durchsatz), und `TTS_BATCH_SIZE=1` in `docker-compose.yml` sorgt dafür, dass gar nicht erst mehrere Anfragen gleichzeitig eintreffen. Ein fehlgeschlagener Podcast lässt sich anschließend ohne Neuanlage wiederholen: ```bash curl -X POST http://127.0.0.1:5055/api/podcasts/episodes/{episode_id}/retry ``` (Der Retry legt einen neuen Episoden-Eintrag an; der alte, fehlgeschlagene verschwindet.) ### Weitere technische Details Siehe [`CLAUDE.md`](CLAUDE.md) — dort stehen alle bisher aufgetretenen Probleme mit exakter Ursache (Codestellen, Bibliotheks-Bugs) dokumentiert.