138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""Voice co-creation session APIs."""
|
|
|
|
from fastapi import APIRouter, Depends, Response, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
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,
|
|
)
|
|
from app.services.voice_session_service import (
|
|
abandon_voice_session_service,
|
|
create_voice_session_service,
|
|
create_voice_turn_from_text_service,
|
|
finalize_voice_session_service,
|
|
get_voice_session_detail_service,
|
|
get_voice_turn_audio_service,
|
|
get_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/{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.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.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}/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)
|