Skip to content

Commit a30c789

Browse files
committed
feat: Integrate with and default to ElevenLabs Scribe v1
1 parent 488da82 commit a30c789

File tree

6 files changed

+89
-28
lines changed

6 files changed

+89
-28
lines changed

apps/web/src/lib/components/Recorder.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { onMount } from 'svelte';
44
import WaveSurfer from 'wavesurfer.js';
55
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.esm.js';
6+
import { ElevenLabsClient } from 'elevenlabs';
67
78
import Button from './Button.svelte';
89
import ButtonPause from './ButtonPause.svelte';
@@ -18,13 +19,15 @@
1819
recordingUrl?: string;
1920
saveRecording: () => void;
2021
scrollingWaveform?: boolean;
22+
transcriptionModel?: 'elevenlabs-scribe-v1' | 'openai-whisper';
2123
}
2224
2325
let {
2426
discardRecording,
2527
recordingUrl = $bindable(''),
2628
saveRecording,
27-
scrollingWaveform = true
29+
scrollingWaveform = true,
30+
transcriptionModel = 'elevenlabs-scribe-v1'
2831
}: RecorderProps = $props();
2932
3033
let defaultDeviceId: string | undefined = $state(undefined);

apps/web/src/lib/components/RecordingTile/RecordingTile.svelte

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
name,
1616
savedRecordings = $bindable([]),
1717
titleSlot,
18-
transcription
18+
transcription,
19+
transcriptionModel = $bindable('elevenlabs-scribe-v1')
1920
}: RecordingTileProps = $props();
2021
22+
$inspect(transcription);
23+
2124
let blob: Blob = $state(base64ToBlob(data));
2225
2326
let isPlaying = $state(false);
@@ -80,7 +83,7 @@
8083
<div class="transcription">
8184
<div class="inner">
8285
<!-- svelte-ignore a11y_click_events_have_key_events -->
83-
{#each transcription.words as { word, start, end }, i}
86+
{#each transcription.words as { word, text, start, end }, i}
8487
<!-- svelte-ignore a11y_click_events_have_key_events -->
8588
<!-- svelte-ignore a11y_no_static_element_interactions -->
8689
<span
@@ -91,11 +94,21 @@
9194
class:read={currentTime >= start}
9295
onclick={() => playFromTime(start)}
9396
>
94-
{#if i !== transcription.words.length - 1}
95-
<span class="word">{word}</span>&nbsp;
96-
{:else}
97-
<span class="word">{word}</span>
98-
{/if}
97+
<span class="word">
98+
{#if text}
99+
{#if text === ' '}
100+
&nbsp;
101+
{:else}
102+
{text}
103+
{/if}
104+
{:else if word}
105+
{#if i !== transcription.words.length - 1}
106+
{word}&nbsp;
107+
{:else}
108+
{word}
109+
{/if}
110+
{/if}
111+
</span>
99112
</span>
100113
{/each}
101114
</div>
@@ -109,6 +122,11 @@
109122
{:else}
110123
<span> You haven't transcribed this recording yet. </span>
111124

125+
<select bind:value={transcriptionModel}>
126+
<option value="elevenlabs-scribe-v1">ElevenLabs Scribe v1</option>
127+
<option value="openai-whisper">OpenAI Whisper</option>
128+
</select>
129+
112130
<Button
113131
kind="secondary"
114132
label="Transcribe"
@@ -119,7 +137,7 @@
119137

120138
isTranscribing = true;
121139

122-
const transcription = await transcribeRecording(blob);
140+
const transcription = await transcribeRecording(blob, transcriptionModel);
123141

124142
isTranscribing = false;
125143

apps/web/src/lib/components/RecordingTile/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export interface RecordingTileProps {
99
savedRecordings?: any[];
1010
titleSlot?: Snippet;
1111
transcription?: Transcription;
12+
transcriptionModel?: string;
1213
}

apps/web/src/lib/methods/transcribe-recording.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export default async function transcribeRecording(
2-
audioBlob: Blob
2+
audioBlob: Blob,
3+
transcriptionModel = 'elevenlabs-scribe-v1'
34
): Promise<{ text: string } | null> {
45
try {
56
const formData = new FormData();
67
const newBlob = new Blob([audioBlob], { type: 'audio/webm' });
78

89
formData.append('audio', newBlob, 'audio.webm');
10+
formData.append('model', transcriptionModel);
911

1012
const response = await fetch('/api/transcribe', {
1113
method: 'POST',

apps/web/src/lib/types/transcription.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export interface Transcription {
22
text: string;
33
vtt: string;
44
word_count: number;
5-
words: { word: string; start: number; end: number }[];
5+
words: { word?: string; text?: string; start: number; end: number }[];
66
}

apps/web/src/routes/api/transcribe/+server.ts

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { error, json } from '@sveltejs/kit';
22
import type { RequestHandler } from './$types';
33
import { env as envPrivate } from '$env/dynamic/private';
4+
import { ElevenLabsClient } from 'elevenlabs';
45

56
const CLOUDFLARE_API_URL = `https://api.cloudflare.com/client/v4/accounts/${envPrivate.CLOUDFLARE_ACCOUNT_ID}/ai/run/@cf/openai/whisper`;
67

@@ -16,33 +17,69 @@ export const POST: RequestHandler = async ({ request, url }) => {
1617
try {
1718
const data = await request.formData();
1819
const audioFile = data.get('audio') as File;
20+
const model = data.get('model') as string;
1921

2022
if (!audioFile) {
2123
return json({ error: 'No audio file provided' }, { status: 400 });
2224
}
2325

24-
const controller = new AbortController();
25-
const timeoutId = setTimeout(() => controller.abort(), 60000);
26+
switch (model) {
27+
case 'openai-whisper': {
28+
const controller = new AbortController();
29+
const timeoutId = setTimeout(() => controller.abort(), 60000);
2630

27-
const response = await fetch(CLOUDFLARE_API_URL, {
28-
method: 'POST',
29-
headers: {
30-
Authorization: `Bearer ${envPrivate.CLOUDFLARE_WORKERS_AI_API_TOKEN}`,
31-
'Content-Type': 'application/octet-stream'
32-
},
33-
body: audioFile,
34-
signal: controller.signal
35-
});
31+
const response = await fetch(CLOUDFLARE_API_URL, {
32+
method: 'POST',
33+
headers: {
34+
Authorization: `Bearer ${envPrivate.CLOUDFLARE_WORKERS_AI_API_TOKEN}`,
35+
'Content-Type': 'application/octet-stream'
36+
},
37+
body: audioFile,
38+
signal: controller.signal
39+
});
3640

37-
clearTimeout(timeoutId);
41+
clearTimeout(timeoutId);
3842

39-
if (!response.ok) {
40-
throw new Error(`Cloudflare API error: ${response.statusText}`);
41-
}
43+
if (!response.ok) {
44+
throw new Error(`Cloudflare API error: ${response.statusText}`);
45+
}
46+
47+
const res = await response.json();
48+
49+
return json({ ...res.result });
50+
}
51+
52+
case 'elevenlabs-scribe-v1':
53+
default: {
54+
if (!envPrivate.ELEVENLABS_API_KEY) {
55+
return error(500, 'ElevenLabs API key not provided');
56+
}
4257

43-
const res = await response.json();
58+
try {
59+
const client = new ElevenLabsClient({
60+
apiKey: envPrivate.ELEVENLABS_API_KEY
61+
});
4462

45-
return json({ ...res.result });
63+
const audioBlob = new Blob([await audioFile.arrayBuffer()], { type: audioFile.type });
64+
65+
const transcription = await client.speechToText.convert({
66+
file: audioBlob,
67+
model_id: 'scribe_v1',
68+
tag_audio_events: true,
69+
language_code: 'en',
70+
diarize: true
71+
});
72+
73+
return json({
74+
text: transcription.words.map(({ text }) => text).toString(),
75+
words: transcription.words
76+
});
77+
} catch (elevenLabsError: any) {
78+
console.error('ElevenLabs API error:', elevenLabsError);
79+
return error(500, `ElevenLabs API error: ${elevenLabsError.message}`);
80+
}
81+
}
82+
}
4683
} catch (err: any) {
4784
if (err.name === 'AbortError') {
4885
console.error('Request timed out:', err);

0 commit comments

Comments
 (0)