feat: Add Text-to-Speech settings and customization options

- Introduced new preference keys for TTS settings: voice, speech rate, pitch, and volume.
- Updated SettingsService to handle TTS settings and persist them.
- Enhanced AppSettings to include TTS-related properties.
- Implemented TTS settings UI in AppCustomizationPage, allowing users to select voice and adjust speech parameters.
- Added localization support for TTS settings in multiple languages.
This commit is contained in:
cogwheel0
2025-10-17 14:40:44 +05:30
parent c6acfa68e1
commit 6c81d68e59
18 changed files with 1185 additions and 9 deletions

View File

@@ -47,7 +47,12 @@ class TextToSpeechService {
}
/// Initialize the native TTS engine lazily
Future<bool> initialize() async {
Future<bool> initialize({
String? voice,
double speechRate = 0.5,
double pitch = 1.0,
double volume = 1.0,
}) async {
if (_initialized) {
return _available;
}
@@ -55,14 +60,14 @@ class TextToSpeechService {
try {
await _tts.awaitSpeakCompletion(false);
// Set volume to maximum
await _tts.setVolume(1.0);
// Set volume
await _tts.setVolume(volume);
// Set speech rate (1.0 is normal)
await _tts.setSpeechRate(0.5);
// Set speech rate
await _tts.setSpeechRate(speechRate);
// Set pitch (1.0 is normal)
await _tts.setPitch(1.0);
// Set pitch
await _tts.setPitch(pitch);
if (!kIsWeb && Platform.isIOS) {
await _tts.setSharedInstance(true);
@@ -74,7 +79,8 @@ class TextToSpeechService {
]);
}
await _configurePreferredVoice();
// Set the voice (specific or default)
await _setVoiceByName(voice);
_available = true;
} catch (e) {
_available = false;
@@ -140,6 +146,114 @@ class TextToSpeechService {
await stop();
}
/// Update TTS settings on-the-fly
Future<void> updateSettings({
String? voice,
double? speechRate,
double? pitch,
double? volume,
}) async {
if (!_initialized || !_available) {
return;
}
try {
if (volume != null) {
await _tts.setVolume(volume);
}
if (speechRate != null) {
await _tts.setSpeechRate(speechRate);
}
if (pitch != null) {
await _tts.setPitch(pitch);
}
// Set specific voice by name
await _setVoiceByName(voice);
} catch (e) {
_onError?.call(e.toString());
}
}
/// Set voice by name, or use system default if null
Future<void> _setVoiceByName(String? voiceName) async {
if (kIsWeb || (!Platform.isIOS && !Platform.isAndroid)) {
return;
}
try {
if (voiceName == null) {
// Use system default - reset voice configuration
_voiceConfigured = false;
await _configurePreferredVoice();
return;
}
// Get all available voices
final voicesRaw = await _tts.getVoices;
if (voicesRaw is! List) {
return;
}
// Find the voice by name
Map<String, dynamic>? targetVoice;
for (final entry in voicesRaw) {
if (entry is Map) {
final normalized = _normalizeVoiceEntry(entry);
final name = normalized['name'] as String?;
if (name == voiceName) {
targetVoice = normalized;
break;
}
}
}
// Set the voice if found
if (targetVoice != null) {
await _tts.setVoice(_voiceCommandFrom(targetVoice));
_voiceConfigured = true;
} else {
// Voice not found, fall back to default
_voiceConfigured = false;
await _configurePreferredVoice();
}
} catch (e) {
_onError?.call(e.toString());
}
}
/// Get available voices from the TTS engine
Future<List<Map<String, dynamic>>> getAvailableVoices() async {
if (!_initialized) {
await initialize();
}
if (!_available) {
return [];
}
try {
final voicesRaw = await _tts.getVoices;
if (voicesRaw is! List) {
return [];
}
final parsedVoices = <Map<String, dynamic>>[];
for (final entry in voicesRaw) {
if (entry is Map) {
final normalized = _normalizeVoiceEntry(entry);
if (normalized.isNotEmpty) {
parsedVoices.add(normalized);
}
}
}
return parsedVoices;
} catch (e) {
_onError?.call(e.toString());
return [];
}
}
Future<void> _configurePreferredVoice() async {
if (_voiceConfigured) {
return;