fix(audio): serve real Content-Type so imports decode in WaveSurfer

/audio/{id} and /audio/version/{id} hardcoded media_type="audio/wav" on
the FileResponse. That was a no-op when every generation came out of
TTS (everything on disk was a .wav anyway), but imported audio keeps
its source format — .mp3 / .m4a / .ogg — and the WaveSurfer MediaElement
backend uses an <audio> tag that checks Content-Type before letting the
clip play, so an MP3 announced as audio/wav silently failed to load.

Both endpoints now derive the type via mimetypes.guess_type and fall
back to audio/wav for unknown suffixes. Download filenames also keep
the real extension instead of always saying ".wav".
This commit is contained in:
Jamie Pine
2026-04-25 02:00:54 -07:00
parent c43f2d45cc
commit 3d4d0a9335
+17 -4
View File
@@ -1,5 +1,8 @@
"""Audio file serving endpoints.""" """Audio file serving endpoints."""
import mimetypes
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -11,6 +14,16 @@ from ..database import get_db
router = APIRouter() router = APIRouter()
def _audio_media_type(path: Path) -> str:
"""Derive the Content-Type from the file extension.
Imported audio retains its source format (.mp3, .m4a, .ogg, …) so a
blanket ``audio/wav`` would mislead strict clients trying to decode
via the response header instead of sniffing the bytes."""
guessed, _ = mimetypes.guess_type(path.name)
return guessed or "audio/wav"
@router.get("/audio/version/{version_id}") @router.get("/audio/version/{version_id}")
async def get_version_audio(version_id: str, db: Session = Depends(get_db)): async def get_version_audio(version_id: str, db: Session = Depends(get_db)):
"""Serve audio for a specific version.""" """Serve audio for a specific version."""
@@ -26,8 +39,8 @@ async def get_version_audio(version_id: str, db: Session = Depends(get_db)):
return FileResponse( return FileResponse(
audio_path, audio_path,
media_type="audio/wav", media_type=_audio_media_type(audio_path),
filename=f"generation_{version.generation_id}_{version.label}.wav", filename=f"generation_{version.generation_id}_{version.label}{audio_path.suffix}",
) )
@@ -44,8 +57,8 @@ async def get_audio(generation_id: str, db: Session = Depends(get_db)):
return FileResponse( return FileResponse(
audio_path, audio_path,
media_type="audio/wav", media_type=_audio_media_type(audio_path),
filename=f"generation_{generation_id}.wav", filename=f"generation_{generation_id}{audio_path.suffix}",
) )