feat(tts): Remove auto engine and fix ios STS

This commit is contained in:
cogwheel0
2025-11-21 13:15:20 +05:30
parent 74807a2f5f
commit 84af6bbe86
15 changed files with 52 additions and 190 deletions

View File

@@ -23,7 +23,7 @@ class TextToSpeechService {
final FlutterTts _tts = FlutterTts();
final AudioPlayer _player = AudioPlayer();
final ApiService? _api;
TtsEngine _engine = TtsEngine.auto;
TtsEngine _engine = TtsEngine.device;
String? _preferredVoice;
String? _serverPreferredVoice;
double _speechRate = 0.5;
@@ -127,11 +127,9 @@ class TextToSpeechService {
final serverAvailable = _api != null;
switch (_engine) {
case TtsEngine.device:
return _deviceEngineAvailable;
return _deviceEngineAvailable || serverAvailable;
case TtsEngine.server:
return serverAvailable;
case TtsEngine.auto:
return _deviceEngineAvailable || serverAvailable;
}
}
@@ -139,10 +137,7 @@ class TextToSpeechService {
if (_engine == TtsEngine.server) {
return _api != null;
}
if (_engine == TtsEngine.device) {
return false;
}
// Auto: prefer device when available, otherwise fall back to server
// Device preference with graceful fallback to server if available.
if (_deviceEngineAvailable) {
return false;
}
@@ -191,7 +186,7 @@ class TextToSpeechService {
double speechRate = 0.5,
double pitch = 1.0,
double volume = 1.0,
TtsEngine engine = TtsEngine.auto,
TtsEngine engine = TtsEngine.device,
}) async {
if (_initialized) {
_engine = engine;

View File

@@ -133,9 +133,8 @@ class VoiceCallService {
final hasLocalStt = _voiceInput.hasLocalStt;
final hasServerStt = _voiceInput.hasServerStt;
final ready = switch (_voiceInput.preference) {
SttPreference.deviceOnly => hasLocalStt,
SttPreference.deviceOnly => hasLocalStt || hasServerStt,
SttPreference.serverOnly => hasServerStt,
SttPreference.auto => hasLocalStt || hasServerStt,
};
if (!ready) {
@@ -240,9 +239,8 @@ class VoiceCallService {
final hasServerStt = _voiceInput.hasServerStt;
final pref = _voiceInput.preference;
final engineAvailable = switch (pref) {
SttPreference.deviceOnly => hasLocalStt,
SttPreference.deviceOnly => hasLocalStt || hasServerStt,
SttPreference.serverOnly => hasServerStt,
SttPreference.auto => hasLocalStt || hasServerStt,
};
if (!engineAvailable) {

View File

@@ -36,7 +36,7 @@ class VoiceInputService {
bool _isInitialized = false;
bool _isListening = false;
bool _localSttAvailable = false;
SttPreference _preference = SttPreference.auto;
SttPreference _preference = SttPreference.deviceOnly;
bool _usingServerStt = false;
String? _selectedLocaleId;
List<LocaleName> _locales = const [];
@@ -63,7 +63,6 @@ class VoiceInputService {
bool get isSupportedPlatform => Platform.isAndroid || Platform.isIOS;
bool get hasServerStt => _api != null;
SttPreference get preference => _preference;
bool get allowsServerFallback => _preference != SttPreference.deviceOnly;
bool get prefersServerOnly => _preference == SttPreference.serverOnly;
bool get prefersDeviceOnly => _preference == SttPreference.deviceOnly;
@@ -101,15 +100,9 @@ class VoiceInputService {
try {
final sttGranted = await _speech.hasPermission();
if (!sttGranted) {
if (prefersDeviceOnly) {
return false;
}
_localSttAvailable = false;
}
} catch (_) {
if (prefersDeviceOnly) {
return false;
}
_localSttAvailable = false;
}
}
@@ -248,11 +241,6 @@ class VoiceInputService {
? 'Speech recognition failed'
: message,
);
if (hasServerStt && allowsServerFallback) {
_textStreamController?.addError(exception);
unawaited(_beginServerFallback());
return;
}
_textStreamController?.addError(exception);
unawaited(_stopListening());
}
@@ -265,6 +253,35 @@ class VoiceInputService {
}
}
Future<void> _startLocalRecognition({
required bool allowOnlineFallback,
}) async {
if (_selectedLocaleId != null) {
await _speech.setLanguage(_selectedLocaleId!);
}
Future<void> attempt(bool offline) => _speech.start(
SttRecognitionOptions(punctuation: true, offline: offline),
);
try {
await attempt(true);
} catch (error) {
if (Platform.isIOS && allowOnlineFallback) {
try {
await attempt(false);
return;
} catch (secondary) {
throw Exception(
'On-device speech failed ($error); '
'online fallback failed ($secondary).',
);
}
}
rethrow;
}
}
Stream<String> startListening() {
if (!_isInitialized) {
throw Exception('Voice input not initialized');
@@ -304,11 +321,10 @@ class VoiceInputService {
final isStillAvailable = await _speech.isSupported();
if (!isStillAvailable && _isListening) {
_localSttAvailable = false;
if (hasServerStt && allowsServerFallback) {
unawaited(_beginServerFallback());
} else {
unawaited(_stopListening());
}
_textStreamController?.addError(
Exception('On-device speech recognition unavailable'),
);
unawaited(_stopListening());
}
} catch (_) {
// ignore availability check errors
@@ -338,19 +354,12 @@ class VoiceInputService {
Future(() async {
try {
if (_selectedLocaleId != null) {
await _speech.setLanguage(_selectedLocaleId!);
}
await _speech.start(SttRecognitionOptions(punctuation: true));
await _startLocalRecognition(allowOnlineFallback: !prefersDeviceOnly);
} catch (error) {
_localSttAvailable = false;
if (!_isListening) return;
if (hasServerStt && allowsServerFallback) {
await _beginServerFallback();
} else {
_textStreamController?.addError(error);
await _stopListening();
}
_textStreamController?.addError(error);
await _stopListening();
}
});
} else if (shouldUseServer) {
@@ -457,39 +466,6 @@ class VoiceInputService {
}
}
Future<void> _beginServerFallback() async {
if (!allowsServerFallback) {
_textStreamController?.addError(
Exception('Server speech-to-text disabled in preferences'),
);
await _stopListening();
return;
}
await _stopLocalStt();
if (!hasServerStt) {
_textStreamController?.addError(
Exception('Server speech-to-text unavailable'),
);
await _stopListening();
return;
}
_usingServerStt = true;
_autoStopTimer?.cancel();
_autoStopTimer = Timer(const Duration(seconds: 90), () {
if (_isListening) {
unawaited(_stopListening());
}
});
try {
await _startServerRecording();
} catch (error) {
_textStreamController?.addError(error);
await _stopListening();
}
}
Future<void> _startServerRecording() async {
await _setupVadStreams();
final settings = _ref?.read(appSettingsProvider);
@@ -823,13 +799,11 @@ Future<bool> voiceInputAvailable(Ref ref) async {
if (!initialized) return false;
switch (service.preference) {
case SttPreference.deviceOnly:
return service.hasLocalStt;
case SttPreference.serverOnly:
return service.hasServerStt;
case SttPreference.auto:
if (service.hasLocalStt) return true;
if (!service.hasServerStt) return false;
break;
case SttPreference.serverOnly:
return service.hasServerStt;
}
final hasPermission = await service.checkPermissions();
if (!hasPermission) return false;

View File

@@ -501,8 +501,6 @@ class AppCustomizationPage extends ConsumerWidget {
warnings.add(l10n.sttServerUnavailableWarning);
}
final bool autoSelectable =
localAvailable || serverAvailable || localLoading;
final bool deviceSelectable = localAvailable || localLoading;
final bool serverSelectable = serverAvailable;
@@ -554,31 +552,6 @@ class AppCustomizationPage extends ConsumerWidget {
spacing: Spacing.sm,
runSpacing: Spacing.sm,
children: [
ChoiceChip(
label: Text(l10n.sttEngineAuto),
selected: settings.sttPreference == SttPreference.auto,
showCheckmark: false,
selectedColor: theme.buttonPrimary,
backgroundColor: theme.cardBackground,
side: BorderSide(
color: settings.sttPreference == SttPreference.auto
? theme.buttonPrimary.withValues(alpha: 0.6)
: theme.textPrimary.withValues(alpha: 0.2),
),
labelStyle: TextStyle(
color: settings.sttPreference == SttPreference.auto
? theme.buttonPrimaryText
: theme.textPrimary,
fontWeight: FontWeight.w600,
),
onSelected: autoSelectable
? (value) {
if (value) {
notifier.setSttPreference(SttPreference.auto);
}
}
: null,
),
ChoiceChip(
label: Text(l10n.sttEngineDevice),
selected:
@@ -684,9 +657,7 @@ class AppCustomizationPage extends ConsumerWidget {
),
),
],
if (settings.sttPreference == SttPreference.serverOnly ||
(settings.sttPreference == SttPreference.auto &&
serverAvailable)) ...[
if (settings.sttPreference == SttPreference.serverOnly) ...[
const SizedBox(height: Spacing.md),
const Divider(),
const SizedBox(height: Spacing.md),
@@ -785,20 +756,11 @@ class AppCustomizationPage extends ConsumerWidget {
final bool deviceAvailable =
ttsService.deviceEngineAvailable || !ttsService.isInitialized;
final bool serverAvailable = ttsService.serverEngineAvailable;
final bool autoSelectable = deviceAvailable || serverAvailable;
final bool deviceSelectable = deviceAvailable;
final bool serverSelectable = serverAvailable;
final ttsDescription = _ttsPreferenceDescription(l10n, settings);
final warnings = <String>[];
switch (settings.ttsEngine) {
case TtsEngine.auto:
if (!deviceAvailable) {
warnings.add(l10n.ttsDeviceUnavailableWarning);
}
if (!serverAvailable) {
warnings.add(l10n.ttsServerUnavailableWarning);
}
break;
case TtsEngine.device:
if (!deviceAvailable) {
warnings.add(l10n.ttsDeviceUnavailableWarning);
@@ -852,37 +814,6 @@ class AppCustomizationPage extends ConsumerWidget {
spacing: Spacing.sm,
runSpacing: Spacing.sm,
children: [
ChoiceChip(
label: Text(l10n.ttsEngineAuto),
selected: settings.ttsEngine == TtsEngine.auto,
showCheckmark: false,
selectedColor: theme.buttonPrimary,
backgroundColor: theme.cardBackground,
side: BorderSide(
color: settings.ttsEngine == TtsEngine.auto
? theme.buttonPrimary.withValues(alpha: 0.6)
: theme.textPrimary.withValues(
alpha: autoSelectable ? 0.2 : 0.12,
),
),
labelStyle: TextStyle(
color: settings.ttsEngine == TtsEngine.auto
? theme.buttonPrimaryText
: theme.textPrimary.withValues(
alpha: autoSelectable ? 1.0 : 0.45,
),
fontWeight: FontWeight.w600,
),
onSelected: autoSelectable
? (value) {
if (value) {
ref
.read(appSettingsProvider.notifier)
.setTtsEngine(TtsEngine.auto);
}
}
: null,
),
ChoiceChip(
label: Text(l10n.ttsEngineDevice),
selected: settings.ttsEngine == TtsEngine.device,
@@ -1060,8 +991,6 @@ class AppCustomizationPage extends ConsumerWidget {
SttPreference preference,
) {
switch (preference) {
case SttPreference.auto:
return l10n.sttEngineAutoDescription;
case SttPreference.deviceOnly:
return l10n.sttEngineDeviceDescription;
case SttPreference.serverOnly:
@@ -1074,8 +1003,6 @@ class AppCustomizationPage extends ConsumerWidget {
AppSettings settings,
) {
switch (settings.ttsEngine) {
case TtsEngine.auto:
return l10n.ttsEngineAutoDescription;
case TtsEngine.device:
return l10n.ttsEngineDeviceDescription;
case TtsEngine.server:
@@ -1093,8 +1020,6 @@ class AppCustomizationPage extends ConsumerWidget {
final serverName = _getDisplayVoiceName(serverVoice, l10n.ttsSystemDefault);
switch (settings.ttsEngine) {
case TtsEngine.auto:
return '${l10n.ttsEngineDevice}: $deviceName${l10n.ttsEngineServer}: $serverName';
case TtsEngine.device:
return deviceName;
case TtsEngine.server: