React SDK

useScribe: real-time speech-to-text transcription in React

For an overview of Scribe and its capabilities, see the Speech to Text overview. For step-by-step usage guides, see Client-side streaming.

Installation

$npm install @elevenlabs/react
$# or
$yarn add @elevenlabs/react
$# or
$pnpm install @elevenlabs/react

Use the ElevenLabs speech-to-text skill to transcribe audio from your AI coding assistant:

$npx skills add elevenlabs/skills --skill speech-to-text

@elevenlabs/react re-exports everything from @elevenlabs/client, so you don’t need to install both packages.

Usage

Here is a minimal working example that connects to Scribe and displays real-time transcription:

1import { useScribe } from '@elevenlabs/react';
2import { useEffect } from 'react';
3
4function MyComponent() {
5 const scribe = useScribe({
6 modelId: 'scribe_v2_realtime',
7 onPartialTranscript: (data) => {
8 console.log('Partial:', data.text);
9 },
10 onCommittedTranscript: (data) => {
11 console.log('Committed:', data.text);
12 },
13 });
14
15 // Start recording
16 const handleStart = async () => {
17 try {
18 const token = await fetchTokenFromServer();
19 await scribe.connect({
20 token,
21 microphone: {
22 echoCancellation: true,
23 noiseSuppression: true,
24 },
25 });
26 } catch (err) {
27 console.error('Failed to start recording:', err);
28 }
29 };
30
31 // Stop recording
32 const handleDisconnect = () => {
33 scribe.disconnect();
34 };
35
36 // Disconnect on unmount
37 useEffect(() => {
38 return () => {
39 if (scribe.isConnected) {
40 scribe.disconnect();
41 }
42 };
43 }, [scribe]);
44
45 return (
46 <div>
47 <button onClick={handleStart} disabled={scribe.isConnected}>
48 Start Recording
49 </button>
50 <button onClick={handleDisconnect} disabled={!scribe.isConnected}>
51 Stop
52 </button>
53
54 {scribe.partialTranscript && <p>Live: {scribe.partialTranscript}</p>}
55
56 <div>
57 {scribe.committedTranscripts.map((t) => (
58 <p key={t.id}>{t.text}</p>
59 ))}
60 </div>
61 </div>
62 );
63}

Getting a token

Scribe requires a single-use token for authentication. Create an API endpoint on your server:

1// Node.js server
2app.get('/scribe-token', yourAuthMiddleware, async (req, res) => {
3 const response = await fetch('https://api.elevenlabs.io/v1/single-use-token/realtime_scribe', {
4 method: 'POST',
5 headers: {
6 'xi-api-key': process.env.ELEVENLABS_API_KEY,
7 },
8 });
9
10 const data = await response.json();
11 res.json({ token: data.token });
12});

Your ElevenLabs API key is sensitive. Never expose it to the client. Always generate the token on the server.

1// Client
2const fetchToken = async () => {
3 const response = await fetch('/scribe-token');
4 const { token } = await response.json();
5 return token;
6};

Hook options

Configure the hook with default options and callbacks:

1const scribe = useScribe({
2 // Connection options (can be overridden in connect())
3 token: 'optional-default-token',
4 modelId: 'scribe_v2_realtime',
5 baseUri: 'wss://api.elevenlabs.io',
6
7 // VAD options
8 commitStrategy: CommitStrategy.VAD,
9 vadSilenceThresholdSecs: 0.5,
10 vadThreshold: 0.5,
11 minSpeechDurationMs: 100,
12 minSilenceDurationMs: 500,
13 languageCode: 'en',
14
15 // Microphone options (for automatic mode)
16 microphone: {
17 deviceId: 'optional-device-id',
18 echoCancellation: true,
19 noiseSuppression: true,
20 autoGainControl: true,
21 },
22
23 // Manual audio options (for file transcription)
24 audioFormat: AudioFormat.PCM_16000,
25 sampleRate: 16000,
26
27 // Auto-connect on mount
28 autoConnect: false,
29
30 // Event callbacks
31 onSessionStarted: () => console.log('Session started'),
32 onPartialTranscript: (data) => console.log('Partial:', data.text),
33 onCommittedTranscript: (data) => console.log('Committed:', data.text),
34 onCommittedTranscriptWithTimestamps: (data) => console.log('With timestamps:', data),
35 onError: (error) => console.error('Error:', error),
36 onAuthError: (data) => console.error('Auth error:', data.error),
37 onQuotaExceededError: (data) => console.error('Quota exceeded:', data.error),
38 onConnect: () => console.log('Connected'),
39 onDisconnect: () => console.log('Disconnected'),
40});

