fix: „Zu prüfen“ übersah Streichungen von „er“/„um“ und Glossar-Allerweltswörter
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
1e3c813137
commit
45bf2da07b
5 changed files with 85 additions and 31 deletions
|
|
@ -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“ →
|
Nicht gemeldet werden ähnlich bleibende Hörfehler-Korrekturen („Gradwanderung“ →
|
||||||
„Gratwanderung“), reine Zusammen-, Getrennt- und Bindestrich-Schreibung („Clan
|
„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:**
|
**So arbeitet man die Liste ab:**
|
||||||
|
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -317,9 +317,13 @@ Ersetzungen des Glossars), mit seinem Ergebnis. Gemeldet werden:
|
||||||
|
|
||||||
Nicht gemeldet werden Hörfehler-Korrekturen, die ähnlich bleiben
|
Nicht gemeldet werden Hörfehler-Korrekturen, die ähnlich bleiben
|
||||||
(„Gradwanderung“ → „Gratwanderung“), reine Zusammen-, Getrennt- und
|
(„Gradwanderung“ → „Gratwanderung“), reine Zusammen-, Getrennt- und
|
||||||
Bindestrich-Schreibung („Clan Kriminalität“ → „Clankriminalität“) und neue Wörter,
|
Bindestrich-Schreibung („Clan Kriminalität“ → „Clankriminalität“) und Schreibweisen
|
||||||
die im Glossar stehen. Die Liste erscheint auch in der Konsole. Kleine
|
aus dem Glossar („Bar“ → „Bahr“ bei „Egon Bahr“ im Glossar): Die neuen Wörter müssen
|
||||||
Grammatik-Korrekturen wie „das“ → „dass“ tauchen ebenfalls auf; ein Blick genügt.
|
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
|
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:
|
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 |
|
| `WORDLISTS` | `/usr/share/dict/ngerman`, `…/american-english` | Wortlisten je Sprache |
|
||||||
| `MIN_SIMILARITY` | 0.6 | darunter gilt eine Ersetzung als „stark verändert“ |
|
| `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`:
|
In `vtt2txt.py`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from aenderungen import (
|
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 extract_text import fetch_text
|
||||||
from vtt2txt import (
|
from vtt2txt import (
|
||||||
|
|
@ -801,7 +801,7 @@ def main():
|
||||||
report = report_path(target)
|
report = report_path(target)
|
||||||
comparison = write_report(
|
comparison = write_report(
|
||||||
raw_text, text, report, target.name, model_input=model_text,
|
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_summary(comparison)
|
||||||
print(f"Bericht: {report}")
|
print(f"Bericht: {report}")
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ WORDLISTS = {
|
||||||
}
|
}
|
||||||
WORDLIST_PACKAGES = {"de": "wngerman", "en": "wamerican"}
|
WORDLIST_PACKAGES = {"de": "wngerman", "en": "wamerican"}
|
||||||
MIN_SIMILARITY = 0.6 # below this a replacement counts as heavily changed
|
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
|
CONTEXT_WORDS = 6
|
||||||
|
|
||||||
VALID_WORD = "gültiges Wort ersetzt"
|
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()}
|
return {line.strip().lower() for line in words if line.strip()}
|
||||||
|
|
||||||
|
|
||||||
def glossary_words(terms, replacements):
|
def glossary_entries(terms, replacements):
|
||||||
"""Normalized words of glossary terms and replacement targets."""
|
"""Glossary terms and replacement targets as tuples of normalized words."""
|
||||||
texts = list(terms) + [right for _, right in replacements]
|
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):
|
def spelling_key(words):
|
||||||
|
|
@ -143,33 +147,47 @@ def spelling_key(words):
|
||||||
return re.sub(r"[\W_]+", "", "".join(words).lower())
|
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.
|
"""Changes the model made on its own that may alter the meaning.
|
||||||
|
|
||||||
Not reported: changes of spacing, hyphens and punctuation only, new words
|
Not reported: changes of spacing, hyphens and punctuation only, glossary
|
||||||
from the glossary, deleted filler words, and replacements of words not in
|
spellings of the old words, deleted filler words, and replacements of
|
||||||
the word list (recognition errors) that stay similar.
|
words not in the word list (recognition errors) that stay similar.
|
||||||
"""
|
"""
|
||||||
suspects = []
|
suspects = []
|
||||||
for tag, i1, i2, j1, j2 in comparison.opcodes:
|
for tag, i1, i2, j1, j2 in comparison.opcodes:
|
||||||
if tag == "equal":
|
if tag == "equal":
|
||||||
continue
|
continue
|
||||||
old, new = comparison.raw[i1:i2], comparison.words[j1:j2]
|
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 tag == "delete":
|
||||||
if all(normalize(word) in FILLERS for word in old):
|
if all(normalize(word) in fillers for word in old):
|
||||||
continue
|
continue
|
||||||
reason = DELETED
|
reason = DELETED
|
||||||
elif tag == "insert":
|
elif tag == "insert":
|
||||||
reason = INSERTED
|
reason = INSERTED
|
||||||
else:
|
else:
|
||||||
old_key, new_key = spelling_key(old), spelling_key(new)
|
if spelling_key(old) == spelling_key(new) or glossary_spelling(old, new, glossary):
|
||||||
if old_key == new_key:
|
|
||||||
continue
|
continue
|
||||||
if wordlist is not None and all(normalize(w) in wordlist for w in old):
|
if wordlist is not None and all(normalize(w) in wordlist for w in old):
|
||||||
reason = VALID_WORD
|
reason = VALID_WORD
|
||||||
elif SequenceMatcher(None, old_key, new_key).ratio() < MIN_SIMILARITY:
|
elif similarity(old, new) < MIN_SIMILARITY:
|
||||||
reason = CHANGED_A_LOT
|
reason = CHANGED_A_LOT
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
@ -336,7 +354,7 @@ def print_summary(comparison, limit=15, suspect_limit=10):
|
||||||
print(f" … und {len(suspects) - suspect_limit} weitere, siehe Bericht")
|
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.
|
"""Write the HTML report; return the comparison with the suspect changes.
|
||||||
|
|
||||||
model_input is the text the model got (raw text after glossary
|
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)
|
comparison = compare(raw, corrected)
|
||||||
by_model = compare(raw if model_input is None else model_input, 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")
|
target.write_text(render_html(comparison, title), encoding="utf-8")
|
||||||
return comparison
|
return comparison
|
||||||
|
|
||||||
|
|
@ -397,6 +417,7 @@ def parse_args():
|
||||||
def main():
|
def main():
|
||||||
# abschrift_korrigieren imports this module: import it only when needed
|
# abschrift_korrigieren imports this module: import it only when needed
|
||||||
from abschrift_korrigieren import apply_replacements, read_glossary
|
from abschrift_korrigieren import apply_replacements, read_glossary
|
||||||
|
from yt_untertitel import base_language
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
raw = read_raw(args.roh)
|
raw = read_raw(args.roh)
|
||||||
|
|
@ -411,8 +432,8 @@ def main():
|
||||||
corrected = args.korrigiert.read_text(encoding="utf-8")
|
corrected = args.korrigiert.read_text(encoding="utf-8")
|
||||||
comparison = write_report(
|
comparison = write_report(
|
||||||
raw, corrected, args.output, args.korrigiert.name,
|
raw, corrected, args.output, args.korrigiert.name,
|
||||||
model_input=model_input, known=glossary_words(terms, replacements),
|
model_input=model_input, glossary=glossary_entries(terms, replacements),
|
||||||
lang=args.lang,
|
lang=base_language(args.lang),
|
||||||
)
|
)
|
||||||
print_summary(comparison)
|
print_summary(comparison)
|
||||||
print(f"Bericht: {args.output}")
|
print(f"Bericht: {args.output}")
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,10 @@ def test_cli_missing_file(tmp_path, monkeypatch, capsys):
|
||||||
WORDS = {"professor", "das", "dass", "ist", "haus", "unterschreiben", "bar"}
|
WORDS = {"professor", "das", "dass", "ist", "haus", "unterschreiben", "bar"}
|
||||||
|
|
||||||
|
|
||||||
def suspects(model_input, corrected, wordlist=WORDS, known=()):
|
def suspects(model_input, corrected, wordlist=WORDS, glossary=(), fillers=ae.FILLERS["de"]):
|
||||||
found = ae.review_candidates(ae.compare(model_input, corrected), wordlist, set(known))
|
found = ae.review_candidates(
|
||||||
|
ae.compare(model_input, corrected), wordlist, glossary, fillers
|
||||||
|
)
|
||||||
return [(s.reason, s.old, s.new) for s in found]
|
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) == []
|
assert suspects(model_input, corrected) == []
|
||||||
|
|
||||||
|
|
||||||
def test_review_candidates_glossary_words_are_known():
|
def test_review_candidates_glossary_spellings_are_not_reported():
|
||||||
known = ae.glossary_words(["Egon Bahr"], [("Bars", "Bahrs")])
|
glossary = ae.glossary_entries(["Egon Bahr", ""], [("Bars", "Bahrs")])
|
||||||
assert known == {"egon", "bahr", "bahrs"}
|
assert glossary == {("egon", "bahr"), ("bahrs",)}
|
||||||
assert suspects("Bar sagte", "Bahr sagte", known=known) == []
|
assert suspects("Bar sagte", "Bahr sagte", glossary=glossary) == []
|
||||||
assert suspects("Bar sagte", "Bahr sagte") == [(ae.VALID_WORD, "Bar", "Bahr")]
|
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():
|
def test_review_candidates_without_wordlist():
|
||||||
# Similar replacements of real words go unnoticed, the rest still counts
|
# Similar replacements of real words go unnoticed, the rest still counts
|
||||||
assert suspects("Der Professor kam", "Der Professorin kam", wordlist=None) == []
|
assert suspects("Der Professor kam", "Der Professorin kam", wordlist=None) == []
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue