pdf-quiz-generator/backend/app/routers/tts.py
Daniel fdebda993c
Some checks failed
Mobile Android Release / android-release (push) Failing after 1s
Improve quiz TTS and Android release build
2026-05-12 01:01:01 +02:00

132 lines
4.5 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from fastapi.responses import Response
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.user import User
from app.models.ai_model_config import AIModelConfig
from app.services import ai_service
from app.utils.auth import get_current_user, check_rate_limit
router = APIRouter()
class TTSRequest(BaseModel):
text: str
voice: str | None = None # model_id override
MAX_AUDIO_UPLOAD_BYTES = 25 * 1024 * 1024
def _task_model(db: Session, task: str, fallback: str) -> tuple[str, str | None]:
config = db.query(AIModelConfig).filter(
AIModelConfig.task == task,
AIModelConfig.is_active == True,
AIModelConfig.is_default == True,
).first()
if config:
return config.model_id, config.api_key or None
return fallback, None
def _default_tts_model(db: Session) -> tuple[str, str | None]:
config = db.query(AIModelConfig).filter(
AIModelConfig.task == "tts",
AIModelConfig.is_active == True,
AIModelConfig.is_default == True,
AIModelConfig.model_id.like("local-%"),
).first()
if config:
return config.model_id, config.api_key or None
return "local-kokoro-tts:am_adam", None
@router.get("/voices")
def get_voices(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Return LiteLLM-routed TTS models only."""
query = db.query(AIModelConfig).filter(
AIModelConfig.task == "tts",
AIModelConfig.is_active == True,
AIModelConfig.model_id.like("local-%"),
)
db_models = query.order_by(AIModelConfig.is_default.desc(), AIModelConfig.name).all()
return [{"id": m.model_id, "name": m.name, "is_default": m.is_default} for m in db_models]
@router.post("/speak")
def text_to_speech(
request: TTSRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Convert text to speech using configured or user-selected TTS model."""
# Rate limit: 60 TTS requests per user per hour (admins/unthrottled users exempt)
check_rate_limit(
key=f"tts_speak:{current_user.id}",
max_calls=240,
window_seconds=3600,
detail="You've reached the audio limit. The limit resets automatically — try again shortly. Contact an admin if you need this raised.",
user=current_user,
)
if not request.text.strip():
raise HTTPException(status_code=400, detail="Text cannot be empty")
text = request.text[:2000]
if request.voice and request.voice.startswith("local-"):
config = db.query(AIModelConfig).filter(
AIModelConfig.task == "tts",
AIModelConfig.is_active == True,
AIModelConfig.model_id == request.voice,
).first()
if not config:
model_id, api_key = _default_tts_model(db)
else:
model_id = config.model_id
api_key = config.api_key or None
else:
model_id, api_key = _default_tts_model(db)
audio = ai_service.generate_tts_audio(text, model_id=model_id, api_key=api_key)
if audio is None:
raise HTTPException(status_code=500, detail="TTS generation failed. Check model configuration.")
return Response(content=audio, media_type="audio/mpeg")
@router.post("/transcribe")
def speech_to_text(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Transcribe microphone audio using the configured STT model."""
check_rate_limit(
key=f"stt_transcribe:{current_user.id}",
max_calls=120,
window_seconds=3600,
detail="You've reached the speech transcription limit. Try again shortly.",
user=current_user,
)
audio = file.file.read()
if not audio:
raise HTTPException(status_code=400, detail="Audio file is empty")
if len(audio) > MAX_AUDIO_UPLOAD_BYTES:
raise HTTPException(status_code=413, detail="Audio file is too large")
model_id, api_key = _task_model(db, "stt", "local-parakeet-v3")
text = ai_service.transcribe_audio(
audio,
filename=file.filename or "audio.webm",
content_type=file.content_type or "audio/webm",
model_id=model_id,
api_key=api_key,
)
if text is None:
raise HTTPException(status_code=502, detail="Speech transcription failed. Check STT model configuration.")
return {"text": text, "model": model_id}