open_notebook/BEDIENUNGSANLEITUNG.md
dschlueter 054f867a8a Fix podcasts generating in English + failing with invalid JSON
Two root causes, both fixed in the bind-mounted podcast prompt templates
(prompts/podcast/*.jinja):

1. Language: the episode profile's `language` field IS passed to both
   podcast_creator templates as {{ language }}, but the stock templates never
   reference it — so podcasts came out English (driven only by the English
   stock briefings/speakers), then read aloud by the German-locked Chatterbox
   TTS = "English with a German accent". Added a CRITICAL LANGUAGE REQUIREMENT
   block keyed on {{ language }} to both templates; now `language: "de"`
   actually forces German. Verified end-to-end: a full run produced a 44-line
   all-German transcript + audio.

2. Invalid json output failures: qwen3.5:27b is a thinking model; on long
   segments the <think> block ate the response-token budget and truncated the
   JSON. Prepended /no_think to both templates (~3x faster, valid JSON).

Templates are bind-mounted read-only via docker-compose (directory mount, so
edits survive a container restart without inode-staleness). Bundled briefings
and speaker backstories were also translated to German to reduce drift.

Known limitation documented: feeding an entire book (~70k tokens) as podcast
content makes each of the 6 LLM calls take ~3.5 min and is unreliable; use a
shorter source or summary. Confirmed working with concise content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 02:30:05 +02:00

18 KiB
Raw Blame History

Bedienungsanleitung — Open Notebook (lokales Deployment)

Diese Anleitung deckt die tägliche Benutzung ab. Installation/Neuaufbau: siehe README.md. Technische Hintergründe und Architektur: siehe CLAUDE.md.


Inhalt

  1. Zugriff
  2. Notebooks und Quellen
  3. Chat: die drei Kontext-Modi
  4. Podcasts erzeugen
  5. Stimmklonung
  6. Modelle und Provider verwalten
  7. Dienste starten/stoppen
  8. Suche über Chunks (Search/Ask)
  9. Die API in eigenen Programmen nutzen
  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 180350 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:
    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.


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).

Eine weitere Stimme hinzufügen

  1. Referenz-Aufnahme besorgen: 1030 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).
  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):
    pkill -f "python tts_server.py"
    ./scripts/start_services.sh
    
  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:
    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):

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:

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.

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

# Open Notebook (Docker)
docker compose up -d
docker compose down

# TTS/STT (lokale Hintergrundprozesse, nicht in Docker)
./scripts/start_services.sh
pkill -f "python tts_server.py"
pkill -f "python stt_server.py"

Ollama selbst läuft als systemd-Dienst und muss normalerweise nicht manuell verwaltet werden:

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

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 (01), 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).

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

# 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

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:

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:

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

docker compose exec open_notebook curl -sf http://host.docker.internal:8901/health

Bei Timeout: ufw-Regeln fehlen (siehe README 2.4) oder die Server laufen nicht (./scripts/start_services.sh).

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:

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.

Weitere technische Details

Siehe CLAUDE.md — dort stehen alle bisher aufgetretenen Probleme mit exakter Ursache (Codestellen, Bibliotheks-Bugs) dokumentiert.