diff --git a/open-sse/handlers/audioTranscription.ts b/open-sse/handlers/audioTranscription.ts index 97f08e415e..b1689ccd78 100644 --- a/open-sse/handlers/audioTranscription.ts +++ b/open-sse/handlers/audioTranscription.ts @@ -65,13 +65,67 @@ function getUploadedFileName(file: Blob & { name?: unknown }): string { return typeof file.name === "string" && file.name.length > 0 ? file.name : "audio.wav"; } +/** + * Infer a suitable Content-Type for Deepgram from the browser-provided MIME + * type and the original filename. Deepgram accepts `audio/*` and many raw + * formats, but `video/*` causes it to silently fail with "no speech detected". + * + * Strategy: + * 1. If the browser says `audio/*`, keep it as-is. + * 2. If it's `video/*` (e.g. `.mp4`), remap to the audio equivalent so + * Deepgram extracts the audio track. `.mp4` → `audio/mp4`, etc. + * 3. Fall back to `application/octet-stream` which tells Deepgram to + * auto-detect from the raw bytes (most reliable for unknown formats). + */ +function resolveAudioContentType(file: Blob & { name?: unknown }): string { + const browserType = (file.type || "").toLowerCase(); + const fileName = typeof file.name === "string" ? file.name.toLowerCase() : ""; + + // 1) Browser already says it's audio — trust it + if (browserType.startsWith("audio/")) return browserType; + + // 2) Derive from file extension (covers video/* and empty MIME) + const ext = fileName.includes(".") ? fileName.split(".").pop() : ""; + const EXT_TO_MIME: Record = { + mp3: "audio/mpeg", + mp4: "audio/mp4", + m4a: "audio/mp4", + wav: "audio/wav", + ogg: "audio/ogg", + flac: "audio/flac", + webm: "audio/webm", + aac: "audio/aac", + wma: "audio/x-ms-wma", + opus: "audio/opus", + }; + if (ext && EXT_TO_MIME[ext]) return EXT_TO_MIME[ext]; + + // 3) Fallback — let Deepgram auto-detect from raw bytes + return "application/octet-stream"; +} + /** * Handle Deepgram transcription (raw binary audio, model via query param) */ -async function handleDeepgramTranscription(providerConfig, file, modelId, token) { +async function handleDeepgramTranscription( + providerConfig, + file, + modelId, + token, + formData?: FormData +) { const url = new URL(providerConfig.baseUrl); url.searchParams.set("model", modelId); url.searchParams.set("smart_format", "true"); + url.searchParams.set("punctuate", "true"); + + // Language: if caller specified one, use it; otherwise let Deepgram auto-detect + const langParam = formData?.get("language"); + if (typeof langParam === "string" && langParam.trim()) { + url.searchParams.set("language", langParam.trim()); + } else { + url.searchParams.set("detect_language", "true"); + } const arrayBuffer = await file.arrayBuffer(); @@ -79,7 +133,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token) method: "POST", headers: { ...buildAuthHeaders(providerConfig, token), - "Content-Type": file.type || "audio/wav", + "Content-Type": resolveAudioContentType(file), }, body: arrayBuffer, }); @@ -212,7 +266,7 @@ async function handleHuggingFaceTranscription(providerConfig, file, modelId, tok method: "POST", headers: { ...buildAuthHeaders(providerConfig, token), - "Content-Type": file.type || "audio/wav", + "Content-Type": resolveAudioContentType(file), }, body: arrayBuffer, }); @@ -283,7 +337,7 @@ export async function handleAudioTranscription({ // Route to provider-specific handler if (providerConfig.format === "deepgram") { - return handleDeepgramTranscription(providerConfig, file, modelId, token); + return handleDeepgramTranscription(providerConfig, file, modelId, token, formData); } if (providerConfig.format === "assemblyai") { diff --git a/tsconfig.json b/tsconfig.json index 5d6f9c9c17..515b9b1c48 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,8 @@ "**/*.js", "**/*.jsx", ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" + ".next/dev/types/**/*.ts", + ".next/dev/dev/types/**/*.ts" ], "exclude": [ "node_modules",