""" API routes for voice-server. Defines all HTTP endpoints: - POST /notify: Submit text for TTS playback - GET /health: Health check endpoint - GET /voices: List available voice models """ from fastapi import APIRouter, HTTPException, Response, status from app.config import get_settings from app.models import ( ErrorResponse, HealthResponse, NotifyRequest, NotifyResponse, QueueStatus, VoiceInfo, VoicesResponse, ) from app.queue_manager import QueueFullError, TTSRequest router = APIRouter() def _get_components(): """Get the TTS components from main module.""" from app import main return main.tts_engine, main.audio_player, main.queue_manager, main.get_uptime_seconds @router.post( "/notify", response_model=NotifyResponse, status_code=status.HTTP_202_ACCEPTED, responses={ 422: {"model": ErrorResponse, "description": "Validation error"}, 503: {"model": ErrorResponse, "description": "Queue full"}, }, ) async def notify(request: NotifyRequest) -> NotifyResponse: """ Submit text for TTS playback. Accepts a text message and queues it for text-to-speech conversion and playback through the system speakers. Returns immediately with queue position; audio plays asynchronously. """ tts_engine, audio_player, queue_manager, _ = _get_components() settings = get_settings() # Validate voice exists if specified voice = request.voice or settings.default_voice if tts_engine and not tts_engine.is_voice_available(voice): available = [v["name"] for v in tts_engine.list_voices()] raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Voice '{voice}' not found. Available: {available}", ) # Create TTS request tts_request = TTSRequest( message=request.message, voice=voice, rate=request.rate, voice_enabled=request.voice_enabled and settings.voice_enabled, ) # Enqueue request try: if queue_manager: position = await queue_manager.enqueue(tts_request) else: position = 1 # Fallback for testing except QueueFullError as e: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(e), ) return NotifyResponse( status="queued", message_length=len(request.message), queue_position=position, voice_model=voice, ) @router.get( "/health", response_model=HealthResponse, responses={ 503: {"model": HealthResponse, "description": "Service unhealthy"}, }, ) async def health(response: Response) -> HealthResponse: """ Health check endpoint. Returns comprehensive health status including: - TTS engine status - Audio output status - Queue status - System metrics """ tts_engine, audio_player, queue_manager, get_uptime_seconds = _get_components() settings = get_settings() errors = [] # Check TTS engine health tts_status = "unknown" if tts_engine: tts_health = tts_engine.health_check() tts_status = tts_health.get("status", "unknown") if tts_status != "healthy": errors.append(f"TTS: {tts_health.get('error', 'Unknown error')}") # Check audio health audio_status = "unknown" if audio_player: audio_health = audio_player.health_check() audio_status = audio_health.get("status", "unknown") if audio_status != "healthy": errors.append(f"Audio: {audio_health.get('error', 'Unknown error')}") # Get queue status if queue_manager: queue_info = queue_manager.get_status() queue_status = QueueStatus( size=queue_info["size"], capacity=queue_info["capacity"], utilization=queue_info["utilization"], ) total_requests = queue_info["processed"] failed_requests = queue_info["errors"] else: queue_status = QueueStatus( size=0, capacity=settings.queue_max_size, utilization=0.0, ) total_requests = None failed_requests = None # Determine overall status overall_status = "healthy" if not errors else "unhealthy" # Set response status code for unhealthy if overall_status == "unhealthy": response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return HealthResponse( status=overall_status, uptime_seconds=get_uptime_seconds(), queue=queue_status, tts_engine="piper", audio_output="available" if audio_status == "healthy" else "unavailable", total_requests=total_requests, failed_requests=failed_requests, errors=errors if errors else None, ) @router.get( "/voices", response_model=VoicesResponse, ) async def list_voices() -> VoicesResponse: """ List available voice models. Returns a list of installed voice models with their metadata and the current default voice. """ tts_engine, _, _, _ = _get_components() settings = get_settings() voices = [] if tts_engine: for voice_data in tts_engine.list_voices(): voices.append( VoiceInfo( name=voice_data["name"], language=voice_data["language"], quality=voice_data["quality"], size_mb=voice_data["size_mb"], installed=voice_data["installed"], ) ) return VoicesResponse( voices=voices, default_voice=settings.default_voice, )