candidate.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. # -*- coding: utf-8 -*-
  2. """
  3. Arbitration-Kandidat Datenklasse.
  4. Repräsentiert einen Satellite, der ein Wakeword erkannt hat
  5. und für die Konversation in Betracht gezogen wird.
  6. """
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from enum import Enum
  10. from typing import Any
  11. class CandidateState(Enum):
  12. """Zustand eines Arbitration-Kandidaten."""
  13. PENDING = "pending" # Wartet auf Entscheidung
  14. SELECTED = "selected" # Wurde ausgewählt
  15. REJECTED = "rejected" # Wurde abgelehnt
  16. TIMEOUT = "timeout" # Zeitüberschreitung
  17. @dataclass
  18. class ArbitrationCandidate:
  19. """
  20. Ein Kandidat für die Wakeword-Arbitration.
  21. Repräsentiert einen Satellite, der ein Wakeword erkannt hat.
  22. Enthält alle relevanten Daten für die Auswahlentscheidung.
  23. """
  24. # Identifikation
  25. satellite_id: str
  26. room_id: str = ""
  27. alias: str = ""
  28. # Wakeword-Daten
  29. wakeword_model: str = ""
  30. confidence: float = 0.0
  31. audio_level: float = 0.0
  32. buffer_samples: int = 0
  33. # Zeitstempel
  34. detected_at: datetime = field(default_factory=datetime.now)
  35. # Zustand
  36. state: CandidateState = CandidateState.PENDING
  37. # Forcierte Auswahl (z.B. durch HID-Taste — ueberspringt Arbitration-Fenster)
  38. forced: bool = False
  39. # Zusätzliche Metadaten für erweiterte Strategien
  40. metadata: dict[str, Any] = field(default_factory=dict)
  41. # Berechneter Score (wird von Strategy gesetzt)
  42. score: float = 0.0
  43. score_details: dict[str, float] = field(default_factory=dict)
  44. def __post_init__(self) -> None:
  45. """Validierung nach Initialisierung."""
  46. if not self.satellite_id:
  47. raise ValueError("satellite_id ist erforderlich")
  48. # Normalisiere audio_level und confidence auf 0.0-1.0
  49. self.audio_level = max(0.0, min(1.0, self.audio_level))
  50. self.confidence = max(0.0, min(1.0, self.confidence))
  51. @property
  52. def is_pending(self) -> bool:
  53. """Prüft ob Kandidat noch auf Entscheidung wartet."""
  54. return self.state == CandidateState.PENDING
  55. @property
  56. def is_selected(self) -> bool:
  57. """Prüft ob Kandidat ausgewählt wurde."""
  58. return self.state == CandidateState.SELECTED
  59. @property
  60. def is_rejected(self) -> bool:
  61. """Prüft ob Kandidat abgelehnt wurde."""
  62. return self.state == CandidateState.REJECTED
  63. @property
  64. def age_ms(self) -> float:
  65. """Alter des Kandidaten in Millisekunden."""
  66. delta = datetime.now() - self.detected_at
  67. return delta.total_seconds() * 1000
  68. def select(self) -> None:
  69. """Markiert Kandidaten als ausgewählt."""
  70. self.state = CandidateState.SELECTED
  71. def reject(self, reason: str = "") -> None:
  72. """Markiert Kandidaten als abgelehnt."""
  73. self.state = CandidateState.REJECTED
  74. if reason:
  75. self.metadata["reject_reason"] = reason
  76. def to_dict(self) -> dict[str, Any]:
  77. """Konvertiert zu Dictionary."""
  78. return {
  79. "satellite_id": self.satellite_id,
  80. "room_id": self.room_id,
  81. "alias": self.alias,
  82. "wakeword_model": self.wakeword_model,
  83. "confidence": self.confidence,
  84. "audio_level": self.audio_level,
  85. "buffer_samples": self.buffer_samples,
  86. "detected_at": self.detected_at.isoformat(),
  87. "state": self.state.value,
  88. "score": self.score,
  89. "score_details": self.score_details,
  90. "metadata": self.metadata,
  91. }
  92. def __repr__(self) -> str:
  93. return (
  94. f"ArbitrationCandidate("
  95. f"satellite_id={self.satellite_id!r}, "
  96. f"audio_level={self.audio_level:.2f}, "
  97. f"confidence={self.confidence:.2f}, "
  98. f"score={self.score:.2f}, "
  99. f"state={self.state.value})"
  100. )