feat: VTT-Untertitel in Rohabschrift umwandeln (vtt2txt.py)
- vtt2txt.py als Kommandozeilenprogramm: -i nimmt mehrere Dateien und Muster (auch ohne Shell-Expansion), -o eine Datei oder ein Verzeichnis - Rollende YouTube-Untertitel über die Überlappung aufeinanderfolgender Blöcke entdoppeln; echte Wiederholungen des Redners bleiben erhalten - Fix: Blöcke nur an wirklich leeren Zeilen trennen; Text nach einer Leerzeichen-Zeile ging verloren (u. a. das letzte [applaus]) - Schutz gegen Überschreiben der Eingabe und doppelte Ausgabedateien, klare Meldung bei Nicht-UTF-8, Stapel läuft bei Fehlern weiter - rede_korrigieren.py nimmt .vtt direkt an - testdaten/ramstein.de.txt neu erzeugt (+ abschließendes [applaus]) - pytest: global installiertes asyncio-Plugin abschalten (Warnung) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6e5117a33b
commit
99238870f7
2 changed files with 386 additions and 3 deletions
|
|
@ -281,6 +281,21 @@ def test_main_output_option_creates_directories(sample_copy, monkeypatch):
|
|||
assert (target.parent / "rede.parts").is_dir()
|
||||
|
||||
|
||||
def test_main_vtt_input_matches_txt_input(sample_copy, monkeypatch):
|
||||
monkeypatch.setattr(rk, "correct_chunk", identity_correct)
|
||||
monkeypatch.setattr(rk, "find_paragraph_starts", every_nth_start(5))
|
||||
vtt = sample_copy.parent / "vtt" / "ramstein.de.vtt"
|
||||
vtt.parent.mkdir()
|
||||
shutil.copy(SAMPLE.with_suffix(".vtt"), vtt)
|
||||
|
||||
run_main(monkeypatch, "-i", sample_copy)
|
||||
run_main(monkeypatch, "-i", vtt)
|
||||
expected = sample_copy.with_name("ramstein.de.korrigiert.txt")
|
||||
assert vtt.with_name("ramstein.de.korrigiert.txt").read_text(
|
||||
encoding="utf-8"
|
||||
) == expected.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_main_warns_on_word_count_deviation(sample_copy, monkeypatch, capsys):
|
||||
def shortening(chunk, index, system):
|
||||
words = chunk.split()
|
||||
|
|
|
|||
157
tests/test_vtt2txt.py
Normal file
157
tests/test_vtt2txt.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
"""Tests for vtt2txt.py."""
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import vtt2txt
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SAMPLE_VTT = REPO / "testdaten" / "ramstein.de.vtt"
|
||||
SAMPLE_TXT = REPO / "testdaten" / "ramstein.de.txt"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_copy(tmp_path):
|
||||
target = tmp_path / SAMPLE_VTT.name
|
||||
shutil.copy(SAMPLE_VTT, target)
|
||||
return target
|
||||
|
||||
|
||||
def cue(start, *lines):
|
||||
return f"00:00:{start:02d}.000 --> 00:00:{start + 1:02d}.000\n" + "\n".join(lines) + "\n\n"
|
||||
|
||||
|
||||
def run_main(monkeypatch, *args):
|
||||
monkeypatch.setattr(sys, "argv", ["vtt2txt.py", *map(str, args)])
|
||||
vtt2txt.main()
|
||||
|
||||
|
||||
def test_vtt_to_lines_strips_tags_and_rolling_duplicates():
|
||||
vtt = (
|
||||
"WEBVTT\nKind: captions\n\n"
|
||||
"00:00:01.000 --> 00:00:02.000 align:start\n"
|
||||
"Erste<00:00:01.500><c> Zeile</c>\n\n"
|
||||
"00:00:02.000 --> 00:00:03.000\nErste Zeile\n \n\n"
|
||||
"cue-id\n00:00:03.000 --> 00:00:04.000\nErste Zeile\nA & B\n"
|
||||
)
|
||||
assert vtt2txt.vtt_to_lines(vtt) == ["Erste Zeile", "A & B"]
|
||||
|
||||
|
||||
def test_vtt_to_lines_keeps_text_after_space_only_line():
|
||||
# YouTube cues start with a line holding a single space; the text after it
|
||||
# must survive even if no later cue repeats it (here: end of file)
|
||||
vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n \n[applaus]\n"
|
||||
assert vtt2txt.vtt_to_lines(vtt) == ["[applaus]"]
|
||||
|
||||
|
||||
def test_vtt_to_lines_skips_note_and_normalizes_whitespace():
|
||||
vtt = (
|
||||
"WEBVTT\n\nNOTE Kommentar\nnicht übernehmen\n\n"
|
||||
"00:00:01.000 --> 00:00:02.000\nA B\n\n"
|
||||
"00:00:02.000 --> 00:00:03.000\nA B\nC\n"
|
||||
)
|
||||
assert vtt2txt.vtt_to_lines(vtt) == ["A B", "C"]
|
||||
|
||||
|
||||
def test_vtt_to_lines_keeps_real_repetition():
|
||||
# Rolling window: each cue repeats the previous line, then adds new text.
|
||||
# The speaker really says "Nein." twice; both must survive.
|
||||
vtt = "WEBVTT\n\n" + "".join([
|
||||
cue(1, " ", "Er sagt:"),
|
||||
cue(2, "Er sagt:", " "),
|
||||
cue(3, "Er sagt:", "Nein."),
|
||||
cue(4, "Nein.", " "),
|
||||
cue(5, "Nein.", "Nein."),
|
||||
cue(6, " ", " "),
|
||||
cue(7, "Nein.", "Ende."),
|
||||
])
|
||||
assert vtt2txt.vtt_to_lines(vtt) == ["Er sagt:", "Nein.", "Nein.", "Ende."]
|
||||
|
||||
|
||||
def test_vtt_to_lines_without_rolling_window():
|
||||
vtt = "WEBVTT\n\n1\n" + cue(1, "A", "B") + "2\n" + cue(2, "C")
|
||||
assert vtt2txt.vtt_to_lines(vtt) == ["A", "B", "C"]
|
||||
|
||||
|
||||
def test_main_default_output_matches_sample(sample_copy, monkeypatch, capsys):
|
||||
run_main(monkeypatch, "-i", sample_copy)
|
||||
target = sample_copy.with_name("ramstein.de.txt")
|
||||
assert target.read_text(encoding="utf-8") == SAMPLE_TXT.read_text(encoding="utf-8")
|
||||
assert f"Erstellt: {target}" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_multiple_inputs(sample_copy, monkeypatch):
|
||||
second = sample_copy.with_name("rede.en.vtt")
|
||||
shutil.copy(sample_copy, second)
|
||||
run_main(monkeypatch, "-i", sample_copy, second)
|
||||
assert sample_copy.with_suffix(".txt").is_file()
|
||||
assert second.with_suffix(".txt").is_file()
|
||||
|
||||
|
||||
def test_main_expands_glob_without_duplicates(sample_copy, monkeypatch, capsys):
|
||||
shutil.copy(sample_copy, sample_copy.with_name("ramstein.en.vtt"))
|
||||
shutil.copy(sample_copy, sample_copy.with_name("anders.de.vtt"))
|
||||
run_main(monkeypatch, "-i", sample_copy.parent / "ramstein.*.vtt", sample_copy)
|
||||
out = capsys.readouterr().out
|
||||
assert out.count("Erstellt:") == 2
|
||||
assert not sample_copy.with_name("anders.de.txt").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("make_dir, suffix", [(True, ""), (False, "/")])
|
||||
def test_main_output_directory(sample_copy, monkeypatch, make_dir, suffix):
|
||||
second = sample_copy.with_name("rede.en.vtt")
|
||||
shutil.copy(sample_copy, second)
|
||||
out_dir = sample_copy.parent / "out"
|
||||
if make_dir:
|
||||
out_dir.mkdir()
|
||||
run_main(monkeypatch, "-i", sample_copy, second, "-o", f"{out_dir}{suffix}")
|
||||
assert (out_dir / "ramstein.de.txt").is_file()
|
||||
assert (out_dir / "rede.en.txt").is_file()
|
||||
|
||||
|
||||
def test_main_output_option_creates_directories(sample_copy, monkeypatch):
|
||||
target = sample_copy.parent / "neu" / "unter" / "rede.txt"
|
||||
run_main(monkeypatch, "-i", sample_copy, "-o", target)
|
||||
assert target.is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content, message", [
|
||||
("WEBVTT\n".encode("utf-8"), "Keine Untertitel"),
|
||||
("WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nGrüße\n".encode("latin-1"),
|
||||
"Keine UTF-8-Datei"),
|
||||
])
|
||||
def test_main_bad_file_continues_batch(sample_copy, monkeypatch, capsys, content, message):
|
||||
bad = sample_copy.with_name("aaa.vtt")
|
||||
bad.write_bytes(content)
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
run_main(monkeypatch, "-i", bad, sample_copy)
|
||||
assert exc.value.code == 1
|
||||
assert message in capsys.readouterr().err
|
||||
assert not bad.with_suffix(".txt").exists()
|
||||
assert sample_copy.with_suffix(".txt").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args, message", [
|
||||
([], "required"),
|
||||
(["-i", "gibtsnicht.vtt"], "Eingabedatei nicht gefunden"),
|
||||
(["-i", "{txt}"], "Keine VTT-Datei"),
|
||||
(["-i", "{vtt}", "{vtt2}", "-o", "{dir}/x.txt"], "muss -o/--output ein Verzeichnis sein"),
|
||||
(["-i", "{dir}/*.gibtsnicht"], "Keine Datei passt zu"),
|
||||
(["-i", "{vtt}", "{dir}/sub/ramstein.de.vtt", "-o", "{dir}/"], "dieselbe Ausgabedatei"),
|
||||
(["-i", "{vtt}", "-o", "{vtt}"], "nicht die Eingabedatei"),
|
||||
])
|
||||
def test_cli_errors(sample_copy, monkeypatch, capsys, args, message):
|
||||
txt = sample_copy.with_suffix(".txt")
|
||||
txt.write_text("x\n", encoding="utf-8")
|
||||
vtt2 = sample_copy.with_name("rede.en.vtt")
|
||||
shutil.copy(sample_copy, vtt2)
|
||||
(sample_copy.parent / "sub").mkdir()
|
||||
shutil.copy(sample_copy, sample_copy.parent / "sub" / sample_copy.name)
|
||||
args = [a.format(vtt=sample_copy, vtt2=vtt2, txt=txt, dir=sample_copy.parent)
|
||||
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
|
||||
Loading…
Reference in a new issue