fix(signal): detect M4A-branded voice notes so iOS audio reaches STT

iOS Signal delivers voice notes as MP4-container AAC carrying an audio
ftyp brand ("M4A "). `_guess_extension()` returned ".mp4" for every
`ftyp` file regardless of brand, so those attachments were cached as
documents instead of audio and STT rejected the upload:

    API error: Error code: 400 - Invalid file format.
    Supported formats: ['flac','m4a','mp3','mp4','mpeg','mpga','oga',
    'ogg','wav','webm']

Read the 4-byte brand at offset 8 and return ".m4a" for audio brands
("M4A ", "M4B ") so they satisfy `_is_audio_ext()` and route to
`cache_audio_from_bytes()`. Video brands (isom/mp42/avc1/qt) still
return ".mp4".

This mirrors the existing brand/form-type disambiguation already used
in this function for RIFF (WEBP vs WAVE) and for ADTS AAC vs MP3.

Verified against a real iOS Signal voice note: pre-fix the upload was
rejected with the 400 above; post-fix the same bytes transcribe
successfully.
This commit is contained in:
gshall 2026-07-27 01:07:12 -04:00 committed by Teknium
parent 38b0b7ae3f
commit 6210642297
2 changed files with 28 additions and 0 deletions

View file

@ -106,6 +106,14 @@ def _guess_extension(data: bytes) -> str:
if data[:4] == b"%PDF":
return ".pdf"
if len(data) >= 8 and data[4:8] == b"ftyp":
# iOS Signal delivers voice notes as MP4-container AAC carrying an
# audio ftyp brand ("M4A ", "M4B "). Returning ".mp4" for those made
# them cache as documents and STT reject them ("Invalid file
# format") even though the bytes are valid audio. Read the brand so
# audio-branded files land as ".m4a" and route to the audio cache.
brand = data[8:12].lower() if len(data) >= 12 else b""
if brand in (b"m4a ", b"m4b "):
return ".m4a"
return ".mp4"
if data[:4] == b"OggS":
return ".ogg"

View file

@ -201,6 +201,26 @@ class TestSignalHelpers:
webp = b"RIFF\x24\x08\x00\x00WEBPVP8 " + b"\x00" * 100
assert _guess_extension(webp) == ".webp"
def test_guess_extension_m4a_audio_brand(self):
"""iOS Signal voice notes are MP4-container AAC with an M4A ftyp brand.
Classifying them as ``.mp4`` sent them to the document cache and made
STT reject the upload ("Invalid file format") even though the bytes
were valid audio. Audio brands must resolve to ``.m4a``.
"""
from gateway.platforms.signal import _guess_extension, _is_audio_ext
for brand in (b"M4A ", b"M4B ", b"m4a "):
data = b"\x00\x00\x00\x1cftyp" + brand + b"\x00" * 100
assert _guess_extension(data) == ".m4a", brand
assert _is_audio_ext(_guess_extension(data)) is True
def test_guess_extension_video_brands_stay_mp4(self):
"""Real video ftyp brands must not be swept up by the M4A fix."""
from gateway.platforms.signal import _guess_extension
for brand in (b"isom", b"mp42", b"avc1", b"qt "):
data = b"\x00\x00\x00\x1cftyp" + brand + b"\x00" * 100
assert _guess_extension(data) == ".mp4", brand
def test_guess_extension_aac_adts_unprotected(self):
"""ADTS AAC, MPEG-4, no CRC (the canonical Android Signal voice note).