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:
Dieter Schlüter 2026-10-10 20:02:39 +02:00
commit bc09ddf3bf

View 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