diff --git a/apps/whispering/src/lib/services/transcription/cloud/elevenlabs.ts b/apps/whispering/src/lib/services/transcription/cloud/elevenlabs.ts index da29f7cbfd..0d14c7e505 100644 --- a/apps/whispering/src/lib/services/transcription/cloud/elevenlabs.ts +++ b/apps/whispering/src/lib/services/transcription/cloud/elevenlabs.ts @@ -1,12 +1,12 @@ -import { ElevenLabsClient } from 'elevenlabs'; import { defineErrors, extractErrorMessage, type InferErrors, } from 'wellcrafted/error'; -import { type Result, tryAsync } from 'wellcrafted/result'; +import { Err, type Result, tryAsync } from 'wellcrafted/result'; const MAX_FILE_SIZE_MB = 1000 as const; +const ELEVENLABS_STT_URL = 'https://api.elevenlabs.io/v1/speech-to-text'; export const ElevenLabsError = defineErrors({ MissingApiKey: () => ({ @@ -23,6 +23,17 @@ export const ElevenLabsError = defineErrors({ sizeMb, maxMb, }), + ApiError: ({ + status, + body, + }: { + status: number; + body: string; + }) => ({ + message: `ElevenLabs API error ${status}: ${body}`, + status, + body, + }), Unexpected: ({ cause }: { cause: unknown }) => ({ message: extractErrorMessage(cause), cause, @@ -51,23 +62,42 @@ export const ElevenLabsTranscriptionServiceLive = { }); } - const client = new ElevenLabsClient({ apiKey: options.apiKey }); + const formData = new FormData(); + formData.append('file', audioBlob); + formData.append('model_id', options.modelName); + if (options.outputLanguage !== 'auto') { + formData.append('language_code', options.outputLanguage); + } + formData.append('tag_audio_events', 'false'); + formData.append('diarize', 'true'); + // Remove filler words, false starts, and non-speech sounds. Only + // supported on scribe_v2 per ElevenLabs API; ignored on other models. + if (options.modelName === 'scribe_v2') { + formData.append('no_verbatim', 'true'); + } + + const { data: response, error: fetchError } = await tryAsync({ + try: () => + fetch(ELEVENLABS_STT_URL, { + method: 'POST', + headers: { 'xi-api-key': options.apiKey }, + body: formData, + }), + catch: (cause) => ElevenLabsError.Unexpected({ cause }), + }); + if (fetchError) return Err(fetchError); + + if (!response.ok) { + const body = await response.text().catch(() => ''); + return ElevenLabsError.ApiError({ status: response.status, body }); + } return tryAsync({ try: async () => { - const transcription = await client.speechToText.convert({ - file: audioBlob, - model_id: options.modelName, - language_code: - options.outputLanguage !== 'auto' - ? options.outputLanguage - : undefined, - tag_audio_events: false, - diarize: true, - }); - return transcription.text.trim(); + const data = (await response.json()) as { text: string }; + return data.text.trim(); }, - catch: (error) => ElevenLabsError.Unexpected({ cause: error }), + catch: (cause) => ElevenLabsError.Unexpected({ cause }), }); }, };