Fifty Speech Engine
One engine, two speech modes -- TTS narration and STT recognition with builder-customizable controls.
Unifies flutter_tts and speech_to_text behind a single initialize()/speak()/startListening() API. Ships three FDL-styled control widgets (SpeechTtsControls, SpeechSttControls, SpeechControlsPanel) with optional contentBuilder callbacks that let you replace the default UI while keeping all state management intact. Part of Fifty Flutter Kit.
| TTS Panel | STT Panel |
|---|---|
![]() |
![]() |
Why fifty_speech_engine
- One engine, two speech modes --
FiftySpeechEngineunifiesflutter_ttsandspeech_to_textbehind a singleinitialize()/speak()/startListening()API; no managing two SDKs. - Custom TTS and STT controls, logic unchanged --
SpeechTtsControlsandSpeechSttControlsaccept optionalcontentBuildercallbacks; replace the default FDL UI while keeping all state management intact. - Type-safe builder state -- Builders receive
SpeechTtsState(enabled, rate, pitch, volume, isSpeaking) andSpeechSttState(enabled, isListening, recognizedText, isAvailable) data classes, not raw booleans; your custom UI compiles cleanly. - Continuous and command modes -- Set
listenContinuously: truefor dictation,falsefor single voice commands; the same engine handles both.
Installation
dependencies:
fifty_speech_engine: ^0.2.1
For Contributors
dependencies:
fifty_speech_engine:
path: ../fifty_speech_engine
Dependencies: speech_to_text, flutter_tts, fifty_tokens, fifty_theme, fifty_ui
Quick Start
import 'package:fifty_speech_engine/fifty_speech_engine.dart';
// 1. Create engine with locale
final engine = FiftySpeechEngine(
locale: Locale('en', 'US'),
onSttResult: (text) async => print('Heard: $text'),
onSttError: (error) => print('Error: $error'),
);
// 2. Initialize
await engine.initialize();
// 3. Speak text
await engine.speak('Hello! How can I help you?');
// 4. Listen for voice input
await engine.startListening(continuous: true);
// 5. Stop listening when done
await engine.stopListening();
// 6. Clean up
engine.dispose();
Architecture
FiftySpeechEngine
|
+-- tts: TtsManager
| Text-to-Speech synthesis via flutter_tts
|
+-- stt: SttManager
Speech-to-Text recognition via speech_to_text
Core Components
| Component | Description |
|---|---|
FiftySpeechEngine |
Unified interface combining TTS and STT |
TtsManager |
Text-to-Speech handler with voice configuration |
SttManager |
Speech-to-Text handler with queue support |
SpeechResultModel |
Result container for recognized speech |
Widgets
Three FDL-styled control widgets for TTS and STT. All are callback-based (no internal state management dependency) and accept optional builder callbacks for custom UI.
SpeechTtsControls
TTS enable/disable toggle with optional rate, pitch, and volume sliders. Speaking indicator shown when active.
SpeechTtsControls(
enabled: ttsEnabled,
onEnabledChanged: (value) => setState(() => ttsEnabled = value),
rate: rate,
onRateChanged: (value) => setState(() => rate = value),
pitch: pitch,
onPitchChanged: (value) => setState(() => pitch = value),
volume: volume,
onVolumeChanged: (value) => setState(() => volume = value),
isSpeaking: isSpeaking,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
required | Whether TTS is enabled |
onEnabledChanged |
ValueChanged<bool> |
required | Toggle callback |
rate |
double |
1.0 |
Speech rate (0.5--2.0) |
onRateChanged |
ValueChanged<double>? |
null |
Rate slider callback; null hides the slider |
pitch |
double |
1.0 |
Speech pitch (0.5--2.0) |
onPitchChanged |
ValueChanged<double>? |
null |
Pitch slider callback; null hides the slider |
volume |
double |
1.0 |
Speech volume (0.0--1.0) |
onVolumeChanged |
ValueChanged<double>? |
null |
Volume slider callback; null hides the slider |
isSpeaking |
bool |
false |
Whether TTS is currently speaking |
compact |
bool |
false |
Use compact layout |
showCard |
bool |
true |
Wrap in FiftyCard; set false when embedding |
contentBuilder |
SpeechTtsContentBuilder? |
null |
Custom content builder (see Customization) |
SpeechSttControls
STT enable/disable toggle with microphone button, pulsing listening indicator, recognized text display, and error handling.
SpeechSttControls(
enabled: sttEnabled,
onEnabledChanged: (value) => setState(() => sttEnabled = value),
isListening: isListening,
onListenPressed: toggleListening,
recognizedText: recognizedText,
onClear: () => setState(() => recognizedText = ''),
errorMessage: errorMessage,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
required | Whether STT is enabled |
onEnabledChanged |
ValueChanged<bool> |
required | Toggle callback |
isListening |
bool |
required | Whether STT is currently listening |
onListenPressed |
VoidCallback |
required | Microphone button callback |
recognizedText |
String |
'' |
Last recognized text |
isAvailable |
bool |
true |
Whether STT is available on this device |
errorMessage |
String? |
null |
Error message to display |
onClear |
VoidCallback? |
null |
Clear text callback; null hides the clear button |
compact |
bool |
false |
Use compact layout |
showCard |
bool |
true |
Wrap in FiftyCard; set false when embedding |
hintText |
String? |
'TAP TO SPEAK' |
Hint text below microphone button |
contentBuilder |
SpeechSttContentBuilder? |
null |
Custom content builder (see Customization) |
SpeechControlsPanel
Combined panel with both TTS and STT controls in a single card. Supports showTts and showStt flags to selectively display either section.
SpeechControlsPanel(
ttsEnabled: ttsEnabled,
onTtsEnabledChanged: (v) => setState(() => ttsEnabled = v),
rate: rate,
onRateChanged: (v) => setState(() => rate = v),
isSpeaking: isSpeaking,
sttEnabled: sttEnabled,
onSttEnabledChanged: (v) => setState(() => sttEnabled = v),
isListening: isListening,
onListenPressed: toggleListening,
recognizedText: recognizedText,
)
| Parameter | Type | Default | Description |
|---|---|---|---|
showTts |
bool |
true |
Show TTS controls section |
showStt |
bool |
true |
Show STT controls section |
compact |
bool |
false |
Use compact layout for both sections |
title |
String? |
null |
Optional panel title |
ttsBuilder |
SpeechTtsContentBuilder? |
null |
Custom TTS content builder |
sttBuilder |
SpeechSttContentBuilder? |
null |
Custom STT content builder |
All TTS and STT parameters from the individual widgets are also available on the panel.
Customization
All three widgets accept optional builder callbacks. When provided, the builder replaces the default FDL rendering while the widget retains ownership of the FiftyCard wrapper (when showCard is true) and all state computation.
Custom TTS Controls
Replace the default TTS controls layout. The builder receives a SpeechTtsState data class with all state values and callbacks:
SpeechTtsControls(
enabled: ttsEnabled,
onEnabledChanged: (v) => setState(() => ttsEnabled = v),
rate: rate,
onRateChanged: (v) => setState(() => rate = v),
isSpeaking: isSpeaking,
contentBuilder: (state) {
return Column(
children: [
Switch(value: state.enabled, onChanged: state.onEnabledChanged),
if (state.enabled) ...[
Slider(value: state.rate, min: 0.5, max: 2.0, onChanged: state.onRateChanged),
if (state.isSpeaking) const Text('Speaking...'),
],
],
);
},
)
SpeechTtsState
| Field | Type | Description |
|---|---|---|
enabled |
bool |
Whether TTS is enabled |
onEnabledChanged |
ValueChanged<bool> |
Toggle callback |
rate |
double |
Current speech rate |
onRateChanged |
ValueChanged<double>? |
Rate change callback |
pitch |
double |
Current pitch |
onPitchChanged |
ValueChanged<double>? |
Pitch change callback |
volume |
double |
Current volume |
onVolumeChanged |
ValueChanged<double>? |
Volume change callback |
isSpeaking |
bool |
Whether TTS is speaking |
compact |
bool |
Whether compact mode is active |
Custom STT Controls
Replace the default STT controls layout. The builder receives a SpeechSttState data class:
SpeechSttControls(
enabled: sttEnabled,
onEnabledChanged: (v) => setState(() => sttEnabled = v),
isListening: isListening,
onListenPressed: toggleListening,
recognizedText: recognizedText,
contentBuilder: (state) {
return Column(
children: [
Switch(value: state.enabled, onChanged: state.onEnabledChanged),
if (state.enabled) ...[
IconButton(
icon: Icon(state.isListening ? Icons.mic : Icons.mic_none),
onPressed: state.onListenPressed,
),
if (state.recognizedText.isNotEmpty)
Text('"${state.recognizedText}"'),
],
],
);
},
)
SpeechSttState
| Field | Type | Description |
|---|---|---|
enabled |
bool |
Whether STT is enabled |
onEnabledChanged |
ValueChanged<bool> |
Toggle callback |
isListening |
bool |
Whether STT is listening |
onListenPressed |
VoidCallback |
Microphone button callback |
recognizedText |
String |
Last recognized text |
isAvailable |
bool |
Whether STT is available |
errorMessage |
String? |
Error message |
onClear |
VoidCallback? |
Clear text callback |
compact |
bool |
Whether compact mode is active |
hintText |
String? |
Hint text for microphone button |
Custom Panel Sections
SpeechControlsPanel forwards builder callbacks to the internal SpeechTtsControls and SpeechSttControls:
SpeechControlsPanel(
ttsEnabled: ttsEnabled,
onTtsEnabledChanged: (v) => setState(() => ttsEnabled = v),
isSpeaking: isSpeaking,
sttEnabled: sttEnabled,
onSttEnabledChanged: (v) => setState(() => sttEnabled = v),
isListening: isListening,
onListenPressed: toggleListening,
recognizedText: recognizedText,
ttsBuilder: (state) => MyTtsControls(state: state),
sttBuilder: (state) => MySttControls(state: state),
)
Builder Signatures
| Widget | Builder parameter | Callback signature |
|---|---|---|
SpeechTtsControls |
contentBuilder |
Widget Function(SpeechTtsState state) |
SpeechSttControls |
contentBuilder |
Widget Function(SpeechSttState state) |
SpeechControlsPanel |
ttsBuilder |
Widget Function(SpeechTtsState state) |
SpeechControlsPanel |
sttBuilder |
Widget Function(SpeechSttState state) |
All builders are optional. Omit them to use the default FDL UI.
API Reference
FiftySpeechEngine
Main unified controller.
/// Create with locale and optional callbacks
FiftySpeechEngine({
required Locale locale,
Future Function(String text)? onSttResult,
void Function(String error)? onSttError,
})
/// Initialize both TTS and STT engines
Future<void> initialize()
/// Text-to-Speech
Future<void> speak(String text)
Future<void> stopSpeaking()
bool get isSpeaking
/// Speech-to-Text
Future<void> startListening({bool continuous = false})
Future<void> stopListening()
Future<void> cancelListening()
bool get isListening
/// Access individual managers
TtsManager get tts
SttManager get stt
/// Clean up resources
void dispose()
TtsManager
Text-to-Speech handler.
/// Initialize with language and optional voice
Future<void> initialize({
String language = 'en-US',
String? voiceId,
})
/// Speak text aloud
Future<void> speak(String text)
/// Stop current speech
Future<void> stop()
/// Change language at runtime
Future<void> changeLanguage(String language, {String? voiceId})
/// Status
bool get isSpeaking
/// Callback when speech completes
VoidCallback? onSpeechComplete
Language Examples:
await tts.initialize(language: 'en-US'); // English (US)
await tts.initialize(language: 'fr-FR'); // French
await tts.initialize(language: 'de-DE'); // German
await tts.initialize(language: 'ja-JP'); // Japanese
SttManager
Speech-to-Text handler with queue support.
/// Initialize the STT engine
Future<bool> initialize()
/// Start listening for speech
Future<void> startListening({
String localeId = 'en_US',
bool partialResults = true,
bool listenContinuously = true,
})
/// Stop listening (returns final result)
Future<void> stopListening()
/// Cancel listening (discards results)
Future<void> cancelListening()
/// Status
bool get isListening
bool get isAvailable
/// Clear queued results
void flushQueue()
/// Callbacks
Future<void> Function(String text)? onResult
void Function(String error)? onError
Listen Modes:
listenContinuously: true- Dictation mode for longer inputlistenContinuously: false- Confirmation mode for commands
SpeechResultModel
Container for recognition results.
/// Properties
final String text // Recognized text
final bool isFinal // Whether result is final
/// Constructors
SpeechResultModel(text, isFinal)
SpeechResultModel.final_(text)
SpeechResultModel.partial(text)
Usage Patterns
Voice Commands
final engine = FiftySpeechEngine(
locale: Locale('en', 'US'),
onSttResult: (text) async {
final command = text.toLowerCase();
if (command.contains('attack')) {
await performAttack();
} else if (command.contains('defend')) {
await performDefend();
} else if (command.contains('help')) {
await engine.speak('Available commands: attack, defend, help');
}
},
);
await engine.initialize();
await engine.startListening();
Interactive Storytelling
// Narrate story segment
await engine.speak('You enter a dark cave. What do you do?');
// Wait for response
engine.stt.onResult = (text) async {
if (text.contains('light') || text.contains('torch')) {
await engine.speak('You light a torch, revealing ancient runes on the walls.');
} else if (text.contains('back') || text.contains('leave')) {
await engine.speak('You step back into the sunlight.');
}
};
await engine.startListening();
Accessibility Features
// Read UI elements aloud
Future<void> announceButton(String label) async {
await engine.speak('Button: $label');
}
// Describe screen content
Future<void> describeScreen(String description) async {
await engine.speak(description);
}
Language Switching
// Switch TTS language dynamically
await engine.tts.changeLanguage('es-ES');
await engine.speak('Hola, bienvenido al juego!');
// Switch back
await engine.tts.changeLanguage('en-US');
await engine.speak('Welcome back!');
Best Practices
- Initialize once - Call
initialize()at app startup - Handle errors - Always provide
onSttErrorcallback - Check availability - Use
stt.isAvailablebefore listening - Clean up - Call
dispose()when done - Request permissions - Handle microphone permissions before STT
The example directory contains a complete demo app with MVVM + Actions architecture, TTS/STT panels, language selection for 9 languages, and FDL styling.
Platform Support
| Platform | TTS | STT | Notes |
|---|---|---|---|
| Android | Yes | Yes | Requires RECORD_AUDIO permission |
| iOS | Yes | Yes | Requires microphone usage description |
| macOS | Yes | Yes | Requires audio-input entitlement |
| Linux | Yes | Limited | STT requires libspeechd |
| Windows | No | No | Not supported |
| Web | Yes | Yes | Browser-dependent |
Platform Configuration
Android
Add to android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Speech Recognition Permissions -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<!-- ... your application config ... -->
</application>
<!-- Required for Android 11+ (API 30+) package visibility -->
<queries>
<intent>
<action android:name="android.speech.RecognitionService"/>
</intent>
</queries>
</manifest>
Important Notes:
RECORD_AUDIOis required for STT to function- The
<queries>block is required for Android 11+ (API 30+) due to package visibility restrictions - Without the queries block,
stt.initialize()will returnfalseon Android 11+ devices
iOS
Add to ios/Runner/Info.plist:
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition for voice commands.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for speech recognition.</string>
Important Notes:
- Both keys are required - iOS will crash without them
- Customize the description strings for your app's use case
- Users must grant permission when prompted
macOS
Add to macos/Runner/*.entitlements (both Debug and Release):
<key>com.apple.security.device.audio-input</key>
<true/>
Fifty Design Language Integration
This package is part of Fifty Flutter Kit:
- FDL-styled widgets -
SpeechTtsControls,SpeechSttControls, andSpeechControlsPaneluseFiftyCard,FiftySwitch,FiftySlider, and FDL design tokens for consistent styling - Builder-customizable - Replace any widget's inner content via
contentBuilderwhile keeping FDL card wrappers and layout intact (see Customization) - Compatible packages - Works with
fifty_audio_engine,fifty_tokens,fifty_theme,fifty_ui - Unified locale handling - Consistent with other Fifty packages
Version
Current: 0.2.1
License
MIT License - see LICENSE for details.
Part of Fifty Flutter Kit.

