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:
parent
faea4bf465
commit
0bf42dee74
1 changed files with 700 additions and 254 deletions
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue