feat: mehrere Zeitbereiche (-z) mit Zeit-Überschriften in einer Datei

- Zeitformat: 00:05:10 (hⓂ️s), 00:20 (h:m = Minute 20), 25 (Minute 25)
- -z "00:05-00:06 00:10-00:12 00:25": A-B, A (bis Ende), -B (ab Anfang)
- jeder Bereich unter einer Überschrift [HH:MM:SS - HH:MM:SS] mit den
  tatsächlichen Zeiten; nach der Satz-Erweiterung überlappende Blöcke
  werden verschmolzen
- rede_korrigieren.py korrigiert und gliedert jeden Block einzeln; das
  Modell sieht die Überschriften nicht

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-11 20:12:49 +02:00
commit 0bf42dee74

View file

@ -13,7 +13,10 @@ import urllib.request
from aenderungen import REVIEW_MARK, print_summary, report_path, write_report
from extract_text import fetch_text
from vtt2txt import add_time_options, check_time_range, time_suffix, vtt_to_lines
from vtt2txt import (
add_time_options, join_segments, resolve_time_ranges, split_segments, time_suffix,
vtt_to_text,
)
from yt_untertitel import YT_DLP, base_language, default_name, original_language
MODEL = "qwen3:32b"
@ -507,8 +510,7 @@ def parse_args():
)
add_time_options(parser)
args = parser.parse_args()
check_time_range(parser, args)
timed = args.start is not None or args.ende is not None
resolve_time_ranges(parser, args)
for glossary in args.glossar:
if not glossary.is_file():
@ -525,10 +527,10 @@ def parse_args():
if not args.input.is_file():
parser.error(f"Eingabedatei nicht gefunden: {args.input}")
if timed and args.input.suffix.lower() != ".vtt":
parser.error("-s/-e brauchen Zeitstempel: nur mit .vtt-Eingabe oder -u.")
if args.bereiche and args.input.suffix.lower() != ".vtt":
parser.error("-s/-e/-z brauchen Zeitstempel: nur mit .vtt-Eingabe oder -u.")
if args.output is None:
suffix = time_suffix(args.start, args.ende)
suffix = time_suffix(args.bereiche)
args.output = args.input.with_name(f"{args.input.stem}{suffix}.korrigiert.txt")
if args.output.resolve() == args.input.resolve():
parser.error("Ausgabedatei darf nicht die Eingabedatei sein.")
@ -555,21 +557,21 @@ def save_raw_text(path, raw):
def read_source(args):
"""Return the raw transcript, the output path and the language of the speech."""
if args.url is not None:
info, lines = fetch_text(args.url, args.lang, args.start, args.ende, args.exakt)
info, text = fetch_text(args.url, args.lang, args.bereiche, args.exakt)
lang = base_language(args.lang or original_language(info) or DEFAULT_LANGUAGE)
if lang not in PROMPTS:
raise SystemExit(
f"Die Rede ist auf „{lang}“; unterstützt: {', '.join(PROMPTS)} (-l)."
)
suffix = time_suffix(args.start, args.ende)
suffix = time_suffix(args.bereiche)
target = args.output or Path(f"{default_name(info)}{suffix}.korrigiert.txt")
raw = "\n".join(lines) + "\n"
raw = text + "\n"
save_raw_text(raw_text_path(target), raw)
return raw, target, lang
raw = args.input.read_text(encoding="utf-8-sig")
if args.input.suffix.lower() == ".vtt":
raw = "\n".join(vtt_to_lines(raw, args.start, args.ende, args.exakt))
raw = vtt_to_text(raw, args.bereiche, args.exakt)
return raw, args.output, base_language(args.lang or DEFAULT_LANGUAGE)
@ -594,58 +596,74 @@ def main():
)
terms += more_terms
replacements += more_replacements
excerpt = args.start is not None or args.ende is not None
# Time headers ("[00:04:57 - 00:06:04]") split the text into blocks that are
# corrected one by one; the model never sees the headers. Subtitle line
# breaks carry no meaning: send plain running text
segments = [(header, " ".join(body.split())) for header, body in split_segments(raw)]
segments = [(header, body) for header, body in segments if body]
# The change report compares against this, so glossary effects show up too
raw_text = join_segments(segments)
excerpt = any(header is not None for header, _ in segments)
system = build_system(terms, lang, excerpt)
# Subtitle line breaks carry no meaning; send plain running text
text = " ".join(raw.split())
# The change report compares against this, so glossary effects show up too
raw_text = text
if replacements:
text = apply_replacements(text, replacements)
# One pass over all blocks (one per line) gives one count per rule
bodies = apply_replacements(
"\n".join(body for _, body in segments), replacements
).split("\n")
segments = [(header, body) for (header, _), body in zip(segments, bodies)]
chunks = split_chunks(text)
blocks = [(header, split_chunks(body)) for header, body in segments]
total = sum(len(chunks) for _, chunks in blocks)
if not chunks:
if args.start is None and args.ende is None:
raise SystemExit("Die Eingabedatei ist leer.")
raise SystemExit("Die Eingabedatei enthält im gewählten Zeitraum keinen Text.")
if not total:
if args.bereiche:
raise SystemExit("Die Eingabedatei enthält im gewählten Zeitraum keinen Text.")
raise SystemExit("Die Eingabedatei ist leer.")
parts_dir.mkdir(parents=True, exist_ok=True)
results = []
corrected_blocks = []
warnings = []
index = 0
for index, chunk in enumerate(chunks, start=1):
# Key covers model, prompt and chunk: any change invalidates the cache
key = hashlib.sha256(
f"{MODEL}\n{system}\n{chunk}".encode("utf-8")
).hexdigest()[:12]
part = parts_dir / f"part{index:03d}-{key}.txt"
for header, chunks in blocks:
if header is not None:
print(f"Block {header}")
results = []
for chunk in chunks:
index += 1
# Key covers model, prompt and chunk: any change invalidates the cache
key = hashlib.sha256(
f"{MODEL}\n{system}\n{chunk}".encode("utf-8")
).hexdigest()[:12]
part = parts_dir / f"part{index:03d}-{key}.txt"
if part.exists():
print(f"Abschnitt {index}/{len(chunks)} bereits vorhanden, übersprungen.")
corrected = part.read_text(encoding="utf-8")
else:
print(f"Bearbeite Abschnitt {index}/{len(chunks)} …", flush=True)
corrected = correct_chunk(chunk, index, system)
part.write_text(corrected, encoding="utf-8")
if part.exists():
print(f"Abschnitt {index}/{total} bereits vorhanden, übersprungen.")
corrected = part.read_text(encoding="utf-8")
else:
print(f"Bearbeite Abschnitt {index}/{total} …", flush=True)
corrected = correct_chunk(chunk, index, system)
part.write_text(corrected, encoding="utf-8")
before = count_words(chunk)
after = count_words(corrected)
deviation = abs(after - before) / before
if deviation > MAX_WORD_DEVIATION:
message = (
f"Abschnitt {index}: {before} → {after} Wörter "
f"({deviation:.0%} Abweichung), auf Auslassungen/Ergänzungen "
f"prüfen: {part}"
)
print(f"WARNUNG: {message}")
warnings.append(message)
before = count_words(chunk)
after = count_words(corrected)
deviation = abs(after - before) / before
if deviation > MAX_WORD_DEVIATION:
message = (
f"Abschnitt {index}: {before} → {after} Wörter "
f"({deviation:.0%} Abweichung), auf Auslassungen/Ergänzungen "
f"prüfen: {part}"
)
print(f"WARNUNG: {message}")
warnings.append(message)
results.append(corrected)
results.append(corrected)
text = build_paragraphs(" ".join(results), parts_dir, lang)
paragraphs = build_paragraphs(" ".join(results), parts_dir, lang)
corrected_blocks.append((header, paragraphs))
text = join_segments(corrected_blocks, header_gap="\n\n")
target.write_text(text + "\n", encoding="utf-8")
print(f"Fertig: {target}")