fix: Untertitel ohne rollendes Fenster verlieren keine wiederholten Blöcke
vtt2txt.py entfernt Überlappungen nur noch, wenn die Datei ein rollendes Fenster zeigt: mindestens die Hälfte der Blöcke wiederholt das Ende des vorigen (YouTube-Auto-Untertitel: 97–99 %). Manuell erstellte Untertitel werden vollständig übernommen, zwei gleiche Blöcke hintereinander bleiben zwei. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
41bcd1eb7b
commit
3da372a941
3 changed files with 53 additions and 10 deletions
|
|
@ -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
|
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
|
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
|
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
|
```bash
|
||||||
./vtt2txt.py -i rede.de.vtt # → rede.de.txt
|
./vtt2txt.py -i rede.de.vtt # → rede.de.txt
|
||||||
|
|
@ -457,9 +460,6 @@ Oben in `abschrift_korrigieren.py`:
|
||||||
gehören ins Glossar.
|
gehören ins Glossar.
|
||||||
- `qwen3:32b` ohne Denkmodus erkennt Hörfehler ohne Glossar nur teilweise und
|
- `qwen3:32b` ohne Denkmodus erkennt Hörfehler ohne Glossar nur teilweise und
|
||||||
setzt kaum `[prüfen: …]`-Marken. Bekannte Fehler gehören deshalb ins Glossar.
|
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
|
- Einzelbuchstaben und ein- bis zweistellige Zahlen mit Punkt gelten nie als
|
||||||
Satzende („Plan B.“, „Es waren 12.“); das beeinflusst nur mögliche Absatzgrenzen,
|
Satzende („Plan B.“, „Es waren 12.“); das beeinflusst nur mögliche Absatzgrenzen,
|
||||||
nicht den Text.
|
nicht den Text.
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,36 @@ def test_vtt_to_lines_without_rolling_window():
|
||||||
assert vtt2txt.vtt_to_lines(vtt) == ["A", "B", "C"]
|
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):
|
def test_main_default_output_matches_sample(sample_copy, monkeypatch, capsys):
|
||||||
run_main(monkeypatch, "-i", sample_copy)
|
run_main(monkeypatch, "-i", sample_copy)
|
||||||
target = sample_copy.with_name("ramstein.de.txt")
|
target = sample_copy.with_name("ramstein.de.txt")
|
||||||
|
|
|
||||||
25
vtt2txt.py
25
vtt2txt.py
|
|
@ -19,6 +19,9 @@ SENTENCE_END = re.compile(r"[.!?…][\"'»«“”)\]]*(?=\s|$)")
|
||||||
LINE_ENDS_SENTENCE = re.compile(r"[.!?…][\"'»«“”)\]]*$")
|
LINE_ENDS_SENTENCE = re.compile(r"[.!?…][\"'»«“”)\]]*$")
|
||||||
|
|
||||||
MAX_SNAP_SECONDS = 30 # so weit darf ein Zeitraum bis zur nächsten Satzgrenze wachsen
|
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)"
|
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)
|
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):
|
def timed_lines(vtt):
|
||||||
"""Return (cue start, cue end, line) for the new caption lines of a VTT document.
|
"""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
|
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
|
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 = []
|
lines = []
|
||||||
previous = []
|
previous = []
|
||||||
|
|
||||||
for start, end, cue in read_cues(vtt):
|
for start, end, cue in cues:
|
||||||
# Empty cues (YouTube transitions) say nothing about the window
|
new = cue[overlap(previous, cue):] if rolling else cue
|
||||||
if not cue:
|
lines.extend((start, end, line) for line in new)
|
||||||
continue
|
|
||||||
lines.extend((start, end, line) for line in cue[overlap(previous, cue):])
|
|
||||||
previous = cue
|
previous = cue
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue