2026-10-10 19:51:32 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Lädt die Untertitel eines YouTube-Videos und speichert ihren Text als Datei
|
|
|
|
|
(yt_untertitel.py + vtt2txt.py in einem Schritt)."""
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import argparse
|
|
|
|
|
import shutil
|
2026-10-11 20:29:34 +02:00
|
|
|
import sys
|
2026-10-10 19:51:32 +02:00
|
|
|
import tempfile
|
2026-10-10 19:53:42 +02:00
|
|
|
import textwrap
|
2026-10-10 19:51:32 +02:00
|
|
|
|
2026-10-11 20:12:49 +02:00
|
|
|
from vtt2txt import (
|
2026-10-11 20:29:34 +02:00
|
|
|
add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments,
|
|
|
|
|
time_suffix, vtt_to_text,
|
2026-10-11 20:12:49 +02:00
|
|
|
)
|
2026-10-10 19:51:32 +02:00
|
|
|
from yt_untertitel import (
|
2026-10-10 21:03:14 +02:00
|
|
|
YT_DLP, choose_track, default_name, download, fetch_info, no_track_message,
|
feat: Arbeitsordner je Video, Quellenangabe und automatisches Video-Glossar
- Ohne -o/-d schreiben yt_untertitel.py, extract_text.py und
abschrift_korrigieren.py -u in abschriften/<Name>/ (ein Ordner je
Video, auch für alle Ausschnitte).
- Beim Download entsteht neben dem Rohtext <Rohtext>.quelle.json mit
Titel, Kanal, Datum und URL, für den Quellenkopf des fertigen Textes.
- Ein Glossar, das wie sein Ordner heißt (abschriften/<Name>/<Name>.glossar.txt),
nutzen abschrift_korrigieren.py, glossar_vorschlag.py und aenderungen.py
automatisch, nach den mit -g angegebenen gemeinsamen Glossaren.
- .gitignore: abschriften/, fertig/, *.quelle.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:24:32 +02:00
|
|
|
original_language, work_folder, write_source,
|
2026-10-10 19:51:32 +02:00
|
|
|
)
|
|
|
|
|
|
2026-10-10 19:53:42 +02:00
|
|
|
FLOW_WIDTH = 80 # Zeilenbreite für --fliesstext
|
|
|
|
|
|
2026-10-10 19:51:32 +02:00
|
|
|
|
|
|
|
|
def parse_args():
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
|
description="Lädt die Untertitel eines YouTube-Videos und speichert den Text."
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument("-u", "--url", required=True, help="YouTube-URL des Videos")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"-l", "--lang",
|
|
|
|
|
help="Sprache der Untertitel, z. B. de (Standard: Sprache des Original-Audios)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
2026-10-10 21:03:14 +02:00
|
|
|
"-o", "--output", type=Path,
|
feat: Arbeitsordner je Video, Quellenangabe und automatisches Video-Glossar
- Ohne -o/-d schreiben yt_untertitel.py, extract_text.py und
abschrift_korrigieren.py -u in abschriften/<Name>/ (ein Ordner je
Video, auch für alle Ausschnitte).
- Beim Download entsteht neben dem Rohtext <Rohtext>.quelle.json mit
Titel, Kanal, Datum und URL, für den Quellenkopf des fertigen Textes.
- Ein Glossar, das wie sein Ordner heißt (abschriften/<Name>/<Name>.glossar.txt),
nutzen abschrift_korrigieren.py, glossar_vorschlag.py und aenderungen.py
automatisch, nach den mit -g angegebenen gemeinsamen Glossaren.
- .gitignore: abschriften/, fertig/, *.quelle.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:24:32 +02:00
|
|
|
help="Ausgabedatei (Standard: abschriften/<Name>/<Name>.txt mit "
|
|
|
|
|
"<Name> = <Kanal>_<Titel>_<Datum>, mit -z zusätzlich die Bereiche)",
|
2026-10-10 19:51:32 +02:00
|
|
|
)
|
2026-10-10 19:53:42 +02:00
|
|
|
parser.add_argument(
|
|
|
|
|
"-f", "--fliesstext", action="store_true",
|
|
|
|
|
help=f"Fließtext, auf {FLOW_WIDTH} Zeichen umbrochen, statt Untertitelzeilen",
|
|
|
|
|
)
|
2026-10-11 19:07:56 +02:00
|
|
|
add_time_options(parser)
|
2026-10-11 20:29:34 +02:00
|
|
|
args = parser.parse_args(range_argv(sys.argv[1:]))
|
2026-10-11 20:12:49 +02:00
|
|
|
resolve_time_ranges(parser, args)
|
2026-10-10 19:51:32 +02:00
|
|
|
|
2026-10-10 21:03:14 +02:00
|
|
|
if args.output is not None and args.output.is_dir():
|
2026-10-10 19:51:32 +02:00
|
|
|
parser.error(f"-o/--output ist ein Verzeichnis: {args.output}")
|
|
|
|
|
if shutil.which(YT_DLP) is None:
|
|
|
|
|
parser.error(f"{YT_DLP} nicht gefunden (Installation: pip install yt-dlp).")
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
2026-10-11 20:12:49 +02:00
|
|
|
def fetch_text(url, lang=None, ranges=(), exact=False):
|
|
|
|
|
"""Download the subtitles of a video; return (video info, text).
|
2026-10-10 19:51:32 +02:00
|
|
|
|
2026-10-11 20:12:49 +02:00
|
|
|
Without `lang` the language of the original audio is used. With time
|
|
|
|
|
`ranges` (seconds) the text has one block per range under a header with
|
|
|
|
|
its actual times, see vtt2txt.vtt_to_text.
|
2026-10-10 20:02:53 +02:00
|
|
|
"""
|
|
|
|
|
info = fetch_info(url)
|
2026-10-10 19:51:32 +02:00
|
|
|
print(f"Video: {info.get('title', '?')}")
|
|
|
|
|
|
2026-10-10 20:02:53 +02:00
|
|
|
lang = lang or original_language(info)
|
2026-10-10 19:51:32 +02:00
|
|
|
chosen = choose_track(info, lang) if lang else None
|
|
|
|
|
if chosen is None:
|
2026-10-10 19:52:20 +02:00
|
|
|
raise SystemExit(no_track_message(info, lang))
|
2026-10-10 19:51:32 +02:00
|
|
|
track, is_auto = chosen
|
|
|
|
|
kind = "automatische" if is_auto else "manuelle"
|
|
|
|
|
print(f"Lade {kind} Untertitel „{track}“ …", flush=True)
|
|
|
|
|
|
|
|
|
|
# The VTT file is only an intermediate step
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
target_base = Path(tmp) / "untertitel"
|
2026-10-10 20:02:53 +02:00
|
|
|
download(url, track, is_auto, target_base)
|
2026-10-10 19:51:32 +02:00
|
|
|
vtt = Path(f"{target_base}.{track}.vtt")
|
|
|
|
|
if not vtt.is_file():
|
|
|
|
|
raise SystemExit("yt-dlp hat keine Untertitel-Datei geschrieben.")
|
2026-10-11 20:12:49 +02:00
|
|
|
text = vtt_to_text(vtt.read_text(encoding="utf-8-sig"), ranges, exact)
|
2026-10-10 19:51:32 +02:00
|
|
|
|
2026-10-11 20:12:49 +02:00
|
|
|
if not text:
|
|
|
|
|
if ranges:
|
|
|
|
|
raise SystemExit("Die Untertitel enthalten im gewählten Zeitraum keinen Text.")
|
|
|
|
|
raise SystemExit("Die Untertitel enthalten keinen Text.")
|
|
|
|
|
return info, text
|
2026-10-10 20:02:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
args = parse_args()
|
2026-10-11 20:12:49 +02:00
|
|
|
info, text = fetch_text(args.url, args.lang, args.bereiche, args.exakt)
|
|
|
|
|
suffix = time_suffix(args.bereiche)
|
feat: Arbeitsordner je Video, Quellenangabe und automatisches Video-Glossar
- Ohne -o/-d schreiben yt_untertitel.py, extract_text.py und
abschrift_korrigieren.py -u in abschriften/<Name>/ (ein Ordner je
Video, auch für alle Ausschnitte).
- Beim Download entsteht neben dem Rohtext <Rohtext>.quelle.json mit
Titel, Kanal, Datum und URL, für den Quellenkopf des fertigen Textes.
- Ein Glossar, das wie sein Ordner heißt (abschriften/<Name>/<Name>.glossar.txt),
nutzen abschrift_korrigieren.py, glossar_vorschlag.py und aenderungen.py
automatisch, nach den mit -g angegebenen gemeinsamen Glossaren.
- .gitignore: abschriften/, fertig/, *.quelle.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:24:32 +02:00
|
|
|
name = default_name(info)
|
|
|
|
|
output = args.output or work_folder(name) / f"{name}{suffix}.txt"
|
2026-10-11 20:12:49 +02:00
|
|
|
segments = split_segments(text)
|
2026-10-10 19:51:32 +02:00
|
|
|
|
2026-10-10 19:53:42 +02:00
|
|
|
if args.fliesstext:
|
2026-10-11 20:12:49 +02:00
|
|
|
# Each block on its own, headers stay lines of their own; never split
|
|
|
|
|
# words, not even at hyphens ("EU-Gipfel")
|
|
|
|
|
segments = [
|
|
|
|
|
(header, textwrap.fill(
|
|
|
|
|
" ".join(body.split()), width=FLOW_WIDTH,
|
|
|
|
|
break_long_words=False, break_on_hyphens=False,
|
|
|
|
|
))
|
|
|
|
|
for header, body in segments
|
|
|
|
|
]
|
|
|
|
|
text = join_segments(segments)
|
2026-10-10 19:53:42 +02:00
|
|
|
|
2026-10-10 21:03:14 +02:00
|
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
output.write_text(text + "\n", encoding="utf-8")
|
feat: Arbeitsordner je Video, Quellenangabe und automatisches Video-Glossar
- Ohne -o/-d schreiben yt_untertitel.py, extract_text.py und
abschrift_korrigieren.py -u in abschriften/<Name>/ (ein Ordner je
Video, auch für alle Ausschnitte).
- Beim Download entsteht neben dem Rohtext <Rohtext>.quelle.json mit
Titel, Kanal, Datum und URL, für den Quellenkopf des fertigen Textes.
- Ein Glossar, das wie sein Ordner heißt (abschriften/<Name>/<Name>.glossar.txt),
nutzen abschrift_korrigieren.py, glossar_vorschlag.py und aenderungen.py
automatisch, nach den mit -g angegebenen gemeinsamen Glossaren.
- .gitignore: abschriften/, fertig/, *.quelle.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:24:32 +02:00
|
|
|
write_source(info, output.with_suffix(".quelle.json"))
|
2026-10-10 19:53:42 +02:00
|
|
|
print(
|
2026-10-10 21:03:14 +02:00
|
|
|
f"Gespeichert: {output} "
|
2026-10-11 20:12:49 +02:00
|
|
|
f"({len(text.splitlines())} Zeilen, "
|
|
|
|
|
f"{sum(len(body.split()) for _, body in segments)} Wörter)"
|
2026-10-10 19:53:42 +02:00
|
|
|
)
|
2026-10-10 19:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|