from fastapi import APIRouter, UploadFile, File, Depends, HTTPException from utils.rate_limiter import with_rate_limit from utils.groq_client import call_groq_transcribe router = APIRouter() SUPPORTED_AUDIO_TYPES = { "audio/webm", "audio/mp4", "audio/mpeg", "audio/wav", "audio/ogg", "audio/flac", "audio/x-m4a", } MAX_AUDIO_BYTES = 25 * 1024 * 1024 # 25 MB — Groq Whisper limit @router.post("/transcribe") async def transcribe_audio( audio: UploadFile = File(...), user: dict = Depends(with_rate_limit("transcription")), ): """ Accepts a browser-recorded audio blob, sends it to Groq Whisper, and returns the transcript text. Voice input is NEVER stored — the audio bytes are discarded after transcription returns. Only the resulting text is saved by the caller. See VUL-019 (no audio persistence). """ # Basic content-type guard — not a hard security boundary since the # browser may send 'application/octet-stream'. Real validation happens # when Groq rejects unsupported formats. content_type = audio.content_type or "audio/webm" if content_type not in SUPPORTED_AUDIO_TYPES and content_type != "application/octet-stream": raise HTTPException( status_code=415, detail=f"Unsupported audio format: {content_type}", ) audio_bytes = await audio.read() if len(audio_bytes) > MAX_AUDIO_BYTES: raise HTTPException( status_code=413, detail="Audio file exceeds 25 MB limit.", ) if len(audio_bytes) == 0: raise HTTPException(status_code=400, detail="Audio file is empty.") try: transcription = call_groq_transcribe( file=(audio.filename or "recording.webm", audio_bytes, content_type), response_format="json", language="en", fixture="transcript", ) return {"transcript": transcription.text} except Exception as e: raise HTTPException( status_code=500, detail=f"Transcription failed: {str(e)}", )