feat: -z -00:20 als Kurzform für 0-00:20 (vom Anfang bis)
Wörter hinter -z, die mit - und einer Ziffer beginnen, werden vor argparse zu 0-… umgeschrieben; parse_ranges nimmt -B auch in Anführungszeichen an. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8edef11c78
commit
955bdc2659
6 changed files with 74 additions and 22 deletions
11
README.md
11
README.md
|
|
@ -96,7 +96,7 @@ 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
|
(„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
|
60 Zeichen an einer Wortgrenze gekürzt. Ohne verwertbaren Kanal und Titel gilt die
|
||||||
Video-ID. Mit `-z` kommen die Bereiche dazu, z. B.
|
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_…`;
|
`…_2026-07-30_00-03-17_bis_00-05-25` (`-z A`: `_ab_…`, `-z 0-B` bzw. `-z -B`: `_bis_…`;
|
||||||
mehrere Bereiche mit `+` verbunden: `…_00-05-00_bis_00-06-00+ab_00-25-00`).
|
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
|
Gleiches Video ergibt immer denselben Namen; eine vorhandene Datei wird
|
||||||
überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie).
|
überschrieben (ein mit `-u` gespeicherter, abweichender Rohtext aber nie).
|
||||||
|
|
@ -171,15 +171,14 @@ Gilt gleich für `vtt2txt.py`, `extract_text.py` und `rede_korrigieren.py`.
|
||||||
| Aufruf | Text |
|
| Aufruf | Text |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-z 00:05:10` | ab 5:10 bis zum Ende des Videos |
|
| `-z 00:05:10` | ab 5:10 bis zum Ende des Videos |
|
||||||
| `-z 0-00:20` | vom Anfang bis Minute 20 |
|
| `-z 0-00:20` oder `-z -00:20` | vom Anfang bis Minute 20 |
|
||||||
| `-z 00:05-00:06` | von Minute 5 bis Minute 6 |
|
| `-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 |
|
| `-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 |
|
||||||
|
|
||||||
Ein Bereich ist `A-B`, `A` (ab A bis zum Ende) oder `0-B` (vom Anfang bis B).
|
Ein Bereich ist `A-B`, `A` (ab A bis zum Ende) oder `0-B` bzw. kurz `-B` (vom
|
||||||
Mehrere Bereiche werden durch Leerzeichen getrennt, mit oder ohne
|
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
|
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
|
nicht überschneiden und werden nach der Zeit sortiert.
|
||||||
die Kommandozeile es für eine Option hält.
|
|
||||||
|
|
||||||
Alle Bereiche landen in **einer** Datei. Vor jedem Bereich steht eine Überschrift
|
Alle Bereiche landen in **einer** Datei. Vor jedem Bereich steht eine Überschrift
|
||||||
mit seinen tatsächlichen Zeiten:
|
mit seinen tatsächlichen Zeiten:
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from vtt2txt import (
|
from vtt2txt import (
|
||||||
add_time_options, join_segments, resolve_time_ranges, split_segments, time_suffix,
|
add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments,
|
||||||
vtt_to_text,
|
time_suffix, vtt_to_text,
|
||||||
)
|
)
|
||||||
from yt_untertitel import (
|
from yt_untertitel import (
|
||||||
YT_DLP, choose_track, default_name, download, fetch_info, no_track_message,
|
YT_DLP, choose_track, default_name, download, fetch_info, no_track_message,
|
||||||
|
|
@ -38,7 +39,7 @@ def parse_args():
|
||||||
help=f"Fließtext, auf {FLOW_WIDTH} Zeichen umbrochen, statt Untertitelzeilen",
|
help=f"Fließtext, auf {FLOW_WIDTH} Zeichen umbrochen, statt Untertitelzeilen",
|
||||||
)
|
)
|
||||||
add_time_options(parser)
|
add_time_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(range_argv(sys.argv[1:]))
|
||||||
resolve_time_ranges(parser, args)
|
resolve_time_ranges(parser, args)
|
||||||
|
|
||||||
if args.output is not None and args.output.is_dir():
|
if args.output is not None and args.output.is_dir():
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import urllib.request
|
||||||
from aenderungen import REVIEW_MARK, print_summary, report_path, write_report
|
from aenderungen import REVIEW_MARK, print_summary, report_path, write_report
|
||||||
from extract_text import fetch_text
|
from extract_text import fetch_text
|
||||||
from vtt2txt import (
|
from vtt2txt import (
|
||||||
add_time_options, join_segments, resolve_time_ranges, split_segments, time_suffix,
|
add_time_options, join_segments, range_argv, resolve_time_ranges, split_segments,
|
||||||
vtt_to_text,
|
time_suffix, vtt_to_text,
|
||||||
)
|
)
|
||||||
from yt_untertitel import YT_DLP, base_language, default_name, original_language
|
from yt_untertitel import YT_DLP, base_language, default_name, original_language
|
||||||
|
|
||||||
|
|
@ -509,7 +509,7 @@ def parse_args():
|
||||||
help="keinen Änderungsbericht <Ausgabe>.aenderungen.html schreiben",
|
help="keinen Änderungsbericht <Ausgabe>.aenderungen.html schreiben",
|
||||||
)
|
)
|
||||||
add_time_options(parser)
|
add_time_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(range_argv(sys.argv[1:]))
|
||||||
resolve_time_ranges(parser, args)
|
resolve_time_ranges(parser, args)
|
||||||
|
|
||||||
for glossary in args.glossar:
|
for glossary in args.glossar:
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,13 @@ def test_main_url_time_range_default_names(from_url, tmp_path, monkeypatch):
|
||||||
assert result == "[00:00:02 - 00:00:03]\n\nDas ist bekannt.\n"
|
assert result == "[00:00:02 - 00:00:03]\n\nDas ist bekannt.\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_url_range_from_beginning(from_url, tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
run_main(monkeypatch, "-u", URL, "-z", "-0:00:02")
|
||||||
|
raw = (tmp_path / "Kanal_Rede_2026-03-28_bis_00-00-02.txt").read_text(encoding="utf-8")
|
||||||
|
assert raw == "[00:00:01 - 00:00:02]\nDer Kremmel ist in Moskau.\n"
|
||||||
|
|
||||||
|
|
||||||
def test_main_url_time_range_without_text(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"):
|
with pytest.raises(SystemExit, match="im gewählten Zeitraum keinen Text"):
|
||||||
run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "x.korrigiert.txt")
|
run_main(monkeypatch, "-u", URL, "-z", "1:00", "-o", tmp_path / "x.korrigiert.txt")
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,8 @@ def test_parse_cue_time_keeps_minutes_seconds(text, seconds):
|
||||||
@pytest.mark.parametrize("text, ranges", [
|
@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:5:00-00:6:00 00:10-00:12:00 00:25", [(300, 360), (600, 720), (1500, None)]),
|
||||||
("0-00:20", [(None, 1200)]),
|
("0-00:20", [(None, 1200)]),
|
||||||
|
("-00:20", [(None, 1200)]),
|
||||||
|
("-00:05 00:10-00:12", [(None, 300), (600, 720)]),
|
||||||
("0:00:00-0:01", [(None, 60)]),
|
("0:00:00-0:01", [(None, 60)]),
|
||||||
("00:25- 00:10-00:12 0-00:05", [(None, 300), (600, 720), (1500, None)]),
|
("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)]),
|
("00:05-00:06 00:06-00:07", [(300, 360), (360, 420)]),
|
||||||
|
|
@ -200,8 +202,9 @@ def test_parse_ranges(text, ranges):
|
||||||
|
|
||||||
@pytest.mark.parametrize("text, message", [
|
@pytest.mark.parametrize("text, message", [
|
||||||
("", "keine Bereiche"),
|
("", "keine Bereiche"),
|
||||||
("-", "vom Anfang: 0-B"),
|
("-", "ungültiger Bereich"),
|
||||||
("-00:20", "vom Anfang: 0-B"),
|
("-0", "Start muss vor Ende"),
|
||||||
|
("-00:05 -00:10", "überschneiden"),
|
||||||
("0", "das ganze Video"),
|
("0", "das ganze Video"),
|
||||||
("0-", "das ganze Video"),
|
("0-", "das ganze Video"),
|
||||||
("00:06-00:05", "Start muss vor Ende"),
|
("00:06-00:05", "Start muss vor Ende"),
|
||||||
|
|
@ -384,6 +387,26 @@ def test_main_ranges_with_or_without_quotes(sample_copy, monkeypatch, ranges):
|
||||||
assert target.is_file()
|
assert target.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("argv, expected", [
|
||||||
|
(["-i", "x", "-z", "-00:20"], ["-i", "x", "-z", "0-00:20"]),
|
||||||
|
(["-z", "00:05-00:06", "-00:20", "-o", "f"], ["-z", "00:05-00:06", "0-00:20", "-o", "f"]),
|
||||||
|
(["--zeitraeume", "-0:01:30"], ["--zeitraeume", "0-0:01:30"]),
|
||||||
|
(["-z", "0:05", "--exakt", "-1"], ["-z", "0:05", "--exakt", "-1"]),
|
||||||
|
(["-i", "-5.vtt"], ["-i", "-5.vtt"]),
|
||||||
|
(["-z=-00:20"], ["-z=-00:20"]),
|
||||||
|
])
|
||||||
|
def test_range_argv(argv, expected):
|
||||||
|
assert vtt2txt.range_argv(argv) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("ranges", [["-0:01"], ["0-0:01"], ["-z=-0:01"]])
|
||||||
|
def test_main_range_from_beginning(sample_copy, monkeypatch, ranges):
|
||||||
|
args = ranges if ranges[0].startswith("-z") else ["-z", *ranges]
|
||||||
|
run_main(monkeypatch, "-i", sample_copy, *args)
|
||||||
|
target = sample_copy.with_name("ramstein.de_bis_00-01-00.txt")
|
||||||
|
assert target.read_text(encoding="utf-8").startswith("[00:00:09 - ")
|
||||||
|
|
||||||
|
|
||||||
def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys):
|
def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys):
|
||||||
with pytest.raises(SystemExit) as exc:
|
with pytest.raises(SystemExit) as exc:
|
||||||
run_main(monkeypatch, "-i", sample_copy, "-z", "99:00:00")
|
run_main(monkeypatch, "-i", sample_copy, "-z", "99:00:00")
|
||||||
|
|
@ -396,7 +419,7 @@ def test_main_time_range_without_captions(sample_copy, monkeypatch, capsys):
|
||||||
(["-z", "1:00-0:30"], "Start muss vor Ende"),
|
(["-z", "1:00-0:30"], "Start muss vor Ende"),
|
||||||
(["-z", "0:05 0:10"], "überschneiden"),
|
(["-z", "0:05 0:10"], "überschneiden"),
|
||||||
(["-z", "0:05", "0:10"], "überschneiden"),
|
(["-z", "0:05", "0:10"], "überschneiden"),
|
||||||
(["-z=-0:20"], "vom Anfang: 0-B"),
|
(["-z", "-"], "ungültiger Bereich"),
|
||||||
(["-z", "0"], "das ganze Video"),
|
(["-z", "0"], "das ganze Video"),
|
||||||
(["-s", "0:01"], "unrecognized arguments: -s"),
|
(["-s", "0:01"], "unrecognized arguments: -s"),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
36
vtt2txt.py
36
vtt2txt.py
|
|
@ -48,20 +48,24 @@ def parse_user_time(text):
|
||||||
def parse_ranges(text):
|
def parse_ranges(text):
|
||||||
"""Sorted (start, end) seconds from "00:05:00-00:06:00 00:10-00:12 00:25".
|
"""Sorted (start, end) seconds from "00:05:00-00:06:00 00:10-00:12 00:25".
|
||||||
|
|
||||||
"A" or "A-" runs to the end, "0-B" from the beginning; None marks an open side.
|
"A" or "A-" runs to the end, "0-B" or "-B" from the beginning; None marks an
|
||||||
|
open side.
|
||||||
"""
|
"""
|
||||||
ranges = []
|
ranges = []
|
||||||
for item in text.split():
|
for item in text.split():
|
||||||
if item.startswith("-"):
|
|
||||||
raise ValueError(f"ungültiger Bereich „{item}“, vom Anfang: 0-B schreiben")
|
|
||||||
start, _, end = item.partition("-")
|
start, _, end = item.partition("-")
|
||||||
|
if item == "-":
|
||||||
|
raise ValueError(f"ungültiger Bereich „{item}“")
|
||||||
try:
|
try:
|
||||||
span = (parse_user_time(start) or None, parse_user_time(end) if end else None)
|
span = (
|
||||||
|
parse_user_time(start) or None if start else None,
|
||||||
|
parse_user_time(end) if end else None,
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f"ungültiger Bereich „{item}“") from None
|
raise ValueError(f"ungültiger Bereich „{item}“") from None
|
||||||
if span == (None, None):
|
if span == (None, None):
|
||||||
raise ValueError(f"Bereich „{item}“ ist das ganze Video: -z weglassen")
|
raise ValueError(f"Bereich „{item}“ ist das ganze Video: -z weglassen")
|
||||||
if None not in span and span[0] >= span[1]:
|
if span[1] is not None and (span[0] or 0) >= span[1]:
|
||||||
raise ValueError(f"Bereich „{item}“: Start muss vor Ende liegen")
|
raise ValueError(f"Bereich „{item}“: Start muss vor Ende liegen")
|
||||||
ranges.append(span)
|
ranges.append(span)
|
||||||
if not ranges:
|
if not ranges:
|
||||||
|
|
@ -103,7 +107,7 @@ def time_suffix(ranges):
|
||||||
|
|
||||||
|
|
||||||
RANGE_FORMS = (
|
RANGE_FORMS = (
|
||||||
"A-B (von A bis B), A (ab A bis zum Ende) oder 0-B (vom Anfang bis B); "
|
"A-B (von A bis B), A (ab A bis zum Ende) oder 0-B bzw. -B (vom Anfang bis B); "
|
||||||
f"Zeiten {TIME_EXAMPLES}"
|
f"Zeiten {TIME_EXAMPLES}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -121,6 +125,24 @@ def add_time_options(parser):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def range_argv(argv):
|
||||||
|
"""Copy of `argv` with "-00:20" after -z written as "0-00:20".
|
||||||
|
|
||||||
|
argparse takes a word starting with "-" for an option and would reject it;
|
||||||
|
no option starts with "-" and a digit, so such a word can only be a range.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
after_ranges = False
|
||||||
|
for word in argv:
|
||||||
|
if re.match(r"-\d", word):
|
||||||
|
if after_ranges:
|
||||||
|
word = f"0{word}"
|
||||||
|
elif word.startswith("-"):
|
||||||
|
after_ranges = word in ("-z", "--zeitraeume")
|
||||||
|
result.append(word)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def resolve_time_ranges(parser, args):
|
def resolve_time_ranges(parser, args):
|
||||||
"""Check -z and set args.bereiche: sorted list of (start, end), maybe empty."""
|
"""Check -z and set args.bereiche: sorted list of (start, end), maybe empty."""
|
||||||
if args.zeitraeume is None:
|
if args.zeitraeume is None:
|
||||||
|
|
@ -398,7 +420,7 @@ def parse_args():
|
||||||
"mit -z zusätzlich die Bereiche im Namen",
|
"mit -z zusätzlich die Bereiche im Namen",
|
||||||
)
|
)
|
||||||
add_time_options(parser)
|
add_time_options(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(range_argv(sys.argv[1:]))
|
||||||
resolve_time_ranges(parser, args)
|
resolve_time_ranges(parser, args)
|
||||||
suffix = time_suffix(args.bereiche)
|
suffix = time_suffix(args.bereiche)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue