phonetic_layer.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. # -*- coding: utf-8 -*-
  2. """
  3. Phonetische Korrekturschicht.
  4. Nutzt Koelner Phonetik um phonetisch aehnliche Woerter zu erkennen
  5. und Domain-Vokabular (Kuenstlernamen, Befehle) zu schuetzen.
  6. """
  7. from trixy_core.stt.layers.base import CorrectionLayer
  8. from trixy_core.utils.debug import pdebug, pwarn
  9. try:
  10. import cologne_phonetics
  11. _HAS_COLOGNE = True
  12. except ImportError:
  13. _HAS_COLOGNE = False
  14. class PhoneticLayer(CorrectionLayer):
  15. """Koelner-Phonetik-basierte Korrektur fuer Domain-Vokabular."""
  16. NAME = "phonetic"
  17. def __init__(self) -> None:
  18. # phonetischer Code → bevorzugtes Wort
  19. self._phonetic_index: dict[str, str] = {}
  20. self._protected_words: set[str] = set()
  21. def is_available(self) -> bool:
  22. """Prueft ob cologne_phonetics installiert ist."""
  23. return _HAS_COLOGNE
  24. def initialize(self, config: dict, language: str, protected_words: list[str]) -> bool:
  25. """Baut phonetischen Index aus geschuetzten Woertern auf."""
  26. if not _HAS_COLOGNE:
  27. return False
  28. try:
  29. self._protected_words = {w.lower() for w in protected_words}
  30. # Phonetischen Index aufbauen
  31. for word in protected_words:
  32. codes = cologne_phonetics.encode(word)
  33. if codes:
  34. # cologne_phonetics.encode gibt Liste von (wort, code) Tupeln
  35. for _, code in codes:
  36. if code:
  37. self._phonetic_index[code] = word.lower()
  38. pdebug(f"[Phonetic] Index aufgebaut mit {len(self._phonetic_index)} Eintraegen")
  39. return True
  40. except Exception as e:
  41. pwarn(f"[Phonetic] Initialisierung fehlgeschlagen: {e}")
  42. return False
  43. def correct(self, text: str) -> str:
  44. """Ersetzt Woerter die phonetisch einem geschuetzten Wort entsprechen."""
  45. if not text.strip():
  46. return text
  47. try:
  48. words = text.split()
  49. corrected_words = []
  50. changed = False
  51. for word in words:
  52. replacement = self._find_phonetic_match(word)
  53. if replacement and replacement != word.lower():
  54. corrected_words.append(replacement)
  55. changed = True
  56. else:
  57. corrected_words.append(word)
  58. if changed:
  59. result = " ".join(corrected_words)
  60. pdebug(f"[Phonetic] '{text}' → '{result}'")
  61. return result
  62. return text
  63. except Exception as e:
  64. pdebug(f"[Phonetic] Fehler bei Korrektur: {e}")
  65. return text
  66. def _find_phonetic_match(self, word: str) -> str | None:
  67. """Sucht phonetische Uebereinstimmung fuer ein Wort."""
  68. if word.lower() in self._protected_words:
  69. return word.lower()
  70. try:
  71. codes = cologne_phonetics.encode(word)
  72. if not codes:
  73. return None
  74. for _, code in codes:
  75. if code and code in self._phonetic_index:
  76. candidate = self._phonetic_index[code]
  77. # Nur ersetzen wenn Edit-Distanz klein genug
  78. if self._edit_distance(word.lower(), candidate) <= 3:
  79. return candidate
  80. return None
  81. except Exception:
  82. return None
  83. @staticmethod
  84. def _edit_distance(s1: str, s2: str) -> int:
  85. """Berechnet Levenshtein-Distanz zwischen zwei Strings."""
  86. if len(s1) < len(s2):
  87. return PhoneticLayer._edit_distance(s2, s1)
  88. if len(s2) == 0:
  89. return len(s1)
  90. prev_row = range(len(s2) + 1)
  91. for i, c1 in enumerate(s1):
  92. curr_row = [i + 1]
  93. for j, c2 in enumerate(s2):
  94. # Einfuegen, Loeschen, Ersetzen
  95. insertions = prev_row[j + 1] + 1
  96. deletions = curr_row[j] + 1
  97. substitutions = prev_row[j] + (c1 != c2)
  98. curr_row.append(min(insertions, deletions, substitutions))
  99. prev_row = curr_row
  100. return prev_row[-1]