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
1 changed files with 234 additions and 28 deletions
88
vtt2txt.py
88
vtt2txt.py
|
|
@ -13,6 +13,10 @@ TAG = re.compile(r"<[^>]*>")
|
|||
GLOB_CHARS = re.compile(r"[*?\[]")
|
||||
TIME = re.compile(r"(?:(\d+):)??(?:(\d+):)?(\d+(?:[.,]\d+)?)")
|
||||
CUE_START = re.compile(r"^\s*([\d:.,]+)\s*-->")
|
||||
SENTENCE_END = re.compile(r"[.!?…][\"'»«“”)\]]*(?=\s|$)")
|
||||
LINE_ENDS_SENTENCE = re.compile(r"[.!?…][\"'»«“”)\]]*$")
|
||||
|
||||
MAX_SNAP_SECONDS = 30 # so weit darf ein Zeitraum bis zur nächsten Satzgrenze wachsen
|
||||
|
||||
|
||||
def parse_time(text):
|
||||
|
|
@ -61,6 +65,11 @@ def add_time_options(parser):
|
|||
"-e", "--ende", type=time_option, metavar="ZEIT",
|
||||
help="nur Untertitel vor dieser Zeit, z. B. 00:05:25",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exakt", action="store_true",
|
||||
help="genau an -s/-e schneiden, statt angeschnittene Sätze vollständig "
|
||||
f"aufzunehmen (bis {MAX_SNAP_SECONDS} s über den Zeitraum hinaus)",
|
||||
)
|
||||
|
||||
|
||||
def check_time_range(parser, args):
|
||||
|
|
@ -117,16 +126,12 @@ def in_range(time, start, end):
|
|||
return (start is None or time >= start) and (end is None or time < end)
|
||||
|
||||
|
||||
def vtt_to_lines(vtt, start=None, end=None):
|
||||
"""Return the caption text lines of a VTT document without rolling duplicates.
|
||||
def timed_lines(vtt):
|
||||
"""Return (cue start in seconds, line) for the new caption lines of a VTT document.
|
||||
|
||||
Auto captions show a rolling window: each cue repeats the end of the previous
|
||||
cue before adding new text. Only the lines after that overlap are new, so a
|
||||
line the speaker really says twice in a row is kept.
|
||||
|
||||
With `start`/`end` (seconds) only the new lines of cues beginning in that
|
||||
range are kept. The window is followed through all cues, so a line repeated
|
||||
from before `start` does not slip in.
|
||||
"""
|
||||
lines = []
|
||||
previous = []
|
||||
|
|
@ -135,21 +140,82 @@ def vtt_to_lines(vtt, start=None, end=None):
|
|||
# Empty cues (YouTube transitions) say nothing about the window
|
||||
if not cue:
|
||||
continue
|
||||
if in_range(time, start, end):
|
||||
lines.extend(cue[overlap(previous, cue):])
|
||||
lines.extend((time, line) for line in cue[overlap(previous, cue):])
|
||||
previous = cue
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def convert(source, target, start=None, end=None):
|
||||
def sentence_start(timed, first, start):
|
||||
"""Lines before index `first` back to the start of the sentence cut by `start`."""
|
||||
if start is None or first == 0 or LINE_ENDS_SENTENCE.search(timed[first - 1][1]):
|
||||
return []
|
||||
extra = []
|
||||
for time, line in reversed(timed[:first]):
|
||||
if time is None or time < start - MAX_SNAP_SECONDS:
|
||||
return []
|
||||
ends = list(SENTENCE_END.finditer(line))
|
||||
if ends:
|
||||
rest = line[ends[-1].end():].strip()
|
||||
return ([rest] if rest else []) + extra
|
||||
extra.insert(0, line)
|
||||
# Reached the first caption: the speech starts there
|
||||
return extra
|
||||
|
||||
|
||||
def sentence_end(timed, last, end):
|
||||
"""Lines after index `last` up to the end of the sentence cut by `end`."""
|
||||
if end is None or LINE_ENDS_SENTENCE.search(timed[last][1]):
|
||||
return []
|
||||
extra = []
|
||||
for time, line in timed[last + 1:]:
|
||||
if time is None or time >= end + MAX_SNAP_SECONDS:
|
||||
return []
|
||||
match = SENTENCE_END.search(line)
|
||||
if match:
|
||||
return extra + [line[:match.end()]]
|
||||
extra.append(line)
|
||||
# Reached the last caption: the speech ends there
|
||||
return extra
|
||||
|
||||
|
||||
def snap_to_sentences(timed, start, end):
|
||||
"""Lines in the range, extended so that no sentence is cut at either side.
|
||||
|
||||
Without a sentence end within MAX_SNAP_SECONDS that side is cut exactly.
|
||||
"""
|
||||
inside = [i for i, (time, _) in enumerate(timed) if in_range(time, start, end)]
|
||||
if not inside:
|
||||
return []
|
||||
first, last = inside[0], inside[-1]
|
||||
lines = [line for _, line in timed[first:last + 1]]
|
||||
return sentence_start(timed, first, start) + lines + sentence_end(timed, last, end)
|
||||
|
||||
|
||||
def vtt_to_lines(vtt, start=None, end=None, exact=False):
|
||||
"""Return the caption text lines of a VTT document without rolling duplicates.
|
||||
|
||||
With `start`/`end` (seconds) only the new lines of cues beginning in that
|
||||
range are kept, completed to whole sentences unless `exact`. The window is
|
||||
followed through all cues, so a line repeated from before `start` does not
|
||||
slip in.
|
||||
"""
|
||||
timed = timed_lines(vtt)
|
||||
if start is None and end is None:
|
||||
return [line for _, line in timed]
|
||||
if exact:
|
||||
return [line for time, line in timed if in_range(time, start, end)]
|
||||
return snap_to_sentences(timed, start, end)
|
||||
|
||||
|
||||
def convert(source, target, start=None, end=None, exact=False):
|
||||
"""Convert one file; return an error message or None on success."""
|
||||
try:
|
||||
vtt = source.read_text(encoding="utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
return f"Keine UTF-8-Datei: {source}"
|
||||
|
||||
lines = vtt_to_lines(vtt, start, end)
|
||||
lines = vtt_to_lines(vtt, start, end, exact)
|
||||
if not lines:
|
||||
where = "" if start is None and end is None else " im gewählten Zeitraum"
|
||||
return f"Keine Untertitel{where} gefunden: {source}"
|
||||
|
|
@ -238,7 +304,7 @@ def main():
|
|||
failed = 0
|
||||
# Keep going on errors so one bad file does not stop a batch
|
||||
for source, target in args.jobs:
|
||||
error = convert(source, target, args.start, args.ende)
|
||||
error = convert(source, target, args.start, args.ende, args.exakt)
|
||||
if error:
|
||||
print(f"FEHLER: {error}", file=sys.stderr)
|
||||
failed += 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue