flutter_davoice 0.0.12
flutter_davoice: ^0.0.12 copied to clipboard
Flutter voice AI plugin for on-device STT, neural TTS, speaker verification, audio playback, and native iOS/Android voice flows.
flutter_davoice #
Flutter voice AI plugin for Davoice on iOS and Android.
Core Capabilities #
- Speaker verification / speaker identification: speaker onboarding and live verification for speaker-gated voice experiences.
- Wake-word speaker JSON support: accepts the speaker enrollment JSON or saved enrollment JSON path created by the Davoice wake-word library: https://github.com/frymanofer/Flutter_WakeWordDetection/
- Multi-language STT: speech-to-text for conversational apps, including speaker identification and speaker-isolated recognition flows.
- On-device TTS: low-latency Davoice text-to-speech with local voice models.
- VAD / voice activity detection flows: voice activity detection is used in the Davoice voice pipeline and in the companion wake-word package for always-listening and conversational microphone control.
See STT with speaker isolation in action: https://www.youtube.com/watch?v=uYpaCXAvjew
Full Example App #
The runnable Flutter example app is distributed separately because it includes large voice, wake-word, and speaker-verification model assets.
Example app: https://github.com/frymanofer/Flutter_DaVoice
The example app demonstrates:
- voice model selection
- license setup
- speaker verification onboarding
- saved speaker signature reuse
- live speaker verification before wake-word/STT activation
- wake-word detection through
flutter_wake_word - speaker-isolated STT initialization
- on-device TTS
- safe STT pause / TTS speak / STT resume sequencing
- manual TTS testing
- speech echo testing
- Full AI Chat style turn taking
Installation #
dependencies:
flutter_davoice: ^0.0.11
For wake word, speaker onboarding, speaker verification mic flows, and VAD / wake-word front-end behavior, also add the companion package:
dependencies:
flutter_wake_word: ^0.0.52
Companion package: https://github.com/frymanofer/Flutter_WakeWordDetection/
These releases retain the packages' earlier Flutter/Dart minimums. On AGP 8
they apply the Kotlin plugin needed by older Flutter projects; on AGP 9 with
built-in Kotlin they do not apply that plugin. Flutter 3.47 or newer is required
when an app enables android.builtInKotlin=true.
Platform Setup #
Android #
The plugin requires Android minSdk 23 or newer. Keep compileSdk current;
it controls which APIs can be compiled and does not raise the oldest supported
phone.
The Android feature contract is:
| Android version | Basic STT | Custom PCM / AEC | Inline STT SpeakerID gate | TTS / TTS2 |
|---|---|---|---|---|
| API 23–32 | Supported with the system recognizer's microphone input | Unavailable | Unavailable | Supported |
| API 33+ | Supported | Supported | Supported | Supported |
Below API 33, the plugin automatically uses the normal
SpeechRecognizer microphone path. Android's custom audio-source extras were
added in API 33, so the plugin does not attempt custom PCM injection, AEC, or
the current inline SpeakerID gate on older releases.
The complete Android speech package currently supports arm64-v8a. It does
not ship the proprietary TTS native dependencies needed for
armeabi-v7a, x86, or x86_64; those ABIs are not part of the support
contract. API 23 support therefore requires a 64-bit ARM device.
Add permissions used by voice apps:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
iOS #
Add usage descriptions:
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice features.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition to convert speech to text.</string>
The plugin supports iOS 13.0 or newer.
Model Assets #
Add Davoice voice models to your Flutter app assets:
flutter:
assets:
- assets/models/model_ex_ariana_fast.dm
- assets/models/model_ex_rich_fast.dm
Then pass the same asset key to initAll or initTTS:
const arianaFastModel = 'assets/models/model_ex_ariana_fast.dm';
const richFastModel = 'assets/models/model_ex_rich_fast.dm';
On iOS, initTTS also accepts an HTTP or HTTPS model URL. The bridge downloads
it away from the Flutter method-channel thread with bounded request and resource
timeouts before initializing the engine.
Create The Speech API #
import 'dart:async';
import 'package:flutter_davoice/flutter_davoice.dart';
final speech = FlutterDavoice();
final subscriptions = <StreamSubscription<dynamic>>[];
License #
Call setLicense before initializing STT/TTS.
final licensed = await speech.setLicense('YOUR_DAVOICE_LICENSE_KEY');
if (!licensed) {
throw StateError('Davoice license is not valid.');
}
You can also validate a key explicitly:
final valid = await speech.isLicenseValid('YOUR_DAVOICE_LICENSE_KEY');
Listen To STT/TTS Events #
Register event listeners before starting the voice flow:
subscriptions.addAll([
speech.onSpeechStart.listen((_) {
print('STT started');
}),
speech.onSpeechPartialResults.listen((event) {
final text = event.value.join(' ').trim();
print('partial: $text');
}),
speech.onSpeechResults.listen((event) {
final text = event.value.join(' ').trim();
print('final: $text');
}),
speech.onSpeechError.listen((event) {
print('speech error: ${event.code} ${event.message}');
}),
speech.onSpeechVolumeChanged.listen((event) {
print('volume: ${event.value}');
}),
speech.onFinishedSpeaking.listen((_) {
print('TTS or marked playback finished');
}),
speech.onNewSpeechWAV.listen((event) {
print('new speech wav: ${event.path}');
}),
]);
Dispose subscriptions and native resources when your screen/app shuts down:
for (final subscription in subscriptions) {
await subscription.cancel();
}
await speech.destroyAll();
Initialize STT + TTS #
Use initAll when your app needs both STT and TTS in the same flow.
await speech.initAll(
const DavoiceInitAllOptions(
locale: 'en-US',
model: 'assets/models/model_ex_ariana_fast.dm',
timeoutMs: 30000,
),
);
For speaker-isolated STT, pass the saved speaker enrollment JSON file path:
await speech.initAll(
DavoiceInitAllOptions(
locale: 'en-US',
model: 'assets/models/model_ex_ariana_fast.dm',
timeoutMs: 30000,
onboardingJsonPath: savedSpeakerEnrollmentJsonPath,
),
);
The onboardingJsonPath value is the enrollment JSON path produced by the
Davoice wake-word package speaker verification onboarding flow.
Start, Pause, And Resume STT #
Start recognition:
await speech.start(
'en-US',
options: const DavoiceStartOptions(
extraPartialResults: true,
extraMaxResults: 5,
),
);
Pause STT:
await speech.pauseSpeechRecognition();
Resume STT:
final resume = await speech.unPauseSpeechRecognition(-1);
if (resume?['ok'] == false) {
print("STT resume was not confirmed: ${resume?['reason']}");
}
unPauseSpeechRecognition parameters:
times: how many recognition cycles to resume for. Use-1for continuous.preFetchMs: optional prefetch window used by native flows.timeoutMs: native resume timeout; defaults to 500 ms.
On iOS, sttHealthSnapshot() returns read-only bridge, recognizer, route, and
audio-session state for diagnosing a listening session that appears stuck.
Stop or cancel recognition:
await speech.stop();
await speech.cancel();
Check availability:
final available = await speech.isAvailable();
final recognizing = await speech.isRecognizing();
Correct STT/TTS Sequencing #
When your app speaks, pause STT first. Resume STT only after speak returns.
Future<void> speakReply(String text) async {
await speech.pauseSpeechRecognition();
await speech.speak(
text,
speakerId: 0,
speed: 0.88,
);
await speech.unPauseSpeechRecognition(-1);
}
This sequence matters in real voice apps because it prevents STT from hearing the app's own TTS output.
On-Device TTS #
If you only need TTS, initialize the TTS model directly:
await speech.setLicense('YOUR_DAVOICE_LICENSE_KEY');
await speech.initTTS('assets/models/model_ex_ariana_fast.dm');
await speech.speak(
'Hello from Davoice.',
speakerId: 0,
speed: 0.88,
);
Stop current TTS playback:
await speech.stopSpeaking();
Speaker Verification + Speaker-Isolated STT #
Speaker onboarding and live verification are handled by the Davoice wake-word package. This package consumes the enrollment JSON path to initialize speaker-aware STT.
Typical production flow:
- Use
flutter_wake_wordto onboard the speaker. - Save the returned enrollment JSON to a file, for example
sv_enrollment.json. - On next launch, offer:
- use saved signature
- create new signature
- skip speaker verification
- If speaker verification is enabled, verify the speaker first.
- Pass the saved enrollment JSON path to
flutter_davoice. - Initialize wake word and STT with the same saved enrollment path.
The Davoice STT side:
await speech.initAll(
DavoiceInitAllOptions(
locale: 'en-US',
model: 'assets/models/model_ex_ariana_fast.dm',
onboardingJsonPath: savedSpeakerEnrollmentJsonPath,
),
);
You can also start speech with a speaker-verification enrollment JSON path:
await speech.startWithSVOnboardingJson(
'en-US',
savedSpeakerEnrollmentJsonPath,
);
The companion wake-word package also accepts the same enrollment path when starting wake-word detection, so wake-word activation and STT can use the same speaker identity.
Wake-word package: https://github.com/frymanofer/Flutter_WakeWordDetection/
Speech Echo Pattern #
The standalone example app includes a useful test flow:
- Wake word fires.
- App pauses wake-word detection.
- App pauses STT.
- Selected Davoice speaker says an intro line.
- App resumes STT only after
await speak(...)returns. - User speaks.
- App waits for a silence timeout.
- App pauses STT.
- App repeats what the user said with TTS.
- App resumes STT.
Minimal implementation:
Timer? silenceTimer;
String transcript = '';
void onTranscript(String text) {
transcript = text.trim();
silenceTimer?.cancel();
silenceTimer = Timer(const Duration(seconds: 2), () async {
if (transcript.isEmpty) return;
final reply = transcript;
transcript = '';
await speech.pauseSpeechRecognition();
await speech.speak(reply, speakerId: 0, speed: 0.88);
await speech.unPauseSpeechRecognition(-1);
});
}
subscriptions.addAll([
speech.onSpeechPartialResults.listen((event) {
if (event.value.isNotEmpty) {
onTranscript(event.value.first);
}
}),
speech.onSpeechResults.listen((event) {
if (event.value.isNotEmpty) {
onTranscript(event.value.first);
}
}),
]);
Full AI Chat Pattern #
The same STT/TTS control pattern works for AI chat:
- Collect partial/final STT.
- Wait for a silence timeout.
- Pause STT.
- Send the transcript to your AI service.
- Speak the AI reply with Davoice TTS.
- Resume STT.
Timer? silenceTimer;
String pendingUserText = '';
bool aiTurnInFlight = false;
void onUserSpeech(String text) {
pendingUserText = text.trim();
silenceTimer?.cancel();
silenceTimer = Timer(const Duration(seconds: 2), () async {
final userText = pendingUserText;
if (userText.isEmpty || aiTurnInFlight) return;
pendingUserText = '';
aiTurnInFlight = true;
await speech.pauseSpeechRecognition();
try {
final aiReply = await sendToYourAIService(userText);
await speech.speak(aiReply, speakerId: 0, speed: 0.88);
} finally {
aiTurnInFlight = false;
await speech.unPauseSpeechRecognition(-1);
}
});
}
Audio Playback #
Play a WAV file or URL:
await speech.playWav('/path/to/file.wav');
If markAsLast is true, playWav completes after onFinishedSpeaking:
await speech.playWav('/path/to/file.wav', markAsLast: true);
Play PCM:
await speech.playPCM(
pcmBytes,
sampleRate: 16000,
channels: 1,
format: DavoicePcmFormat.i16,
);
Or pass an external PCM descriptor:
await speech.playBuffer(
DavoiceExternalPcm(
base64: base64Pcm,
sampleRate: 16000,
channels: 1,
format: DavoicePcmFormat.i16,
),
);
iOS Permission Helpers #
final hasMic = await speech.hasIOSMicPermissions();
final micGranted = hasMic || await speech.requestIOSMicPermissions(2500);
final hasSpeech = await speech.hasIOSSpeechRecognitionPermissions();
final speechGranted =
hasSpeech || await speech.requestIOSSpeechRecognitionPermissions(2500);
Microphone And Audio Controls #
await speech.pauseMicrophone();
await speech.unPauseMicrophone();
await speech.setAECEnabled(true);
Android Remote STT Helpers #
For Android remote-STT workflows:
await speech.initAllRemoteSTT(
model: 'assets/models/model_ex_ariana_fast.dm',
onboardingJsonPath: savedSpeakerEnrollmentJsonPath,
);
await speech.initAllRemoteSTTAndTTS(
model: 'assets/models/model_ex_ariana_fast.dm',
onboardingJsonPath: savedSpeakerEnrollmentJsonPath,
);
API Reference #
Initialization #
FlutterDavoice({ttsCompletionTimeout})initAll(DavoiceInitAllOptions options)destroyAll()initWithoutModel()destroyWithoutModel()destroyWihtouModel()initTTS(String model)initAllRemoteSTT({required String model, String? onboardingJsonPath})initAllRemoteSTTAndTTS({String? model, String? onboardingJsonPath})clearAudioRoutingConfig()sttListenerDiag()
Speech Recognition #
start(String locale, {DavoiceStartOptions? options})startWithSVOnboardingJson(String locale, String onboardingJsonPath)stop()cancel()isAvailable()isRecognizing()pauseSpeechRecognition({int timeoutMs})unPauseSpeechRecognition(int times, {int preFetchMs, int timeoutMs})sttHealthSnapshot()pauseMicrophone()unPauseMicrophone()
Text To Speech And Playback #
speak(String text, {int speakerId, double speed})stopSpeaking()playWav(String pathOrUrl, {bool markAsLast})playPCM(Uint8List data, {required int sampleRate, int channels, bool interleaved, DavoicePcmFormat format, bool markAsLast})playBuffer(DavoiceExternalPcm pcm)
License And Audio #
setLicense(String licenseKey)isLicenseValid(String licenseKey)setAECEnabled(bool enabled)
Events #
eventsonSpeechStartonSpeechRecognizedonSpeechEndonSpeechErroronSpeechResultsonSpeechPartialResultsonSpeechVolumeChangedonNewSpeechWAVonFinishedSpeaking
iOS Permissions #
hasIOSMicPermissions()requestIOSMicPermissions(int waitTimeoutMs)hasIOSSpeechRecognitionPermissions()requestIOSSpeechRecognitionPermissions(int waitTimeoutMs)
Validation #
flutter analyze
flutter test
flutter pub publish --dry-run
The package repo intentionally excludes the runnable example app and large model assets. Use the standalone example repo for a complete app: https://github.com/frymanofer/Flutter_DaVoice