QC: Umlaut-Normalisierung + Prompt-Ergaenzung
Drei unabhaengige Schutzschichten gegen falsche Umschreibungen
(ae/oe/ue/ss statt ä/ö/ü/ß) im Lagebild:
1. Prompt-Ergaenzung in INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE und
INCREMENTAL_BRIEFING_PROMPT_TEMPLATE (analyzer.py): explizite
Priorisierung, dass die Regel "echte UTF-8-Umlaute" Vorrang vor
"bestehende Formulierungen beibehalten" hat. Adressiert den Fall,
dass Claude beim inkrementellen Update Altlasten weitertraegt.
2. Deterministische Normalisierung in post_refresh_qc.py:
- normalize_german_umlauts(text) - Regex mit Wortgrenzen, case-
preserving, Whitelist-tauglich, ~140 Eintraege im Woerterbuch
abgeleitet aus den 140 Hard-Hits in Lage #6
- normalize_umlaut_fields(db, incident_id) - laedt summary und
latest_developments, normalisiert, schreibt nur bei Aenderungen
zurueck (idempotent)
- Eingehaengt in run_post_refresh_qc() nach dem Location-Check,
Fehler stoppen die Pipeline nicht (identisches Muster wie
bestehende Checks)
3. scripts/bootstrap_umlaut_repair.py - Einmal-Skript zur
Bestandsbereinigung der bereits gespeicherten summary-Felder.
Idempotent. Beim initialen Lauf auf Produktiv-DB: 14 Lagen
aktualisiert, 431 Ersetzungen insgesamt, Lage #6 von 140 auf
15 Rest-Treffer reduziert.
Whitelist (leer): aktuell kein Konflikt zwischen deutschen Ziel-
Woertern und englischen Fremdwoertern. Kann bei Bedarf erweitert
werden ohne Schema-Aenderung.
Verifikation:
- py_compile OK fuer alle drei Dateien
- Service-Restart ohne Errors
- Unit-Tests: positive Faelle ("Oeffnung der Strasse" -> 4 Ersetzungen),
Whitelist ("Boeing liefert Business-Access" -> 0 Ersetzungen),
Komposita ("Wasserstrasse", "Parlamentspraesident") korrekt
- Bootstrap 2x ausgefuehrt (erster Lauf 288 Ersetzungen, zweiter 143
nach Dict-Erweiterung), kumulativ 431
Architektur bleibt dormant ohne Daten-Altlasten: wenn keine Lage
Umschreibungen enthaelt, arbeitet normalize_umlaut_fields in <1ms
und schreibt nichts. Kein Overhead im Refresh-Pfad.
Dieser Commit ist enthalten in:
78
scripts/bootstrap_umlaut_repair.py
Normale Datei
78
scripts/bootstrap_umlaut_repair.py
Normale Datei
@@ -0,0 +1,78 @@
|
|||||||
|
"""Einmal-Repair: normalisiert Umlaute in summary und latest_developments
|
||||||
|
aller aktiven Lagen deterministisch (deutsche Umschreibungs-Form -> echte Umlaute).
|
||||||
|
|
||||||
|
Idempotent: mehrfaches Ausfuehren hat keinen zusaetzlichen Effekt, wenn
|
||||||
|
bereits normalisierte Texte vorliegen.
|
||||||
|
|
||||||
|
Aufruf (auf dem Monitor-Server):
|
||||||
|
cd /home/claude-dev/AegisSight-Monitor/src
|
||||||
|
python3 ../scripts/bootstrap_umlaut_repair.py
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Sicherstellen, dass src/ im PYTHONPATH ist, damit services/post_refresh_qc importiert werden kann
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_src = os.path.abspath(os.path.join(_here, "..", "src"))
|
||||||
|
if _src not in sys.path:
|
||||||
|
sys.path.insert(0, _src)
|
||||||
|
|
||||||
|
from services.post_refresh_qc import normalize_german_umlauts # noqa: E402
|
||||||
|
|
||||||
|
DB_PATH = "/home/claude-dev/osint-data/osint.db"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
rows = c.execute(
|
||||||
|
"SELECT id, title, summary, latest_developments FROM incidents "
|
||||||
|
"WHERE status IN ('active', 'archived') ORDER BY id"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
total_summary = 0
|
||||||
|
total_dev = 0
|
||||||
|
updated = 0
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
iid = r["id"]
|
||||||
|
title = r["title"] or ""
|
||||||
|
summary_orig = r["summary"] or ""
|
||||||
|
dev_orig = r["latest_developments"] or ""
|
||||||
|
|
||||||
|
new_summary, n_s = normalize_german_umlauts(summary_orig)
|
||||||
|
new_dev, n_d = normalize_german_umlauts(dev_orig)
|
||||||
|
|
||||||
|
if n_s == 0 and n_d == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"UPDATE incidents SET summary = ?, latest_developments = ? WHERE id = ?",
|
||||||
|
(
|
||||||
|
new_summary if n_s > 0 else summary_orig,
|
||||||
|
new_dev if n_d > 0 else dev_orig,
|
||||||
|
iid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
total_summary += n_s
|
||||||
|
total_dev += n_d
|
||||||
|
print(
|
||||||
|
f" Lage #{iid:>3} {title[:50]:50} "
|
||||||
|
f"summary: {n_s:>4} | latest_developments: {n_d:>3}"
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print()
|
||||||
|
print(f"Ergebnis: {updated} Lagen aktualisiert. "
|
||||||
|
f"{total_summary} Ersetzungen in summary, {total_dev} in latest_developments "
|
||||||
|
f"(gesamt {total_summary + total_dev}).")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -140,6 +140,7 @@ REGELN:
|
|||||||
- Neutral und sachlich - keine Wertungen oder Spekulationen
|
- Neutral und sachlich - keine Wertungen oder Spekulationen
|
||||||
- KEINE Gedankenstriche (—, –) verwenden — stattdessen Kommas, Doppelpunkte oder neue Saetze
|
- KEINE Gedankenstriche (—, –) verwenden — stattdessen Kommas, Doppelpunkte oder neue Saetze
|
||||||
- Bei widersprüchlichen Angaben beide Seiten erwähnen
|
- Bei widersprüchlichen Angaben beide Seiten erwähnen
|
||||||
|
- Falls das BISHERIGE LAGEBILD Umschreibungen enthält (ae, oe, ue, ss anstelle von ä, ö, ü, ß), ersetze diese beim Aktualisieren durch echte Umlaute. Die Regel "echte UTF-8-Umlaute" hat Vorrang vor der Regel "bestehende Formulierungen beibehalten".
|
||||||
- Wenn eine Quelle eine erkennbare Ausrichtung hat (z.B. pro-russisch, pro-iranisch, staatsnah, rechtsextrem), muss dies im Fliesstext erwaehnt werden, damit der Leser die Information einordnen kann. Beispiel: "Laut dem pro-russischen Telegram-Kanal Rybar..." oder "Die iranische Nachrichtenagentur Fars meldete..." oder "Der rechtsextreme Kanal Compact behauptete..."
|
- Wenn eine Quelle eine erkennbare Ausrichtung hat (z.B. pro-russisch, pro-iranisch, staatsnah, rechtsextrem), muss dies im Fliesstext erwaehnt werden, damit der Leser die Information einordnen kann. Beispiel: "Laut dem pro-russischen Telegram-Kanal Rybar..." oder "Die iranische Nachrichtenagentur Fars meldete..." oder "Der rechtsextreme Kanal Compact behauptete..."
|
||||||
- Quellen immer mit [Nr] referenzieren
|
- Quellen immer mit [Nr] referenzieren
|
||||||
- Ältere Quellen zeitlich einordnen
|
- Ältere Quellen zeitlich einordnen
|
||||||
@@ -189,6 +190,7 @@ WICHTIG zur Sektion ZUSAMMENFASSUNG:
|
|||||||
REGELN:
|
REGELN:
|
||||||
- Bisherige gesicherte Fakten beibehalten
|
- Bisherige gesicherte Fakten beibehalten
|
||||||
- KEINE Gedankenstriche (—, –) verwenden — stattdessen Kommas, Doppelpunkte oder neue Saetze
|
- KEINE Gedankenstriche (—, –) verwenden — stattdessen Kommas, Doppelpunkte oder neue Saetze
|
||||||
|
- Falls das bisherige Briefing Umschreibungen enthält (ae, oe, ue, ss anstelle von ä, ö, ü, ß), ersetze diese beim Aktualisieren durch echte Umlaute. Die Regel "echte UTF-8-Umlaute" hat Vorrang vor der Regel "bestehende Formulierungen beibehalten".
|
||||||
- Neue Erkenntnisse einarbeiten
|
- Neue Erkenntnisse einarbeiten
|
||||||
- Veraltete Informationen aktualisieren
|
- Veraltete Informationen aktualisieren
|
||||||
- Wenn eine Quelle eine erkennbare Ausrichtung hat (z.B. pro-russisch, pro-iranisch, staatsnah, rechtsextrem), muss dies im Fliesstext erwaehnt werden, damit der Leser die Information einordnen kann. Beispiel: "Laut dem pro-russischen Telegram-Kanal Rybar..." oder "Die iranische Nachrichtenagentur Fars meldete..." oder "Der rechtsextreme Kanal Compact behauptete..."
|
- Wenn eine Quelle eine erkennbare Ausrichtung hat (z.B. pro-russisch, pro-iranisch, staatsnah, rechtsextrem), muss dies im Fliesstext erwaehnt werden, damit der Leser die Information einordnen kann. Beispiel: "Laut dem pro-russischen Telegram-Kanal Rybar..." oder "Die iranische Nachrichtenagentur Fars meldete..." oder "Der rechtsextreme Kanal Compact behauptete..."
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Prueft nach jedem Refresh:
|
Prueft nach jedem Refresh:
|
||||||
1. Semantische Faktencheck-Duplikate (Haiku-Clustering mit Fuzzy-Vorfilter)
|
1. Semantische Faktencheck-Duplikate (Haiku-Clustering mit Fuzzy-Vorfilter)
|
||||||
2. Falsch kategorisierte Karten-Locations (Haiku bewertet Kontext der Lage)
|
2. Falsch kategorisierte Karten-Locations (Haiku bewertet Kontext der Lage)
|
||||||
|
3. Umlaut-Normalisierung in summary + latest_developments (deterministisch)
|
||||||
|
|
||||||
Regelbasierte Listen dienen als Fallback falls Haiku fehlschlaegt.
|
Regelbasierte Listen dienen als Fallback falls Haiku fehlschlaegt.
|
||||||
"""
|
"""
|
||||||
@@ -397,19 +398,281 @@ async def run_post_refresh_qc(db, incident_id: int) -> dict:
|
|||||||
locations_fixed = await check_location_categories(
|
locations_fixed = await check_location_categories(
|
||||||
db, incident_id, incident_title, incident_desc
|
db, incident_id, incident_title, incident_desc
|
||||||
)
|
)
|
||||||
|
umlauts_fixed = await normalize_umlaut_fields(db, incident_id)
|
||||||
|
|
||||||
if facts_removed > 0 or locations_fixed > 0:
|
if facts_removed > 0 or locations_fixed > 0 or umlauts_fixed > 0:
|
||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Post-Refresh QC fuer Incident %d: %d Duplikate entfernt, %d Locations korrigiert",
|
"Post-Refresh QC fuer Incident %d: %d Duplikate entfernt, %d Locations korrigiert, %d Umlaute normalisiert",
|
||||||
incident_id, facts_removed, locations_fixed,
|
incident_id, facts_removed, locations_fixed, umlauts_fixed,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"facts_removed": facts_removed, "locations_fixed": locations_fixed}
|
return {
|
||||||
|
"facts_removed": facts_removed,
|
||||||
|
"locations_fixed": locations_fixed,
|
||||||
|
"umlauts_fixed": umlauts_fixed,
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Post-Refresh QC Fehler fuer Incident %d: %s",
|
"Post-Refresh QC Fehler fuer Incident %d: %s",
|
||||||
incident_id, e, exc_info=True,
|
incident_id, e, exc_info=True,
|
||||||
)
|
)
|
||||||
return {"facts_removed": 0, "locations_fixed": 0, "error": str(e)}
|
return {"facts_removed": 0, "locations_fixed": 0, "umlauts_fixed": 0, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Umlaut-Normalisierung (deterministisch, Sicherheitsnetz gegen LLM-Drift)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Basis-Woerterbuch: Umschreibungsform (lowercase) -> Umlaut-Form.
|
||||||
|
# Die Capitalize-Variante wird automatisch generiert (z. B. "Oeffnung" -> "Öffnung").
|
||||||
|
_UMLAUT_BASE = {
|
||||||
|
# Oeffnung / Schliessung / Strasse / Schuesse (Topic-spezifisch aus Lage #6)
|
||||||
|
"oeffnung": "öffnung", "oeffnungen": "öffnungen",
|
||||||
|
"oeffne": "öffne", "oeffnen": "öffnen", "oeffnet": "öffnet",
|
||||||
|
"geoeffnet": "geöffnet", "geoeffnete": "geöffnete", "geoeffneten": "geöffneten",
|
||||||
|
"oeffentlich": "öffentlich", "oeffentlichkeit": "öffentlichkeit",
|
||||||
|
"schliessung": "schließung", "schliessen": "schließen", "schliesst": "schließt",
|
||||||
|
"schliesse": "schließe", "schliesslich": "schließlich",
|
||||||
|
"strasse": "straße", "strassen": "straßen",
|
||||||
|
"schuss": "schuss", "schuesse": "schüsse", "schuessen": "schüssen",
|
||||||
|
"beschuss": "beschuss",
|
||||||
|
# Hilfsverben + haeufige Woerter
|
||||||
|
"fuer": "für", "fuers": "fürs",
|
||||||
|
"ueber": "über", "ueberall": "überall", "ueberfall": "überfall",
|
||||||
|
"moeglich": "möglich", "moegliche": "mögliche", "moeglicher": "möglicher",
|
||||||
|
"moegliches": "mögliches", "moeglichen": "möglichen",
|
||||||
|
"moeglichkeit": "möglichkeit", "moeglichkeiten": "möglichkeiten",
|
||||||
|
"koennen": "können", "koennte": "könnte", "koennten": "könnten",
|
||||||
|
"koenne": "könne", "koennt": "könnt",
|
||||||
|
"muessen": "müssen", "muesse": "müsse", "muessten": "müssten",
|
||||||
|
"muesste": "müsste", "muesst": "müsst",
|
||||||
|
"duerfen": "dürfen", "duerfte": "dürfte", "duerften": "dürften",
|
||||||
|
# fuehr*, waehr*, loes*, erklaer*
|
||||||
|
"fuehren": "führen", "fuehrt": "führt", "fuehrte": "führte", "gefuehrt": "geführt",
|
||||||
|
"fuehrung": "führung", "fuehrungen": "führungen", "fuehrer": "führer",
|
||||||
|
"ausfuehren": "ausführen", "ausgefuehrt": "ausgeführt", "ausfuehrlich": "ausführlich",
|
||||||
|
"einfuehrung": "einführung", "einfuehren": "einführen",
|
||||||
|
"waehrend": "während", "waehrung": "währung", "waehrungen": "währungen",
|
||||||
|
"gewaehren": "gewähren", "gewaehrt": "gewährt", "gewaehrleisten": "gewährleisten",
|
||||||
|
"erwaehnt": "erwähnt", "erwaehnung": "erwähnung", "erwaehnen": "erwähnen",
|
||||||
|
"loesen": "lösen", "loest": "löst", "geloest": "gelöst",
|
||||||
|
"loesung": "lösung", "loesungen": "lösungen",
|
||||||
|
"loeschen": "löschen", "geloescht": "gelöscht",
|
||||||
|
"erklaeren": "erklären", "erklaert": "erklärt", "erklaerte": "erklärte",
|
||||||
|
"erklaerten": "erklärten", "erklaertes": "erklärtes", "erklaertem": "erklärtem",
|
||||||
|
"erklaerung": "erklärung", "erklaerungen": "erklärungen",
|
||||||
|
"verspaetet": "verspätet", "verspaetung": "verspätung", "verspaetungen": "verspätungen",
|
||||||
|
"veroeffentlichen": "veröffentlichen", "veroeffentlicht": "veröffentlicht",
|
||||||
|
"veroeffentlichte": "veröffentlichte", "veroeffentlichten": "veröffentlichten",
|
||||||
|
"veroeffentlichung": "veröffentlichung", "veroeffentlichungen": "veröffentlichungen",
|
||||||
|
# Auslaender, Verstaendnis, Bevoelkerung
|
||||||
|
"auslaender": "ausländer", "auslaendisch": "ausländisch",
|
||||||
|
"auslaendische": "ausländische", "auslaendischen": "ausländischen",
|
||||||
|
"verstaendnis": "verständnis", "verstaendigung": "verständigung",
|
||||||
|
"verstaendlich": "verständlich", "verstaendlicher": "verständlicher",
|
||||||
|
"bevoelkerung": "bevölkerung", "bevoelkerungen": "bevölkerungen",
|
||||||
|
# Aussen, Aeusser, Kuenftig, Grundsaetzlich
|
||||||
|
"aussen": "außen", "aussenminister": "außenminister",
|
||||||
|
"aussenministerin": "außenministerin", "aussenpolitik": "außenpolitik",
|
||||||
|
"aussenpolitisch": "außenpolitisch", "aussenpolitische": "außenpolitische",
|
||||||
|
"aeusserung": "äußerung", "aeusserungen": "äußerungen",
|
||||||
|
"aeussern": "äußern", "aeussert": "äußert", "aeusserte": "äußerte",
|
||||||
|
"aeusserst": "äußerst", "aeussere": "äußere", "aeusseren": "äußeren",
|
||||||
|
"kuenftig": "künftig", "kuenftige": "künftige", "kuenftigen": "künftigen",
|
||||||
|
"grundsaetzlich": "grundsätzlich", "grundsaetze": "grundsätze", "grundsatz": "grundsatz",
|
||||||
|
# Maenner, Aerzte, Aendern, Entsteh*
|
||||||
|
"maenner": "männer", "maennlich": "männlich", "maennliche": "männliche",
|
||||||
|
"aerzte": "ärzte", "aerztlich": "ärztlich", "aerztliche": "ärztliche",
|
||||||
|
"aendern": "ändern", "aenderung": "änderung", "aenderungen": "änderungen",
|
||||||
|
"geaendert": "geändert", "unveraendert": "unverändert",
|
||||||
|
"verstaerken": "verstärken", "verstaerkt": "verstärkt", "verstaerkung": "verstärkung",
|
||||||
|
# Gruend*, Rueck*, Zurueck*
|
||||||
|
"gruenden": "gründen", "gruendung": "gründung", "gegruendet": "gegründet",
|
||||||
|
"gruende": "gründe", "begruendung": "begründung", "begruendet": "begründet",
|
||||||
|
"rueckkehr": "rückkehr", "rueckzug": "rückzug", "ruecken": "rücken",
|
||||||
|
"rueckseite": "rückseite", "rueckfall": "rückfall",
|
||||||
|
"zuruecknahme": "zurücknahme", "zurueck": "zurück", "zuruecknehmen": "zurücknehmen",
|
||||||
|
"zurueckgezogen": "zurückgezogen", "zurueckgekehrt": "zurückgekehrt",
|
||||||
|
# Fluege, Behoerden, Buerg*
|
||||||
|
"fluege": "flüge", "fluegel": "flügel",
|
||||||
|
"ruestung": "rüstung", "ruestungen": "rüstungen", "ruestungsexport": "rüstungsexport",
|
||||||
|
"aufruestung": "aufrüstung", "abruestung": "abrüstung",
|
||||||
|
"anschliessen": "anschließen", "anschliesst": "anschließt",
|
||||||
|
"angeschlossen": "angeschlossen",
|
||||||
|
"entschliessen": "entschließen", "entschliesst": "entschließt",
|
||||||
|
"entschloss": "entschloss", "entschlossen": "entschlossen",
|
||||||
|
"entschliessung": "entschließung",
|
||||||
|
"beschliessen": "beschließen", "beschliesst": "beschließt",
|
||||||
|
"beschloss": "beschloss", "beschlossen": "beschlossen", "beschluss": "beschluss",
|
||||||
|
"ausschliessen": "ausschließen", "ausschliesst": "ausschließt",
|
||||||
|
"ausgeschlossen": "ausgeschlossen", "ausschluss": "ausschluss",
|
||||||
|
"erfuellen": "erfüllen", "erfuellt": "erfüllt", "erfuellung": "erfüllung",
|
||||||
|
"durchfuehren": "durchführen", "durchgefuehrt": "durchgeführt",
|
||||||
|
"durchfuehrung": "durchführung",
|
||||||
|
"zurueckfuehren": "zurückführen", "zurueckgefuehrt": "zurückgeführt",
|
||||||
|
"zurueckziehen": "zurückziehen", "zurueckgezogen": "zurückgezogen",
|
||||||
|
"zurueckgewiesen": "zurückgewiesen",
|
||||||
|
"behoerde": "behörde", "behoerden": "behörden", "behoerdlich": "behördlich",
|
||||||
|
"buerger": "bürger", "buergerlich": "bürgerlich", "buergermeister": "bürgermeister",
|
||||||
|
"buergerrechte": "bürgerrechte",
|
||||||
|
"tuerkei": "türkei", "tuerkisch": "türkisch", "tuerkische": "türkische",
|
||||||
|
"tuerkischen": "türkischen", "tuerke": "türke", "tuerken": "türken",
|
||||||
|
# Staedtenamen (Spezialfaelle)
|
||||||
|
"koeln": "köln", "koelner": "kölner",
|
||||||
|
"muenchen": "münchen", "muenchner": "münchner",
|
||||||
|
# Thema aktueller Lage: Praesident, Druecker, etc.
|
||||||
|
"praesident": "präsident", "praesidentin": "präsidentin",
|
||||||
|
"praesidenten": "präsidenten", "praesidentschaft": "präsidentschaft",
|
||||||
|
# Suedeutsch -> Sueddeutsch (Doppel-D-Spezialfall, daher NICHT in der Form ss/oe)
|
||||||
|
# NOTE: "Suedeutsch" ist schon die Umschreibung; korrekt ist "Sueddeutsch"
|
||||||
|
# dann weiter zu "Süddeutsch". Wir loesen nur die ue->ü-Wandlung:
|
||||||
|
"suedeutsch": "süddeutsch", "suedeutsche": "süddeutsche",
|
||||||
|
"suedeutschen": "süddeutschen",
|
||||||
|
"sueddeutsch": "süddeutsch", "sueddeutsche": "süddeutsche",
|
||||||
|
"sueddeutschen": "süddeutschen",
|
||||||
|
"sued": "süd", "sueden": "süden", "suedlich": "südlich",
|
||||||
|
"suedost": "südost", "suedwest": "südwest",
|
||||||
|
"suedkorea": "südkorea", "suedkoreas": "südkoreas", "suedkoreanisch": "südkoreanisch",
|
||||||
|
# Bestaetigen, Gespraech, Toetung, Sekretaer, Franzoesisch
|
||||||
|
"bestaetigen": "bestätigen", "bestaetigt": "bestätigt",
|
||||||
|
"bestaetigte": "bestätigte", "bestaetigten": "bestätigten",
|
||||||
|
"bestaetigung": "bestätigung", "bestaetigungen": "bestätigungen",
|
||||||
|
"gespraech": "gespräch", "gespraeche": "gespräche",
|
||||||
|
"gespraechen": "gesprächen", "gespraechs": "gesprächs",
|
||||||
|
"toetung": "tötung", "toetungen": "tötungen",
|
||||||
|
"toeten": "töten", "toetet": "tötet", "getoetet": "getötet",
|
||||||
|
"sekretaer": "sekretär", "sekretaerin": "sekretärin",
|
||||||
|
"generalsekretaer": "generalsekretär", "generalsekretaerin": "generalsekretärin",
|
||||||
|
"franzoesisch": "französisch", "franzoesische": "französische",
|
||||||
|
"franzoesischen": "französischen", "franzoesischer": "französischer",
|
||||||
|
"franzoesisches": "französisches",
|
||||||
|
# Sanitaeter, Stationaer, Militaer, Maerz, Vollstaendig
|
||||||
|
"sanitaet": "sanität", "sanitaeter": "sanitäter",
|
||||||
|
"stationaer": "stationär", "stationaere": "stationäre", "stationaeren": "stationären",
|
||||||
|
"militaer": "militär", "militaerisch": "militärisch",
|
||||||
|
"militaerische": "militärische", "militaerischen": "militärischen",
|
||||||
|
"militaerkommando": "militärkommando", "militaerbasis": "militärbasis",
|
||||||
|
"militaerschlag": "militärschlag", "militaerschlaege": "militärschläge",
|
||||||
|
"maerz": "märz",
|
||||||
|
"vollstaendig": "vollständig", "vollstaendige": "vollständige",
|
||||||
|
"vollstaendigen": "vollständigen", "vollstaendigkeit": "vollständigkeit",
|
||||||
|
# Gegenueber, Abhaengig, Zehntaegig, Geloest, Enthuellt, Wuerden, Weiss
|
||||||
|
"gegenueber": "gegenüber",
|
||||||
|
"abhaengig": "abhängig", "abhaengige": "abhängige", "abhaengigen": "abhängigen",
|
||||||
|
"abhaengigkeit": "abhängigkeit", "unabhaengig": "unabhängig",
|
||||||
|
"taeglich": "täglich", "taegliche": "tägliche", "taeglichen": "täglichen",
|
||||||
|
"taegig": "tägig", "zehntaegig": "zehntägig", "zehntaegige": "zehntägige",
|
||||||
|
"geloest": "gelöst", "geloeste": "gelöste", "geloesten": "gelösten",
|
||||||
|
"enthuellen": "enthüllen", "enthuellt": "enthüllt",
|
||||||
|
"enthuellte": "enthüllte", "enthuellung": "enthüllung",
|
||||||
|
"wuerde": "würde", "wuerden": "würden", "wuerdig": "würdig",
|
||||||
|
"wuerdigung": "würdigung",
|
||||||
|
"weiss": "weiß", "weisse": "weiße", "weissen": "weißen", "weisses": "weißes",
|
||||||
|
# Vize- und Parlamentspraesident, Ankuendigung, Knuepfen
|
||||||
|
"vizepraesident": "vizepräsident", "vizepraesidentin": "vizepräsidentin",
|
||||||
|
"vizepraesidenten": "vizepräsidenten",
|
||||||
|
"parlamentspraesident": "parlamentspräsident",
|
||||||
|
"ankuendigung": "ankündigung", "ankuendigungen": "ankündigungen",
|
||||||
|
"ankuendigen": "ankündigen", "angekuendigt": "angekündigt",
|
||||||
|
"knuepfen": "knüpfen", "knuepft": "knüpft", "geknuepft": "geknüpft",
|
||||||
|
"anknuepfen": "anknüpfen",
|
||||||
|
# Komposita mit Strasse, Oeffnung
|
||||||
|
"wasserstrasse": "wasserstraße", "wasserstrassen": "wasserstraßen",
|
||||||
|
"autostrasse": "autostraße", "hauptstrasse": "hauptstraße",
|
||||||
|
# Weitere zusammengesetzte Formen
|
||||||
|
"zugefuehrt": "zugeführt", "ueberfuehrt": "überführt",
|
||||||
|
"hergefuehrt": "hergeführt", "hingefuehrt": "hingeführt",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_umlaut_map() -> dict:
|
||||||
|
"""Baut die vollstaendige Ersetzungs-Map inklusive Capitalize-Varianten."""
|
||||||
|
result = {}
|
||||||
|
for k, v in _UMLAUT_BASE.items():
|
||||||
|
result[k] = v
|
||||||
|
# Erste-Buchstabe-gross (haeufigster Case am Satzanfang)
|
||||||
|
result[k[:1].upper() + k[1:]] = v[:1].upper() + v[1:]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
_UMLAUT_REPLACEMENTS = _build_umlaut_map()
|
||||||
|
|
||||||
|
# Whitelist: Tokens, die trotz potenziellen Matches NIE ersetzt werden.
|
||||||
|
# Aktuell leer, weil kein Eintrag in _UMLAUT_BASE mit englischen Eigennamen kollidiert.
|
||||||
|
# Falls in Zukunft Ambiguitaeten auftreten (z. B. Nachname "Kuehn"), hier ergaenzen.
|
||||||
|
_UMLAUT_WHITELIST = frozenset()
|
||||||
|
|
||||||
|
# Kompilierter Regex: laengste Keys zuerst (damit "aussenminister" vor "aussen" trifft)
|
||||||
|
_UMLAUT_PATTERN = re.compile(
|
||||||
|
r"\b(" + "|".join(
|
||||||
|
re.escape(k) for k in sorted(_UMLAUT_REPLACEMENTS.keys(), key=len, reverse=True)
|
||||||
|
) + r")\b"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_german_umlauts(text: str) -> tuple[str, int]:
|
||||||
|
"""Ersetzt typische deutsche Umschreibungen durch echte Umlaute.
|
||||||
|
|
||||||
|
Deterministisch, wortgrenzen-basiert, case-preserving. Sicher gegen
|
||||||
|
englische Wortbestandteile (Boeing, Business, Access) weil nur
|
||||||
|
explizit gelistete deutsche Woerter ersetzt werden.
|
||||||
|
|
||||||
|
Rueckgabe: (normalisierter_text, anzahl_ersetzungen)
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text, 0
|
||||||
|
count = [0]
|
||||||
|
|
||||||
|
def _replace(match: re.Match) -> str:
|
||||||
|
word = match.group(0)
|
||||||
|
if word in _UMLAUT_WHITELIST:
|
||||||
|
return word
|
||||||
|
replacement = _UMLAUT_REPLACEMENTS.get(word)
|
||||||
|
if replacement is None:
|
||||||
|
return word
|
||||||
|
count[0] += 1
|
||||||
|
return replacement
|
||||||
|
|
||||||
|
new_text = _UMLAUT_PATTERN.sub(_replace, text)
|
||||||
|
return new_text, count[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def normalize_umlaut_fields(db, incident_id: int) -> int:
|
||||||
|
"""Liest summary + latest_developments eines Incidents, normalisiert Umlaute,
|
||||||
|
schreibt bei tatsaechlichen Aenderungen zurueck.
|
||||||
|
|
||||||
|
Rueckgabe: Anzahl der Ersetzungen insgesamt (summary + latest_developments).
|
||||||
|
"""
|
||||||
|
cursor = await db.execute(
|
||||||
|
"SELECT summary, latest_developments FROM incidents WHERE id = ?",
|
||||||
|
(incident_id,),
|
||||||
|
)
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
orig_summary = row["summary"] or ""
|
||||||
|
orig_dev = row["latest_developments"] or ""
|
||||||
|
|
||||||
|
new_summary, count_summary = normalize_german_umlauts(orig_summary)
|
||||||
|
new_dev, count_dev = normalize_german_umlauts(orig_dev)
|
||||||
|
|
||||||
|
total = count_summary + count_dev
|
||||||
|
if total == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE incidents SET summary = ?, latest_developments = ? WHERE id = ?",
|
||||||
|
(
|
||||||
|
new_summary if count_summary > 0 else orig_summary,
|
||||||
|
new_dev if count_dev > 0 else orig_dev,
|
||||||
|
incident_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Umlaut-Normalisierung Incident %d: %d in summary, %d in latest_developments",
|
||||||
|
incident_id, count_summary, count_dev,
|
||||||
|
)
|
||||||
|
return total
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren