Loest das Abdeckungs-Problem des handkuratierten Dicts (~300 Eintraege, ~95%). Neu: vollautomatisch erzeugtes Korpus-Dict aus hunspell-de-de mit 153.869 Eintraegen (>99% Abdeckung), plus schlankes Supplement fuer Komposita, die hunspell nicht liefert. Build-Skript (scripts/build_umlaut_dict.py): - ruft /usr/bin/unmunch gegen /usr/share/hunspell/de_DE.dic+aff auf - filtert Woerter mit echten Umlauten (ä/ö/ü/ß) - generiert je Wort die Umschreibungsform (ae/oe/ue/ss) + Capitalize - Mehrdeutigkeits-Check: skippt Paare wo die Umschreibung selbst ein gueltiges deutsches Wort ist (z. B. dass/daß, Masse/Maße, Busse/Buße) - Ergebnis: 153.869 Eintraege, 27 mehrdeutige Formen ausgefiltert - Alphabetisch sortiertes JSON (diff-freundlich) Laufzeit-Refactor (src/services/post_refresh_qc.py): - _UMLAUT_BASE Dict (handkuratiert) entfernt, dafuer JSON-Loader beim Modul-Import aus src/services/umlaut_dict.json - _MANUAL_SUPPLEMENT fuer Luecken (Konjunktiv saeen, Amtstitel- Komposita wie Aussenminister/Parlamentspraesident, Strassen- Komposita, Fuehrungs-Komposita) — ueberlagert Korpus-Dict - _UMLAUT_WHITELIST erweitert um englische Fremdwoerter (Boeing, Business, Access, Process, Message, Password, Miss, Boss, Goethe, Yahoo, Israel, Israels) - Regex-Strategie umgestellt: statt riesigem alternierenden Pattern ueber alle Keys jetzt Tokenizer (_WORD_PATTERN) + O(1) Dict-Lookup pro Wort. Deutlich performanter bei 150k+ Eintraegen. - normalize_german_umlauts() Signatur unveraendert - normalize_umlaut_fields() unveraendert - Einhaengung in run_post_refresh_qc() unveraendert Daten-Artefakt (src/services/umlaut_dict.json): - 4.88 MB alphabetisch sortiertes JSON - Im Repo committet zwecks Reproduzierbarkeit und kein hunspell- Laufzeit-Abhaengigkeit im Container Verwerfbarkeit voll erhalten: - git revert entfernt alle drei neuen Elemente - Bestand in DB bleibt repariert (korrektes Deutsch, kein Schaden) - hunspell-Paket kann bleiben oder mit apt purge entfernt werden Bootstrap-Rerun mit neuem Dict: - 7 Lagen aktualisiert, 306 zusaetzliche Ersetzungen - Lage #6 (Irankonflikt) von 140 ursprungs- und 15 Rest-Treffern nach voriger Runde jetzt auf 0 Hard-Hits - andere aktive Lagen insgesamt 8 verbleibende Rest-Treffer (spezielle Eigennamen, koennen bei Bedarf ins Supplement) Performance: - Dict-Load beim Modul-Import: ~100 ms - Gesamt Unit-Tests (11 Faelle): 161 ms - Refresh-Pfad unveraendert schnell: O(Wortzahl) mit Hashmap-Lookup
167 Zeilen
5.7 KiB
Python
167 Zeilen
5.7 KiB
Python
"""Generiert src/services/umlaut_dict.json aus hunspell-de-de.
|
|
|
|
Aufruf (auf dem Monitor-Server):
|
|
cd /home/claude-dev/AegisSight-Monitor
|
|
python3 scripts/build_umlaut_dict.py
|
|
|
|
Voraussetzungen:
|
|
- hunspell-de-de (liefert /usr/share/hunspell/de_DE.dic + de_DE.aff)
|
|
- hunspell-tools (liefert /usr/bin/unmunch)
|
|
|
|
Ablauf:
|
|
1. unmunch rollt alle Flexionsformen aus dem hunspell-Dict aus
|
|
2. Wir filtern Woerter mit echten Umlauten (ä, ö, ü, ß)
|
|
3. Wir generieren fuer jedes Wort die Umschreibungs-Form (ae/oe/ue/ss)
|
|
4. Mehrdeutigkeits-Check: Wenn die Umschreibungs-Form selbst ein
|
|
gueltiges deutsches Wort ist (z. B. "dass" vs "daß"), skippen
|
|
5. Ausgabe als alphabetisch sortiertes JSON (diff-freundlich)
|
|
"""
|
|
import json
|
|
import locale
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
DIC_PATH = "/usr/share/hunspell/de_DE.dic"
|
|
AFF_PATH = "/usr/share/hunspell/de_DE.aff"
|
|
UNMUNCH_BIN = "/usr/bin/unmunch"
|
|
|
|
OUTPUT_PATH = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"src", "services", "umlaut_dict.json",
|
|
)
|
|
|
|
UMLAUT_MAP = (
|
|
("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss"),
|
|
("Ä", "Ae"), ("Ö", "Oe"), ("Ü", "Ue"),
|
|
)
|
|
|
|
|
|
def to_ascii_form(word: str) -> str:
|
|
"""Konvertiert ein Wort mit Umlauten in seine Umschreibungs-Form."""
|
|
out = word
|
|
for uml, asc in UMLAUT_MAP:
|
|
out = out.replace(uml, asc)
|
|
return out
|
|
|
|
|
|
def has_umlaut(word: str) -> bool:
|
|
return any(ch in word for ch in "äöüßÄÖÜ")
|
|
|
|
|
|
def run_unmunch() -> set:
|
|
"""Fuehrt unmunch aus und gibt die Menge aller hunspell-Woerter zurueck."""
|
|
env = os.environ.copy()
|
|
# unmunch arbeitet mit Latin-1 als Voreinstellung; das .dic/.aff in de_DE
|
|
# ist aber UTF-8 (siehe SET UTF-8 im .aff). Wir setzen die Locale explizit.
|
|
env["LC_ALL"] = "C.UTF-8"
|
|
result = subprocess.run(
|
|
[UNMUNCH_BIN, DIC_PATH, AFF_PATH],
|
|
capture_output=True,
|
|
check=True,
|
|
env=env,
|
|
)
|
|
raw = result.stdout.decode("utf-8", errors="replace")
|
|
words = set()
|
|
for line in raw.splitlines():
|
|
w = line.strip()
|
|
if not w or w.startswith("#"):
|
|
continue
|
|
words.add(w)
|
|
return words
|
|
|
|
|
|
def build_mapping(all_words: set) -> tuple[dict, int, int]:
|
|
"""Baut das Umlaut-Ersetzungs-Mapping.
|
|
|
|
Rueckgabe: (mapping, skipped_ambiguous, words_with_umlaut)
|
|
"""
|
|
mapping = {}
|
|
skipped_ambiguous = 0
|
|
words_with_umlaut = 0
|
|
|
|
for word in all_words:
|
|
if not has_umlaut(word):
|
|
continue
|
|
words_with_umlaut += 1
|
|
|
|
ascii_form = to_ascii_form(word)
|
|
# Mehrdeutigkeits-Check: Umschreibung ist selbst ein gueltiges Wort?
|
|
if ascii_form in all_words:
|
|
skipped_ambiguous += 1
|
|
continue
|
|
|
|
# Standardfall: Mapping Umschreibung -> Umlaut-Form
|
|
mapping[ascii_form] = word
|
|
|
|
# Zusaetzlich Capitalize-Variante erzeugen (wenn anders als Original)
|
|
if ascii_form[:1].islower():
|
|
cap_ascii = ascii_form[:1].upper() + ascii_form[1:]
|
|
cap_umlaut = word[:1].upper() + word[1:]
|
|
if cap_ascii != ascii_form and cap_ascii not in all_words:
|
|
mapping[cap_ascii] = cap_umlaut
|
|
|
|
return mapping, skipped_ambiguous, words_with_umlaut
|
|
|
|
|
|
def sanity_spot_check(mapping: dict) -> None:
|
|
"""Prueft ob einige typische Testfaelle korrekt im Mapping abgebildet sind."""
|
|
expected_in = [
|
|
"oeffnung", "Oeffnung", "strasse", "Strasse", "fuer", "Fuer",
|
|
"ueber", "Ueber", "koennen", "Koennen", "muessen", "Muessen",
|
|
"moeglich", "Moeglich", "schliessen", "Schliessen",
|
|
"aussenminister", "Aussenminister", "praesident", "Praesident",
|
|
"buerger", "Buerger", "zurueck", "Zurueck", "fuehren", "Fuehren",
|
|
]
|
|
expected_not_in = [
|
|
"dass", "Dass", # moderne Form gueltig
|
|
"masse", "Masse", # Bedeutungsunterschied zu "Masse"/"Maße"
|
|
"busse", "Busse", # Bedeutungsunterschied zu "Busse"/"Buße"
|
|
]
|
|
missing = [w for w in expected_in if w not in mapping]
|
|
wrong = [w for w in expected_not_in if w in mapping]
|
|
print("Sanity-Check:")
|
|
print(f" Erwartete Eintraege gefunden: {len(expected_in) - len(missing)}/{len(expected_in)}")
|
|
if missing:
|
|
print(f" FEHLEND: {missing}")
|
|
print(f" Erwartete Ausschluesse korrekt: {len(expected_not_in) - len(wrong)}/{len(expected_not_in)}")
|
|
if wrong:
|
|
print(f" FAELSCHLICH DRIN: {wrong}")
|
|
|
|
|
|
def main():
|
|
if not os.path.exists(DIC_PATH):
|
|
print(f"FEHLER: {DIC_PATH} nicht gefunden. Paket hunspell-de-de installiert?",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
if not os.path.exists(UNMUNCH_BIN):
|
|
print(f"FEHLER: {UNMUNCH_BIN} nicht gefunden. Paket hunspell-tools installiert?",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
print(f"Lese hunspell-Dict via {UNMUNCH_BIN} ...")
|
|
all_words = run_unmunch()
|
|
print(f" {len(all_words)} hunspell-Wortformen geladen")
|
|
|
|
print("Baue Umlaut-Ersetzungs-Mapping ...")
|
|
mapping, skipped, umlaut_words = build_mapping(all_words)
|
|
print(f" {umlaut_words} Woerter mit Umlaut gefunden")
|
|
print(f" {skipped} mehrdeutige Formen uebersprungen (z.B. dass/daß)")
|
|
print(f" {len(mapping)} Eintraege im finalen Mapping")
|
|
|
|
sanity_spot_check(mapping)
|
|
|
|
print(f"\nSchreibe {OUTPUT_PATH} ...")
|
|
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
|
# Alphabetisch sortiert (diff-freundlich)
|
|
sorted_mapping = dict(sorted(mapping.items(), key=lambda kv: kv[0]))
|
|
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(sorted_mapping, f, ensure_ascii=False, indent=None, separators=(",", ":"))
|
|
size_mb = os.path.getsize(OUTPUT_PATH) / (1024 * 1024)
|
|
print(f" {size_mb:.2f} MB geschrieben")
|
|
print("Fertig.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|