Connection options

PropertyTypeDescription
tokenstringSingle-use token for WebSocket authentication.
modelIdstringModel ID (e.g., "scribe_v2_realtime").
baseUristringCustom WebSocket base URI. Defaults to wss://api.elevenlabs.io.

VAD options

These options control when transcripts are automatically committed when using the VAD commit strategy.

PropertyTypeDefaultDescription
commitStrategyCommitStrategy"manual""manual" or "vad".
vadSilenceThresholdSecsnumber1.5Seconds of silence before VAD commits (0.3-3.0).
vadThresholdnumber0.4VAD sensitivity (0.1-0.9, lower is more sensitive).
minSpeechDurationMsnumber100Minimum speech duration in ms (50-2000).
minSilenceDurationMsnumber100Minimum silence duration in ms (50-2000).

Audio options

PropertyTypeDescription
languageCodestringISO-639-1 or ISO-639-3 language code. Leave empty for auto-detection.
microphoneobjectMicrophone settings for microphone mode. See below.
audioFormatAudioFormatAudio encoding format for manual mode (e.g., AudioFormat.PCM_16000).
sampleRatenumberSample rate for manual mode. Must match audioFormat.

The microphone object accepts:

PropertyTypeDescription
deviceIdstringSpecific microphone device ID.
echoCancellationbooleanEnable echo cancellation.
noiseSuppressionbooleanEnable noise suppression.
autoGainControlbooleanEnable automatic gain control.

Behavior options

PropertyTypeDefaultDescription
autoConnectbooleanfalseAutomatically connect on component mount.
includeTimestampsbooleanfalseReceive word-level timestamps. Auto-enabled when onCommittedTranscriptWithTimestamps is provided.

Callbacks

All event callbacks are optional and can be provided as hook options:

  • onConnect - handler called when the WebSocket connection is established.
  • onDisconnect - handler called when the WebSocket connection is closed.
  • onSessionStarted - handler called when the Scribe session starts.
  • onPartialTranscript - handler called with interim transcription results. Receives { text: string }.
  • onCommittedTranscript - handler called with finalized transcription results. Receives { text: string }.
  • onCommittedTranscriptWithTimestamps - handler called with finalized transcription results including word-level timing. Receives { text: string; words?: { start: number; end: number }[] }.
  • onError - generic error handler for all errors. Receives Error | Event.
  • onAuthError - handler called on authentication errors. Receives { error: string }.

Error callbacks

The generic onError callback fires for all errors. Specific error callbacks are also available for granular handling. All specific error callbacks receive { error: string }.

CallbackDescription
onErrorGeneric error handler for all errors.
onAuthErrorAuthentication error.
onQuotaExceededErrorUsage quota exceeded.
onCommitThrottledErrorCommit request throttled.
onTranscriberErrorTranscription engine error.
onUnacceptedTermsErrorTerms of service not accepted.
onRateLimitedErrorRate limited.
onInputErrorInvalid input format.
onQueueOverflowErrorProcessing queue full.
onResourceExhaustedErrorServer resources at capacity.
onSessionTimeLimitExceededErrorMaximum session time reached.
onChunkSizeExceededErrorAudio chunk too large.
onInsufficientAudioActivityErrorNot enough audio activity to maintain the connection.

Microphone mode

Stream audio directly from the user’s microphone:

1function MicrophoneTranscription() {
2 const scribe = useScribe({
3 modelId: 'scribe_v2_realtime',
4 });
5
6 const startRecording = async () => {
7 const token = await fetchToken();
8 await scribe.connect({
9 token,
10 microphone: {
11 echoCancellation: true,
12 noiseSuppression: true,
13 autoGainControl: true,
14 },
15 });
16 };
17
18 return (
19 <div>
20 <button onClick={startRecording} disabled={scribe.isConnected}>
21 {scribe.status === 'connecting' ? 'Connecting...' : 'Start'}
22 </button>
23 <button onClick={scribe.disconnect} disabled={!scribe.isConnected}>
24 Stop
25 </button>
26
27 {scribe.partialTranscript && (
28 <div>
29 <strong>Speaking:</strong> {scribe.partialTranscript}
30 </div>
31 )}
32
33 {scribe.committedTranscripts.map((transcript) => (
34 <div key={transcript.id}>{transcript.text}</div>
35 ))}
36 </div>
37 );
38}

Manual audio mode (file transcription)

Transcribe pre-recorded audio files:

1import { useScribe, AudioFormat } from '@elevenlabs/react';
2import { useState } from 'react';
3
4function FileTranscription() {
5 const [file, setFile] = useState<File | null>(null);
6 const scribe = useScribe({
7 modelId: 'scribe_v2_realtime',
8 audioFormat: AudioFormat.PCM_16000,
9 sampleRate: 16000,
10 });
11
12 const transcribeFile = async () => {
13 if (!file) return;
14
15 const token = await fetchToken();
16 await scribe.connect({ token });
17
18 // Decode audio file
19 const arrayBuffer = await file.arrayBuffer();
20 const audioContext = new AudioContext({ sampleRate: 16000 });
21 const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
22
23 // Convert to PCM16
24 const channelData = audioBuffer.getChannelData(0);
25 const pcmData = new Int16Array(channelData.length);
26
27 for (let i = 0; i < channelData.length; i++) {
28 const sample = Math.max(-1, Math.min(1, channelData[i]));
29 pcmData[i] = sample < 0 ? sample * 32768 : sample * 32767;
30 }
31
32 // Send in chunks
33 const chunkSize = 4096;
34 for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
35 const chunk = pcmData.slice(offset, offset + chunkSize);
36 const bytes = new Uint8Array(chunk.buffer);
37 const base64 = btoa(String.fromCharCode(...bytes));
38
39 scribe.sendAudio(base64);
40 await new Promise((resolve) => setTimeout(resolve, 50));
41 }
42
43 // Commit transcription
44 scribe.commit();
45 };
46
47 return (
48 <div>
49 <input type="file" accept="audio/*" onChange={(e) => setFile(e.target.files?.[0] || null)} />
50 <button onClick={transcribeFile} disabled={!file || scribe.isConnected}>
51 Transcribe
52 </button>
53
54 {scribe.committedTranscripts.map((transcript) => (
55 <div key={transcript.id}>{transcript.text}</div>
56 ))}
57 </div>
58 );
59}

Return values

State

  • status - current connection status: "disconnected", "connecting", "connected", "transcribing", or "error".
  • isConnected - boolean indicating if connected.
  • isTranscribing - boolean indicating if actively transcribing.
  • partialTranscript - current partial (interim) transcript string.
  • committedTranscripts - array of TranscriptSegment objects (see below).
  • error - current error message, or null.
1const scribe = useScribe(/* options */);
2
3console.log(scribe.status); // "connected"
4console.log(scribe.isConnected); // true
5console.log(scribe.partialTranscript); // "hello world"
6console.log(scribe.committedTranscripts); // [{ id: "...", text: "...", words: ..., isFinal: true }]
7console.log(scribe.error); // null or error string

Each committed transcript segment has the following structure:

1interface TranscriptSegment {
2 id: string; // Unique identifier
3 text: string; // Transcript text
4 timestamp: number; // Unix timestamp
5 isFinal: boolean; // Always true for committed transcripts
6}

