Files
audiobook/backend/main.py
Paul Bailey 77437348ed Implement role detection service with pattern matching
- Created RoleDetector class with colon and bracket pattern extraction
- Implemented role deduplication logic
- Added fallback to default 'Narrator' role when no patterns found
- Created /api/tts/roles endpoint using RoleDetector
- Added comprehensive test suite for role detection
- All tests passing (7/7)

Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6
2025-12-09 01:04:09 +00:00

127 lines
4.3 KiB
Python

"""
FastAPI backend for Text to Audiobook application.
"""
import logging
from typing import List, Optional
from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import BaseModel, Field
# Import services
from services.role_detector import RoleDetector
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Pydantic Models
class RoleDetectionRequest(BaseModel):
"""Request model for role detection endpoint."""
text: str = Field(..., description="Text to analyze for role detection")
class RoleInfo(BaseModel):
"""Model representing a detected role."""
id: str = Field(..., description="Unique identifier for the role")
label: str = Field(..., description="Display name for the role")
class RolesResponse(BaseModel):
"""Response model for role detection endpoint."""
roles: List[RoleInfo] = Field(..., description="List of detected roles")
class RoleVoiceMapping(BaseModel):
"""Model for role-to-voice mapping."""
id: str = Field(..., description="Role identifier")
voice: str = Field(..., description="Kokoro voice ID")
class TTSRequest(BaseModel):
"""Request model for TTS generation endpoint."""
text: str = Field(..., description="Text to convert to speech")
lang: str = Field(..., description="Language code (a, b, f, j)")
roles: List[RoleVoiceMapping] = Field(..., description="Role to voice mappings")
speed: Optional[float] = Field(1.0, description="Playback speed (0.5-1.5)")
class ErrorResponse(BaseModel):
"""Standard error response model."""
error: str = Field(..., description="Error type")
message: str = Field(..., description="Human-readable error message")
details: Optional[dict] = Field(None, description="Additional error details")
# Initialize FastAPI app
app = FastAPI(
title="Text to Audiobook API",
description="API for converting text to multi-voice audiobooks using Kokoro TTS",
version="1.0.0"
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Error handling middleware
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Handle validation errors with custom error response."""
logger.error(f"Validation error: {exc.errors()}")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"error": "ValidationError",
"message": "Invalid request data",
"details": {"errors": exc.errors()}
}
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""Handle general exceptions with custom error response."""
logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "InternalServerError",
"message": "An unexpected error occurred",
"details": {"error": str(exc)}
}
)
# Initialize services
role_detector = RoleDetector()
# API Endpoints
@app.get("/api/health")
async def health_check():
"""Health check endpoint."""
logger.info("Health check requested")
return {"status": "ok"}
@app.post("/api/tts/roles", response_model=RolesResponse)
async def detect_roles(request: RoleDetectionRequest):
"""
Analyzes text and returns detected speaker roles.
Supports two pattern types:
- Colon pattern: "RoleName:" at line start
- Bracket pattern: "[RoleName]" preceding dialogue
Returns default "Narrator" role if no patterns found.
"""
logger.info(f"Role detection requested for text of length {len(request.text)}")
try:
roles = role_detector.detect_roles(request.text)
logger.info(f"Detected {len(roles)} roles: {[r['label'] for r in roles]}")
return RolesResponse(roles=roles)
except Exception as e:
logger.error(f"Error during role detection: {str(e)}", exc_info=True)
raise