Adding a German YouTube video created a source with a title but no content — nothing to chat with, nothing to summarise. The failure was silent in the UI; the reason only showed up in the container log as "Failed to get transcript for video <id> after retries: No suitable transcript found". Open Notebook extracts YouTube via content-core, which reads captions with youtube-transcript-api. content_core/processors/youtube.py only looks for the languages listed in preferred_languages, and the built-in default is ["en", "es", "pt"] — no German. All four fallback attempts in _fetch_best_transcript use that same list, so a video carrying only a German transcript raises NoTranscriptFound, get_best_transcript swallows it and returns None, and the source is stored with an empty body. The title still arrives because it is scraped separately, which is what makes this look like a success. The youtube_transcripts.preferred_languages key in the package's own cc_config.yaml appears to be the knob for this, but it is dead code on the default path: load_config() copies only the "extraction" block out of that file, so the key never reaches CONFIG and the hardcoded fallback always wins. The only way to set it is CCORE_CONFIG_PATH. config/content_core.yaml (bind-mounted read-only) therefore sets preferred_languages: ["de", "en", "es", "pt"]. Note CCORE_CONFIG_PATH REPLACES the config wholesale rather than merging it (config.py: return yaml.safe_load(file)). The file is consequently a full dump of the effective default config plus the new key — a minimal override file would silently drop the extraction engines and the model/timeout defaults. Its header comment carries the command that regenerates it from inside the container, for when an image update changes content-core's defaults. Verified end to end on the reported video (hzxiegk9QAg): extraction now yields 21553 characters of German transcript, the source is created through POST /api/sources with that full text, embedding produces 17 chunks in 3.4s, and vector search returns the video as top hit. This only covers caption languages. A video with no captions at all still yields an empty source — there is no download-and-transcribe fallback in the image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
499 lines
22 KiB
Markdown
499 lines
22 KiB
Markdown
# 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 **Dateigröß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": "<id aus GET /api/transformations>"}'
|
||
```
|
||
- 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/<uuid>/
|
||
├── clips/ ← 1 MP3 pro Dialogsegment BLEIBT LIEGEN
|
||
├── outline.json BLEIBT LIEGEN
|
||
├── transcript.json BLEIBT LIEGEN
|
||
└── audio/<uuid>.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/<name>.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 `<name>` in der `voices`-Liste zeigen.
|
||
5. In einem Sprecherprofil (Podcast) den betreffenden Sprecher auf `voice_id: "<name>"` 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 <stimme>.onnx --output_file services/voices/<name>.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": "<model_id>"}'
|
||
```
|
||
|
||
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.
|