profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
sttOpenai.ts 1,820 bytes
1 import { readEnvKey } from '../config/runtimeConfig'
2 import { EmptyTranscriptError, ProviderNotConfiguredError, type SttProvider } from './types'
3
4 const DEFAULT_STT_MODEL = 'gpt-4o-mini-transcribe'
5
6 // OpenAI decodes by file extension, so the name must match the real container
7 // format or valid audio is rejected as corrupted.
8 function audioFileName(mimeType: string): string {
9 const subtype = mimeType.split('/')[1]?.split(';')[0]?.toLowerCase() ?? 'webm'
10 const ext = subtype === 'mpeg' ? 'mp3' : subtype === 'x-wav' || subtype === 'wave' ? 'wav' : subtype
11 const supported = ['flac', 'm4a', 'mp3', 'mp4', 'mpga', 'oga', 'ogg', 'wav', 'webm']
12 return `audio.${supported.includes(ext) ? ext : 'webm'}`
13 }
14
15 export function createSttOpenai(): SttProvider {
16 const apiKey = readEnvKey('OPENAI_API_KEY')
17 if (!apiKey) throw new ProviderNotConfiguredError('OpenAI speech-to-text')
18 const model = process.env.STT_MODEL ?? DEFAULT_STT_MODEL
19
20 return {
21 async transcribe(audio: Buffer, mimeType: string): Promise<string> {
22 const form = new FormData()
23 form.append('model', model)
24 form.append('file', new Blob([audio], { type: mimeType }), audioFileName(mimeType))
25
26 const res = await fetch('https://api.openai.com/v1/audio/transcriptions', {
27 method: 'POST',
28 headers: { Authorization: `Bearer ${apiKey}` },
29 body: form,
30 })
31
32 if (!res.ok) {
33 const body = await res.text()
34 throw new Error(
35 `OpenAI STT request failed (${res.status}) [sent ${audioFileName(mimeType)}, type "${mimeType}", ${audio.length} bytes]: ${body}`,
36 )
37 }
38
39 const data = (await res.json()) as { text?: string }
40 const text = (data.text ?? '').trim()
41 if (text.length === 0) throw new EmptyTranscriptError()
42 return text
43 },
44 }
45 }
46