"""Voice co-creation session APIs.""" from fastapi import ( APIRouter, Depends, File, Form, Query, Response, UploadFile, status, ) from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.deps import require_user from app.core.rate_limiter import check_rate_limit from app.db.database import get_db from app.db.models import User from app.schemas.voice_session_schemas import ( VoiceSessionAbandonRequest, VoiceSessionCreateRequest, VoiceSessionDetailResponse, VoiceSessionFinalizeRequest, VoiceSessionFinalizeResponse, VoiceSessionSummaryResponse, VoiceTurnAcceptedResponse, VoiceTurnCreateFallbackRequest, VoiceTurnSummaryResponse, VoiceTurnUploadAcceptedResponse, ) from app.services.voice_session_service import ( abandon_voice_session_service, create_voice_session_service, create_voice_turn_from_text_service, create_voice_turn_from_upload_service, finalize_voice_session_service, get_latest_active_voice_session_service, get_voice_session_detail_service, get_voice_turn_audio_service, get_voice_turn_service, get_voice_turn_user_audio_service, list_voice_sessions_service, retry_voice_turn_audio_service, retry_voice_turn_service, ) router = APIRouter() VOICE_SESSION_RATE_LIMIT_WINDOW = 60 VOICE_SESSION_RATE_LIMIT_REQUESTS = 20 @router.post( "/voice-sessions", response_model=VoiceSessionSummaryResponse, status_code=status.HTTP_201_CREATED, ) async def create_voice_session( request: VoiceSessionCreateRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Create one draft voice co-creation session.""" await check_rate_limit( f"voice-session:{user.id}", VOICE_SESSION_RATE_LIMIT_REQUESTS, VOICE_SESSION_RATE_LIMIT_WINDOW, ) return await create_voice_session_service(request, user.id, db) @router.get("/voice-sessions", response_model=list[VoiceSessionSummaryResponse]) async def list_voice_sessions( limit: int = Query( default=settings.voice_session_default_list_limit, ge=1, le=settings.voice_session_max_list_limit, ), active_only: bool = Query(default=False), active_first: bool = Query(default=True), user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """List recent voice co-creation sessions for restore/resume behavior.""" return await list_voice_sessions_service( user.id, db, limit=limit, active_only=active_only, active_first=active_first, ) @router.get("/voice-sessions/active", response_model=VoiceSessionSummaryResponse | None) async def get_latest_active_voice_session( user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get the latest active voice session for quick resume behavior.""" return await get_latest_active_voice_session_service(user.id, db) @router.get("/voice-sessions/{session_id}", response_model=VoiceSessionDetailResponse) async def get_voice_session( session_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get one voice co-creation session with recent turns and events.""" return await get_voice_session_detail_service(session_id, user.id, db) @router.post( "/voice-sessions/{session_id}/turns/fallback", response_model=VoiceTurnAcceptedResponse, status_code=status.HTTP_202_ACCEPTED, ) async def create_voice_turn_from_text( session_id: str, request: VoiceTurnCreateFallbackRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Create one turn using text fallback before real audio upload is added.""" await check_rate_limit( f"voice-turn:{user.id}", VOICE_SESSION_RATE_LIMIT_REQUESTS, VOICE_SESSION_RATE_LIMIT_WINDOW, ) return await create_voice_turn_from_text_service(session_id, request, user.id, db) @router.post( "/voice-sessions/{session_id}/turns", response_model=VoiceTurnUploadAcceptedResponse, status_code=status.HTTP_202_ACCEPTED, ) async def create_voice_turn_from_upload( session_id: str, audio_file: UploadFile = File(...), duration_ms: int | None = Form(default=None), transcript_hint: str | None = Form(default=None), user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Create one turn from uploaded audio and configured ASR behavior.""" await check_rate_limit( f"voice-turn:{user.id}", VOICE_SESSION_RATE_LIMIT_REQUESTS, VOICE_SESSION_RATE_LIMIT_WINDOW, ) audio_bytes = await audio_file.read() return await create_voice_turn_from_upload_service( session_id=session_id, user_id=user.id, audio_bytes=audio_bytes, file_name=audio_file.filename or "voice-turn.webm", mime_type=audio_file.content_type, duration_ms=duration_ms, transcript_hint=transcript_hint, db=db, ) @router.get( "/voice-sessions/{session_id}/turns/{turn_id}", response_model=VoiceTurnSummaryResponse, ) async def get_voice_turn( session_id: str, turn_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get one processed turn within a voice session.""" return await get_voice_turn_service(session_id, turn_id, user.id, db) @router.post( "/voice-sessions/{session_id}/turns/{turn_id}/retry", response_model=VoiceTurnAcceptedResponse, status_code=status.HTTP_202_ACCEPTED, ) async def retry_voice_turn( session_id: str, turn_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Retry one failed voice turn using its saved transcript.""" return await retry_voice_turn_service(session_id, turn_id, user.id, db) @router.get("/voice-sessions/{session_id}/turns/{turn_id}/audio") async def get_voice_turn_audio( session_id: str, turn_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get synthesized assistant audio for one completed voice turn.""" audio_bytes = await get_voice_turn_audio_service(session_id, turn_id, user.id, db) return Response(content=audio_bytes, media_type="audio/mpeg") @router.post( "/voice-sessions/{session_id}/turns/{turn_id}/retry-audio", response_model=VoiceTurnSummaryResponse, ) async def retry_voice_turn_audio( session_id: str, turn_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Retry assistant audio synthesis when one turn only has text output.""" return await retry_voice_turn_audio_service(session_id, turn_id, user.id, db) @router.get("/voice-sessions/{session_id}/turns/{turn_id}/user-audio") async def get_voice_turn_user_audio( session_id: str, turn_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get uploaded user audio for one voice turn.""" audio_bytes, mime_type = await get_voice_turn_user_audio_service( session_id, turn_id, user.id, db, ) return Response(content=audio_bytes, media_type=mime_type) @router.post( "/voice-sessions/{session_id}/finalize", response_model=VoiceSessionFinalizeResponse, ) async def finalize_voice_session( session_id: str, request: VoiceSessionFinalizeRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Finalize one voice session into a persisted story.""" return await finalize_voice_session_service(session_id, request, user.id, db) @router.post( "/voice-sessions/{session_id}/abandon", response_model=VoiceSessionSummaryResponse, ) async def abandon_voice_session( session_id: str, request: VoiceSessionAbandonRequest, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Abandon one in-progress voice session without saving it as a story.""" return await abandon_voice_session_service(session_id, request, user.id, db)