| """ |
| Voice Narration for Incident Alerts |
| Supports multiple TTS providers with graceful fallback |
| """ |
|
|
| import os |
| import requests |
| import logging |
| from typing import Optional, Dict, Any |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class VoiceNarrator: |
| """ |
| Narrate critical incidents using TTS API |
| |
| Supports: |
| - Hathora Voice API (REST) |
| - Generic TTS APIs |
| - Graceful fallback (silent fail) |
| """ |
| |
| def __init__(self): |
| |
| self.api_key = os.environ.get("HATHORA_VOICE_API_KEY", "") or \ |
| os.environ.get("VOICE_API_KEY", "") |
| |
| |
| self.api_endpoint = os.environ.get( |
| "VOICE_API_ENDPOINT", |
| "https://api.hathora.dev/v1/tts" |
| ) |
| |
| self.enabled = bool(self.api_key) |
| |
| if self.enabled: |
| logger.info("✅ Voice narrator initialized with API key") |
| else: |
| logger.warning("⚠️ Voice narrator disabled (no API key found)") |
| |
| def narrate_incident( |
| self, |
| component: str, |
| severity: str, |
| latency: float, |
| error_rate: float, |
| root_cause: str = "Unknown", |
| recovery_action: str = "Investigating" |
| ) -> Optional[str]: |
| """ |
| Generate voice narration for a critical incident |
| |
| Returns: |
| Audio URL (str) if successful, None if failed |
| """ |
| |
| if not self.enabled: |
| logger.debug("Voice narration skipped (disabled)") |
| return None |
| |
| |
| if severity not in ["HIGH", "CRITICAL"]: |
| return None |
| |
| try: |
| |
| narration_text = self._build_narration( |
| component, severity, latency, error_rate, root_cause, recovery_action |
| ) |
| |
| |
| audio_url = self._call_tts_api(narration_text) |
| |
| if audio_url: |
| logger.info(f"✅ Generated voice narration for {component}") |
| return audio_url |
| else: |
| logger.warning("Voice API returned no audio URL") |
| return None |
| |
| except Exception as e: |
| |
| logger.error(f"Voice narration failed: {e}", exc_info=True) |
| return None |
| |
| def _build_narration( |
| self, |
| component: str, |
| severity: str, |
| latency: float, |
| error_rate: float, |
| root_cause: str, |
| recovery_action: str |
| ) -> str: |
| """Build dramatic narration text""" |
| |
| |
| component_spoken = component.replace("-", " ").title() |
| |
| |
| if severity == "CRITICAL": |
| intro = f"Critical alert. {component_spoken} is experiencing severe failure." |
| else: |
| intro = f"High priority alert. {component_spoken} degradation detected." |
| |
| |
| metrics = f"Latency: {int(latency)} milliseconds. Error rate: {error_rate*100:.0f} percent." |
| |
| |
| if root_cause and root_cause != "Unknown": |
| cause = f"Root cause: {root_cause}." |
| else: |
| cause = "Root cause under investigation." |
| |
| |
| action = f"Recovery action: {recovery_action}." |
| |
| |
| full_text = f"{intro} {metrics} {cause} {action} Immediate attention required." |
| |
| logger.debug(f"Narration text: {full_text[:100]}...") |
| return full_text |
| |
| def _call_tts_api(self, text: str) -> Optional[str]: |
| """ |
| Call TTS API to generate audio |
| |
| GENERIC implementation - adapt to actual Hathora Voice API format |
| """ |
| |
| try: |
| |
| response = requests.post( |
| self.api_endpoint, |
| headers={ |
| "Authorization": f"Bearer {self.api_key}", |
| "Content-Type": "application/json" |
| }, |
| json={ |
| "text": text, |
| "voice": "en-US-neural", |
| "speed": 1.0, |
| "format": "mp3" |
| }, |
| timeout=10.0 |
| ) |
| |
| if response.status_code == 200: |
| data = response.json() |
| |
| |
| audio_url = data.get("audio_url") or \ |
| data.get("url") or \ |
| data.get("audioUrl") |
| |
| return audio_url |
| else: |
| logger.error(f"TTS API error: {response.status_code} - {response.text[:200]}") |
| return None |
| |
| except requests.exceptions.Timeout: |
| logger.warning("TTS API timeout") |
| return None |
| except Exception as e: |
| logger.error(f"TTS API call failed: {e}") |
| return None |
|
|
|
|
| |
| _narrator = None |
|
|
| def get_narrator() -> VoiceNarrator: |
| """Get global narrator instance""" |
| global _narrator |
| if _narrator is None: |
| _narrator = VoiceNarrator() |
| return _narrator |
|
|
|
|
| |
| VoiceHandler = VoiceNarrator |
| get_voice_handler = get_narrator |