| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- # -*- coding: utf-8 -*-
- """
- KenLM-Korrekturschicht.
- Nutzt N-Gram Sprachmodell fuer kontextuelle Kandidatenauswahl.
- Sehr schnell (<1ms/Query).
- """
- from pathlib import Path
- from trixy_core.stt.layers.base import CorrectionLayer
- from trixy_core.utils.debug import pdebug, pwarn
- try:
- import kenlm
- _HAS_KENLM = True
- except ImportError:
- _HAS_KENLM = False
- class KenLMLayer(CorrectionLayer):
- """KenLM N-Gram basierte kontextuelle Korrektur."""
- NAME = "kenlm"
- def __init__(self) -> None:
- self._model: "kenlm.Model | None" = None
- self._protected_words: set[str] = set()
- def is_available(self) -> bool:
- """Prueft ob kenlm installiert ist."""
- return _HAS_KENLM
- def initialize(self, config: dict, language: str, protected_words: list[str]) -> bool:
- """Laedt N-Gram Modell aus konfiguriertem Pfad."""
- if not _HAS_KENLM:
- return False
- model_path = config.get("kenlm_model_path", "")
- if not model_path or not Path(model_path).exists():
- pdebug(f"[KenLM] Modell nicht gefunden: {model_path}")
- return False
- try:
- self._model = kenlm.Model(str(model_path))
- self._protected_words = {w.lower() for w in protected_words}
- pdebug(f"[KenLM] Modell geladen: {model_path} (order={self._model.order})")
- return True
- except Exception as e:
- pwarn(f"[KenLM] Modell-Laden fehlgeschlagen: {e}")
- self._model = None
- return False
- def correct(self, text: str) -> str:
- """Bewertet Text und waehlt beste Kandidaten per N-Gram Score."""
- if not self._model or not text.strip():
- return text
- try:
- words = text.lower().split()
- if len(words) < 2:
- return text
- # Score des Original-Satzes
- original_score = self._model.score(text.lower(), bos=True, eos=True)
- # Kandidaten generieren: pro Wort Edit-Distanz-1 Varianten
- best_text = text.lower()
- best_score = original_score
- for i, word in enumerate(words):
- if word in self._protected_words:
- continue
- candidates = self._generate_candidates(word)
- for candidate in candidates:
- test_words = words.copy()
- test_words[i] = candidate
- test_text = " ".join(test_words)
- score = self._model.score(test_text, bos=True, eos=True)
- if score > best_score:
- best_score = score
- best_text = test_text
- if best_text != text.lower():
- pdebug(f"[KenLM] '{text}' → '{best_text}' (score: {original_score:.2f} → {best_score:.2f})")
- return best_text
- return text
- except Exception as e:
- pdebug(f"[KenLM] Fehler bei Korrektur: {e}")
- return text
- def _generate_candidates(self, word: str) -> list[str]:
- """Generiert Edit-Distanz-1 Kandidaten fuer ein Wort."""
- candidates = set()
- letters = "abcdefghijklmnopqrstuvwxyzäöüß"
- # Loeschungen
- for i in range(len(word)):
- candidates.add(word[:i] + word[i + 1:])
- # Transpositionen
- for i in range(len(word) - 1):
- candidates.add(word[:i] + word[i + 1] + word[i] + word[i + 2:])
- # Ersetzungen
- for i in range(len(word)):
- for c in letters:
- if c != word[i]:
- candidates.add(word[:i] + c + word[i + 1:])
- # Einfuegungen
- for i in range(len(word) + 1):
- for c in letters:
- candidates.add(word[:i] + c + word[i:])
- # Leere und zu kurze entfernen
- candidates.discard("")
- candidates.discard(word)
- return list(candidates)
- def shutdown(self) -> None:
- """Gibt Ressourcen frei."""
- self._model = None
|