From 22d52cf5441c62ef1a492bbf965073421bd6dfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kujund=C5=BEi=C4=87?= Date: Fri, 2 Oct 2026 19:28:13 +0200 Subject: [PATCH] fix(ai): name data-URL audio with its real extension A data URL's filename came from its MIME subtype, so `audio/mpeg` became `input.mpeg` and `audio/x-wav` became `input.x-wav`. OpenAI's transcription API picks the decoder from the extension and rejected both. Map the MIME types whose subtype isn't the extension. Co-Authored-By: Claude Opus 5.5 --- src/backend/drivers/util/fileInput.test.ts | 18 ++++++++++++++++++ src/backend/drivers/util/fileInput.ts | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/backend/drivers/util/fileInput.test.ts b/src/backend/drivers/util/fileInput.test.ts index 8cc0fca93..1dd17d610 100644 --- a/src/backend/drivers/util/fileInput.test.ts +++ b/src/backend/drivers/util/fileInput.test.ts @@ -237,6 +237,24 @@ describe('loadFileInput data URL', () => { expect(result.mimeType).toBe('image/svg+xml'); }); + // Audio upstreams pick the decoder from the extension; `input.mpeg` is + // rejected as corrupt. + it.each([ + ['audio/mpeg', 'input.mp3'], + ['audio/x-wav', 'input.wav'], + ['audio/wave', 'input.wav'], + ['audio/x-m4a', 'input.m4a'], + ['audio/ogg', 'input.ogg'], + ])('names a %s data URL %s', async (mime, filename) => { + const { actor } = await makeUser(); + const result = await callLoadFileInput( + actor, + `data:${mime};base64,QUJD`, + ); + expect(result.filename).toBe(filename); + expect(result.mimeType).toBe(mime); + }); + it('defaults MIME to application/octet-stream when omitted', async () => { const { actor } = await makeUser(); const result = await callLoadFileInput(actor, 'data:;base64,QUJD'); diff --git a/src/backend/drivers/util/fileInput.ts b/src/backend/drivers/util/fileInput.ts index ea46b1c4c..66e19f243 100644 --- a/src/backend/drivers/util/fileInput.ts +++ b/src/backend/drivers/util/fileInput.ts @@ -321,8 +321,22 @@ function assertMax(buffer: Buffer, maxBytes?: number): void { } } +/** + * MIME types whose subtype isn't the file extension. Upstreams like OpenAI's + * audio API pick the decoder from the extension and reject `input.mpeg`. + */ +const EXT_BY_MIME: Record = { + 'audio/mpeg': 'mp3', + 'audio/x-wav': 'wav', + 'audio/wave': 'wav', + 'audio/x-m4a': 'm4a', +}; + function filenameFromMime(mime: string): string { - const ext = mime.split('/')[1]?.split('+')[0] ?? 'bin'; + const ext = + EXT_BY_MIME[mime.toLowerCase()] ?? + mime.split('/')[1]?.split('+')[0] ?? + 'bin'; return `input.${ext}`; }