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>
This commit is contained in:
parent
acf9e33382
commit
3aa04fff4b
5 changed files with 195 additions and 34 deletions
|
|
@ -636,13 +636,63 @@ def kreml_glossary(folder):
|
|||
|
||||
def test_main_url_default_names(from_url, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28"
|
||||
run_main(monkeypatch, "-u", URL, "-g", kreml_glossary(tmp_path))
|
||||
|
||||
raw = (tmp_path / "Kanal_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||
raw = (work / "Kanal_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||
assert raw == "Der Kremmel ist in Moskau.\nDas ist bekannt.\n"
|
||||
result = (tmp_path / "Kanal_Rede_2026-03-28.korrigiert.txt").read_text(encoding="utf-8")
|
||||
result = (work / "Kanal_Rede_2026-03-28.korrigiert.txt").read_text(encoding="utf-8")
|
||||
assert result == "Der Kreml ist in Moskau. Das ist bekannt.\n"
|
||||
assert (tmp_path / "Kanal_Rede_2026-03-28.aenderungen.html").is_file()
|
||||
assert (work / "Kanal_Rede_2026-03-28.aenderungen.html").is_file()
|
||||
source = json.loads((work / "Kanal_Rede_2026-03-28.quelle.json").read_text(encoding="utf-8"))
|
||||
assert source["titel"] == "Rede" and source["kanal"] == "Kanal"
|
||||
assert source["datum"] == "20260328"
|
||||
assert source["url"] == "https://www.youtube.com/watch?v=abc123"
|
||||
|
||||
|
||||
def video_folder(tmp_path):
|
||||
"""abschriften/rede/ with raw text and its video glossary."""
|
||||
folder = tmp_path / "abschriften" / "rede"
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "rede.txt").write_text("Der Kremmel ist in Moskau.\n", encoding="utf-8")
|
||||
(folder / "rede.glossar.txt").write_text("Kremmel => Kreml\n", encoding="utf-8")
|
||||
return folder
|
||||
|
||||
|
||||
def test_main_uses_video_glossary(tmp_path, monkeypatch, capsys):
|
||||
folder = video_folder(tmp_path)
|
||||
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||
run_main(monkeypatch, "-i", folder / "rede.txt")
|
||||
|
||||
result = (folder / "rede.korrigiert.txt").read_text(encoding="utf-8")
|
||||
assert result == "Der Kreml ist in Moskau.\n"
|
||||
assert "rede.glossar.txt (Video-Glossar, automatisch)" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_video_glossary_given_explicitly_counts_once(tmp_path, monkeypatch, capsys):
|
||||
folder = video_folder(tmp_path)
|
||||
shared = tmp_path / "gemeinsam.glossar.txt"
|
||||
shared.write_text("Moskau\n", encoding="utf-8")
|
||||
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||
run_main(monkeypatch, "-i", folder / "rede.txt", "-g", shared, "-g", folder / "rede.glossar.txt")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("rede.glossar.txt") == 1
|
||||
assert "automatisch" not in out
|
||||
# Shared glossaries first, as given
|
||||
assert out.index("gemeinsam.glossar.txt") < out.index("rede.glossar.txt")
|
||||
|
||||
|
||||
def test_with_video_glossary(tmp_path):
|
||||
folder = video_folder(tmp_path)
|
||||
shared = tmp_path / "gemeinsam.glossar.txt"
|
||||
assert ak.with_video_glossary([shared], folder) == (
|
||||
[shared, folder / "rede.glossar.txt"], folder / "rede.glossar.txt"
|
||||
)
|
||||
# Without a glossary named like the folder nothing is added
|
||||
assert ak.with_video_glossary([shared], tmp_path) == ([shared], None)
|
||||
|
||||
|
||||
def test_main_url_output_option_and_kept_raw_text(from_url, tmp_path, monkeypatch, capsys):
|
||||
|
|
@ -762,19 +812,21 @@ def test_cli_errors(sample_copy, monkeypatch, capsys, args, message):
|
|||
|
||||
def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28"
|
||||
run_main(monkeypatch, "-u", URL, "-z", "0:00:02")
|
||||
|
||||
name = "Kanal_Rede_2026-03-28_ab_00-00-02"
|
||||
raw = (tmp_path / f"{name}.txt").read_text(encoding="utf-8")
|
||||
raw = (work / f"{name}.txt").read_text(encoding="utf-8")
|
||||
assert raw == "[00:00:02 - 00:00:03]\nDas ist bekannt.\n"
|
||||
result = (tmp_path / f"{name}.korrigiert.txt").read_text(encoding="utf-8")
|
||||
result = (work / f"{name}.korrigiert.txt").read_text(encoding="utf-8")
|
||||
assert result == "[00:00:02 - 00:00:03]\n\nDas ist bekannt.\n"
|
||||
|
||||
|
||||
def test_main_url_range_from_beginning(from_url, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28"
|
||||
run_main(monkeypatch, "-u", URL, "-z", "-0:00:02")
|
||||
raw = (tmp_path / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8")
|
||||
raw = (work / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8")
|
||||
assert raw == "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n"
|
||||
|
||||
|
||||
|
|
@ -926,14 +978,15 @@ def test_main_url_several_ranges(fake_yt_dlp, yt_dlp_installed, tmp_path, monkey
|
|||
monkeypatch.setattr(subprocess, "run", fake_yt_dlp(VIDEO, vtt=MULTI_VTT))
|
||||
sent = chunk_recorder(monkeypatch)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
work = tmp_path / "abschriften" / "Kanal_Rede_2026-03-28"
|
||||
run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-g", kreml_glossary(tmp_path))
|
||||
|
||||
name = "Kanal_Rede_2026-03-28_bis_00-00-30+ab_00-02-00"
|
||||
assert (tmp_path / f"{name}.txt").read_text(encoding="utf-8") == (
|
||||
assert (work / f"{name}.txt").read_text(encoding="utf-8") == (
|
||||
"[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n\n"
|
||||
"[00:02:00 - 00:02:01]\nDas ist bekannt.\n"
|
||||
)
|
||||
assert (tmp_path / f"{name}.korrigiert.txt").read_text(encoding="utf-8") == (
|
||||
assert (work / f"{name}.korrigiert.txt").read_text(encoding="utf-8") == (
|
||||
"[00:00:01 - 00:00:02]\n\nDer Kreml ist in Moskau.\n\n"
|
||||
"[00:02:00 - 00:02:01]\n\nDas ist bekannt.\n"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -321,3 +321,19 @@ def test_report_and_summary_put_word_forms_last(tmp_path, capsys):
|
|||
out = capsys.readouterr().out.split("Zu prüfen: ")[1]
|
||||
assert out.startswith("2 Stellen (davon 1 Wortformen)")
|
||||
assert out.index("Professorin") < out.index("„das“ → „dass“")
|
||||
|
||||
|
||||
def test_cli_uses_video_glossary(tmp_path, monkeypatch, capsys):
|
||||
folder = tmp_path / "rede"
|
||||
folder.mkdir()
|
||||
raw = folder / "rede.txt"
|
||||
raw.write_text("das Kremmel und\nBar heute\n", encoding="utf-8")
|
||||
corrected = folder / "rede.korrigiert.txt"
|
||||
corrected.write_text("Das Kreml und Bahr heute.\n", encoding="utf-8")
|
||||
(folder / "rede.glossar.txt").write_text("Kremmel => Kreml\nEgon Bahr\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(sys, "argv", ["aenderungen.py", "-a", str(raw), "-b", str(corrected)])
|
||||
ae.main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Video-Glossar (automatisch)" in out
|
||||
assert "Zu prüfen: 0 Stellen" in out
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tests for extract_text.py; yt-dlp is always replaced by fakes (no network)."""
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
|
@ -38,14 +39,19 @@ def test_default_language_is_original_audio(fake_yt_dlp, tmp_path, monkeypatch,
|
|||
monkeypatch.chdir(tmp_path)
|
||||
run_main(monkeypatch, "-u", URL)
|
||||
|
||||
text = (tmp_path / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
||||
text = (work / "Kanal_Eine_Rede_2026-03-28.txt").read_text(encoding="utf-8")
|
||||
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
|
||||
assert "Kanal_Eine_Rede_2026-03-28.txt (2 Zeilen, 7 Wörter)" in out
|
||||
# Only the text file remains, no VTT
|
||||
assert [p.name for p in tmp_path.iterdir()] == ["Kanal_Eine_Rede_2026-03-28.txt"]
|
||||
# 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")
|
||||
|
||||
|
||||
def test_default_language_ignores_dubbed_tracks(fake_yt_dlp, tmp_path, monkeypatch):
|
||||
|
|
@ -133,7 +139,8 @@ def test_time_range_default_name(fake_yt_dlp, tmp_path, monkeypatch):
|
|||
run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03")
|
||||
|
||||
name = "Kanal_Eine_Rede_2026-03-28_00-00-02_bis_00-00-03.txt"
|
||||
assert (tmp_path / name).read_text(encoding="utf-8") == (
|
||||
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
||||
assert (work / name).read_text(encoding="utf-8") == (
|
||||
"[00:00:02 - 00:00:03]\nSchön, dass Sie da sind.\n"
|
||||
)
|
||||
|
||||
|
|
@ -183,7 +190,8 @@ def test_several_ranges_flow_text_keeps_headers(fake_yt_dlp, tmp_path, monkeypat
|
|||
run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-f")
|
||||
|
||||
name = "Kanal_Eine_Rede_2026-03-28_bis_00-00-30+ab_00-02-00.txt"
|
||||
assert (tmp_path / name).read_text(encoding="utf-8") == (
|
||||
work = tmp_path / "abschriften" / "Kanal_Eine_Rede_2026-03-28"
|
||||
assert (work / name).read_text(encoding="utf-8") == (
|
||||
"[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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -160,3 +160,18 @@ def test_time_headers_never_reach_model_or_changes(tmp_path, monkeypatch):
|
|||
draft = out.read_text(encoding="utf-8")
|
||||
assert "# ? Kremmel => Kreml (1×)" in draft
|
||||
assert "00:0" not in draft.replace(str(raw), "")
|
||||
|
||||
|
||||
def test_main_uses_video_glossary(tmp_path, monkeypatch, capsys):
|
||||
folder = tmp_path / "rede"
|
||||
folder.mkdir()
|
||||
raw = folder / "rede.txt"
|
||||
raw.write_text(RAW + "\n", encoding="utf-8")
|
||||
(folder / "rede.glossar.txt").write_text("Kremmel => Kreml\n", encoding="utf-8")
|
||||
model = fake_model(ENTRIES)
|
||||
monkeypatch.setattr(urllib.request, "urlopen", model)
|
||||
run_main(monkeypatch, "-i", raw)
|
||||
|
||||
sent = model.calls[0]["messages"][1]["content"]
|
||||
assert "Kreml" in sent and "Kremmel" not in sent
|
||||
assert "Video-Glossar (automatisch)" in capsys.readouterr().out
|
||||
|
|
|
|||
|
|
@ -140,8 +140,11 @@ def test_yt_dlp_runs_in_temporary_directory(fake_yt_dlp, tmp_path, monkeypatch):
|
|||
|
||||
assert all(cwd is not None and Path(cwd) != tmp_path for cwd in run.cwds)
|
||||
template = run.calls[1][run.calls[1].index("-o") + 1]
|
||||
assert template == f"{tmp_path / 'Rede'}.%(ext)s"
|
||||
assert (tmp_path / "Rede.de-orig.vtt").is_file()
|
||||
# Without -d: working folder abschriften/<Name>/, named after -n
|
||||
work = tmp_path / "abschriften" / "Rede"
|
||||
assert template == f"{work / 'Rede'}.%(ext)s"
|
||||
assert (work / "Rede.de-orig.vtt").is_file()
|
||||
assert (work / "Rede.quelle.json").is_file()
|
||||
|
||||
|
||||
def test_main_prefers_manual_and_default_name(fake_yt_dlp, tmp_path, monkeypatch):
|
||||
|
|
|
|||
Loading…
Reference in a new issue