From 8edef11c78e8d3af285210a710029a979a8e866e Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sun, 11 Oct 2026 20:20:54 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20Zeitauswahl=20nur=20noch=20=C3=BCbe?= =?UTF-8?q?r=20-z,=20-s/-e=20entfallen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -z 00:05:10 (ab), -z 0-00:20 (vom Anfang bis), -z 00:05-00:06 und mehrere Bereiche, mit oder ohne Anführungszeichen. Co-Authored-By: Claude Opus 5.5 --- README.md | 48 ++++++++++----------- extract_text.py | 2 +- rede_korrigieren.py | 6 +-- tests/test_extract_text.py | 12 +++--- tests/test_rede_korrigieren.py | 18 ++++---- tests/test_vtt2txt.py | 37 ++++++++++------ vtt2txt.py | 78 +++++++++++----------------------- 7 files changed, 93 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 6602304..8225c94 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei # → John_Mearsheimer_Offensive_Realism_Episode_1_Origins_of_the_War_In_Ukraine_2026-07-30.txt ./extract_text.py -u URL -l de -o testdaten/rede.txt # Sprache und Ziel ./extract_text.py -u URL -f -o rede.txt # lesbarer Fließtext -./extract_text.py -u URL -s 0:03:17 -e 0:05:25 # nur ein Ausschnitt -./extract_text.py -u URL -z "00:05-00:06 00:10-00:12 00:25" # mehrere Bereiche +./extract_text.py -u URL -z 0:03:17-0:05:25 # nur ein Ausschnitt +./extract_text.py -u URL -z 00:05-00:06 00:10-00:12 00:25 # mehrere Bereiche ./rede_korrigieren.py -i rede.txt # → rede.korrigiert.txt ``` @@ -78,7 +78,7 @@ Zwischenschritt im temporären Verzeichnis. Die Spurwahl ist dieselbe wie bei | `-u`, `--url` | YouTube-URL, Pflicht | | `-l`, `--lang` | Sprache; Standard: Sprache des Original-Audios (`-orig`) | | `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `__.txt`, siehe unten | -| `-s`, `--start` / `-e`, `--ende` / `-z`, `--zeitraeume` | nur Untertitel in diesen Zeiträumen, siehe [Zeitauswahl](#zeitauswahl--s--e--z) | +| `-z`, `--zeitraeume` | nur Untertitel in diesen Bereichen, siehe [Zeitauswahl](#zeitauswahl--z) | | `--exakt` | genau an den Zeiten schneiden statt an Satzgrenzen | | `-f`, `--fliesstext` | Fließtext, auf 80 Zeichen umbrochen (Wörter werden nie getrennt), statt der kurzen Untertitelzeilen; beides versteht `rede_korrigieren.py` | @@ -95,8 +95,8 @@ Ohne `-o` bzw. `-n` bilden `extract_text.py`, `yt_untertitel.py` und Wörter werden mit `_` verbunden. Bindestriche und Punkte im Wort bleiben („Krone-Schmalz“), andere Zeichen fallen weg. Der Kanal wird auf 40, der Titel auf 60 Zeichen an einer Wortgrenze gekürzt. Ohne verwertbaren Kanal und Titel gilt die -Video-ID. Mit `-s`/`-e`/`-z` kommen die Zeiträume dazu, z. B. -`…_2026-07-30_00-03-17_bis_00-05-25` (nur `-s`: `_ab_…`, nur `-e`: `_bis_…`; +Video-ID. Mit `-z` kommen die Bereiche dazu, z. B. +`…_2026-07-30_00-03-17_bis_00-05-25` (`-z A`: `_ab_…`, `-z 0-B`: `_bis_…`; mehrere Bereiche mit `+` verbunden: `…_00-05-00_bis_00-06-00+ab_00-25-00`). Gleiches Video ergibt immer denselben Namen; eine vorhandene Datei wird überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie). @@ -147,20 +147,18 @@ zweimal, bleibt sie zweimal erhalten. ./vtt2txt.py -i "ramstein.de*.vtt" # Muster löst das Programm auf (Windows) ./vtt2txt.py -i rede.de.vtt -o out/rede.txt # eigene Ausgabedatei ./vtt2txt.py -i *.vtt -o out/ # alle in ein Verzeichnis -./vtt2txt.py -i rede.de.vtt -s 0:03:17 -e 0:05:25 # Ausschnitt → rede.de_00-03-17_bis_00-05-25.txt -./vtt2txt.py -i rede.de.vtt -z "00:05-00:06 00:25" # mehrere Bereiche in einer Datei +./vtt2txt.py -i rede.de.vtt -z 0:03:17-0:05:25 # Ausschnitt → rede.de_00-03-17_bis_00-05-25.txt +./vtt2txt.py -i rede.de.vtt -z 00:05-00:06 00:25 # mehrere Bereiche in einer Datei ``` | Option | Bedeutung | |---|---| | `-i`, `--input` | eine oder mehrere VTT-Dateien oder Muster, Pflicht | -| `-o`, `--output` | Ausgabedatei (nur bei einer Eingabedatei) oder Verzeichnis (vorhanden oder mit `/` am Ende); Standard: `.txt` neben der Eingabe, mit `-s`/`-e`/`-z` zusätzlich die Zeiträume im Namen | -| `-s`, `--start` | nur Untertitel ab dieser Zeit | -| `-e`, `--ende` | nur Untertitel vor dieser Zeit | -| `-z`, `--zeitraeume` | mehrere Bereiche statt `-s`/`-e` | +| `-o`, `--output` | Ausgabedatei (nur bei einer Eingabedatei) oder Verzeichnis (vorhanden oder mit `/` am Ende); Standard: `.txt` neben der Eingabe, mit `-z` zusätzlich die Bereiche im Namen | +| `-z`, `--zeitraeume` | nur Untertitel in diesen Bereichen, siehe unten | | `--exakt` | genau an den Zeiten schneiden, auch mitten im Satz | -#### Zeitauswahl (`-s`, `-e`, `-z`) +#### Zeitauswahl (`-z`) Gilt gleich für `vtt2txt.py`, `extract_text.py` und `rede_korrigieren.py`. @@ -172,14 +170,16 @@ Gilt gleich für `vtt2txt.py`, `extract_text.py` und `rede_korrigieren.py`. | Aufruf | Text | |---|---| -| `-s 00:05:10` | ab 5:10 bis zum Ende des Videos | -| `-e 00:20` | vom Anfang bis Minute 20 | -| `-s 00:05 -e 00:06` | von Minute 5 bis Minute 6 | -| `-z "00:5:00-00:6:00 00:10-00:12:00 00:25"` | Minute 5–6, Minute 10–12 und alles ab Minute 25 | +| `-z 00:05:10` | ab 5:10 bis zum Ende des Videos | +| `-z 0-00:20` | vom Anfang bis Minute 20 | +| `-z 00:05-00:06` | von Minute 5 bis Minute 6 | +| `-z 00:5:00-00:6:00 00:10-00:12:00 00:25` | Minute 5–6, Minute 10–12 und alles ab Minute 25 | -Bei `-z` werden die Bereiche durch Leerzeichen getrennt: `A-B`, `A` (ab A bis -zum Ende) oder `-B` (vom Anfang bis B). Sie dürfen sich nicht überschneiden und -werden nach der Zeit sortiert. +Ein Bereich ist `A-B`, `A` (ab A bis zum Ende) oder `0-B` (vom Anfang bis B). +Mehrere Bereiche werden durch Leerzeichen getrennt, mit oder ohne +Anführungszeichen (`-z "00:05-00:06 00:25"` ist dasselbe). Sie dürfen sich +nicht überschneiden und werden nach der Zeit sortiert. `-B` geht nicht, weil +die Kommandozeile es für eine Option hält. Alle Bereiche landen in **einer** Datei. Vor jedem Bereich steht eine Überschrift mit seinen tatsächlichen Zeiten: @@ -217,8 +217,8 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1. ./rede_korrigieren.py -i rede.de.vtt # VTT direkt → rede.de.korrigiert.txt ./rede_korrigieren.py -u URL # YouTube → __.korrigiert.txt ./rede_korrigieren.py -i speech.txt -l en # englische Rede -./rede_korrigieren.py -u URL -s 0:03:17 -e 0:05:25 # nur ein Ausschnitt des Videos -./rede_korrigieren.py -u URL -z "00:05-00:06 00:25" # mehrere Bereiche, eine Datei +./rede_korrigieren.py -u URL -z 0:03:17-0:05:25 # nur ein Ausschnitt des Videos +./rede_korrigieren.py -u URL -z 00:05-00:06 00:25 # mehrere Bereiche, eine Datei ``` | Option | Bedeutung | @@ -226,8 +226,8 @@ werden trotzdem umgewandelt; der Exit-Code ist dann 1. | `-i`, `--input` | Eingabedatei: Rohabschrift (`.txt`) oder Untertitel (`.vtt`, wird wie mit `vtt2txt.py` umgewandelt) | | `-u`, `--url` | statt `-i`: YouTube-URL; Untertitel wie bei `extract_text.py`. Der Rohtext wird neben der Ausgabe gespeichert (`rede.korrigiert.txt` → `rede.txt`, sonst `.roh.txt`), eine vorhandene, abweichende Datei aber nie überschrieben | | `-l`, `--lang` | Sprache der Rede: `de` oder `en` (auch mit Region, z. B. `en-US`); wählt Prompts und Prüfmarke. Mit `-u` zugleich die Sprache der Untertitel. Standard: `de`, mit `-u` die Sprache des Original-Audios | -| `-o`, `--output` | Ausgabedatei; Standard: `.korrigiert.txt` neben der Eingabe, mit `-u` `__.korrigiert.txt`; mit `-s`/`-e`/`-z` zusätzlich die Zeiträume im Namen | -| `-s`, `--start` / `-e`, `--ende` / `-z`, `--zeitraeume` | nur diese Zeiträume korrigieren, siehe [Zeitauswahl](#zeitauswahl--s--e--z); nur mit `.vtt`-Eingabe oder `-u`, da reiner Text keine Zeitstempel hat | +| `-o`, `--output` | Ausgabedatei; Standard: `.korrigiert.txt` neben der Eingabe, mit `-u` `__.korrigiert.txt`; mit `-z` zusätzlich die Bereiche im Namen | +| `-z`, `--zeitraeume` | nur diese Bereiche korrigieren, siehe [Zeitauswahl](#zeitauswahl--z); nur mit `.vtt`-Eingabe oder `-u`, da reiner Text keine Zeitstempel hat | | `--exakt` | genau an den Zeiten schneiden statt an Satzgrenzen | Mit Zeitauswahl wird jeder Bereich als eigener Block korrigiert und gegliedert; @@ -330,7 +330,7 @@ falsche feste Ersetzung verdirbt den Text unbemerkt. Zum Übernehmen `# ? ` entf Einträge aus den mit `-g` angegebenen Glossaren werden vorher angewendet und nicht erneut vorgeschlagen. Zeit-Überschriften von Ausschnitten -(`[00:04:57 - 00:06:04]`, siehe [Zeitauswahl](#zeitauswahl--s--e--z)) werden aus +(`[00:04:57 - 00:06:04]`, siehe [Zeitauswahl](#zeitauswahl--z)) werden aus Roh- und korrigiertem Text entfernt; das Modell bekommt sie nicht zu sehen. ```bash diff --git a/extract_text.py b/extract_text.py index 85a7542..b5f7d4f 100755 --- a/extract_text.py +++ b/extract_text.py @@ -31,7 +31,7 @@ def parse_args(): parser.add_argument( "-o", "--output", type=Path, help="Ausgabedatei (Standard: __.txt, " - "mit -s/-e/-z zusätzlich die Zeiträume)", + "mit -z zusätzlich die Bereiche)", ) parser.add_argument( "-f", "--fliesstext", action="store_true", diff --git a/rede_korrigieren.py b/rede_korrigieren.py index e34c19b..f13f6af 100755 --- a/rede_korrigieren.py +++ b/rede_korrigieren.py @@ -493,8 +493,8 @@ def parse_args(): parser.add_argument( "-o", "--output", type=Path, help="Ausgabedatei (Standard: .korrigiert.txt, " - "mit -u __.korrigiert.txt; mit -s/-e zusätzlich " - "der Zeitraum)", + "mit -u __.korrigiert.txt; mit -z zusätzlich " + "die Bereiche)", ) parser.add_argument( "-g", "--glossar", type=Path, action="append", default=[], @@ -528,7 +528,7 @@ def parse_args(): if not args.input.is_file(): parser.error(f"Eingabedatei nicht gefunden: {args.input}") if args.bereiche and args.input.suffix.lower() != ".vtt": - parser.error("-s/-e/-z brauchen Zeitstempel: nur mit .vtt-Eingabe oder -u.") + parser.error("-z braucht Zeitstempel: nur mit .vtt-Eingabe oder -u.") if args.output is None: suffix = time_suffix(args.bereiche) args.output = args.input.with_name(f"{args.input.stem}{suffix}.korrigiert.txt") diff --git a/tests/test_extract_text.py b/tests/test_extract_text.py index 4b8b801..07131b9 100644 --- a/tests/test_extract_text.py +++ b/tests/test_extract_text.py @@ -130,7 +130,7 @@ def test_cli_errors(tmp_path, monkeypatch, capsys, args, message): def test_time_range_default_name(fake_yt_dlp, tmp_path, monkeypatch): monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=VTT)) monkeypatch.chdir(tmp_path) - run_main(monkeypatch, "-u", URL, "-s", "0:00:02", "-e", "0:00:03") + run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03") name = "Kanal_Eine_Rede_2026-03-28_00-00-02_bis_00-00-03.txt" assert (tmp_path / name).read_text(encoding="utf-8") == ( @@ -141,14 +141,14 @@ def test_time_range_default_name(fake_yt_dlp, tmp_path, monkeypatch): def test_time_range_without_text(fake_yt_dlp, tmp_path, monkeypatch): monkeypatch.setattr(yt.subprocess, "run", fake_yt_dlp(GERMAN_VIDEO, vtt=VTT)) with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"): - run_main(monkeypatch, "-u", URL, "-s", "1:00", "-o", tmp_path / "out.txt") + run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "out.txt") def test_time_range_order_error(monkeypatch, capsys): with pytest.raises(SystemExit) as exc: - run_main(monkeypatch, "-u", URL, "-s", "2:00", "-e", "1:00") + run_main(monkeypatch, "-u", URL, "-z", "2:00-1:00") assert exc.value.code == 2 - assert "muss vor -e/--ende liegen" in capsys.readouterr().err + assert "Start muss vor Ende" in capsys.readouterr().err CUT_VTT = ( @@ -165,7 +165,7 @@ CUT_VTT = ( 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", "0:00:02", "-e", "0:00:03", "-o", target, *exact) + run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03", "-o", target, *exact) assert target.read_text(encoding="utf-8") == text @@ -182,7 +182,7 @@ def test_several_ranges_flow_text_keeps_headers(fake_yt_dlp, tmp_path, monkeypat monkeypatch.chdir(tmp_path) run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-f") - name = "Kanal_Eine_Rede_2026-03-28_00-00-00_bis_00-00-30+ab_00-02-00.txt" + name = "Kanal_Eine_Rede_2026-03-28_bis_00-00-30+ab_00-02-00.txt" assert (tmp_path / name).read_text(encoding="utf-8") == ( "[00:00:01 - 00:00:02]\nErster Satz der\nRede.\n\n" "[00:02:00 - 00:02:01]\nDritter Satz, der\netwas länger ist.\n" diff --git a/tests/test_rede_korrigieren.py b/tests/test_rede_korrigieren.py index 1850248..ee581e2 100644 --- a/tests/test_rede_korrigieren.py +++ b/tests/test_rede_korrigieren.py @@ -523,7 +523,7 @@ def test_cli_errors(sample_copy, monkeypatch, capsys, args, message): def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) - run_main(monkeypatch, "-u", URL, "-s", "0:00:02") + run_main(monkeypatch, "-u", URL, "-z", "0:00:02") name = "Kanal_Rede_2026-03-28_ab_00-00-02" raw = (tmp_path / f"{name}.txt").read_text(encoding="utf-8") @@ -534,7 +534,7 @@ def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch): def test_main_url_time_range_without_text(from_url, tmp_path, monkeypatch): with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"): - run_main(monkeypatch, "-u", URL, "-s", "1:00", "-o", tmp_path / "x.korrigiert.txt") + run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "x.korrigiert.txt") def test_main_vtt_time_range(tmp_path, monkeypatch): @@ -542,7 +542,7 @@ def test_main_vtt_time_range(tmp_path, monkeypatch): vtt.write_text(VTT, encoding="utf-8") monkeypatch.setattr(rk, "correct_chunk", identity_correct) monkeypatch.setattr(rk, "find_paragraph_starts", every_nth_start(5)) - run_main(monkeypatch, "-i", vtt, "-e", "0:00:02") + run_main(monkeypatch, "-i", vtt, "-z", "0-0:00:02") result = tmp_path / "rede.de_bis_00-00-02.korrigiert.txt" assert result.read_text(encoding="utf-8") == ( @@ -555,13 +555,13 @@ def test_main_vtt_time_range_without_text(tmp_path, monkeypatch): vtt.write_text(VTT, encoding="utf-8") monkeypatch.setattr(rk, "correct_chunk", no_model_calls) with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"): - run_main(monkeypatch, "-i", vtt, "-s", "1:00") + run_main(monkeypatch, "-i", vtt, "-z", "1:00") @pytest.mark.parametrize("args, message", [ - (["-i", "{sample}", "-s", "1:00"], "nur mit .vtt-Eingabe oder -u"), - (["-i", "{sample}", "-s", "x"], "ungültige Zeit"), - (["-i", "{sample}", "-s", "2:00", "-e", "1:00"], "muss vor -e/--ende liegen"), + (["-i", "{sample}", "-z", "1:00"], "nur mit .vtt-Eingabe oder -u"), + (["-i", "{sample}", "-z", "x"], "ungültiger Bereich"), + (["-i", "{sample}", "-z", "2:00-1:00"], "Start muss vor Ende"), (["-i", "{sample}", "-z", "0:05 0:10"], "überschneiden"), ]) def test_cli_time_errors(sample_copy, monkeypatch, capsys, args, message): @@ -648,7 +648,7 @@ def test_main_url_excerpt(fake_yt_dlp, yt_dlp_installed, tmp_path, monkeypatch, 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", "0:00:02", "-e", "0:00:03", "-o", target, *exact) + run_main(monkeypatch, "-u", URL, "-z", "0:00:02-0:00:03", "-o", target, *exact) assert (tmp_path / "rede.txt").read_text(encoding="utf-8") == raw assert rk.PROMPTS["de"]["excerpt"] in systems[0] @@ -682,7 +682,7 @@ def test_main_url_several_ranges(fake_yt_dlp, yt_dlp_installed, tmp_path, monkey monkeypatch.chdir(tmp_path) run_main(monkeypatch, "-u", URL, "-z", "0:00:00-0:00:30 0:02", "-g", SHARED_GLOSSARY) - name = "Kanal_Rede_2026-03-28_00-00-00_bis_00-00-30+ab_00-02-00" + name = "Kanal_Rede_2026-03-28_bis_00-00-30+ab_00-02-00" assert (tmp_path / f"{name}.txt").read_text(encoding="utf-8") == ( "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n\n" "[00:02:00 - 00:02:01]\nDas ist bekannt.\n" diff --git a/tests/test_vtt2txt.py b/tests/test_vtt2txt.py index d5201b4..01bebe0 100644 --- a/tests/test_vtt2txt.py +++ b/tests/test_vtt2txt.py @@ -189,8 +189,9 @@ def test_parse_cue_time_keeps_minutes_seconds(text, seconds): @pytest.mark.parametrize("text, ranges", [ ("00:5:00-00:6:00 00:10-00:12:00 00:25", [(300, 360), (600, 720), (1500, None)]), - ("-00:20", [(None, 1200)]), - ("00:25- 00:10-00:12 -00:05", [(None, 300), (600, 720), (1500, None)]), + ("0-00:20", [(None, 1200)]), + ("0:00:00-0:01", [(None, 60)]), + ("00:25- 00:10-00:12 0-00:05", [(None, 300), (600, 720), (1500, None)]), ("00:05-00:06 00:06-00:07", [(300, 360), (360, 420)]), ]) def test_parse_ranges(text, ranges): @@ -199,11 +200,14 @@ def test_parse_ranges(text, ranges): @pytest.mark.parametrize("text, message", [ ("", "keine Bereiche"), - ("-", "ungültiger Bereich"), + ("-", "vom Anfang: 0-B"), + ("-00:20", "vom Anfang: 0-B"), + ("0", "das ganze Video"), + ("0-", "das ganze Video"), ("00:06-00:05", "Start muss vor Ende"), ("00:05-00:10 00:08-00:12", "überschneiden"), ("00:05 00:10", "überschneiden"), - ("-00:05 -00:10", "überschneiden"), + ("0-00:05 0-00:10", "überschneiden"), ("00:05-x", "ungültiger Bereich"), ]) def test_parse_ranges_rejects(text, message): @@ -356,7 +360,7 @@ def test_split_segments_inverts_join_segments(): # === Command line === def test_main_time_range_default_name(sample_copy, monkeypatch): - run_main(monkeypatch, "-i", sample_copy, "-s", "0:00:30", "-e", "0:01") + run_main(monkeypatch, "-i", sample_copy, "-z", "0:00:30-0:01") target = sample_copy.with_name("ramstein.de_00-00-30_bis_00-01-00.txt") full = SAMPLE_TXT.read_text(encoding="utf-8").splitlines() part = target.read_text(encoding="utf-8").splitlines() @@ -373,19 +377,28 @@ def test_main_several_ranges(sample_copy, monkeypatch): ] +@pytest.mark.parametrize("ranges", [["0:01-0:02 0:05"], ["0:01-0:02", "0:05"]]) +def test_main_ranges_with_or_without_quotes(sample_copy, monkeypatch, ranges): + run_main(monkeypatch, "-i", sample_copy, "-z", *ranges) + target = sample_copy.with_name("ramstein.de_00-01-00_bis_00-02-00+ab_00-05-00.txt") + assert target.is_file() + + def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys): with pytest.raises(SystemExit) as exc: - run_main(monkeypatch, "-i", sample_copy, "-s", "99:00:00") + run_main(monkeypatch, "-i", sample_copy, "-z", "99:00:00") assert exc.value.code == 1 assert "im gewählten Zeitraum" in capsys.readouterr().err @pytest.mark.parametrize("args, message", [ - (["-s", "abc"], "ungültige Zeit"), - (["-s", "1:00", "-e", "0:30"], "muss vor -e/--ende liegen"), - (["-z", "0:05-0:04"], "Start muss vor Ende"), + (["-z", "abc"], "ungültiger Bereich"), + (["-z", "1:00-0:30"], "Start muss vor Ende"), (["-z", "0:05 0:10"], "überschneiden"), - (["-z", "0:05", "-s", "0:01"], "nicht zusammen mit -s/-e"), + (["-z", "0:05", "0:10"], "überschneiden"), + (["-z=-0:20"], "vom Anfang: 0-B"), + (["-z", "0"], "das ganze Video"), + (["-s", "0:01"], "unrecognized arguments: -s"), ]) def test_cli_time_errors(sample_copy, monkeypatch, capsys, args, message): with pytest.raises(SystemExit) as exc: @@ -397,10 +410,10 @@ def test_cli_time_errors(sample_copy, monkeypatch, capsys, args, message): 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", "0:01:46", "-e", "0:01:50", "--exakt") + run_main(monkeypatch, "-i", source, "-z", "0:01:46-0:01:50", "--exakt") target = tmp_path / "rede_00-01-46_bis_00-01-50.txt" assert target.read_text(encoding="utf-8") == ( "[00:01:46 - 00:01:49]\nkommt der dritte\nund endet. Der vierte\n" ) - run_main(monkeypatch, "-i", source, "-s", "0:01:46", "-e", "0:01:50") + run_main(monkeypatch, "-i", source, "-z", "0:01:46-0:01:50") assert target.read_text(encoding="utf-8").endswith("und endet auch.\n") diff --git a/vtt2txt.py b/vtt2txt.py index 04f88d6..04b2d09 100755 --- a/vtt2txt.py +++ b/vtt2txt.py @@ -45,33 +45,22 @@ def parse_user_time(text): return int(first) * 3600 + int(minutes) * 60 + seconds -def time_option(text): - """argparse type for -s/--start and -e/--ende.""" - try: - return parse_user_time(text) - except ValueError: - raise argparse.ArgumentTypeError( - f"ungültige Zeit „{text}“ ({TIME_EXAMPLES})" - ) from None - - def parse_ranges(text): """Sorted (start, end) seconds from "00:05:00-00:06:00 00:10-00:12 00:25". - "A" or "A-" runs to the end, "-B" from the beginning; None marks an open side. + "A" or "A-" runs to the end, "0-B" from the beginning; None marks an open side. """ ranges = [] for item in text.split(): - start, dash, end = item.partition("-") + if item.startswith("-"): + raise ValueError(f"ungültiger Bereich „{item}“, vom Anfang: 0-B schreiben") + start, _, end = item.partition("-") try: - span = ( - parse_user_time(start) if start else None, - parse_user_time(end) if end else None, - ) + span = (parse_user_time(start) or None, parse_user_time(end) if end else None) except ValueError: raise ValueError(f"ungültiger Bereich „{item}“") from None if span == (None, None): - raise ValueError(f"ungültiger Bereich „{item}“") + raise ValueError(f"Bereich „{item}“ ist das ganze Video: -z weglassen") if None not in span and span[0] >= span[1]: raise ValueError(f"Bereich „{item}“: Start muss vor Ende liegen") ranges.append(span) @@ -86,17 +75,6 @@ def parse_ranges(text): return ranges -def ranges_option(text): - """argparse type for -z/--zeitraeume.""" - try: - return parse_ranges(text) - except ValueError as error: - raise argparse.ArgumentTypeError( - f"{error} (Bereiche durch Leerzeichen getrennt, je A-B, A = ab A, " - f"-B = bis B; Zeiten {TIME_EXAMPLES})" - ) from None - - def format_time(seconds, separator=":"): seconds = int(seconds) return separator.join( @@ -124,41 +102,35 @@ def time_suffix(ranges): return "_" + "+".join(range_name(start, end) for start, end in ranges) +RANGE_FORMS = ( + "A-B (von A bis B), A (ab A bis zum Ende) oder 0-B (vom Anfang bis B); " + f"Zeiten {TIME_EXAMPLES}" +) + + def add_time_options(parser): parser.add_argument( - "-s", "--start", type=time_option, metavar="ZEIT", - help=f"nur Untertitel ab dieser Zeit, {TIME_EXAMPLES}", - ) - parser.add_argument( - "-e", "--ende", type=time_option, metavar="ZEIT", - help="nur Untertitel vor dieser Zeit (Format wie -s)", - ) - parser.add_argument( - "-z", "--zeitraeume", type=ranges_option, metavar="BEREICHE", - help="mehrere Bereiche statt -s/-e, durch Leerzeichen getrennt: A-B, " - "A (ab A bis zum Ende) oder -B (vom Anfang bis B), " - "z. B. \"00:05:00-00:06:00 00:10-00:12 00:25\"", + "-z", "--zeitraeume", nargs="+", metavar="BEREICH", + help=f"nur Untertitel in diesen Bereichen: {RANGE_FORMS}; mehrere durch " + "Leerzeichen getrennt, z. B. -z 00:05-00:06 00:10-00:12 00:25", ) parser.add_argument( "--exakt", action="store_true", help="genau an den Zeiten schneiden, statt angeschnittene Sätze vollständig " - f"aufzunehmen (bis {MAX_SNAP_SECONDS} s über den Zeitraum hinaus)", + f"aufzunehmen (bis {MAX_SNAP_SECONDS} s über den Bereich hinaus)", ) def resolve_time_ranges(parser, args): - """Check the time options and set args.bereiche: list of (start, end), maybe empty.""" - single = args.start is not None or args.ende is not None - if args.zeitraeume is not None: - if single: - parser.error("-z/--zeitraeume nicht zusammen mit -s/-e angeben.") - args.bereiche = args.zeitraeume - elif single: - if args.start is not None and args.ende is not None and args.start >= args.ende: - parser.error("-s/--start muss vor -e/--ende liegen.") - args.bereiche = [(args.start, args.ende)] - else: + """Check -z and set args.bereiche: sorted list of (start, end), maybe empty.""" + if args.zeitraeume is None: args.bereiche = [] + return + # Parts with and without quotes alike: "-z '0:05-0:06 0:25'" or "-z 0:05-0:06 0:25" + try: + args.bereiche = parse_ranges(" ".join(args.zeitraeume)) + except ValueError as error: + parser.error(f"-z/--zeitraeume: {error} ({RANGE_FORMS})") def read_cues(vtt): @@ -423,7 +395,7 @@ def parse_args(): "-o", "--output", help="Ausgabedatei (nur bei einer Eingabedatei) oder Verzeichnis " "(vorhanden oder mit / am Ende); Standard: .txt, " - "mit -s/-e/-z zusätzlich die Zeiträume im Namen", + "mit -z zusätzlich die Bereiche im Namen", ) add_time_options(parser) args = parser.parse_args()