feat: „Zu prüfen“ zeigt bloße Wortformen getrennt am Ende
Ersetzt das Modell ein gültiges Wort nur durch eine andere Form („Punkten“ → „Punkte“, „das“ → „dass“, „sehe“ → „sähe“), heißt der Grund jetzt „Wortform geändert“. Diese Fälle stehen im Bericht in einer eigenen Tabelle nach den sinnverändernden, im Text gestrichelt umrahmt, und in der Konsole am Ende. „-in“ zählt nicht als Endung: „Professor“ → „Professorin“ bleibt oben. Außerdem: Die Tabellen scrollen auf schmalen Bildschirmen waagrecht, und aenderungen.py meldet Zeit-Überschriften nicht als Einfügung, wenn der Rohtext (z. B. eine .vtt) keine hat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
011d201215
commit
cd62feab69
4 changed files with 163 additions and 32 deletions
|
|
@ -399,10 +399,11 @@ Fertig: rede.korrigiert.txt
|
|||
Änderungen gegenüber dem Rohtext: 82
|
||||
5× „Rammstein“ → „Ramstein“
|
||||
…
|
||||
Zu prüfen: 19 Stellen ← diese Stellen ansehen!
|
||||
Zu prüfen: 20 Stellen (davon 2 Wortformen) ← diese Stellen ansehen!
|
||||
gültiges Wort ersetzt: „in“ → „dass“
|
||||
stark verändert: „OSola“ → „Ursula“
|
||||
…
|
||||
Wortform geändert: „sehe“ → „sähe“ ← Wortformen am Ende
|
||||
Bericht: rede.aenderungen.html
|
||||
|
||||
19 Stelle(n) zu prüfen, siehe rede.aenderungen.html
|
||||
|
|
@ -444,7 +445,8 @@ xdg-open rede.aenderungen.html
|
|||
1. **Kopfzeile:** Wortzahlen, Anzahl der Ersetzungen, Einfügungen, Streichungen,
|
||||
Prüfmarken und Stellen „zu prüfen“.
|
||||
2. **Zu prüfen:** Tabelle der verdächtigen Änderungen mit Grund, Text vorher,
|
||||
korrigiertem Text und Zusammenhang. **Hier beginnen.**
|
||||
korrigiertem Text und Zusammenhang. **Hier beginnen.** Darunter in einer eigenen
|
||||
Tabelle die bloßen Wortformen („Wortform geändert“).
|
||||
3. **Alle Änderungen nach Häufigkeit:** jede Wortänderung mit Anzahl, z. B.
|
||||
`5 Rammstein → Ramstein`. Gut, um wiederkehrende Hörfehler fürs Glossar zu
|
||||
finden.
|
||||
|
|
@ -457,6 +459,7 @@ xdg-open rede.aenderungen.html
|
|||
| rot durchgestrichen | gestrichenes Wort |
|
||||
| orange `[prüfen: …]` | Prüfmarke des Modells |
|
||||
| rot umrahmt | Stelle „zu prüfen“; beim Darüberfahren erscheint der Grund |
|
||||
| rot gestrichelt umrahmt | geänderte Wortform (meist Grammatik) |
|
||||
| rot umrahmtes ⚠ | hier hat das Modell eigenmächtig etwas gestrichen |
|
||||
|
||||
Groß-/Kleinschreibung, Satzzeichen und Schreibweisen wie „z.B.“ → „z. B.“ werden
|
||||
|
|
@ -470,6 +473,7 @@ Sinn verändern können. Was die Glossare ersetzt haben, erscheint hier nie.
|
|||
| Grund | Bedeutung | Beispiel |
|
||||
|---|---|---|
|
||||
| gültiges Wort ersetzt | Das alte Wort ist ein echtes Wort (steht in der Wortliste), also vermutlich kein Hörfehler. | „Professor“ → „Professorin“, „unterschreiben“ → „unterscheiden“ |
|
||||
| Wortform geändert | Ein echtes Wort hat nur eine andere Endung oder einen Umlaut bekommen. Meist eine richtige Grammatik-Korrektur; steht in einer eigenen Gruppe am Ende. | „Punkten“ → „Punkte“, „das“ → „dass“, „sehe“ → „sähe“ |
|
||||
| stark verändert | Das neue Wort ähnelt dem alten kaum (unter 60 %). | „CT1“ → „ZDF“ |
|
||||
| eingefügt | Das Modell hat ein Wort ergänzt. | „∅“ → „ist“ |
|
||||
| gestrichen | Das Modell hat ein Wort weggelassen (Füllwörter wie „äh“ zählen nicht). | „Sicherungskonzepte geht“ → „∅“ |
|
||||
|
|
@ -484,8 +488,8 @@ volle Name im Glossar steht: Gesagt wurde er so nicht.
|
|||
**So arbeitet man die Liste ab:**
|
||||
|
||||
1. Für jede Zeile den Zusammenhang lesen.
|
||||
2. Ist die Änderung richtig (oft bei Grammatik: „das“ → „dass“, „sehe“ → „sähe“),
|
||||
nichts tun.
|
||||
2. Ist die Änderung richtig, nichts tun. Bei den Wortformen am Ende ist das der
|
||||
Normalfall; dort genügt ein kurzer Blick.
|
||||
3. Ist sie falsch, im korrigierten Text zurückändern. Tritt der Fehler öfter auf
|
||||
oder geht es um einen Namen, besser das Glossar ergänzen (nächster Schritt).
|
||||
4. Bei „gestrichen“ prüfen, ob wirklich etwas fehlt: Oft entfernt das Modell
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -319,6 +319,7 @@ Ersetzungen des Glossars), mit seinem Ergebnis. Gemeldet werden:
|
|||
| Grund | Beispiel |
|
||||
|---|---|
|
||||
| gültiges Wort ersetzt | „Professor“ → „Professorin“, „unterschreiben“ → „unterscheiden“ (das alte Wort steht in der Wortliste, war also kein Hörfehler) |
|
||||
| Wortform geändert | „Punkten“ → „Punkte“, „das“ → „dass“, „sehe“ → „sähe“: ein gültiges Wort, nur Endung oder Umlaut anders (meist Grammatik); eigene Gruppe am Ende der Liste |
|
||||
| stark verändert | „CT1“ → „ZDF“ (Ähnlichkeit unter 60 %) |
|
||||
| eingefügt | „∅“ → „ist“ |
|
||||
| gestrichen | „Sicherungskonzepte geht“ → „∅“ (Füllwörter wie „äh“ nicht) |
|
||||
|
|
@ -330,8 +331,12 @@ aus dem Glossar („Bar“ → „Bahr“ bei „Egon Bahr“ im Glossar): Die n
|
|||
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.
|
||||
erscheint auch in der Konsole, sinnverändernde Fälle zuerst, Wortformen danach.
|
||||
|
||||
Als Wortform gilt ein einzelnes Wort, das bis auf die Endung gleich bleibt (Stamm
|
||||
mindestens 3 Buchstaben, beide Endungen aus `INFLECTIONS`, z. B. -e, -n, -s, -en,
|
||||
-er, -es, -st, -t) oder sich nur im Umlaut unterscheidet. „-in“ gehört bewusst nicht
|
||||
dazu: „Professor“ → „Professorin“ bleibt in der Hauptliste.
|
||||
|
||||
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:
|
||||
|
|
@ -491,6 +496,8 @@ 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` | je Sprache: äh, ähm, …; uh, um, er, … | Füllwörter, deren Streichung nicht gemeldet wird |
|
||||
| `INFLECTIONS` | je Sprache: -e, -n, -s, -en, …; -s, -ed, -ing, … | Endungen, die nur die Wortform ändern |
|
||||
| `MIN_STEM` | 3 | kürzester gemeinsamer Wortstamm einer Wortform-Änderung |
|
||||
|
||||
In `vtt2txt.py`:
|
||||
|
||||
|
|
|
|||
100
aenderungen.py
100
aenderungen.py
|
|
@ -10,7 +10,7 @@ import argparse
|
|||
import html
|
||||
import re
|
||||
|
||||
from vtt2txt import vtt_to_lines
|
||||
from vtt2txt import split_segments, vtt_to_lines
|
||||
|
||||
# "[prüfen: …]" in German transcripts, "[check: …]" in English ones
|
||||
REVIEW_MARK = re.compile(r"\[(?:prüfen|check):[^\]]*\]")
|
||||
|
|
@ -35,12 +35,20 @@ FILLERS = {
|
|||
"de": {"äh", "ähm", "öh", "öhm", "hm", "hmm"},
|
||||
"en": {"uh", "uhm", "um", "er", "erm", "hm", "hmm"},
|
||||
}
|
||||
# Endings that turn one form of a word into another ("Punkten" -> "Punkte")
|
||||
INFLECTIONS = {
|
||||
"de": {"", "e", "n", "r", "s", "t", "em", "en", "er", "es", "et", "st", "te", "est", "ten"},
|
||||
"en": {"", "d", "s", "ed", "es", "ing"},
|
||||
}
|
||||
UMLAUT_FOLD = str.maketrans({"ä": "e", "ö": "o", "ü": "u", "ß": "ss"})
|
||||
MIN_STEM = 3 # shortest common stem of two word forms
|
||||
CONTEXT_WORDS = 6
|
||||
|
||||
VALID_WORD = "gültiges Wort ersetzt"
|
||||
CHANGED_A_LOT = "stark verändert"
|
||||
INSERTED = "eingefügt"
|
||||
DELETED = "gestrichen"
|
||||
WORD_FORM = "Wortform geändert"
|
||||
|
||||
|
||||
def add_token(words, token):
|
||||
|
|
@ -164,13 +172,31 @@ def glossary_spelling(old, new, glossary):
|
|||
return in_entry and similarity(old, new) >= MIN_SIMILARITY
|
||||
|
||||
|
||||
def review_candidates(comparison, wordlist=None, glossary=(), fillers=FILLERS["de"]):
|
||||
def word_form_change(old, new, lang="de"):
|
||||
"""True if one word becomes another form of itself: only the ending
|
||||
("Punkten" -> "Punkte", "das" -> "dass") or an umlaut ("sehe" -> "sähe")
|
||||
differs. "-in" is no such ending: "Professor" -> "Professorin" counts."""
|
||||
if len(old) != 1 or len(new) != 1:
|
||||
return False
|
||||
a, b = (NON_WORD.sub("", w).lower().translate(UMLAUT_FOLD) for w in (old[0], new[0]))
|
||||
if a == b:
|
||||
return True
|
||||
stem = 0
|
||||
while stem < min(len(a), len(b)) and a[stem] == b[stem]:
|
||||
stem += 1
|
||||
endings = INFLECTIONS.get(lang, set())
|
||||
return stem >= MIN_STEM and a[stem:] in endings and b[stem:] in endings
|
||||
|
||||
|
||||
def review_candidates(comparison, wordlist=None, glossary=(), lang="de"):
|
||||
"""Changes the model made on its own that may alter the meaning.
|
||||
|
||||
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.
|
||||
Replaced real words that only change their form get their own reason.
|
||||
"""
|
||||
fillers = FILLERS.get(lang, set())
|
||||
suspects = []
|
||||
for tag, i1, i2, j1, j2 in comparison.opcodes:
|
||||
if tag == "equal":
|
||||
|
|
@ -186,7 +212,7 @@ def review_candidates(comparison, wordlist=None, glossary=(), fillers=FILLERS["d
|
|||
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
|
||||
reason = WORD_FORM if word_form_change(old, new, lang) else VALID_WORD
|
||||
elif similarity(old, new) < MIN_SIMILARITY:
|
||||
reason = CHANGED_A_LOT
|
||||
else:
|
||||
|
|
@ -223,6 +249,9 @@ del { background: var(--del); }
|
|||
.orig { color: var(--muted); font-size: 0.8em; }
|
||||
.check { background: var(--check); font-size: 0.85em; }
|
||||
.warn { outline: 2px solid var(--warn); outline-offset: 1px; border-radius: 2px; }
|
||||
.warn.form { outline-style: dashed; outline-width: 1px; }
|
||||
h3 { font-size: 1rem; margin-top: 1.5rem; }
|
||||
.scroll { overflow-x: auto; }
|
||||
td.ctx { color: var(--muted); } td.ctx b { color: var(--fg); }
|
||||
"""
|
||||
|
||||
|
|
@ -239,28 +268,43 @@ def render_html(comparison, title):
|
|||
for old, new, count in summary
|
||||
)
|
||||
suspects = comparison.suspects
|
||||
suspect_rows = "\n".join(
|
||||
f"<tr><td>{esc(s.reason)}</td><td><del>{esc(s.old) or EMPTY}</del></td>"
|
||||
f"<td><ins>{esc(s.new) or EMPTY}</ins></td>"
|
||||
f"<td class=ctx>{suspect_context(comparison, s)}</td></tr>"
|
||||
for s in suspects
|
||||
)
|
||||
if suspects:
|
||||
suspect_table = (
|
||||
"<table><tr><th>Grund</th><th>vorher</th><th>korrigiert</th>"
|
||||
f"<th>Zusammenhang</th></tr>\n{suspect_rows}\n</table>"
|
||||
important, forms = split_suspects(suspects)
|
||||
|
||||
def table(rows):
|
||||
body = "\n".join(
|
||||
f"<tr><td>{esc(s.reason)}</td><td><del>{esc(s.old) or EMPTY}</del></td>"
|
||||
f"<td><ins>{esc(s.new) or EMPTY}</ins></td>"
|
||||
f"<td class=ctx>{suspect_context(comparison, s)}</td></tr>"
|
||||
for s in rows
|
||||
)
|
||||
return (
|
||||
"<div class=scroll><table><tr><th>Grund</th><th>vorher</th><th>korrigiert</th>"
|
||||
f"<th>Zusammenhang</th></tr>\n{body}\n</table></div>"
|
||||
)
|
||||
|
||||
if important:
|
||||
suspect_table = table(important)
|
||||
elif forms:
|
||||
suspect_table = "<p class=muted>Keine sinnverändernden Änderungen gefunden.</p>"
|
||||
else:
|
||||
suspect_table = "<p class=muted>Keine auffälligen Änderungen gefunden.</p>"
|
||||
if forms:
|
||||
suspect_table += (
|
||||
f"\n<h3>Wortform geändert ({len(forms)})</h3>\n"
|
||||
"<p class=muted>Nur Endung oder Umlaut anders („Punkten“ → „Punkte“, "
|
||||
"„das“ → „dass“), meist Grammatik; ein kurzer Blick genügt.</p>\n"
|
||||
+ table(forms)
|
||||
)
|
||||
|
||||
# Suspect words are framed; a suspect deletion shows ⚠ after the word before it
|
||||
warn_words, warn_after = {}, {}
|
||||
for s in suspects:
|
||||
note = esc(f"{s.reason}: „{s.old or EMPTY}“ → „{s.new or EMPTY}“", quote=True)
|
||||
css = "warn form" if s.reason == WORD_FORM else "warn"
|
||||
if s.j1 == s.j2:
|
||||
warn_after.setdefault(max(s.j1 - 1, 0), []).append(note)
|
||||
for j in range(s.j1, s.j2):
|
||||
warn_words[j] = note
|
||||
warn_words[j] = (css, note)
|
||||
|
||||
# Corrected text with its paragraphs; each word knows its paragraph
|
||||
parts = []
|
||||
|
|
@ -272,7 +316,8 @@ def render_html(comparison, title):
|
|||
current = comparison.paragraph[j]
|
||||
parts.append("</p>\n<p>")
|
||||
if j in warn_words:
|
||||
piece = f"<span class=warn title='{warn_words[j]}'>{piece}</span>"
|
||||
css, note = warn_words[j]
|
||||
piece = f"<span class='{css}' title='{note}'>{piece}</span>"
|
||||
parts.append(piece)
|
||||
for mark in comparison.marks.get(j, []):
|
||||
parts.append(f"<span class=check>{esc(mark)}</span>")
|
||||
|
|
@ -325,6 +370,12 @@ gültige Wörter ersetzt, stark veränderte, eingefügte oder gestrichene Wörte
|
|||
"""
|
||||
|
||||
|
||||
def split_suspects(suspects):
|
||||
"""(possible meaning changes, mere word forms), each in text order."""
|
||||
forms = [s for s in suspects if s.reason == WORD_FORM]
|
||||
return [s for s in suspects if s.reason != WORD_FORM], forms
|
||||
|
||||
|
||||
def suspect_context(comparison, suspect):
|
||||
"""HTML: the change in bold with a few corrected words around it."""
|
||||
esc = html.escape
|
||||
|
|
@ -346,8 +397,11 @@ def print_summary(comparison, limit=15, suspect_limit=10):
|
|||
if len(summary) > limit:
|
||||
print(f" … und {len(summary) - limit} weitere, siehe Bericht")
|
||||
|
||||
suspects = comparison.suspects
|
||||
print(f"Zu prüfen: {len(suspects)} Stellen")
|
||||
important, forms = split_suspects(comparison.suspects)
|
||||
# Meaning changes first, word forms after them
|
||||
suspects = important + forms
|
||||
of_which = f" (davon {len(forms)} Wortformen)" if forms else ""
|
||||
print(f"Zu prüfen: {len(suspects)} Stellen{of_which}")
|
||||
for s in suspects[:suspect_limit]:
|
||||
print(f" {s.reason}: „{s.old or EMPTY}“ → „{s.new or EMPTY}“")
|
||||
if len(suspects) > suspect_limit:
|
||||
|
|
@ -362,9 +416,7 @@ def write_report(raw, corrected, target, title, model_input=None, glossary=(), l
|
|||
"""
|
||||
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), glossary, FILLERS.get(lang, set())
|
||||
)
|
||||
comparison.suspects = review_candidates(by_model, load_wordlist(lang), glossary, lang)
|
||||
target.write_text(render_html(comparison, title), encoding="utf-8")
|
||||
return comparison
|
||||
|
||||
|
|
@ -375,6 +427,11 @@ def report_path(corrected_path):
|
|||
return corrected_path.with_name(f"{stem}.aenderungen.html")
|
||||
|
||||
|
||||
def without_headers(text):
|
||||
"""Text without time headers ("[00:04:57 - 00:06:04]") of excerpts."""
|
||||
return "\n\n".join(body for _, body in split_segments(text))
|
||||
|
||||
|
||||
def read_raw(path):
|
||||
text = path.read_text(encoding="utf-8-sig")
|
||||
if path.suffix.lower() == ".vtt":
|
||||
|
|
@ -430,6 +487,9 @@ def main():
|
|||
model_input = apply_replacements(" ".join(raw.split()), replacements)
|
||||
|
||||
corrected = args.korrigiert.read_text(encoding="utf-8")
|
||||
# A raw .vtt has no time headers: those of the excerpts are no insertions
|
||||
if without_headers(raw) == raw.strip():
|
||||
corrected = without_headers(corrected)
|
||||
comparison = write_report(
|
||||
raw, corrected, args.output, args.korrigiert.name,
|
||||
model_input=model_input, glossary=glossary_entries(terms, replacements),
|
||||
|
|
|
|||
|
|
@ -124,10 +124,8 @@ def test_cli_missing_file(tmp_path, monkeypatch, capsys):
|
|||
WORDS = {"professor", "das", "dass", "ist", "haus", "unterschreiben", "bar"}
|
||||
|
||||
|
||||
def suspects(model_input, corrected, wordlist=WORDS, glossary=(), fillers=ae.FILLERS["de"]):
|
||||
found = ae.review_candidates(
|
||||
ae.compare(model_input, corrected), wordlist, glossary, fillers
|
||||
)
|
||||
def suspects(model_input, corrected, wordlist=WORDS, glossary=(), lang="de"):
|
||||
found = ae.review_candidates(ae.compare(model_input, corrected), wordlist, glossary, lang)
|
||||
return [(s.reason, s.old, s.new) for s in found]
|
||||
|
||||
|
||||
|
|
@ -182,7 +180,7 @@ def test_review_candidates_fillers_per_language():
|
|||
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"])
|
||||
english = suspects("so er we um go", "So we go.", lang="en")
|
||||
assert english == []
|
||||
|
||||
|
||||
|
|
@ -224,7 +222,7 @@ def test_report_suspects_only_against_model_input(tmp_path, capsys):
|
|||
assert [s.reason for s in comparison.suspects] == [ae.VALID_WORD, ae.DELETED]
|
||||
assert "Zu prüfen (2)" in page
|
||||
assert "Professorin" in section and "Kremmel" not in section
|
||||
assert "<span class=warn title='gültiges Wort ersetzt: „Professor“ → „Professorin“'>" in page
|
||||
assert "<span class='warn' title='gültiges Wort ersetzt: „Professor“ → „Professorin“'>" in page
|
||||
assert "<span class=warn title='gestrichen: „heute“ → „∅“'>⚠</span>" in page
|
||||
# Framing changes nothing about the text itself
|
||||
assert report_text(page) == "Der Professorin im Kreml ⚠ dort.".split()
|
||||
|
|
@ -242,6 +240,20 @@ def test_report_without_suspects(tmp_path):
|
|||
assert "Keine auffälligen Änderungen" in suspect_section(target.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_cli_ignores_time_headers_missing_in_raw(tmp_path, monkeypatch, capsys):
|
||||
raw = tmp_path / "rede.vtt"
|
||||
raw.write_text(
|
||||
"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\ndas Kremmel heute\n", encoding="utf-8"
|
||||
)
|
||||
corrected = tmp_path / "rede.korrigiert.txt"
|
||||
corrected.write_text("[00:00:01 - 00:00:02]\n\nDas Kreml heute.\n", encoding="utf-8")
|
||||
monkeypatch.setattr(sys, "argv", ["aenderungen.py", "-a", str(raw), "-b", str(corrected)])
|
||||
ae.main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Zu prüfen: 0 Stellen" in out
|
||||
assert "00:00:01" not in out
|
||||
|
||||
|
||||
def test_cli_glossary(tmp_path, monkeypatch, capsys):
|
||||
raw = tmp_path / "rede.txt"
|
||||
raw.write_text("das Kremmel und\nBar heute\n", encoding="utf-8")
|
||||
|
|
@ -261,3 +273,51 @@ def test_cli_glossary(tmp_path, monkeypatch, capsys):
|
|||
assert "Zu prüfen: 0 Stellen" in out
|
||||
# The report still compares against the raw transcript
|
||||
assert "1× „Kremmel“ → „Kreml“" in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("old, new, expected", [
|
||||
("Punkten", "Punkte", True),
|
||||
("das", "dass", True),
|
||||
("sehe", "sähe", True), # umlaut
|
||||
("sein", "seien", True), # both endings change
|
||||
("kommt,", "kommen,", True), # punctuation does not count
|
||||
("Professor", "Professorin", False), # "-in" is no inflection
|
||||
("geworben", "geworden", False),
|
||||
("unterschreiben", "unterscheiden", False),
|
||||
("ich", "sich", False),
|
||||
("den", "der", False), # stem too short
|
||||
("sich dazu", "zu", False), # more than one word
|
||||
])
|
||||
def test_word_form_change(old, new, expected):
|
||||
assert ae.word_form_change(old.split(), new.split()) is expected
|
||||
|
||||
|
||||
def test_word_form_change_english():
|
||||
assert ae.word_form_change(["walk"], ["walked"], "en")
|
||||
assert not ae.word_form_change(["walk"], ["walker"], "en")
|
||||
|
||||
|
||||
def test_review_candidates_word_forms():
|
||||
words = WORDS | {"punkten", "punkte"}
|
||||
assert suspects("die Punkten und das", "die Punkte und dass", wordlist=words) == [
|
||||
(ae.WORD_FORM, "Punkten", "Punkte"), (ae.WORD_FORM, "das", "dass"),
|
||||
]
|
||||
|
||||
|
||||
def test_report_and_summary_put_word_forms_last(tmp_path, capsys):
|
||||
target = tmp_path / "bericht.html"
|
||||
comparison = ae.write_report(
|
||||
"das sagte der Professor", "dass sagte der Professorin", target, "rede"
|
||||
)
|
||||
assert [s.reason for s in comparison.suspects] == [ae.WORD_FORM, ae.VALID_WORD]
|
||||
page = target.read_text(encoding="utf-8")
|
||||
section = suspect_section(page)
|
||||
# Meaning change in the main table, the word form in its own group after it
|
||||
assert section.index("Professorin") < section.index("Wortform geändert (1)")
|
||||
assert section.index("Wortform geändert (1)") < section.index("<ins>dass</ins>")
|
||||
assert "<span class='warn form' title='Wortform geändert: „das“ → „dass“'>" in page
|
||||
|
||||
ae.print_summary(comparison)
|
||||
out = capsys.readouterr().out.split("Zu prüfen: ")[1]
|
||||
assert out.startswith("2 Stellen (davon 1 Wortformen)")
|
||||
assert out.index("Professorin") < out.index("„das“ → „dass“")
|
||||
|
|
|
|||
Loading…
Reference in a new issue