diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index d757f51..b6882b8 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -444,6 +444,29 @@ wird abgeschnitten. Behoben durch `/no_think` als erste Zeile beider Podcast-Vor 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 diff --git a/CLAUDE.md b/CLAUDE.md index 2fb698b..b3c5db3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,36 @@ the `clips/` of completed episodes. Two guards matter — it aborts if any job i skips directories touched in the last 60 minutes. **Deletion runs inside the container** (`docker compose exec … rm -rf`), because the container writes as root and the host user can't remove those directories. +### YouTube sources — German transcripts need `CCORE_CONFIG_PATH` + +Open Notebook extracts YouTube via `content-core`, which pulls captions with `youtube-transcript-api` +(no `yt-dlp`/`pytube` in the image). `content_core/processors/youtube.py` only ever looks for the +languages 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 with only a +German transcript raises `NoTranscriptFound`, `get_best_transcript` swallows it and returns `None`, +and the source is created with an **empty body**. The title is still fetched (separate HTTP scrape), +so the failure is quiet: a source that looks fine but has no content, with the real reason only in +the container log (`Failed to get transcript for video after retries`). + +The `youtube_transcripts.preferred_languages` key in the package's `cc_config.yaml` looks like the +fix, but it is **dead code** on the default path: `load_config()` copies only the `extraction` block +out of `cc_config.yaml`, so the key never reaches `CONFIG` and the hardcoded `["en","es","pt"]` +fallback always wins. The only way to set it is `CCORE_CONFIG_PATH`. + +Fix: `config/content_core.yaml` (bind-mounted read-only, `CCORE_CONFIG_PATH=/app/config/content_core.yaml` +in `docker-compose.yml`) sets `preferred_languages: ["de", "en", "es", "pt"]`. + +**`CCORE_CONFIG_PATH` replaces the config wholesale — it does not merge** (`config.py`: +`return yaml.safe_load(file)`). So that file is a full dump of the effective default config plus the +`youtube_transcripts` key, not a small override; a minimal file would silently drop `extraction` +(engine selection) and the model/timeout defaults. Its header comment carries the one-liner that +regenerates it from inside the container — do that after an image update if content-core's defaults +change. The `openai`/`gpt-4o-mini` entries in it are content-core's internal defaults and are unused +here (Open Notebook picks its own models); they're only present because the file must be complete. + +Note this only fixes *caption* languages. A video with no captions at all still yields an empty +source — there is no audio-download-and-transcribe fallback wired in. + ### Text-to-speech / speech-to-text Open Notebook's `openai_compatible` provider type talks to any endpoint implementing OpenAI's audio API shape (`POST /audio/speech`, `POST /audio/transcriptions` — see `esperanto.providers.tts.openai_compatible` / `.stt.openai_compatible` inside the app container for the exact contract). Since neither Ollama nor OpenRouter support TTS/STT, two thin wrapper servers in `services/` expose the locally installed tools this way: diff --git a/README.md b/README.md index b017a0b..ba754b1 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,8 @@ open-notebook/ ├── .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 @@ -226,6 +228,11 @@ Kurzreferenz — Details jeweils in [`CLAUDE.md`](CLAUDE.md): 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. - **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 diff --git a/config/content_core.yaml b/config/content_core.yaml new file mode 100644 index 0000000..9728194 --- /dev/null +++ b/config/content_core.yaml @@ -0,0 +1,78 @@ +# content-core Konfiguration fuer dieses Deployment. +# +# Zweck: YouTube-Quellen mit deutschem Transkript nutzbar machen. +# +# content-core sucht Transkripte nur in "preferred_languages". Der eingebaute +# Standard ist ["en", "es", "pt"] — ohne Deutsch. Ein Video mit ausschliesslich +# deutschem Transkript liefert deshalb NoTranscriptFound, und Open Notebook legt +# die Quelle mit LEEREM Inhalt an (der Titel wird trotzdem geholt, der Fehler +# taucht nur im Log auf). +# +# Der Schluessel youtube_transcripts steht zwar in der cc_config.yaml des Pakets, +# wird von load_config() im Standardfall aber gar nicht eingelesen (dort wird nur +# der Block "extraction" uebernommen) — er ist also wirkungslos. Einziger Weg, +# ihn zu setzen: CCORE_CONFIG_PATH auf eine eigene Datei zeigen lassen. +# +# ACHTUNG: CCORE_CONFIG_PATH ERSETZT die Konfiguration vollstaendig, es wird +# nicht zusammengefuehrt (content_core/config.py: return yaml.safe_load(file)). +# Diese Datei ist deshalb ein vollstaendiger Abzug der effektiven Standardconfig +# plus youtube_transcripts. Bei einem Image-Update mit geaenderten Defaults neu +# erzeugen: +# +# docker compose exec -T open_notebook /app/.venv/bin/python -c " +# from content_core.config import CONFIG; import yaml, copy +# cfg = copy.deepcopy(CONFIG) +# cfg['youtube_transcripts'] = {'preferred_languages': ['de','en','es','pt']} +# print(yaml.safe_dump(cfg, sort_keys=False, allow_unicode=True))" > config/content_core.yaml +# +# Die openai/gpt-4o-mini-Eintraege unten sind content-core-interne Defaults; sie +# werden hier nicht benutzt (Open Notebook waehlt seine Modelle selbst) und +# stehen nur drin, weil die Datei vollstaendig sein muss. +speech_to_text: + provider: openai + model_name: whisper-1 + timeout: 3600 +default_model: + provider: openai + model_name: gpt-4o-mini + config: + temperature: 0.5 + top_p: 1 + max_tokens: 2000 + timeout: 300 +cleanup_model: + provider: openai + model_name: gpt-4o-mini + config: + temperature: 0 + max_tokens: 8000 + output_format: json + timeout: 600 +summary_model: + provider: openai + model_name: gpt-4o-mini + config: + temperature: 0 + top_p: 1 + max_tokens: 2000 + timeout: 300 +extraction: + document_engine: auto + url_engine: auto + audio: + concurrency: 3 + firecrawl: + api_url: null + docling: + output_format: markdown + pymupdf: + enable_formula_ocr: false + formula_threshold: 3 + ocr_fallback: true +youtube_transcripts: + preferred_languages: + - de + - en + - es + - pt + diff --git a/docker-compose.yml b/docker-compose.yml index d09f4a0..c513dc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,9 @@ services: - TTS_BATCH_SIZE=1 # Kopfraum fuer lange Segmente; Default waere 300s pro Clip. - ESPERANTO_TTS_TIMEOUT=600 + # content-core sucht YouTube-Transkripte sonst nur in en/es/pt — deutsche + # Videos landen dann als LEERE Quelle im Notebook. Siehe config/content_core.yaml. + - CCORE_CONFIG_PATH=/app/config/content_core.yaml extra_hosts: - "host.docker.internal:host-gateway" volumes: @@ -50,3 +53,6 @@ services: # nach einem Container-Neustart greifen. Der Ordner im Image enthält nur # genau diese zwei Vorlagen. - ./prompts/podcast:/app/prompts/podcast:ro + # content-core-Konfiguration (u.a. deutsche YouTube-Transkripte), per + # CCORE_CONFIG_PATH oben referenziert. + - ./config:/app/config:ro