Expo / React Native
Example: examples/react-native-expo
Full binding: expo-adapters.ts.
cd examples/react-native-expocp .env.example .envbun run startPreferred input: 16 kHz · mono · PCM16. The runtime does not depend on Expo packages; you inject small adapters:
import { createExpoRuntime } from '@ottervoice/runtime-react-native';import { createVoiceSession } from '@ottervoice/core';
const runtime = createExpoRuntime({ input: { /* Expo AudioStream / recorder bindings */ }, output: { /* AudioPlaylist / playback bindings */ },});
const session = createVoiceSession({ mode: 'full_duplex', audioLlmStartTiming: 'after_audio', // Run final ASR and reply generation in parallel. asrPartial: false, // One final ASR call; clients cannot amplify rolling requests. runtime, providers: { asr, audioLlm },});The example’s createMobileProviders() always returns the unified
providers.audioLlm contract:
EXPO_PUBLIC_OTTERVOICE_BACKEND |
Server implementation | Client providers |
|---|---|---|
composite (default) |
One /asr-llm-tts request runs ASR → LLM → TTS |
audioLlm |
native |
/audio-llm native speech model; /asr is optional captions only |
audioLlm + optional asr |
Switching backends does not change Session, Runtime, events, or UI. Mobile has no classic ASR / LLM / TTS three-provider path.
Dual delivery:
delivery: 'stream'— small PCM buffers → streaming ASRdelivery: 'turn'— complete WAV → batch ASR / Audio LLM
The example explicitly uses audioLlmStartTiming: 'after_audio'. Once the full
user audio is ready, final ASR and the Audio LLM reply request start in parallel,
reducing user_audio_end to assistant_audio_start latency. The assistant reply
may arrive before asr_final, so correlate UI and persistence events by turnId.
This mode may spend on a natural pause that later speech supersedes; enforce
per-user, conversation, and profile budgets, rate limits, and idempotency at the
gateway.
Suspend encoded capture during assistant playback while keeping volume callbacks for barge-in. See runtime-react-native.
App lifecycle
Section titled “App lifecycle”When the app leaves active, the example calls finish('app_background') and
then dispose() to stop capture, requests, and playback. Returning to the
foreground requires an explicit new session. The runtime cancels asynchronous
microphone/player setup that completes after stop() and removes temporary
audio files, preventing background resource leaks.