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.
"""
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"
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]:
"""Upload audio bytes to GCS and return the canonical gs:// URI, or None if disabled."""
if not settings.gcs_bucket:
@@ -65,7 +87,7 @@ async def upload_audio(data: bytes, filename: str, call_id: str = "") -> Optiona
else:
client = storage.Client()
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:
await asyncio.to_thread(_upload)