- 3 tables: Meeting / TranscriptSegment / Summary (with state machine) - Routes: /api/upload-url + /api/upload-complete + meetings CRUD - MinIO presigned PUT for direct browser upload - BackgroundTasks state-machine stub for A5 to flesh out - SQLite for local dev, PostgreSQL+asyncpg for prod - CORS configured for frontend on 4490 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
3.6 KiB
Python
115 lines
3.6 KiB
Python
import uuid
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import select
|
|
|
|
from ..db import get_session
|
|
from ..models import Meeting, MeetingStatus, TranscriptSegment, Summary
|
|
from ..schemas import (
|
|
MeetingRead,
|
|
UploadCompleteRequest,
|
|
UploadUrlRequest,
|
|
UploadUrlResponse,
|
|
)
|
|
from ..services.storage import presign_put
|
|
from ..services.transcribe import process_meeting
|
|
|
|
router = APIRouter(prefix="/api", tags=["meetings"])
|
|
|
|
|
|
@router.post("/upload-url", response_model=UploadUrlResponse)
|
|
async def request_upload_url(
|
|
body: UploadUrlRequest, session: AsyncSession = Depends(get_session)
|
|
):
|
|
object_key = f"audio/{uuid.uuid4().hex}-{body.filename}"
|
|
|
|
meeting = Meeting(
|
|
title=body.title or body.filename,
|
|
participants=body.participants,
|
|
object_key=object_key,
|
|
status=MeetingStatus.uploading,
|
|
)
|
|
session.add(meeting)
|
|
await session.commit()
|
|
await session.refresh(meeting)
|
|
|
|
upload_url = presign_put(object_key, body.content_type)
|
|
return UploadUrlResponse(
|
|
meeting_id=meeting.id,
|
|
upload_url=upload_url,
|
|
object_key=object_key,
|
|
)
|
|
|
|
|
|
@router.post("/upload-complete")
|
|
async def upload_complete(
|
|
body: UploadCompleteRequest,
|
|
background: BackgroundTasks,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
meeting = await session.get(Meeting, body.meeting_id)
|
|
if not meeting:
|
|
raise HTTPException(404, "meeting not found")
|
|
|
|
meeting.file_size = body.file_size
|
|
meeting.status = MeetingStatus.uploaded
|
|
await session.commit()
|
|
|
|
background.add_task(process_meeting, meeting.id)
|
|
return {"ok": True, "meeting_id": meeting.id}
|
|
|
|
|
|
@router.get("/meetings", response_model=list[MeetingRead])
|
|
async def list_meetings(session: AsyncSession = Depends(get_session)):
|
|
result = await session.execute(select(Meeting).order_by(Meeting.created_at.desc()))
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/meetings/{meeting_id}", response_model=MeetingRead)
|
|
async def get_meeting(meeting_id: int, session: AsyncSession = Depends(get_session)):
|
|
meeting = await session.get(Meeting, meeting_id)
|
|
if not meeting:
|
|
raise HTTPException(404, "meeting not found")
|
|
return meeting
|
|
|
|
|
|
@router.get("/meetings/{meeting_id}/transcript")
|
|
async def get_transcript(meeting_id: int, session: AsyncSession = Depends(get_session)):
|
|
result = await session.execute(
|
|
select(TranscriptSegment)
|
|
.where(TranscriptSegment.meeting_id == meeting_id)
|
|
.order_by(TranscriptSegment.start)
|
|
)
|
|
segments = result.scalars().all()
|
|
return [
|
|
{"start": s.start, "end": s.end, "speaker": s.speaker, "text": s.text}
|
|
for s in segments
|
|
]
|
|
|
|
|
|
@router.get("/meetings/{meeting_id}/summary")
|
|
async def get_summary(meeting_id: int, session: AsyncSession = Depends(get_session)):
|
|
result = await session.execute(
|
|
select(Summary).where(Summary.meeting_id == meeting_id)
|
|
)
|
|
summary = result.scalar_one_or_none()
|
|
if not summary:
|
|
return None
|
|
return {
|
|
"key_points": summary.key_points,
|
|
"todos": summary.todos,
|
|
"decisions": summary.decisions,
|
|
"keywords": summary.keywords,
|
|
"preview": summary.preview,
|
|
}
|
|
|
|
|
|
@router.delete("/meetings/{meeting_id}")
|
|
async def delete_meeting(meeting_id: int, session: AsyncSession = Depends(get_session)):
|
|
meeting = await session.get(Meeting, meeting_id)
|
|
if not meeting:
|
|
raise HTTPException(404, "meeting not found")
|
|
await session.delete(meeting)
|
|
await session.commit()
|
|
return {"ok": True}
|