combined.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. # -*- coding: utf-8 -*-
  2. """
  3. Combined-Strategie für Arbitration.
  4. Kombiniert mehrere Faktoren (Audio-Level, Confidence, etc.)
  5. zu einem gewichteten Score.
  6. """
  7. from dataclasses import dataclass
  8. from typing import Any
  9. from trixy_core.arbitration.candidate import ArbitrationCandidate
  10. from trixy_core.arbitration.strategies.base import ArbitrationStrategy
  11. @dataclass
  12. class CombinedWeights:
  13. """Gewichtungen für die Combined-Strategie."""
  14. audio_level: float = 0.6 # Audio-Level Gewichtung
  15. confidence: float = 0.3 # Confidence Gewichtung
  16. recency: float = 0.1 # Bonus für frühe Erkennung
  17. def __post_init__(self) -> None:
  18. """Validierung der Gewichtungen."""
  19. total = self.audio_level + self.confidence + self.recency
  20. if abs(total - 1.0) > 0.01:
  21. # Normalisieren wenn Summe != 1.0
  22. self.audio_level /= total
  23. self.confidence /= total
  24. self.recency /= total
  25. class CombinedStrategy(ArbitrationStrategy):
  26. """
  27. Strategie: Gewichtete Kombination mehrerer Faktoren.
  28. Kombiniert Audio-Level, Confidence und Reaktionszeit
  29. zu einem Gesamtscore. Die Gewichtungen sind konfigurierbar.
  30. Beispiel:
  31. ```python
  32. # Standard-Gewichtung (60% Audio, 30% Confidence, 10% Zeit)
  33. strategy = CombinedStrategy()
  34. # Benutzerdefinierte Gewichtung
  35. weights = CombinedWeights(audio_level=0.5, confidence=0.4, recency=0.1)
  36. strategy = CombinedStrategy(weights=weights)
  37. candidates = [
  38. ArbitrationCandidate(
  39. satellite_id="kitchen",
  40. audio_level=0.7,
  41. confidence=0.95,
  42. ),
  43. ArbitrationCandidate(
  44. satellite_id="living_room",
  45. audio_level=0.85,
  46. confidence=0.85,
  47. ),
  48. ]
  49. winner = strategy.select(candidates)
  50. ```
  51. """
  52. def __init__(
  53. self,
  54. weights: CombinedWeights | None = None,
  55. min_audio_level: float = 0.1,
  56. min_confidence: float = 0.5,
  57. ) -> None:
  58. """
  59. Initialisiert die Strategie.
  60. Args:
  61. weights: Gewichtungen für Score-Berechnung
  62. min_audio_level: Minimales Audio-Level
  63. min_confidence: Minimale Confidence
  64. """
  65. self.weights = weights or CombinedWeights()
  66. self.min_audio_level = min_audio_level
  67. self.min_confidence = min_confidence
  68. @property
  69. def name(self) -> str:
  70. return "combined"
  71. @property
  72. def description(self) -> str:
  73. return (
  74. f"Kombinierte Bewertung: "
  75. f"{self.weights.audio_level:.0%} Audio, "
  76. f"{self.weights.confidence:.0%} Confidence, "
  77. f"{self.weights.recency:.0%} Zeit"
  78. )
  79. def calculate_score(
  80. self,
  81. candidate: ArbitrationCandidate,
  82. context: dict[str, Any] | None = None,
  83. ) -> float:
  84. """
  85. Berechnet gewichteten Gesamtscore.
  86. Args:
  87. candidate: Der Kandidat
  88. context: Optionaler Kontext mit:
  89. - window_duration_ms: Fensterdauer für Recency-Berechnung
  90. Returns:
  91. Gewichteter Score (0.0-1.0)
  92. """
  93. context = context or {}
  94. # Audio-Level Komponente
  95. audio_score = candidate.audio_level
  96. # Confidence Komponente
  97. confidence_score = candidate.confidence
  98. # Recency Komponente (früher = besser)
  99. # Normalisiert auf Fensterdauer
  100. window_duration = context.get("window_duration_ms", 1000)
  101. age_normalized = min(1.0, candidate.age_ms / window_duration)
  102. recency_score = 1.0 - age_normalized # Früher = höherer Score
  103. # Gewichteter Score
  104. score = (
  105. audio_score * self.weights.audio_level +
  106. confidence_score * self.weights.confidence +
  107. recency_score * self.weights.recency
  108. )
  109. # Score-Details speichern
  110. candidate.score_details = {
  111. "audio_level": audio_score,
  112. "audio_weighted": audio_score * self.weights.audio_level,
  113. "confidence": confidence_score,
  114. "confidence_weighted": confidence_score * self.weights.confidence,
  115. "recency": recency_score,
  116. "recency_weighted": recency_score * self.weights.recency,
  117. }
  118. return score
  119. def select(
  120. self,
  121. candidates: list[ArbitrationCandidate],
  122. context: dict[str, Any] | None = None,
  123. ) -> ArbitrationCandidate | None:
  124. """
  125. Wählt Kandidaten mit höchstem kombinierten Score.
  126. Args:
  127. candidates: Liste der Kandidaten
  128. context: Optionaler Kontext
  129. Returns:
  130. Kandidat mit höchstem Score oder None
  131. """
  132. valid = self.filter_valid(candidates)
  133. if not valid:
  134. return None
  135. # Scores berechnen
  136. for candidate in valid:
  137. candidate.score = self.calculate_score(candidate, context)
  138. # Höchsten Score auswählen
  139. winner = max(valid, key=lambda c: c.score)
  140. return winner
  141. def validate_candidate(self, candidate: ArbitrationCandidate) -> bool:
  142. """Prüft Audio-Level und Confidence."""
  143. base_valid = super().validate_candidate(candidate)
  144. return (
  145. base_valid and
  146. candidate.audio_level >= self.min_audio_level and
  147. candidate.confidence >= self.min_confidence
  148. )