Serve audio as whatever it actually is
Build & Deploy / Build & push images (push) Successful in 4m4s
Build & Deploy / Deploy to VM (push) Successful in 1m53s
Build & Deploy / Report a failed deploy (push) Skipped

audio/mpeg was hardcoded at both points call audio is written and served, from
back when the node produced nothing but 16 kbps MP3. It now uploads FLAC, and a
browser will not play a FLAC body labelled audio/mpeg.

storage.py grows one extension -> Content-Type map, used by the GCS upload and
by /media. Keyed off the object's real extension, so every existing .mp3
recording keeps working with no migration -- and _safe_audio_filename already
accepted .flac, so object naming needed nothing.

Also flags what this costs: /media sends the whole body with Accept-Ranges:
none, which was fine at ~60 KB per call and is not fine at ~1.3 MB/min. Noted
at the header and in DEFERRED.md, whose stated reason for deferring Range
support was the old file size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-08-23 12:48:47 -04:00
co-authored by Claude Opus 5
parent 457e6d7e0f
commit 1bfa856d1b
2 changed files with 35 additions and 7 deletions
+24 -2
View File
@@ -44,11 +44,33 @@ def _safe_audio_filename(filename: str, call_id: str) -> str:
The original extension is preserved only if it's a known audio type. The original extension is preserved only if it's a known audio type.
""" """
ext = os.path.splitext(filename)[-1].lower() if filename else "" ext = os.path.splitext(filename)[-1].lower() if filename else ""
if ext not in (".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"): if ext not in AUDIO_CONTENT_TYPES:
ext = ".mp3" ext = ".mp3"
return f"{call_id}{ext}" return f"{call_id}{ext}"
# Extension → Content-Type. The node used to send nothing but 16 kbps MP3, so
# "audio/mpeg" was hardcoded at every point audio is written or served; it now
# sends FLAC (lossless, for Whisper's benefit — see call_recorder.py's AUDIO_*
# constants) and a stored object mislabelled audio/mpeg will not play in a
# browser. Old .mp3 objects keep working: the map is keyed off the real
# extension, not off what the current node happens to produce.
AUDIO_CONTENT_TYPES = {
".flac": "audio/flac",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".aac": "audio/aac",
}
def content_type_for(name: str) -> str:
"""Content-Type for a stored audio object, by extension. Defaults to MP3."""
ext = os.path.splitext(name or "")[-1].lower()
return AUDIO_CONTENT_TYPES.get(ext, "audio/mpeg")
async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optional[str]: async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optional[str]:
"""Upload audio bytes to GCS and return the canonical gs:// URI, or None if disabled.""" """Upload audio bytes to GCS and return the canonical gs:// URI, or None if disabled."""
if not settings.gcs_bucket: if not settings.gcs_bucket:
@@ -65,7 +87,7 @@ async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optiona
else: else:
client = storage.Client() client = storage.Client()
blob = client.bucket(settings.gcs_bucket).blob(blob_path) blob = client.bucket(settings.gcs_bucket).blob(blob_path)
blob.upload_from_string(data, content_type="audio/mpeg") blob.upload_from_string(data, content_type=content_type_for(safe_name))
try: try:
await asyncio.to_thread(_upload) await asyncio.to_thread(_upload)
+11 -5
View File
@@ -13,7 +13,7 @@ service-account private key on the VM — is involved anywhere in this path.
""" """
from fastapi import APIRouter, HTTPException, Query, Response from fastapi import APIRouter, HTTPException, Query, Response
from app.internal import firestore as fstore from app.internal import firestore as fstore
from app.internal.storage import verify_audio_link, gcs_uri_for_call, download_audio from app.internal.storage import verify_audio_link, gcs_uri_for_call, download_audio, content_type_for
router = APIRouter(prefix="/media", tags=["media"]) router = APIRouter(prefix="/media", tags=["media"])
@@ -42,12 +42,18 @@ async def get_call_audio(
return Response( return Response(
content=data, content=data,
media_type="audio/mpeg", # Was hardcoded audio/mpeg. The node now uploads FLAC, and a browser
# will not play a FLAC body labelled audio/mpeg. Derived from the stored
# object's extension so old .mp3 recordings keep working unchanged.
media_type=content_type_for(gcs_uri),
headers={ headers={
"Content-Length": str(len(data)), "Content-Length": str(len(data)),
# Recordings are small (16 kbps mono — a 30s call is ~60 KB), so the # Whole body at once, no Range support. This was comfortable at
# whole body is sent at once and the browser seeks within its own # 16 kbps mono (~60 KB for a 30 s call); FLAC is ~1.3 MB/min, so a
# buffer. Range support would only matter for long files. # long call is now tens of MB and the browser must download all of
# it before playback starts. Acceptable for typical few-second
# transmissions, but this is the change that makes Range support
# actually matter — see DEFERRED.md.
"Accept-Ranges": "none", "Accept-Ranges": "none",
# Immutable content, but the URL expires — cache privately only. # Immutable content, but the URL expires — cache privately only.
"Cache-Control": "private, max-age=3600", "Cache-Control": "private, max-age=3600",