diff --git a/README.md b/README.md index a6ae55e..ad32b66 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,10 @@ Macht aus VTT-Untertiteln (z. B. von `yt-dlp --write-auto-subs`) die Rohabschrif für `abschrift_korrigieren.py`: Zeitstempel und Tags werden entfernt. YouTube-Auto-Untertitel zeigen ein rollendes Fenster, jeder Block wiederholt das Ende des vorigen. Übernommen wird nur, was nach dieser Überlappung neu ist; sagt der Redner eine Zeile wirklich -zweimal, bleibt sie zweimal erhalten. +zweimal, bleibt sie zweimal erhalten. Ob eine Datei rollt, wird pro Datei bestimmt: +Wiederholt mindestens die Hälfte der Blöcke das Ende des vorigen (Auto-Untertitel: +über 95 %), gilt sie als rollend. Sonst, etwa bei manuell erstellten Untertiteln, +wird jeder Block vollständig übernommen. ```bash ./vtt2txt.py -i rede.de.vtt # → rede.de.txt @@ -457,9 +460,6 @@ Oben in `abschrift_korrigieren.py`: gehören ins Glossar. - `qwen3:32b` ohne Denkmodus erkennt Hörfehler ohne Glossar nur teilweise und setzt kaum `[prüfen: …]`-Marken. Bekannte Fehler gehören deshalb ins Glossar. -- `vtt2txt.py` erkennt Wiederholungen an der Überlappung aufeinanderfolgender - Untertitel-Blöcke. Bei Untertiteln ohne rollendes Fenster gehen deshalb zwei - direkt aufeinanderfolgende, identische Blöcke zu einem zusammen. - Einzelbuchstaben und ein- bis zweistellige Zahlen mit Punkt gelten nie als Satzende („Plan B.“, „Es waren 12.“); das beeinflusst nur mögliche Absatzgrenzen, nicht den Text. diff --git a/tests/test_vtt2txt.py b/tests/test_vtt2txt.py index 7e324b2..69ad399 100644 --- a/tests/test_vtt2txt.py +++ b/tests/test_vtt2txt.py @@ -75,6 +75,36 @@ def test_vtt_to_lines_without_rolling_window(): assert vtt2txt.vtt_to_lines(vtt) == ["A", "B", "C"] +def test_vtt_to_lines_without_rolling_window_keeps_identical_cues(): + # Hand-written captions: the speaker says "Nein." twice, as two cues, and + # a cue may start with the line that ended the previous one + vtt = "WEBVTT\n\n" + "".join([ + cue(1, "Er sagt:"), + cue(2, "Nein."), + cue(3, "Nein."), + cue(4, "Ja, wirklich", "Nein."), + cue(5, "Nein.", "Und dann?"), + cue(6, "Dann kam"), + cue(7, "der Rest."), + cue(8, "Ende."), + ]) + assert vtt2txt.vtt_to_lines(vtt) == [ + "Er sagt:", "Nein.", "Nein.", "Ja, wirklich", "Nein.", "Nein.", "Und dann?", + "Dann kam", "der Rest.", "Ende.", + ] + + +@pytest.mark.parametrize("cues, expected", [ + ([], False), + ([["A"]], False), + ([["A"], ["A", "B"]], True), + ([["A"], ["A", "B"], ["C"]], True), # half of the pairs overlap + ([["A"], ["B"], ["C"], ["C", "D"]], False), # one of three +]) +def test_is_rolling(cues, expected): + assert vtt2txt.is_rolling(cues) is expected + + 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") diff --git a/vtt2txt.py b/vtt2txt.py index 80287a1..4e7f2af 100755 --- a/vtt2txt.py +++ b/vtt2txt.py @@ -19,6 +19,9 @@ SENTENCE_END = re.compile(r"[.!?…][\"'»«“”)\]]*(?=\s|$)") LINE_ENDS_SENTENCE = re.compile(r"[.!?…][\"'»«“”)\]]*$") MAX_SNAP_SECONDS = 30 # so weit darf ein Zeitraum bis zur nächsten Satzgrenze wachsen +# Ab diesem Anteil von Blöcken, die das Ende des vorherigen wiederholen, gelten +# die Untertitel als rollendes Fenster (YouTube-Auto-Untertitel: über 95 %) +ROLLING_SHARE = 0.5 TIME_EXAMPLES = "z. B. 00:05:10, 00:20 (Minute 20) oder 25 (Minute 25)" @@ -204,21 +207,31 @@ def in_range(time, start, end): return (start is None or time >= start) and (end is None or time < end) +def is_rolling(cues): + """True if most cues repeat the end of the previous one (rolling window).""" + pairs = list(zip(cues, cues[1:])) + overlapping = sum(1 for previous, cue in pairs if overlap(previous, cue)) + return bool(pairs) and overlapping >= ROLLING_SHARE * len(pairs) + + def timed_lines(vtt): """Return (cue start, cue end, line) for the new caption lines of a VTT document. Auto captions show a rolling window: each cue repeats the end of the previous cue before adding new text. Only the lines after that overlap are new, so a - line the speaker really says twice in a row is kept. + line the speaker really says twice in a row is kept. Captions without a + rolling window (e.g. written by hand) are taken as they are, so identical + cues in a row stay separate. """ + # Empty cues (YouTube transitions) say nothing about the window + cues = [(start, end, cue) for start, end, cue in read_cues(vtt) if cue] + rolling = is_rolling([cue for _, _, cue in cues]) lines = [] previous = [] - for start, end, cue in read_cues(vtt): - # Empty cues (YouTube transitions) say nothing about the window - if not cue: - continue - lines.extend((start, end, line) for line in cue[overlap(previous, cue):]) + for start, end, cue in cues: + new = cue[overlap(previous, cue):] if rolling else cue + lines.extend((start, end, line) for line in new) previous = cue return lines