refactor: Zeitauswahl nur noch über -z, -s/-e entfallen
-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 <noreply@anthropic.com>
This commit is contained in:
parent
5e9a11d0f4
commit
8edef11c78
7 changed files with 93 additions and 108 deletions
48
README.md
48
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 (`<sprache>-orig`) |
|
||||
| `-o`, `--output` | Ausgabedatei (Verzeichnisse werden angelegt, vorhandene Datei überschrieben); Standard: `<Kanal>_<Titel>_<Datum>.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: `<Eingabe ohne Endung>.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: `<Eingabe ohne Endung>.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 → <Kanal>_<Titel>_<Datum>.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 `<Name>.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: `<Eingabe ohne Endung>.korrigiert.txt` neben der Eingabe, mit `-u` `<Kanal>_<Titel>_<Datum>.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: `<Eingabe ohne Endung>.korrigiert.txt` neben der Eingabe, mit `-u` `<Kanal>_<Titel>_<Datum>.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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def parse_args():
|
|||
parser.add_argument(
|
||||
"-o", "--output", type=Path,
|
||||
help="Ausgabedatei (Standard: <Kanal>_<Titel>_<Datum>.txt, "
|
||||
"mit -s/-e/-z zusätzlich die Zeiträume)",
|
||||
"mit -z zusätzlich die Bereiche)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f", "--fliesstext", action="store_true",
|
||||
|
|
|
|||
|
|
@ -493,8 +493,8 @@ def parse_args():
|
|||
parser.add_argument(
|
||||
"-o", "--output", type=Path,
|
||||
help="Ausgabedatei (Standard: <Eingabe ohne Endung>.korrigiert.txt, "
|
||||
"mit -u <Kanal>_<Titel>_<Datum>.korrigiert.txt; mit -s/-e zusätzlich "
|
||||
"der Zeitraum)",
|
||||
"mit -u <Kanal>_<Titel>_<Datum>.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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
78
vtt2txt.py
78
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: <Eingabe ohne Endung>.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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue