2026-10-10 19:51:32 +02:00
|
|
|
"""Tests for extract_text.py; yt-dlp is always replaced by fakes (no network)."""
|
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
|
|
|
import json
|
2026-10-10 19:51:32 +02:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import extract_text
|
|
|
|
|
import yt_untertitel as yt
|
|
|
|
|
|
|
|
|
|
URL = "https://www.youtube.com/watch?v=abc123"
|
|
|
|
|
|
|
|
|
|
VTT = (
|
|
|
|
|
"WEBVTT\n\n"
|
|
|
|
|
"00:00:01.000 --> 00:00:02.000\n \nGuten<00:00:01.500><c> Abend.</c>\n\n"
|
|
|
|
|
"00:00:02.000 --> 00:00:03.000\nGuten Abend.\nSchön, dass Sie da sind.\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
GERMAN_VIDEO = {
|
|
|
|
|
"id": "abc123",
|
|
|
|
|
"title": "Eine Rede",
|
2026-10-10 21:03:14 +02:00
|
|
|
"channel": "Kanal",
|
|
|
|
|
"upload_date": "20260328",
|
2026-10-10 19:51:32 +02:00
|
|
|
"subtitles": {"en": []},
|
|
|
|
|
"automatic_captions": {"de-orig": [], "de": [], "en": []},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
pytestmark = pytest.mark.usefixtures("yt_dlp_installed")
|
2026-10-10 19:51:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_main(monkeypatch, *args):
|
|
|
|
|
monkeypatch.setattr(sys, "argv", ["extract_text.py", *map(str, args)])
|
|
|
|
|
extract_text.main()
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
def test_default_language_is_original_audio(fake_yt_dlp, tmp_path, monkeypatch, capsys):
|
|
|
|
|
run = fake_yt_dlp(GERMAN_VIDEO, vtt=VTT)
|
2026-10-10 19:51:32 +02:00
|
|
|
monkeypatch.setattr(yt.subprocess, "run", run)
|
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
|
run_main(monkeypatch, "-u", URL)
|
|
|
|
|
|
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
|
|
|
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
|
|
|
|
text = (work / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
2026-10-10 19:51:32 +02:00
|
|
|
assert text == "Guten Abend.\nSchön, dass Sie da sind.\n"
|
|
|
|
|
assert "de-orig" in run.calls[1]
|
|
|
|
|
assert "--write-auto-subs" in run.calls[1]
|
|
|
|
|
out = capsys.readouterr().out
|
2026-10-10 21:03:14 +02:00
|
|
|
assert "Kanal_Eine_Rede_2026-03-28.txt (2 Zeilen, 7 Wörter)" in out
|
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
|
|
|
# Only the text file and where it comes from remain, no VTT
|
|
|
|
|
assert sorted(p.name for p in work.iterdir()) == [
|
|
|
|
|
"Kanal_Eine_Rede_2026-03-28.quelle.json", "Kanal_Eine_Rede_2026-03-28.txt",
|
|
|
|
|
]
|
|
|
|
|
source = json.loads((work / "Kanal_Eine_Rede_2026-03-28.quelle.json").read_text())
|
|
|
|
|
assert source["kanal"] == "Kanal" and source["url"].endswith("v=abc123")
|
2026-10-10 19:51:32 +02:00
|
|
|
|
|
|
|
|
|
2026-10-10 20:28:05 +02:00
|
|
|
def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch):
|
|
|
|
|
info = {
|
|
|
|
|
"id": "dub", "language": "en-US", "subtitles": {"en-US": []},
|
|
|
|
|
"automatic_captions": {"ar-orig": [], "en-orig": []},
|
|
|
|
|
}
|
|
|
|
|
run = fake_yt_dlp(info, vtt=VTT)
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", run)
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-o", tmp_path / "out.txt")
|
|
|
|
|
download = run.calls[1]
|
|
|
|
|
assert download[download.index("--sub-langs") + 1] == "en-US"
|
|
|
|
|
assert "--write-subs" in download
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:53:42 +02:00
|
|
|
def test_fliesstext(fake_yt_dlp, tmp_path, monkeypatch, capsys):
|
|
|
|
|
long_vtt = "WEBVTT\n\n" + "".join(
|
|
|
|
|
f"00:00:{i:02d}.000 --> 00:00:{i + 1:02d}.000\nSatz Nummer {i} zum EU-Gipfel.\n\n"
|
|
|
|
|
for i in range(20)
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=long_vtt))
|
|
|
|
|
target = tmp_path / "rede.txt"
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-f", "-o", target)
|
|
|
|
|
|
|
|
|
|
lines = target.read_text(encoding="utf-8").splitlines()
|
|
|
|
|
assert 1 < len(lines) < 20
|
|
|
|
|
assert all(len(line) <= extract_text.FLOW_WIDTH for line in lines)
|
|
|
|
|
assert " ".join(lines) == " ".join(f"Satz Nummer {i} zum EU-Gipfel." for i in range(20))
|
|
|
|
|
assert "100 Wörter" in capsys.readouterr().out
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
def test_explicit_language_prefers_manual(fake_yt_dlp, tmp_path, monkeypatch):
|
|
|
|
|
run = fake_yt_dlp(GERMAN_VIDEO, vtt=VTT)
|
2026-10-10 19:51:32 +02:00
|
|
|
monkeypatch.setattr(yt.subprocess, "run", run)
|
|
|
|
|
target = tmp_path / "neu" / "rede.en.txt"
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-l", "en", "-o", target)
|
|
|
|
|
|
|
|
|
|
assert target.is_file()
|
|
|
|
|
assert "--write-subs" in run.calls[1]
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
def test_original_language_from_metadata_without_auto_captions(
|
|
|
|
|
fake_yt_dlp, tmp_path, monkeypatch
|
|
|
|
|
):
|
2026-10-10 19:51:32 +02:00
|
|
|
info = {"id": "x", "language": "fr", "subtitles": {"fr": []}, "automatic_captions": {}}
|
2026-10-10 19:52:20 +02:00
|
|
|
run = fake_yt_dlp(info, vtt=VTT)
|
2026-10-10 19:51:32 +02:00
|
|
|
monkeypatch.setattr(yt.subprocess, "run", run)
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-o", tmp_path / "out.txt")
|
|
|
|
|
assert run.calls[1][run.calls[1].index("--sub-langs") + 1] == "fr"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("info, args, message", [
|
|
|
|
|
# Translation only: no "en-orig", no manual "en"
|
|
|
|
|
(GERMAN_VIDEO, ["-l", "fr"], "Keine Untertitel in „fr“"),
|
|
|
|
|
({"id": "x", "subtitles": {}, "automatic_captions": {}}, [], "Originalsprache"),
|
|
|
|
|
])
|
2026-10-10 19:52:20 +02:00
|
|
|
def test_no_matching_track(fake_yt_dlp, tmp_path, monkeypatch, info, args, message):
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(info, vtt=VTT))
|
2026-10-10 19:51:32 +02:00
|
|
|
with pytest.raises(SystemExit, match=message):
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-o", tmp_path / "out.txt", *args)
|
|
|
|
|
assert not (tmp_path / "out.txt").exists()
|
|
|
|
|
|
|
|
|
|
|
2026-10-10 19:52:20 +02:00
|
|
|
def test_empty_subtitles(fake_yt_dlp, tmp_path, monkeypatch):
|
2026-10-10 19:51:32 +02:00
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt="WEBVTT\n"))
|
|
|
|
|
with pytest.raises(SystemExit, match="keinen Text"):
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-o", tmp_path / "out.txt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("args, message", [
|
|
|
|
|
([], "required"),
|
|
|
|
|
(["-u", URL, "-o", "{dir}"], "ist ein Verzeichnis"),
|
|
|
|
|
])
|
|
|
|
|
def test_cli_errors(tmp_path, monkeypatch, capsys, args, message):
|
|
|
|
|
args = [a.format(dir=tmp_path) for a in args]
|
|
|
|
|
with pytest.raises(SystemExit) as exc:
|
|
|
|
|
run_main(monkeypatch, *args)
|
|
|
|
|
assert exc.value.code == 2
|
|
|
|
|
assert message in capsys.readouterr().err
|
2026-10-11 19:07:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_time_range_default_name(fake_yt_dlp, tmp_path, monkeypatch):
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=VTT))
|
|
|
|
|
monkeypatch.chdir(tmp_path)
|
2026-10-11 20:20:54 +02:00
|
|
|
run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03")
|
2026-10-11 19:07:56 +02:00
|
|
|
|
|
|
|
|
name = "Kanal_Eine_Rede_2026-03-28_00-00-02_bis_00-00-03.txt"
|
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
|
|
|
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
|
|
|
|
assert (work / name).read_text(encoding="utf-8") == (
|
2026-10-11 20:12:49 +02:00
|
|
|
"[00:00:02 - 00:00:03]\nSchön, dass Sie da sind.\n"
|
|
|
|
|
)
|
2026-10-11 19:07:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_time_range_without_text(fake_yt_dlp, tmp_path, monkeypatch):
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=VTT))
|
|
|
|
|
with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"):
|
2026-10-11 20:20:54 +02:00
|
|
|
run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "out.txt")
|
2026-10-11 19:07:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_time_range_order_error(monkeypatch, capsys):
|
|
|
|
|
with pytest.raises(SystemExit) as exc:
|
2026-10-11 20:20:54 +02:00
|
|
|
run_main(monkeypatch, "-u", URL, "-z", "2:00-1:00")
|
2026-10-11 19:07:56 +02:00
|
|
|
assert exc.value.code == 2
|
2026-10-11 20:20:54 +02:00
|
|
|
assert "Start muss vor Ende" in capsys.readouterr().err
|
2026-10-11 19:34:36 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
CUT_VTT = (
|
|
|
|
|
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nGuten Abend und\n\n"
|
|
|
|
|
"00:00:02.000 --> 00:00:03.000\nwillkommen. Schön, dass\n\n"
|
|
|
|
|
"00:00:03.000 --> 00:00:04.000\nSie da sind.\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("exact, text", [
|
2026-10-11 20:12:49 +02:00
|
|
|
([], "[00:00:01 - 00:00:04]\nGuten Abend und\nwillkommen. Schön, dass\nSie da sind.\n"),
|
|
|
|
|
(["--exakt"], "[00:00:02 - 00:00:03]\nwillkommen. Schön, dass\n"),
|
2026-10-11 19:34:36 +02:00
|
|
|
])
|
|
|
|
|
def test_time_range_sentence_bounds(fake_yt_dlp, tmp_path, monkeypatch, exact, text):
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=CUT_VTT))
|
|
|
|
|
target = tmp_path / "out.txt"
|
2026-10-11 20:20:54 +02:00
|
|
|
run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03", "-o", target, *exact)
|
2026-10-11 19:34:36 +02:00
|
|
|
assert target.read_text(encoding="utf-8") == text
|
2026-10-11 20:12:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
MULTI_VTT = (
|
|
|
|
|
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nErster Satz der Rede.\n\n"
|
|
|
|
|
"00:01:00.000 --> 00:01:01.000\nZweiter Satz.\n\n"
|
|
|
|
|
"00:02:00.000 --> 00:02:01.000\nDritter Satz, der etwas länger ist.\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_several_ranges_flow_text_keeps_headers(fake_yt_dlp, tmp_path, monkeypatch, capsys):
|
|
|
|
|
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=MULTI_VTT))
|
|
|
|
|
monkeypatch.setattr(extract_text, "FLOW_WIDTH", 20)
|
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
|
run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-f")
|
|
|
|
|
|
2026-10-11 20:20:54 +02:00
|
|
|
name = "Kanal_Eine_Rede_2026-03-28_bis_00-00-30+ab_00-02-00.txt"
|
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
|
|
|
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
|
|
|
|
assert (work / name).read_text(encoding="utf-8") == (
|
2026-10-11 20:12:49 +02:00
|
|
|
"[00:00:01 - 00:00:02]\nErster Satz der\nRede.\n\n"
|
|
|
|
|
"[00:02:00 - 00:02:01]\nDritter Satz, der\netwas länger ist.\n"
|
|
|
|
|
)
|
|
|
|
|
assert "(7 Zeilen, 10 Wörter)" in capsys.readouterr().out
|