Methods

connect(options?)

Connect to Scribe. Options provided here override hook defaults:

1await scribe.connect({
2 token: 'your-token', // Required
3 microphone: {
4 /* ... */
5 }, // For microphone mode
6 // OR
7 audioFormat: AudioFormat.PCM_16000, // For manual mode
8 sampleRate: 16000,
9});

disconnect()

Disconnect and clean up resources:

1scribe.disconnect();

sendAudio(audioBase64, options?)

Send audio data (manual mode only):

1scribe.sendAudio(base64AudioChunk, {
2 commit: false, // Optional: commit immediately
3 sampleRate: 16000, // Optional: override sample rate
4 previousText: 'Previous transcription text', // Optional: context from a previous transcription. Can only be sent in the first audio chunk.
5});

The previousText field can only be sent in the first audio chunk of a session. Sending it in subsequent chunks results in an error.

commit()

Manually commit the current transcription:

1scribe.commit();

clearTranscripts()

Clear all transcripts from state:

1scribe.clearTranscripts();

getConnection()

Get the underlying connection instance:

1const connection = scribe.getConnection();
2// Returns RealtimeConnection | null

Commit strategies

Control when transcriptions are committed:

1import { CommitStrategy } from '@elevenlabs/react';
2
3// Manual (default) - you control when to commit
4const scribe = useScribe({
5 commitStrategy: CommitStrategy.MANUAL,
6});
7
8// Later...
9scribe.commit(); // Commit transcription
10
11// Voice Activity Detection - model detects silences and automatically commits
12const scribe = useScribe({
13 commitStrategy: CommitStrategy.VAD,
14});

For more details, see Transcripts and commit strategies.

Complete example

Here is a complete example of a React component using the useScribe hook with VAD-based commit strategy:

1import { useScribe, CommitStrategy } from '@elevenlabs/react';
2import { useEffect } from 'react';
3
4function ScribeDemo() {
5 const scribe = useScribe({
6 modelId: 'scribe_v2_realtime',
7 commitStrategy: CommitStrategy.VAD,
8 onSessionStarted: () => console.log('Started'),
9 onCommittedTranscript: (data) => console.log('Committed:', data.text),
10 onError: (error) => console.error('Error:', error),
11 });
12
13 const startMicrophone = async () => {
14 const token = await fetchToken();
15 await scribe.connect({
16 token,
17 microphone: {
18 echoCancellation: true,
19 noiseSuppression: true,
20 },
21 });
22 };
23
24 const handleDisconnect = () => scribe.disconnect();
25
26 const handleClearTranscripts = () => scribe.clearTranscripts();
27
28 useEffect(() => {
29 return () => {
30 handleDisconnect();
31 };
32 }, []);
33
34 return (
35 <div>
36 <h1>Scribe Demo</h1>
37
38 {/* Status */}
39 <div>
40 Status: {scribe.status}
41 {scribe.error && <span>Error: {scribe.error}</span>}
42 </div>
43
44 {/* Controls */}
45 <div>
46 {!scribe.isConnected ? (
47 <button onClick={startMicrophone}>Start Recording</button>
48 ) : (
49 <button onClick={handleDisconnect}>Stop</button>
50 )}
51 <button onClick={handleClearTranscripts}>Clear</button>
52 </div>
53
54 {/* Live Transcript */}
55 {scribe.partialTranscript && (
56 <div>
57 <strong>Live:</strong> {scribe.partialTranscript}
58 </div>
59 )}
60
61 {/* Committed Transcripts */}
62 <div>
63 <h2>Transcripts ({scribe.committedTranscripts.length})</h2>
64 {scribe.committedTranscripts.map((t) => (
65 <div key={t.id}>
66 <span>{new Date(t.timestamp).toLocaleTimeString()}</span>
67 <p>{t.text}</p>
68 </div>
69 ))}
70 </div>
71 </div>
72 );
73}