Fix YouTube sources with German transcripts landing empty
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>
This commit is contained in:
parent
ba98ac4eb4
commit
2a057d04e1
5 changed files with 144 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
30
CLAUDE.md
30
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 <id> 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:
|
||||
|
|
|
|||
|
|
@ -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/<uuid>/` mit den Einzelclips, `outline.json` und
|
||||
|
|
|
|||
78
config/content_core.yaml
Normal file
78
config/content_core.yaml
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue