feat: Änderungsbericht listet eigenmächtige Modelländerungen „Zu prüfen“
Verglichen wird der Text nach den Glossar-Ersetzungen mit dem Ergebnis des Modells. Gemeldet werden ersetzte gültige Wörter (Systemwortliste /usr/share/dict/ngerman bzw. american-english), stark veränderte, eingefügte und gestrichene Wörter; Hörfehler-Korrekturen, reine Zusammen-/Getrennt- schreibung, Füllwörter und Glossarwörter nicht. Eigener Abschnitt im HTML-Bericht, Rahmen im Text, Liste in der Konsole. aenderungen.py versteht dafür -g und -l. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5a59baef88
commit
82ce30ca72
3 changed files with 389 additions and 14 deletions
|
|
@ -18,6 +18,22 @@ def ollama_available(request, monkeypatch):
|
|||
monkeypatch.setattr(module, "check_ollama", lambda model: None)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def small_wordlist(tmp_path_factory, monkeypatch):
|
||||
"""Reports use a small word list instead of the system one."""
|
||||
import aenderungen
|
||||
folder = tmp_path_factory.mktemp("wordlist")
|
||||
de = folder / "de.txt"
|
||||
de.write_text(
|
||||
"\n".join(["Professor", "das", "dass", "ist", "Haus", "unterschreiben", "Bar"]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(aenderungen, "WORDLISTS", {"de": de, "en": folder / "fehlt.txt"})
|
||||
aenderungen.load_wordlist.cache_clear()
|
||||
yield
|
||||
aenderungen.load_wordlist.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_yt_dlp():
|
||||
"""Factory for a subprocess.run stand-in.
|
||||
|
|
|
|||
|
|
@ -336,6 +336,26 @@ def test_main_writes_change_report(sample_copy, monkeypatch, capsys):
|
|||
assert f"Bericht: {report}" in out
|
||||
|
||||
|
||||
def test_main_report_suspects_only_model_changes(sample_copy, monkeypatch, capsys):
|
||||
glossary = sample_copy.with_name("g.txt")
|
||||
glossary.write_text("Rammstein => Ramstein\n", encoding="utf-8")
|
||||
|
||||
def correct(chunk, index, system):
|
||||
return chunk.strip().replace(" das ", " dies ", 1)
|
||||
|
||||
monkeypatch.setattr(ak, "correct_chunk", correct)
|
||||
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||
run_main(monkeypatch, "-i", sample_copy, "-g", glossary)
|
||||
|
||||
page = sample_copy.with_name("ramstein.de.aenderungen.html").read_text(encoding="utf-8")
|
||||
section = page.split("<h2>Zu prüfen")[1].split("<h2>Alle Änderungen")[0]
|
||||
assert "Rammstein" in page and "Rammstein" not in section
|
||||
assert "dies" in section
|
||||
out = capsys.readouterr().out
|
||||
assert "gültiges Wort ersetzt: „das“ → „dies“" in out
|
||||
assert "Stelle(n) zu prüfen, siehe" in out
|
||||
|
||||
|
||||
def test_main_without_change_report(sample_copy, monkeypatch):
|
||||
monkeypatch.setattr(ak, "correct_chunk", identity_correct)
|
||||
monkeypatch.setattr(ak, "find_paragraph_starts", every_nth_start(5))
|
||||
|
|
|
|||
|
|
@ -117,3 +117,121 @@ def test_cli_missing_file(tmp_path, monkeypatch, capsys):
|
|||
with pytest.raises(SystemExit):
|
||||
ae.main()
|
||||
assert "Datei nicht gefunden" in capsys.readouterr().err
|
||||
|
||||
|
||||
# === Suspect changes ===
|
||||
|
||||
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))
|
||||
return [(s.reason, s.old, s.new) for s in found]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_input, corrected, expected", [
|
||||
("Der Professor kam", "Der Professorin kam", [(ae.VALID_WORD, "Professor", "Professorin")]),
|
||||
("wir unterschreiben das", "wir unterscheiden das",
|
||||
[(ae.VALID_WORD, "unterschreiben", "unterscheiden")]),
|
||||
("im CT1 heute", "im ZDF heute", [(ae.CHANGED_A_LOT, "CT1", "ZDF")]),
|
||||
("er kam heute", "er ist kam heute", [(ae.INSERTED, "", "ist")]),
|
||||
("er kam heute", "er kam", [(ae.DELETED, "heute", "")]),
|
||||
])
|
||||
def test_review_candidates_reports(model_input, corrected, expected):
|
||||
assert suspects(model_input, corrected) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_input, corrected", [
|
||||
("das Kremmel heute", "das Kreml heute"), # recognition error
|
||||
("die Gradwanderung", "die Gratwanderung"),
|
||||
("die Clan Kriminalität", "die Clankriminalität"), # spacing only
|
||||
("KI generierte Videos", "KI-generierte Videos"), # hyphen only
|
||||
("er äh kam ähm heute", "er kam heute"), # fillers
|
||||
("das ist gut", "Das ist gut."), # case, punctuation
|
||||
])
|
||||
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) == []
|
||||
assert suspects("Bar sagte", "Bahr sagte") == [(ae.VALID_WORD, "Bar", "Bahr")]
|
||||
|
||||
|
||||
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) == []
|
||||
assert suspects("im CT1 heute", "im ZDF heute", wordlist=None) == [
|
||||
(ae.CHANGED_A_LOT, "CT1", "ZDF")
|
||||
]
|
||||
|
||||
|
||||
def test_review_candidates_indices_point_into_corrected_words():
|
||||
comparison = ae.compare("a b Professor c d", "A b Professorin c d.")
|
||||
[suspect] = ae.review_candidates(comparison, WORDS, set())
|
||||
assert comparison.words[suspect.j1:suspect.j2] == ["Professorin"]
|
||||
|
||||
|
||||
def test_load_wordlist(capsys):
|
||||
assert "professor" in ae.load_wordlist("de")
|
||||
assert ae.load_wordlist("en") is None
|
||||
assert "keine Wortliste für „en“ (Paket wamerican)" in capsys.readouterr().out
|
||||
|
||||
|
||||
def suspect_section(page):
|
||||
return page.split("<h2>Zu prüfen")[1].split("<h2>Alle Änderungen")[0]
|
||||
|
||||
|
||||
def test_report_suspects_only_against_model_input(tmp_path, capsys):
|
||||
target = tmp_path / "bericht.html"
|
||||
# Glossary turned "Kremmel" into "Kreml" before the model; the model
|
||||
# replaced "Professor" and deleted "heute" on its own
|
||||
comparison = ae.write_report(
|
||||
"der Professor im Kremmel heute dort", "Der Professorin im Kreml dort.", target,
|
||||
"rede", model_input="der Professor im Kreml heute dort",
|
||||
)
|
||||
page = target.read_text(encoding="utf-8")
|
||||
section = suspect_section(page)
|
||||
|
||||
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='gestrichen: „heute“ → „∅“'>⚠</span>" in page
|
||||
# Framing changes nothing about the text itself
|
||||
assert report_text(page) == "Der Professorin im Kreml ⚠ dort.".split()
|
||||
|
||||
ae.print_summary(comparison)
|
||||
out = capsys.readouterr().out
|
||||
assert "Zu prüfen: 2 Stellen" in out
|
||||
assert "gültiges Wort ersetzt: „Professor“ → „Professorin“" in out
|
||||
|
||||
|
||||
def test_report_without_suspects(tmp_path):
|
||||
target = tmp_path / "bericht.html"
|
||||
comparison = ae.write_report("das Kremmel", "Das Kreml.", target, "rede")
|
||||
assert comparison.suspects == []
|
||||
assert "Keine auffälligen Änderungen" in suspect_section(target.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
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")
|
||||
corrected = tmp_path / "rede.korrigiert.txt"
|
||||
corrected.write_text("Das Kreml und Bahr heute.\n", encoding="utf-8")
|
||||
glossary = tmp_path / "g.txt"
|
||||
glossary.write_text("Kremmel => Kreml\nEgon Bahr\n", encoding="utf-8")
|
||||
|
||||
argv = ["aenderungen.py", "-a", str(raw), "-b", str(corrected)]
|
||||
monkeypatch.setattr(sys, "argv", argv)
|
||||
ae.main()
|
||||
assert "gültiges Wort ersetzt: „Bar“ → „Bahr“" in capsys.readouterr().out
|
||||
|
||||
monkeypatch.setattr(sys, "argv", [*argv, "-g", str(glossary)])
|
||||
ae.main()
|
||||
out = capsys.readouterr().out
|
||||
assert "Zu prüfen: 0 Stellen" in out
|
||||
# The report still compares against the raw transcript
|
||||
assert "1× „Kremmel“ → „Kreml“" in out
|
||||
|
|
|
|||
Loading…
Reference in a new issue