Give your app a voice your users can just talk to.
Website · Example · GitHub · Sponsor
Vocra is the low-latency voice layer for Flutter: natural turn-taking, lifelike speech, and streaming replies — the user speaks, an AI replies out loud, speech-to-text → LLM → text-to-speech, all orchestrated on-device. No backend, no per-minute platform fees: each app brings its own provider API keys.
sub-second replies · bring your own keys · no backend · open source
- Natural turn-taking — Vocra knows when to listen, when to pause, and when to jump in. Half-duplex by default (the mic is suspended while the AI talks, so it never hears itself); optional full-duplex barge-in with native echo cancellation.
- Pluggable providers — LLM: Groq, OpenAI, Gemini, xAI, Z.ai · STT: Deepgram · TTS: Deepgram, ElevenLabs. Swap any of them in one line, with typed model & voice catalogs built in.
- AI speaks first — an optional greeting (fixed or LLM-generated).
- Human feel — natural-speech mode for brief, spoken-style replies;
[laughs]-style audio tags on ElevenLabs v3. - Full conversation control — mute, interrupt, a live aggregated transcript,
session policies (max duration, silence timeout, end phrases, farewell), and a
SessionReportwhen it's over.
Install
flutter pub add vocra_flutter
Requires Flutter >=3.44.0, Dart ^3.12.0. Android and iOS only.
Platform setup
iOS — add the mic usage string to ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone so you can talk to the AI assistant.</string>
<key>UIBackgroundModes</key>
<array><string>audio</string></array>
…and enable the microphone permission in permission_handler by adding this to
the post_install block of your ios/Podfile. This is required: without
it, iOS never shows the mic prompt and session.start() fails with a permission
ConfigError.
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_MICROPHONE=1',
]
end
end
end
Android (android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
Quickstart
import 'package:vocra_flutter/vocra_flutter.dart';
final session = VocraSession(
config: VocraConfig(
llm: VocraLlm.openAi(apiKey: openAiKey),
stt: VocraStt.deepgram(apiKey: deepgramKey),
tts: VocraTts.elevenLabs(apiKey: elevenLabsKey),
systemPrompt: 'You are a friendly cooking assistant.',
greeting: const Greeting.text('Hey! What are we cooking today?'),
naturalSpeech: true,
),
);
final sub = session.observe(
onState: (state) => print(state), // idle / listening / thinking / speaking
onMessages: (messages) => render(messages), // the live conversation
onError: (error) => showError(error.message),
);
await session.start(); // asks for mic permission, opens the audio session, starts listening
// ... later:
final report = await session.endSession(); // stops and returns a SessionReport
await sub.cancel();
await session.dispose();
That's the whole surface. Pick providers with the VocraLlm / VocraTts /
VocraStt factories, drive your UI from observe, and end with stop() /
endSession() / dispose().
Providers
| Kind | Factory | Catalog / notes |
|---|---|---|
| LLM | VocraLlm.groq(apiKey:, model:) |
GroqModel — default GPT-OSS 20B |
| LLM | VocraLlm.openAi(apiKey:, model:) |
OpenAiModel — default GPT-4.1 Mini |
| LLM | VocraLlm.gemini(apiKey:, model:) |
GeminiModel — default 2.5 Flash |
| LLM | VocraLlm.xai(apiKey:, model:) |
XaiModel — Grok |
| LLM | VocraLlm.zai(apiKey:, model:) |
ZaiModel — GLM |
| STT | VocraStt.deepgram(apiKey:, model:, language:) |
DeepgramSttModel (nova-2 / nova-3) |
| TTS | VocraTts.deepgram(apiKey:, voice:) |
DeepgramVoice (12 Aura voices) |
| TTS | VocraTts.elevenLabs(apiKey:, voice:, model:) |
ElevenLabsVoice / ElevenLabsModel (v3 = [laughs] tags) |
Model & voice catalogs
Every provider ships a typed catalog, so you never hand-maintain model lists.
Pick a constant, enumerate .values for a dropdown, or use .custom('id') for
anything newer than the SDK:
llm: VocraLlm.xai(apiKey: xaiKey, model: XaiModel.grok45),
tts: VocraTts.elevenLabs(apiKey: elKey, voice: ElevenLabsVoice.rachel),
// Build a picker straight from the catalog:
for (final m in GroqModel.values) Text('${m.displayName} — ${m.tier?.displayName}');
// Or an id the catalog doesn't list yet:
llm: VocraLlm.openAi(apiKey: key, model: const OpenAiModel.custom('gpt-5')),
Every catalog entry is a CatalogEntry (id, displayName, note); model
catalogs also carry a ModelTier (budget / balanced / flagship).
Need a custom base URL or client? Construct the underlying adapter
(GroqLlm, OpenAiLlm, XaiLlm, ZaiLlm, DeepgramTts, …) directly — they
satisfy the same interfaces and slot into VocraConfig too.
Structured prompts
For multi-section personas or JSON context, use prompt: instead of
systemPrompt::
prompt: VocraPrompt.sections([
PromptSection('Persona', 'You are Riley, a warm scheduling assistant.'),
PromptSection.json('Business hours', {'mon-fri': '9-5', 'sat': '10-2'}),
]),
Load JSON from a file/asset yourself and pass it to
PromptSection.jsonText(title, jsonString).
Greeting
Greeting.text('Hi! How can I help?')— spoken instantly, no LLM call.Greeting.generated()— the LLM writes the opener from your persona.Greeting.none()(or leavegreetingnull) — the user speaks first.
Session policies
policies: SessionPolicies(
maxDuration: Duration(minutes: 10),
silenceTimeout: Duration(seconds: 30),
endPhrases: ['goodbye', 'talk to you later'],
endMessage: 'Thanks for calling. Take care!', // spoken before auto-ends
),
assistantName: 'Riley',
Any policy ends the session automatically (speaking endMessage first, if set)
and emits a SessionReport on session.sessionEnded.
Control & retrieval
session.mute()/unmute()/isMuted— gate the mic without stopping it.session.interrupt()— cut the current reply, back to listening.session.sendText('...')— typed input;session.speak('...')— scripted assistant line.session.conversation— the messages so far;session.messages— a live stream of the aggregated transcript.session.sessionEnded/session.endSession()— aSessionReportwith the messages, duration, turn count, and why it ended.
Errors & metrics
Every failure (auth, rate limit, network, provider) surfaces as a typed
VoiceError on errors — including mid-stream drops — never a raw exception.
Per-turn latency (ttft, timeToFirstVoice, total, …) arrives as
TurnMetrics via observe(onMetrics:).
Sponsor
Vocra is MIT-licensed and free to use, with no server and no per-minute fees. If it's useful to you, you can support development on GitHub Sponsors.
More at www.vocra.cloud.
License
MIT © 2026 Abdelrahman Shehata — see LICENSE.
Libraries
- vocra_flutter
- The Flutter platform layer of the Vocra voice AI SDK.