From 07bba426d144229ae4b39522bf5d0413c25e4c0d Mon Sep 17 00:00:00 2001 From: Pratik Bodkhe Date: Mon, 20 Jul 2026 08:50:14 +0900 Subject: [PATCH] Map companion m4a mime variants to the m4a transcription format supabase-swift derives the storage content type from the .m4a extension instead of the explicit FileOptions value, so companion recordings are stored as audio/x-m4a. audioFormatFromMime mapped only audio/mp4 and fell through to webm, so the STT provider rejected every companion upload with 415 unsupported_format; no companion recording could ever transcribe. audio/mp4, audio/x-m4a, and audio/m4a now map to m4a. audio/aac keeps its raw-ADTS aac mapping since nothing in the pipeline labels m4a files that way. --- scripts/verify-transcription-client.test.mjs | 22 ++++++++++++++++++++ src/lib/transcription/openrouter-stt.ts | 9 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/verify-transcription-client.test.mjs b/scripts/verify-transcription-client.test.mjs index 017d0a6..45ae2bb 100644 --- a/scripts/verify-transcription-client.test.mjs +++ b/scripts/verify-transcription-client.test.mjs @@ -173,6 +173,28 @@ test("transcribeWithOpenRouter posts base64 JSON with attribution headers", asyn assert.equal(result.durationSeconds, 3); // from usage.seconds }); +test("transcribeWithOpenRouter maps the macOS companion's m4a mime variants to the m4a format", async () => { + // supabase-swift stores the companion's AAC-in-MP4 upload under audio/x-m4a + // (see supabase/migrations/20260704120000_meeting_audio_m4a_mime.sql); every + // mime the storage layer admits for that same file must resolve to "m4a" here, + // or the provider 415s real companion recordings as unsupported. + const cases = [ + ["audio/mp4", "m4a"], + ["audio/x-m4a", "m4a"], + ["audio/m4a", "m4a"], + ["audio/aac", "aac"], + ["audio/x-m4a; codecs=mp4a.40.2", "m4a"], + ["audio/webm", "webm"], + ["audio/unknown", "webm"], + ]; + for (const [mimeType, expectedFormat] of cases) { + const calls = fakeFetch(() => okResponse({ text: "x" })); + await transcribeWithOpenRouter(audioBlob(64), { apiKey: "or-key", mimeType }); + const body = JSON.parse(calls[0].options.body); + assert.equal(body.input_audio.format, expectedFormat, `${mimeType} -> ${expectedFormat}`); + } +}); + // --------------------------------------------------------------------------- // AssemblyAI client (diarizing primary) // --------------------------------------------------------------------------- diff --git a/src/lib/transcription/openrouter-stt.ts b/src/lib/transcription/openrouter-stt.ts index 40817cb..e7a6f13 100644 --- a/src/lib/transcription/openrouter-stt.ts +++ b/src/lib/transcription/openrouter-stt.ts @@ -12,10 +12,17 @@ import { sendTranscription, type TranscriptionResult } from "./shared"; export const OPENROUTER_STT_URL = "https://openrouter.ai/api/v1/audio/transcriptions"; export const OPENROUTER_STT_DEFAULT_MODEL = "openai/whisper-1"; +// supabase-swift stores the macOS companion's AAC-in-MP4 upload under a mime +// derived from the ".m4a" extension, not the explicit FileOptions content +// type, so the same recording can arrive as audio/mp4, audio/x-m4a, or +// audio/m4a (see supabase/migrations/20260704120000_meeting_audio_m4a_mime.sql). +// All must map to "m4a" or the provider 415s a real companion upload. +const M4A_MIME_TYPES = new Set(["audio/mp4", "audio/x-m4a", "audio/m4a"]); + /** OpenRouter's `input_audio.format` from a MIME type (codecs suffix ignored). */ function audioFormatFromMime(mime: string): string { const base = mime.split(";")[0].trim(); - if (base === "audio/mp4") return "m4a"; + if (M4A_MIME_TYPES.has(base)) return "m4a"; if (base === "audio/ogg") return "ogg"; if (base === "audio/mpeg") return "mp3"; if (base === "audio/wav") return "wav";