Skip to content

Repository files navigation

Chat Voice Input

A composable voice input for chat composers, with a microphone button, live waveform, timer, and built-in loading, recording, and error states.

Use the AI SDK adapter, native browser speech recognition, or connect any service or local model through the Transcriber interface. The component coordinates capture, stopping, empty results, failures, and cleanup. Audio is never stored by the package.

Live demo

Chat Voice Input recording interface

Install

pnpm add chat-voice-input

React 18 or newer is required.

Use the AI SDK transcriber

The optional AI SDK adapter streams 24 kHz PCM audio through Vercel AI Gateway:

pnpm add ai @ai-sdk/gateway
import { useState } from "react";
import ChatVoiceInput from "chat-voice-input";
import { createAiSdkTranscriber } from "chat-voice-input/ai-sdk";
import "chat-voice-input/style.css";

const transcriber = createAiSdkTranscriber();

function Composer() {
  const [value, setValue] = useState("");

  function appendTranscript(delta: string): void {
    setValue((current) => current + delta);
  }

  return (
    <>
      <textarea onChange={(event) => setValue(event.target.value)} value={value} />
      <ChatVoiceInput
        disabled={false}
        onDelta={appendTranscript}
        transcriber={transcriber}
      />
    </>
  );
}

ChatVoiceInput emits transcription deltas without owning or modifying the editor value. This example appends each delta verbatim; spacing and punctuation come from the transcriber. The consumer can instead decide where and how to apply each delta.

The adapter requests a short-lived token from POST /api/transcription. Add that route to your server:

import { createTranscriptionTokenResponse } from "chat-voice-input/server";

export function POST(): Promise<Response> {
  return createTranscriptionTokenResponse({
    apiKey: process.env.AI_GATEWAY_API_KEY,
  });
}

The provider requests the microphone immediately. The adapter starts capturing PCM and requests its token as soon as the stream is available. Audio captured while the token is pending is consumed when transcription connects.

Keep AI_GATEWAY_API_KEY on the server. Protect the token route with authentication and rate limiting because it spends against your Gateway account. Use tokenEndpoint to configure another route.

Use the native browser transcriber

For a setup without a backend, API key, or additional dependency, use the browser's built-in speech recognition:

import ChatVoiceInput, { createNativeTranscriber } from "chat-voice-input";

const transcriber = createNativeTranscriber();

Pass it to ChatVoiceInput exactly like the AI SDK adapter. You can optionally set the recognition language; otherwise it uses navigator.language:

const transcriber = createNativeTranscriber({ language: "es-ES" });

This adapter uses SpeechRecognition or webkitSpeechRecognition, so availability and transcription quality depend on the browser. On WebKit, speech recognition controls its own audio capture because the browser API cannot consume a provided MediaStream.

Use a custom transcriber

Implement the small Transcriber contract and pass the object to the component:

import type { Transcriber } from "chat-voice-input";

const transcriber: Transcriber = {
  async start({ stream, onDelta, signal }) {
    const recording = await startYourTranscription({ stream, onDelta, signal });

    return {
      stop: recording.stop,
      text: recording.text,
    };
  },
};

The provider opens and closes the microphone. The transcriber receives that stream with an abort signal and a callback for text deltas. It returns a stop function and a promise for the final text.

If your service needs PCM, use the same converter as the AI SDK adapter:

import { createPcmStream } from "chat-voice-input/audio";

const pcm = await createPcmStream(stream); // 24 kHz s16le by default

Pass { sampleRate } to select another rate. Closing the PCM stream releases only its audio graph; microphone ownership stays with the provider.

Compose your own layout

import ChatVoiceInput, { useChatVoiceInput } from "chat-voice-input";

<ChatVoiceInput.Provider
  disabled={disabled}
  onDelta={appendTranscript}
  transcriber={transcriber}
>
  <ChatVoiceInput.Error />
  <ChatVoiceInput.Waveform />
  <ChatVoiceInput.Timer />
  <ChatVoiceInput.Button />
</ChatVoiceInput.Provider>;

useChatVoiceInput() exposes status, stream, start, and stop. Every component is also available as a named export.

The optional stylesheet contains only the built-in control styles and exposes --chat-voice-input-button-background, --chat-voice-input-button-background-hover, and --chat-voice-input-muted for theming. The component does not own its surrounding layout.

Covered edge cases

ChatVoiceInputProvider handles every Transcriber through the same lifecycle. Adapters report microphone and transcription failures through that contract. Any failure shows Voice input is unavailable. and changes the button to Retry.

Scenario Behavior
Voice input is disabled Disables Start; does not call the transcriber
Waiting for microphone permission or transcriber start Shows Loading; disables the button; shows no recording UI
Capture and transcription are active Shows Stop, waveform, and timer
User denies microphone permission Rejects the start; shows the error and Retry
Native transcriber is selected but unavailable Rejects before requesting the microphone; shows the error and Retry
Microphone is unavailable or busy Rejects the start; shows the error and Retry
Transcriber emits text Calls onDelta immediately
User presses Stop Stops capture; shows Loading until final text settles
Its containing form is submitted Stops an active or pending session
Transcriber finishes on its own Stops capture, emits final text if no deltas arrived, and returns to idle
Transcriber ends without text Returns to idle without an error or delta
Remote transcriber is still connecting Captures audio; shows recording only after the adapter confirms it started
Transcriber fails during recording Aborts capture; does not retract emitted deltas; shows the error and Retry
Microphone disconnects during recording Aborts transcription; does not retract emitted deltas; shows the error and Retry
Voice input is disabled or unmounted while active Aborts capture and ignores late results

iOS limitation

In Safari on iOS and iPadOS, native SpeechRecognition can conflict with the component's microphone stream and mute an audio track. After stopping, later recording or recognition attempts may receive no audio. See the WebKit reports for SpeechRecognition muting an existing track and subsequent captures producing no audio. Prefer a stream-based transcriber such as the AI SDK adapter on these platforms.

Development

pnpm install
pnpm check
pnpm test
pnpm build

Run the demo with pnpm demo. Its selector switches between native browser transcription and the streaming AI SDK models available through AI Gateway.

License

MIT

About

Composable React voice input for live transcription.

Topics

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages