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:
parent
9bbf325bd3
commit
faea4bf465
3 changed files with 234 additions and 28 deletions
|
|
@ -147,3 +147,21 @@ def test_time_range_order_error(monkeypatch, capsys):
|
|||
run_main(monkeypatch, "-u", URL, "-s", "2:00", "-e", "1:00")
|
||||
assert exc.value.code == 2
|
||||
assert "muss vor -e/--ende liegen" in capsys.readouterr().err
|
||||
|
||||
|
||||
CUT_VTT = (
|
||||
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nGuten Abend und\n\n"
|
||||
"00:00:02.000 --> 00:00:03.000\nwillkommen. Schön, dass\n\n"
|
||||
"00:00:03.000 --> 00:00:04.000\nSie da sind.\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exact, text", [
|
||||
([], "Guten Abend und\nwillkommen. Schön, dass\nSie da sind.\n"),
|
||||
(["--exakt"], "willkommen. Schön, dass\n"),
|
||||
])
|
||||
def test_time_range_sentence_bounds(fake_yt_dlp, tmp_path, monkeypatch, exact, text):
|
||||
monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=CUT_VTT))
|
||||
target = tmp_path / "out.txt"
|
||||
run_main(monkeypatch, "-u", URL, "-s", "2", "-e", "3", "-o", target, *exact)
|
||||
assert target.read_text(encoding="utf-8") == text
|
||||
|
|
|
|||
|
|
@ -615,3 +615,36 @@ def test_ollama_open_passes_http_errors(monkeypatch):
|
|||
monkeypatch.setattr(rk.urllib.request, "urlopen", not_found)
|
||||
with pytest.raises(rk.urllib.error.HTTPError):
|
||||
rk.ollama_open(rk.OLLAMA_URL)
|
||||
|
||||
|
||||
# === Excerpts: sentence bounds and prompt note ===
|
||||
|
||||
@pytest.mark.parametrize("lang", ["de", "en"])
|
||||
def test_build_system_excerpt_note(lang):
|
||||
prompts = rk.PROMPTS[lang]
|
||||
assert prompts["excerpt"] not in rk.build_system(["X"], lang)
|
||||
system = rk.build_system(["X"], lang, excerpt=True)
|
||||
assert system.startswith(prompts["system"])
|
||||
assert system.index(prompts["excerpt"]) < system.index(prompts["glossary"])
|
||||
assert rk.build_system([], lang, excerpt=True).endswith(prompts["excerpt"] + "\n")
|
||||
|
||||
|
||||
CUT_VTT = (
|
||||
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nDer Kremmel ist\n\n"
|
||||
"00:00:02.000 --> 00:00:03.000\nin Moskau. Das ist\n\n"
|
||||
"00:00:03.000 --> 00:00:04.000\nbekannt.\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exact, raw", [
|
||||
([], "Der Kremmel ist\nin Moskau. Das ist\nbekannt.\n"),
|
||||
(["--exakt"], "in Moskau. Das ist\n"),
|
||||
])
|
||||
def test_main_url_excerpt(fake_yt_dlp, yt_dlp_installed, tmp_path, monkeypatch, exact, raw):
|
||||
monkeypatch.setattr(subprocess, "run", fake_yt_dlp(VIDEO, vtt=CUT_VTT))
|
||||
systems = recording_model(monkeypatch)
|
||||
target = tmp_path / "rede.korrigiert.txt"
|
||||
run_main(monkeypatch, "-u", URL, "-s", "2", "-e", "3", "-o", target, *exact)
|
||||
|
||||
assert (tmp_path / "rede.txt").read_text(encoding="utf-8") == raw
|
||||
assert rk.PROMPTS["de"]["excerpt"] in systems[0]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue