feat: Glossar-Entwurf aus Rohtext und Korrekturlauf (glossar_vorschlag.py)
- Das Modell sucht abschnittsweise vermutete Hörfehler (Namen, Orte, Fremdwörter, Komposita); nur Funde, die wörtlich im Rohtext stehen - Sichere Funde als Einträge, unsichere auskommentiert („# ?“) - -k: Änderungen eines Korrekturlaufs, immer auskommentiert, da oft zusammenhangsabhängig - -g: vorhandene Glossare werden vorher angewendet und nicht erneut vorgeschlagen - Modellantworten im Zwischenspeicher <Entwurf>.parts/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c1d270bc0e
commit
bc09ddf3bf
1 changed files with 391 additions and 0 deletions
129
tests/test_glossar_vorschlag.py
Normal file
129
tests/test_glossar_vorschlag.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""Tests for glossar_vorschlag.py; the Ollama model is always replaced by fakes."""
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import glossar_vorschlag as gv
|
||||
import rede_korrigieren as rk
|
||||
|
||||
RAW = (
|
||||
"Der Kremmel und Egon Bar trafen Host Telchik. Die Rechtsstaatbahn war gut. "
|
||||
"Herr Grotzki und Ghard Kocher sprachen über das gedeiliche Verhältnis."
|
||||
)
|
||||
|
||||
|
||||
def fake_model(entries):
|
||||
"""urlopen stand-in answering every chunk with the same entries."""
|
||||
calls = []
|
||||
|
||||
def urlopen(request, timeout=None):
|
||||
calls.append(json.loads(request.data))
|
||||
content = json.dumps({"eintraege": entries}, ensure_ascii=False)
|
||||
return io.BytesIO(json.dumps({"message": {"content": content}}).encode("utf-8"))
|
||||
|
||||
urlopen.calls = calls
|
||||
return urlopen
|
||||
|
||||
|
||||
ENTRIES = [
|
||||
{"falsch": "Grotzki", "richtig": "Grotzky", "sicher": True},
|
||||
{"falsch": "Ghard Kocher", "richtig": "Gerhard Kocher", "sicher": False},
|
||||
{"falsch": "Kremmel", "richtig": "Kreml", "sicher": True}, # in glossary
|
||||
{"falsch": "Nirgendwo", "richtig": "Irgendwo", "sicher": True}, # not in text
|
||||
{"falsch": "gut", "richtig": "gut", "sicher": True}, # no change
|
||||
{"falsch": "", "richtig": "x", "sicher": True},
|
||||
"kein Objekt",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def raw_file(tmp_path):
|
||||
path = tmp_path / "rede.txt"
|
||||
path.write_text(RAW + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def glossary(tmp_path):
|
||||
path = tmp_path / "g.txt"
|
||||
path.write_text("Kremmel => Kreml\nEgon Bar => Egon Bahr\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def no_model_calls(*args, **kwargs):
|
||||
raise AssertionError("Modell wurde unerwartet aufgerufen")
|
||||
|
||||
|
||||
def run_main(monkeypatch, *args):
|
||||
monkeypatch.setattr(sys, "argv", ["glossar_vorschlag.py", *map(str, args)])
|
||||
gv.main()
|
||||
|
||||
|
||||
def test_main_filters_and_formats(raw_file, glossary, monkeypatch, capsys):
|
||||
model = fake_model(ENTRIES)
|
||||
monkeypatch.setattr(gv.urllib.request, "urlopen", model)
|
||||
run_main(monkeypatch, "-i", raw_file, "-g", glossary, "-m", "test:1b")
|
||||
|
||||
draft = raw_file.with_name("rede.glossar.entwurf.txt").read_text(encoding="utf-8")
|
||||
lines = draft.splitlines()
|
||||
assert "Grotzki => Grotzky" in lines
|
||||
assert "# ? Ghard Kocher => Gerhard Kocher" in lines
|
||||
for dropped in ("Kremmel", "Nirgendwo", "gut =>", " => x"):
|
||||
assert dropped not in draft
|
||||
assert "Modell test:1b" in lines[0]
|
||||
# The model sees the text with the known replacements already applied
|
||||
sent = model.calls[0]["messages"][1]["content"]
|
||||
assert "Kreml" in sent and "Kremmel" not in sent
|
||||
assert model.calls[0]["model"] == "test:1b"
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Vermutete Hörfehler: 2 (1 sicher, 1 unsicher)" in out
|
||||
|
||||
# Second run uses the cache
|
||||
monkeypatch.setattr(gv.urllib.request, "urlopen", no_model_calls)
|
||||
run_main(monkeypatch, "-i", raw_file, "-g", glossary, "-m", "test:1b")
|
||||
|
||||
|
||||
def test_correction_changes_are_commented_out(raw_file, glossary, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(gv.urllib.request, "urlopen", fake_model(ENTRIES[:2]))
|
||||
corrected = tmp_path / "rede.korrigiert.txt"
|
||||
corrected.write_text(
|
||||
"Der Kreml und Egon Bahr trafen Host Telchik. Die Rechtsstaat Bahn war gut. "
|
||||
"Herr Grotzky und Ghard Kocher sprachen über das gedeihliche Verhältnis.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
out = tmp_path / "entwurf.txt"
|
||||
run_main(monkeypatch, "-i", raw_file, "-k", corrected, "-g", glossary, "-o", out)
|
||||
|
||||
lines = out.read_text(encoding="utf-8").splitlines()
|
||||
assert "# ? gedeiliche => gedeihliche (1×)" in lines
|
||||
assert "# ? Rechtsstaatbahn => Rechtsstaat Bahn (1×)" in lines
|
||||
# Already a model suggestion, already in the glossary: not listed twice
|
||||
assert not any("Grotzki => Grotzky (" in line for line in lines)
|
||||
assert not any("Egon Bar =>" in line for line in lines)
|
||||
|
||||
|
||||
def test_drafted_entries_are_valid_glossary(raw_file, monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(gv.urllib.request, "urlopen", fake_model(ENTRIES[:2]))
|
||||
out = tmp_path / "entwurf.txt"
|
||||
run_main(monkeypatch, "-i", raw_file, "-o", out)
|
||||
terms, replacements = rk.read_glossary(out)
|
||||
assert terms == []
|
||||
assert replacements == [("Grotzki", "Grotzky")]
|
||||
|
||||
|
||||
def test_unreadable_model_answer(raw_file, monkeypatch, tmp_path, capsys):
|
||||
def urlopen(request, timeout=None):
|
||||
return io.BytesIO(json.dumps({"message": {"content": "kein JSON"}}).encode())
|
||||
|
||||
monkeypatch.setattr(gv.urllib.request, "urlopen", urlopen)
|
||||
run_main(monkeypatch, "-i", raw_file, "-o", tmp_path / "e.txt")
|
||||
assert "unlesbar" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_cli_missing_file(monkeypatch, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
run_main(monkeypatch, "-i", "fehlt.txt")
|
||||
assert "Datei nicht gefunden" in capsys.readouterr().err
|
||||
Loading…
Reference in a new issue