Speech-to-Text

Turn speech into text without sending audio anywhere — live from the microphone or from a recorded file.

iOSAndroidNew

Overview

expo-ai-kit uses each platform's own speech engine: Apple's SpeechAnalyzer on iOS 26+ and ML Kit GenAI Speech Recognition on Android 12+ (which upgrades itself to Gemini Nano on devices that have it). Both run entirely on the device, so transcription works offline and audio never leaves the phone.

  • streamTranscription() — live microphone transcription with updates that revise as the engine hears more
  • transcribe() — a complete transcript from an audio file (WAV, M4A, MP3, …)
  • Explicit availability, model preparation, locales, and typed errors — the same lifecycle style as the rest of the library

Enable speech

Speech is off by default because it adds microphone permissions to your app. Turn it on in your app config and make a new native build (dev client or EAS — not an OTA update):

app.jsonjson
{
  "expo": {
    "plugins": [["expo-ai-kit", { "speech": true }]]
  }
}

The flag compiles the Android speech backend, adds RECORD_AUDIO on Android, and adds NSMicrophoneUsageDescription on iOS. Customize the iOS purpose string with { "speech": { "microphonePermission": "…" } }. Without the flag, speech APIs throw a typed SPEECH_NOT_ENABLED error and your app pays zero size or permission cost.

Availability & preparation

Check support first, and download the OS-managed speech model when the device asks for it. English often comes preinstalled; other languages usually need a one-time download.

import {
  getSpeechRecognitionAvailability,
  prepareSpeechRecognition,
} from 'expo-ai-kit';

const availability = await getSpeechRecognitionAvailability({ locale: 'en-US' });
// { status: 'available' } — ready now
// { status: 'downloadable' | 'downloading' } — supported, model not ready yet
// { status: 'unavailable', reason: 'platform' | 'os-version' | 'device' | 'locale' | 'not-enabled' }

if (availability.status === 'downloadable') {
  await prepareSpeechRecognition({
    locale: 'en-US',
    onProgress: (progress) => console.log(progress), // 0..1
  });
}

Live transcription

Ask for microphone permission, start listening, and render the transcript as it forms. Updates carry the full transcript so far; isFinal marks the moments the engine commits a segment.

import {
  requestSpeechPermissionsAsync,
  streamTranscription,
} from 'expo-ai-kit';

const permission = await requestSpeechPermissionsAsync();
if (!permission.granted) return;

const { promise, stop } = streamTranscription(
  (update) => setText(update.text),   // full transcript so far
  { locale: 'en-US' }                 // optional — defaults to the device locale
);

// When the user releases the button:
stop();
const { text } = await promise;

One speech session runs at a time; a second call rejects with SPEECH_BUSY. Speech never blocks text generation — a voice pipeline like listen → send to the model → answer works without tripping INFERENCE_BUSY.

Transcribe a file

import { transcribe } from 'expo-ai-kit';

const result = await transcribe({
  audio: { uri: recording.uri },  // or { base64, mediaType }
  locale: 'en-US',                // optional
  signal: controller.signal,      // optional AbortSignal
});

result.text;             // the transcript
result.segments;         // iOS: [{ text, startSeconds, endSeconds }]; Android: []
result.durationSeconds;  // decoded audio length
result.language;         // the locale the engine used

Android transcribes at real-time rate

The Android engine ingests audio at playback speed, so a 60-second file takes about a minute — right for voice notes and dictation, wrong for podcast-length audio. iOS is faster than real time and returns timestamped segments. Android also requires the microphone permission even for file input (an engine requirement).

Languages

Every speech API takes an optional BCP-47 locale and defaults to the device language. Android supports 15 languages in Basic mode (21 with Gemini Nano); iOS supports about 20 languages across 42 regional variants. Ask the running device for the authoritative list:

import { getSupportedSpeechLocales } from 'expo-ai-kit';

const locales = await getSupportedSpeechLocales();
// e.g. ['de-DE', 'en-US', 'ja-JP', 'tr-TR', …]

AI SDK

The provider exposes the same engine as an AI SDK transcription model — the first cross-platform on-device one:

import { transcribe } from 'ai';
import { expoAiKit } from 'expo-ai-kit/ai';

const result = await transcribe({
  model: expoAiKit.transcriptionModel(),
  audio: audioUint8Array,
  providerOptions: { 'expo-ai-kit': { locale: 'en-US' } },
});

Platform notes

  • iOS (26+): SpeechAnalyzer — batch is faster than real time with native timestamped segments; file transcription needs no permission at all. Model assets are OS-managed and shared across apps, so they add nothing to your app size.
  • Android (12+): ML Kit GenAI Speech Recognition — text-only results at real-time rate; Gemini Nano quality on devices that support it, automatically.
  • On devices below these OS versions, getSpeechRecognitionAvailability() reports { status: 'unavailable', reason: 'os-version' } and the calls throw typed errors — nothing fails silently.

Errors

Speech failures throw ModelError with a typed .code: SPEECH_NOT_ENABLED, SPEECH_BUSY, MIC_PERMISSION_DENIED, AUDIO_DECODE_FAILED, TRANSCRIPTION_FAILED, plus the shared DEVICE_NOT_SUPPORTED, MODEL_NOT_DOWNLOADED, and INFERENCE_CANCELLED. See Troubleshooting for the full table.