Features
- Real-time Transcription: Live speech-to-text using ElevenLabs Scribe
- Compound Components: Flexible composition with record button, preview, and cancel
- Animated Transitions: Smooth expand/collapse animations using Framer Motion
- Voice Activity Detection: Automatic transcript commits based on speech pauses
- Visual Feedback: Distinct states for idle, connecting, and recording
- Accessibility: Proper ARIA labels and keyboard interaction
Usage
import {
SpeechInput,
SpeechInputCancelButton,
SpeechInputPreview,
SpeechInputRecordButton,
} from "@/components/ui/speech-input";
Basic Usage
async function getToken() {
const response = await fetch("/api/get-scribe-token", { method: "POST" });
const json = await response.json();
return json.token;
}
export default function Example() {
return (
<SpeechInput
getToken={getToken}
onChange={(data) => console.log(data.transcript)}
onStop={(data) => console.log("Final:", data.transcript)}
>
<SpeechInputRecordButton />
<SpeechInputPreview placeholder="Start speaking..." />
<SpeechInputCancelButton />
</SpeechInput>
);
}
With Form Input
import { useState } from "react";
export default function Example() {
const [value, setValue] = useState("");
return (
<div className="flex items-center gap-2">
<input
value={value}
onChange={(e) => setValue(e.target.value)}
className="flex-1 rounded border px-3 py-2"
/>
<SpeechInput
getToken={getToken}
onStop={(data) => setValue((prev) => prev + " " + data.transcript)}
>
<SpeechInputRecordButton />
<SpeechInputPreview />
<SpeechInputCancelButton />
</SpeechInput>
</div>
);
}
Reversed Layout
The component automatically adjusts its layout based on child order:
<SpeechInput getToken={getToken}>
<SpeechInputCancelButton />
<SpeechInputPreview />
<SpeechInputRecordButton />
</SpeechInput>
Minimal (Record Button Only)
<SpeechInput getToken={getToken} onStop={(data) => console.log(data.transcript)}>
<SpeechInputRecordButton />
</SpeechInput>
Custom Placeholder
<SpeechInput getToken={getToken}>
<SpeechInputRecordButton />
<SpeechInputPreview placeholder="Say something..." />
<SpeechInputCancelButton />
</SpeechInput>
Using the Hook
Access the speech input context in child components:
import { useSpeechInput } from "@/components/ui/speech-input";
function TranscriptDisplay() {
const { transcript, isConnected, isConnecting } = useSpeechInput();
return (
<div>
<p>Status: {isConnecting ? "Connecting" : isConnected ? "Recording" : "Idle"}</p>
<p>Transcript: {transcript}</p>
</div>
);
}
Server Action for Token
Create a server action to securely fetch the Scribe token:
"use server";
export async function getScribeToken() {
const response = await fetch("https://api.elevenlabs.io/v1/speech-to-text/get-realtime-token", {
method: "POST",
headers: {
"Content-Type": "application/json",
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
},
body: JSON.stringify({
model_id: "scribe_v2_realtime",
ttl_secs: 300,
}),
});
const data = await response.json();
return data.token;
}
Microphone input requires a secure browser context and user permission. Provide a usable permission-denied state.