Speech Input

Installation

No MCP yet? Connect your agent.

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.