From 45bf2da07b623646de03c0d009aa3d0bb670de36 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sun, 11 Oct 2026 21:53:17 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E2=80=9EZu=20pr=C3=BCfen=E2=80=9C=20?= =?UTF-8?q?=C3=BCbersah=20Streichungen=20von=20=E2=80=9Eer=E2=80=9C/?= =?UTF-8?q?=E2=80=9Eum=E2=80=9C=20und=20Glossar-Allerweltsw=C3=B6rter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Füllwörter je Sprache: „er“ und „um“ sind nur im Englischen Füllwörter; im Deutschen wurde ihre Streichung bisher nie gemeldet. - Eine Änderung gilt nur noch als vom Glossar erklärt, wenn die neuen Wörter zusammenhängend in einem Eintrag stehen und dem alten Text ähneln. Bisher machte jedes Wort eines Eintrags („Sie“ aus „finden Sie“) Änderungen unsichtbar, auch eingefügte Vornamen. - aenderungen.py -l en-US findet die englische Wortliste. Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 5 +++- README.md | 12 ++++++--- abschrift_korrigieren.py | 4 +-- aenderungen.py | 57 ++++++++++++++++++++++++++------------- tests/test_aenderungen.py | 38 +++++++++++++++++++++----- 5 files changed, 85 insertions(+), 31 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index bf1b34f..8da24e7 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -476,7 +476,10 @@ Sinn verändern können. Was die Glossare ersetzt haben, erscheint hier nie. Nicht gemeldet werden ähnlich bleibende Hörfehler-Korrekturen („Gradwanderung“ → „Gratwanderung“), reine Zusammen-, Getrennt- und Bindestrich-Schreibung („Clan -Kriminalität“ → „Clankriminalität“) und Wörter, die im Glossar stehen. +Kriminalität“ → „Clankriminalität“) und Schreibweisen aus dem Glossar („Bar“ → +„Bahr“, wenn „Egon Bahr“ im Glossar steht). Ergänzt das Modell dagegen Wörter, etwa +einen Vornamen („Dobrind“ → „Alexander Dobrindt“), erscheint das auch dann, wenn der +volle Name im Glossar steht: Gesagt wurde er so nicht. **So arbeitet man die Liste ab:** diff --git a/README.md b/README.md index 7e7646e..1e5bebd 100644 --- a/README.md +++ b/README.md @@ -317,9 +317,13 @@ Ersetzungen des Glossars), mit seinem Ergebnis. Gemeldet werden: Nicht gemeldet werden Hörfehler-Korrekturen, die ähnlich bleiben („Gradwanderung“ → „Gratwanderung“), reine Zusammen-, Getrennt- und -Bindestrich-Schreibung („Clan Kriminalität“ → „Clankriminalität“) und neue Wörter, -die im Glossar stehen. Die Liste erscheint auch in der Konsole. Kleine -Grammatik-Korrekturen wie „das“ → „dass“ tauchen ebenfalls auf; ein Blick genügt. +Bindestrich-Schreibung („Clan Kriminalität“ → „Clankriminalität“) und Schreibweisen +aus dem Glossar („Bar“ → „Bahr“ bei „Egon Bahr“ im Glossar): Die neuen Wörter müssen +zusammenhängend in einem Glossareintrag stehen und dem alten Text ähneln. Einzelne +Allerweltswörter aus längeren Einträgen („Sie“ aus „finden Sie“) und eingefügte +Wörter (ein ergänzter Vorname „Alexander“) erklärt das Glossar nicht. Die Liste +erscheint auch in der Konsole. Kleine Grammatik-Korrekturen wie „das“ → „dass“ +tauchen ebenfalls auf; ein Blick genügt. Für vorhandene Dateien gibt es das eigene Skript. Mit denselben Glossaren wie bei der Korrektur gelten deren Ersetzungen und Begriffe nicht als verdächtig: @@ -478,7 +482,7 @@ In `aenderungen.py`: |---|---|---| | `WORDLISTS` | `/usr/share/dict/ngerman`, `…/american-english` | Wortlisten je Sprache | | `MIN_SIMILARITY` | 0.6 | darunter gilt eine Ersetzung als „stark verändert“ | -| `FILLERS` | äh, ähm, … | Füllwörter, deren Streichung nicht gemeldet wird | +| `FILLERS` | je Sprache: äh, ähm, …; uh, um, er, … | Füllwörter, deren Streichung nicht gemeldet wird | In `vtt2txt.py`: diff --git a/abschrift_korrigieren.py b/abschrift_korrigieren.py index 02bc4b9..b6171d4 100755 --- a/abschrift_korrigieren.py +++ b/abschrift_korrigieren.py @@ -14,7 +14,7 @@ import urllib.error import urllib.request from aenderungen import ( - REVIEW_MARK, glossary_words, print_summary, report_path, write_report, + REVIEW_MARK, glossary_entries, print_summary, report_path, write_report, ) from extract_text import fetch_text from vtt2txt import ( @@ -801,7 +801,7 @@ def main(): report = report_path(target) comparison = write_report( raw_text, text, report, target.name, model_input=model_text, - known=glossary_words(terms, replacements), lang=lang, + glossary=glossary_entries(terms, replacements), lang=lang, ) print_summary(comparison) print(f"Bericht: {report}") diff --git a/aenderungen.py b/aenderungen.py index adb7fe6..8874f72 100755 --- a/aenderungen.py +++ b/aenderungen.py @@ -30,7 +30,11 @@ WORDLISTS = { } WORDLIST_PACKAGES = {"de": "wngerman", "en": "wamerican"} MIN_SIMILARITY = 0.6 # below this a replacement counts as heavily changed -FILLERS = {"äh", "ähm", "öh", "öhm", "hm", "hmm", "uh", "uhm", "um", "er", "erm"} +# Per language: "er" and "um" are fillers in English, real words in German +FILLERS = { + "de": {"äh", "ähm", "öh", "öhm", "hm", "hmm"}, + "en": {"uh", "uhm", "um", "er", "erm", "hm", "hmm"}, +} CONTEXT_WORDS = 6 VALID_WORD = "gültiges Wort ersetzt" @@ -132,10 +136,10 @@ def load_wordlist(lang): return {line.strip().lower() for line in words if line.strip()} -def glossary_words(terms, replacements): - """Normalized words of glossary terms and replacement targets.""" +def glossary_entries(terms, replacements): + """Glossary terms and replacement targets as tuples of normalized words.""" texts = list(terms) + [right for _, right in replacements] - return {normalize(word) for text in texts for word in text.split()} + return {tuple(normalize(word) for word in text.split()) for text in texts} - {()} def spelling_key(words): @@ -143,33 +147,47 @@ def spelling_key(words): return re.sub(r"[\W_]+", "", "".join(words).lower()) -def review_candidates(comparison, wordlist=None, known=()): +def similarity(old, new): + return SequenceMatcher(None, spelling_key(old), spelling_key(new)).ratio() + + +def glossary_spelling(old, new, glossary): + """True if `new` is a glossary spelling of `old`: the new words stand + together in one glossary entry and resemble the old ones. Common words of + longer entries ("finden Sie") explain nothing on their own.""" + words = tuple(normalize(word) for word in new) + size = len(words) + in_entry = any( + entry[start:start + size] == words + for entry in glossary for start in range(len(entry) - size + 1) + ) + return in_entry and similarity(old, new) >= MIN_SIMILARITY + + +def review_candidates(comparison, wordlist=None, glossary=(), fillers=FILLERS["de"]): """Changes the model made on its own that may alter the meaning. - Not reported: changes of spacing, hyphens and punctuation only, new words - from the glossary, deleted filler words, and replacements of words not in - the word list (recognition errors) that stay similar. + Not reported: changes of spacing, hyphens and punctuation only, glossary + spellings of the old words, deleted filler words, and replacements of + words not in the word list (recognition errors) that stay similar. """ suspects = [] for tag, i1, i2, j1, j2 in comparison.opcodes: if tag == "equal": continue old, new = comparison.raw[i1:i2], comparison.words[j1:j2] - if new and all(normalize(word) in known for word in new): - continue if tag == "delete": - if all(normalize(word) in FILLERS for word in old): + if all(normalize(word) in fillers for word in old): continue reason = DELETED elif tag == "insert": reason = INSERTED else: - old_key, new_key = spelling_key(old), spelling_key(new) - if old_key == new_key: + if spelling_key(old) == spelling_key(new) or glossary_spelling(old, new, glossary): continue if wordlist is not None and all(normalize(w) in wordlist for w in old): reason = VALID_WORD - elif SequenceMatcher(None, old_key, new_key).ratio() < MIN_SIMILARITY: + elif similarity(old, new) < MIN_SIMILARITY: reason = CHANGED_A_LOT else: continue @@ -336,7 +354,7 @@ def print_summary(comparison, limit=15, suspect_limit=10): print(f" … und {len(suspects) - suspect_limit} weitere, siehe Bericht") -def write_report(raw, corrected, target, title, model_input=None, known=(), lang="de"): +def write_report(raw, corrected, target, title, model_input=None, glossary=(), lang="de"): """Write the HTML report; return the comparison with the suspect changes. model_input is the text the model got (raw text after glossary @@ -344,7 +362,9 @@ def write_report(raw, corrected, target, title, model_input=None, known=(), lang """ comparison = compare(raw, corrected) by_model = compare(raw if model_input is None else model_input, corrected) - comparison.suspects = review_candidates(by_model, load_wordlist(lang), set(known)) + comparison.suspects = review_candidates( + by_model, load_wordlist(lang), glossary, FILLERS.get(lang, set()) + ) target.write_text(render_html(comparison, title), encoding="utf-8") return comparison @@ -397,6 +417,7 @@ def parse_args(): def main(): # abschrift_korrigieren imports this module: import it only when needed from abschrift_korrigieren import apply_replacements, read_glossary + from yt_untertitel import base_language args = parse_args() raw = read_raw(args.roh) @@ -411,8 +432,8 @@ def main(): corrected = args.korrigiert.read_text(encoding="utf-8") comparison = write_report( raw, corrected, args.output, args.korrigiert.name, - model_input=model_input, known=glossary_words(terms, replacements), - lang=args.lang, + model_input=model_input, glossary=glossary_entries(terms, replacements), + lang=base_language(args.lang), ) print_summary(comparison) print(f"Bericht: {args.output}") diff --git a/tests/test_aenderungen.py b/tests/test_aenderungen.py index 49fdfef..13c7506 100644 --- a/tests/test_aenderungen.py +++ b/tests/test_aenderungen.py @@ -124,8 +124,10 @@ def test_cli_missing_file(tmp_path, monkeypatch, capsys): WORDS = {"professor", "das", "dass", "ist", "haus", "unterschreiben", "bar"} -def suspects(model_input, corrected, wordlist=WORDS, known=()): - found = ae.review_candidates(ae.compare(model_input, corrected), wordlist, set(known)) +def suspects(model_input, corrected, wordlist=WORDS, glossary=(), fillers=ae.FILLERS["de"]): + found = ae.review_candidates( + ae.compare(model_input, corrected), wordlist, glossary, fillers + ) return [(s.reason, s.old, s.new) for s in found] @@ -153,13 +155,37 @@ def test_review_candidates_ignores(model_input, corrected): assert suspects(model_input, corrected) == [] -def test_review_candidates_glossary_words_are_known(): - known = ae.glossary_words(["Egon Bahr"], [("Bars", "Bahrs")]) - assert known == {"egon", "bahr", "bahrs"} - assert suspects("Bar sagte", "Bahr sagte", known=known) == [] +def test_review_candidates_glossary_spellings_are_not_reported(): + glossary = ae.glossary_entries(["Egon Bahr", ""], [("Bars", "Bahrs")]) + assert glossary == {("egon", "bahr"), ("bahrs",)} + assert suspects("Bar sagte", "Bahr sagte", glossary=glossary) == [] assert suspects("Bar sagte", "Bahr sagte") == [(ae.VALID_WORD, "Bar", "Bahr")] +@pytest.mark.parametrize("model_input, corrected, expected", [ + # A common word of a longer entry explains nothing on its own + ("das sagt ihn nicht", "Das sagt Sie nicht.", [(ae.CHANGED_A_LOT, "ihn", "Sie")]), + # Inserted words stay suspect even if they are in the glossary + ("er kam heute", "er ist kam heute", [(ae.INSERTED, "", "ist")]), + ("Dobrind sagte", "Alexander Dobrindt sagte", + [(ae.CHANGED_A_LOT, "Dobrind", "Alexander Dobrindt")]), +]) +def test_review_candidates_glossary_explains_only_spellings(model_input, corrected, expected): + glossary = ae.glossary_entries( + ["Alexander Dobrindt"], [("befinden Sie", "finden Sie"), ("is", "ist")] + ) + assert suspects(model_input, corrected, glossary=glossary) == expected + + +def test_review_candidates_fillers_per_language(): + # "er" and "um" are real German words; in English they are fillers + assert suspects("dann kam er um zu helfen", "Dann kam, zu helfen.") == [ + (ae.DELETED, "er um", "") + ] + english = suspects("so er we um go", "So we go.", fillers=ae.FILLERS["en"]) + assert english == [] + + def test_review_candidates_without_wordlist(): # Similar replacements of real words go unnoticed, the rest still counts assert suspects("Der Professor kam", "Der Professorin kam", wordlist=None) == []