voice-server/app/routes.py
Cal Corum 5f7dd68bf6 Add urgent flag for higher volume playback
Added optional 'urgent' boolean field to POST /notify requests.
When urgent=true, audio is played at 1.5x volume with clipping
protection for critical messages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 09:10:26 -06:00

200 lines
5.6 KiB
Python

"""
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,
urgent=request.urgent,
)
# 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,
)