local_voice
On-device, zero-cloud voice commands for Flutter. No API keys, no
network calls, no per-request cost — speech recognition runs entirely on
the device via SFSpeechRecognizer (iOS, requiresOnDeviceRecognition = true) and SpeechRecognizer (Android, EXTRA_PREFER_OFFLINE). Once the
platform's on-device model is downloaded, local_voice works in airplane
mode.
🔒 We never send audio anywhere, ever. There is no server component, no analytics, and no third-party SDK in this package. Every method call in
local_voicemaps directly onto an on-device platform API — if a device can't recognize speech on-device,local_voicefails loudly rather than silently falling back to the network.
Define voice commands the way you'd define routes:
final registry = VoiceCommandRegistry(
commands: [
VoiceCommand(
phrases: ['open settings', 'go to settings', 'settings please'],
onMatch: () => Navigator.pushNamed(context, '/settings'),
intent: const NavigateIntent('/settings'),
),
VoiceCommand(
phrases: ['turn on wifi', 'enable wifi'],
intent: const ToggleIntent('wifi', value: true),
onMatch: () => wifi.enable(),
),
],
);
Phrases are matched fuzzily (Levenshtein distance + token-overlap
scoring), not exact-string-matched — "go to settings" and "settings please" both trigger the open settings command above.
Quickstart
final controller = VoiceListenerController();
VoiceListener(
registry: registry,
controller: controller, // drives push-to-talk
mode: VoiceListeningMode.pushToTalk,
child: Scaffold(
body: ...,
floatingActionButton: GestureDetector(
onLongPressStart: (_) => controller.start(),
onLongPressEnd: (_) => controller.stop(),
child: const FloatingActionButton(child: Icon(Icons.mic)),
),
),
)
Before starting a session, check availability/permission:
const localVoice = LocalVoice();
if (await localVoice.isAvailable() && !await localVoice.hasPermission()) {
await localVoice.requestPermission();
}
Listening modes
VoiceListeningMode.pushToTalk(default) — the app starts/stops each session explicitly (hold a mic button, or toggle it). One session, one final transcript.VoiceListeningMode.continuous— the widget automatically restarts a session after every final transcript, so the app stays listening for the next command. Costs more battery, soVoiceListeneralso pauses listening while the app is backgrounded and resumes on foreground.
Ambiguity, not silent guesses
If two or more commands score close together, local_voice refuses to
guess. Register onAmbiguous on the registry to show a "did you mean X or
Y?" prompt, then call registry.resolve(chosen):
VoiceCommandRegistry(
onAmbiguous: (candidates) => showDidYouMeanSheet(candidates),
...
)
Intents, for a fallback UI
Commands can carry a typed VoiceIntent (NavigateIntent, ToggleIntent,
or your own CustomIntent) alongside — or instead of — an onMatch
callback. This lets the same registry that drives voice input also
generate a plain menu/button UI for users who can't or won't use voice:
for (final command in registry.commands)
ListTile(title: Text(command.label), onTap: command.onMatch),
Multi-language phrase sets
CommonPhrases ships pre-translated open/close/back/next/yes/no/cancel/
confirm phrases in 11 languages (en, es, fr, de, pt, hi, bn,
ar, zh, ja, ru) so an internationalized app gets a reasonable
command set without sourcing its own translations:
final locale = CommonPhrases.byLocale(Localizations.localeOf(context).languageCode);
registry.register(locale!.backCommand(onMatch: () => Navigator.pop(context)));
Platform setup
iOS — add to Info.plist:
<key>NSSpeechRecognitionUsageDescription</key>
<string>Used to recognize voice commands, entirely on-device.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used to capture voice commands, entirely on-device.</string>
Android — RECORD_AUDIO is declared by the plugin's manifest and
merged into your app automatically; no action needed beyond calling
requestPermission() at runtime.
Architecture
FuzzyMatcher— pure Dart, Levenshtein + token-overlap scoring. No Flutter dependency, fully unit-testable without a device.VoiceCommandRegistry— holdsVoiceCommands, scores transcripts, applies a confidence threshold, and callsonAmbiguousinstead of guessing when candidates are close.VoiceListener— wraps a subtree, drives the platform event stream into the registry, and manages push-to-talk/continuous session lifecycle.- Platform layer — thin Kotlin/Swift wrappers over
SpeechRecognizer/SFSpeechRecognizer, explicitly requesting on-device recognition.
Status
The example app's native Android/iOS runner projects aren't checked in —
generate them locally with flutter create . from inside example/ (this
package was authored without the Flutter SDK available in-session). The
Dart API, fuzzy matcher, command registry, and VoiceListener widget are
complete and unit-tested; the platform channel implementations are written
to the documented SpeechRecognizer/SFSpeechRecognizer on-device APIs
but need on-device verification before a 1.0 release.
Libraries
- local_voice
- A privacy-first voice command SDK: on-device speech recognition only, no API keys, no cloud, no per-request cost. Declarative command registration with fuzzy phrase matching — define voice commands the way you'd define routes.
- local_voice_method_channel
- local_voice_platform_interface