Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 45 additions & 15 deletions apps/whispering/src/lib/services/transcription/cloud/elevenlabs.ts
Original file line number Diff line number Diff line change
@@ -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: () => ({
Expand All @@ -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,
Expand Down Expand Up @@ -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 }),
});
},
};
Expand Down