A local HTTP service that accepts text via POST and speaks it through system speakers using Piper TTS neural voice synthesis. Features: - POST /notify - Queue text for TTS playback - GET /health - Health check with TTS/audio/queue status - GET /voices - List installed voice models - Async queue processing (no overlapping audio) - Non-blocking audio via sounddevice - 73 tests covering API contract Tech stack: - FastAPI + Uvicorn - Piper TTS (neural voices, offline) - sounddevice (PortAudio) - Pydantic for validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
99 lines
2.6 KiB
Python
99 lines
2.6 KiB
Python
"""
|
|
Configuration management for voice-server.
|
|
|
|
Loads configuration from environment variables with sensible defaults.
|
|
Uses pydantic-settings for type-safe configuration loading and validation.
|
|
"""
|
|
|
|
from functools import lru_cache
|
|
from typing import Annotated, Literal
|
|
|
|
from pydantic import Field, field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
# Valid log levels
|
|
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""
|
|
Application settings loaded from environment variables.
|
|
|
|
All settings have sensible defaults and can be overridden via environment
|
|
variables or a .env file.
|
|
"""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
case_sensitive=False,
|
|
extra="ignore",
|
|
)
|
|
|
|
# Server settings
|
|
host: Annotated[str, Field(default="0.0.0.0", description="Host to bind to")]
|
|
port: Annotated[
|
|
int,
|
|
Field(default=8888, ge=1, le=65535, description="Port to listen on"),
|
|
]
|
|
|
|
# TTS settings
|
|
model_dir: Annotated[
|
|
str,
|
|
Field(default="./models", description="Directory containing voice models"),
|
|
]
|
|
default_voice: Annotated[
|
|
str,
|
|
Field(default="en_US-lessac-medium", description="Default voice model"),
|
|
]
|
|
default_rate: Annotated[
|
|
int,
|
|
Field(default=170, ge=50, le=400, description="Default speech rate (WPM)"),
|
|
]
|
|
|
|
# Queue settings
|
|
queue_max_size: Annotated[
|
|
int,
|
|
Field(default=50, gt=0, description="Maximum TTS queue size"),
|
|
]
|
|
request_timeout_seconds: Annotated[
|
|
int,
|
|
Field(default=60, gt=0, description="Request processing timeout"),
|
|
]
|
|
|
|
# Logging
|
|
log_level: Annotated[
|
|
LogLevel,
|
|
Field(default="INFO", description="Logging level"),
|
|
]
|
|
log_file: Annotated[
|
|
str | None,
|
|
Field(default=None, description="Log file path (None for stdout only)"),
|
|
]
|
|
|
|
# Debug
|
|
voice_enabled: Annotated[
|
|
bool,
|
|
Field(default=True, description="Enable/disable TTS playback"),
|
|
]
|
|
|
|
@field_validator("log_level", mode="before")
|
|
@classmethod
|
|
def uppercase_log_level(cls, v: str) -> str:
|
|
"""Ensure log level is uppercase."""
|
|
if isinstance(v, str):
|
|
return v.upper()
|
|
return v
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
"""
|
|
Get cached application settings.
|
|
|
|
Returns the same Settings instance on subsequent calls for efficiency.
|
|
The cache can be cleared by calling get_settings.cache_clear().
|
|
"""
|
|
return Settings()
|