feat: Ausschnitte an Satzgrenzen erweitern (--exakt schneidet genau)

Bei -s/-e werden angeschnittene Sätze an beiden Enden vollständig
aufgenommen (höchstens 30 s über den Zeitraum hinaus). rede_korrigieren.py
weist das Modell bei Ausschnitten zusätzlich an, nichts zu ergänzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-11 19:34:36 +02:00
commit faea4bf465

View file

@ -188,17 +188,17 @@ def test_time_suffix(start, end, suffix):
assert vtt2txt.time_suffix(start, end) == suffix
def test_vtt_to_lines_time_range():
def test_vtt_to_lines_exact_time_range():
vtt = "WEBVTT\n\n" + cue(1, "A") + cue(2, "A", "B") + cue(3, "B", "C") + cue(4, "C", "D")
assert vtt2txt.vtt_to_lines(vtt, start=2, end=4) == ["B", "C"]
assert vtt2txt.vtt_to_lines(vtt, start=3) == ["C", "D"]
assert vtt2txt.vtt_to_lines(vtt, end=2) == ["A"]
assert vtt2txt.vtt_to_lines(vtt, start=2, end=4, exact=True) == ["B", "C"]
assert vtt2txt.vtt_to_lines(vtt, start=3, exact=True) == ["C", "D"]
assert vtt2txt.vtt_to_lines(vtt, end=2, exact=True) == ["A"]
def test_vtt_to_lines_time_range_skips_window_from_before_start():
# "B" was said before the range; its repetition in the first cue must not count
vtt = "WEBVTT\n\n" + cue(1, "A", "B") + cue(2, "B", "C")
assert vtt2txt.vtt_to_lines(vtt, start=2) == ["C"]
assert vtt2txt.vtt_to_lines(vtt, start=2, exact=True) == ["C"]
def test_main_time_range_default_name(sample_copy, monkeypatch):
@ -225,3 +225,70 @@ def test_cli_time_errors(sample_copy, monkeypatch, capsys, args, message):
run_main(monkeypatch, "-i", sample_copy, *args)
assert exc.value.code == 2
assert message in capsys.readouterr().err
def timed_cue(seconds, *lines):
return f"{vtt2txt.format_time(seconds)}.000 --> {vtt2txt.format_time(seconds + 1)}.000\n" \
+ "\n".join(lines) + "\n\n"
def captions(*cues):
"""VTT with one new line per cue: cues are (seconds, text)."""
return "WEBVTT\n\n" + "".join(timed_cue(t, text) for t, text in cues)
SPEECH = captions(
(100, "Erster Satz ist hier."), (102, "Der zweite Satz"), (104, "beginnt vorher. Dann"),
(106, "kommt der dritte"), (108, "und endet. Der vierte"), (110, "läuft weiter"),
(112, "und endet auch. Schluss"), (114, "folgt."),
)
def test_snap_extends_both_sides_to_sentences():
# Range holds only "kommt der dritte" and "und endet. Der vierte"
assert vtt2txt.vtt_to_lines(SPEECH, start=106, end=110) == [
"Dann", "kommt der dritte", "und endet. Der vierte", "läuft weiter",
"und endet auch.",
]
def test_snap_keeps_range_already_at_sentence_bounds():
assert vtt2txt.vtt_to_lines(SPEECH, start=102, end=104) == [
"Der zweite Satz", "beginnt vorher.",
]
assert vtt2txt.vtt_to_lines(SPEECH, start=100, end=102) == ["Erster Satz ist hier."]
def test_snap_reaches_start_and_end_of_captions():
assert vtt2txt.vtt_to_lines(SPEECH, start=114) == ["Schluss", "folgt."]
speech = captions((0, "ohne Satzzeichen"), (2, "am Anfang. Rest"))
assert vtt2txt.vtt_to_lines(speech, start=2) == ["ohne Satzzeichen", "am Anfang. Rest"]
def test_snap_cuts_exactly_without_sentence_end_in_reach():
far = vtt2txt.MAX_SNAP_SECONDS + 5
speech = captions(
(0, "Satz."), (2, "ein endloser"), (far, "Satz ohne"), (2 * far, "Ende"), (3 * far, "jetzt."),
)
assert vtt2txt.vtt_to_lines(speech, start=far, end=far + 1) == ["Satz ohne"]
def test_snap_exact_keeps_cut():
assert vtt2txt.vtt_to_lines(SPEECH, start=106, end=110, exact=True) == [
"kommt der dritte", "und endet. Der vierte",
]
def test_snap_closing_quote_belongs_to_sentence():
speech = captions((0, "Er sagte „Nein.“ Und"), (2, "dann"), (4, "ging er."))
assert vtt2txt.vtt_to_lines(speech, start=2, end=4) == ["Und", "dann", "ging er."]
def test_main_exact_option(tmp_path, monkeypatch):
source = tmp_path / "rede.vtt"
source.write_text(SPEECH, encoding="utf-8")
run_main(monkeypatch, "-i", source, "-s", "106", "-e", "110", "--exakt")
target = tmp_path / "rede_00-01-46_bis_00-01-50.txt"
assert target.read_text(encoding="utf-8") == "kommt der dritte\nund endet. Der vierte\n"
run_main(monkeypatch, "-i", source, "-s", "106", "-e", "110")
assert target.read_text(encoding="utf-8").endswith("und endet auch.\n")