diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 0000000..8ed4c34 Binary files /dev/null and b/flutter_01.png differ diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index 89693c0..6d95faa 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -12,7 +12,7 @@ part 'settings_service.g.dart'; enum SttPreference { auto, deviceOnly, serverOnly } /// TTS engine selection -enum TtsEngine { device, server } +enum TtsEngine { auto, device, server } /// Service for managing app-wide settings including accessibility preferences class SettingsService { @@ -223,11 +223,15 @@ class SettingsService { static TtsEngine _parseTtsEngine(String? raw) { switch ((raw ?? '').toLowerCase()) { + case 'auto': + case '': + return TtsEngine.auto; case 'server': return TtsEngine.server; case 'device': - default: return TtsEngine.device; + default: + return TtsEngine.auto; } } @@ -409,7 +413,7 @@ class AppSettings { this.ttsSpeechRate = 0.5, this.ttsPitch = 1.0, this.ttsVolume = 1.0, - this.ttsEngine = TtsEngine.device, + this.ttsEngine = TtsEngine.auto, this.ttsServerVoiceId, this.ttsServerVoiceName, }); diff --git a/lib/features/chat/providers/text_to_speech_provider.dart b/lib/features/chat/providers/text_to_speech_provider.dart index 2f53bd6..36bcb03 100644 --- a/lib/features/chat/providers/text_to_speech_provider.dart +++ b/lib/features/chat/providers/text_to_speech_provider.dart @@ -107,11 +107,9 @@ class TextToSpeechController extends Notifier { // Listen to settings changes and update TTS when initialized ref.listen(appSettingsProvider, (previous, next) { if (_service.isInitialized && _service.isAvailable) { - final selectedVoice = next.ttsEngine == TtsEngine.server - ? next.ttsServerVoiceId - : next.ttsVoice; _service.updateSettings( - voice: selectedVoice, + voice: next.ttsVoice, + serverVoice: next.ttsServerVoiceId, speechRate: next.ttsSpeechRate, pitch: next.ttsPitch, volume: next.ttsVolume, @@ -137,9 +135,8 @@ class TextToSpeechController extends Notifier { final settings = ref.read(appSettingsProvider); final future = _service .initialize( - voice: settings.ttsEngine == TtsEngine.server - ? settings.ttsServerVoiceId - : settings.ttsVoice, + deviceVoice: settings.ttsVoice, + serverVoice: settings.ttsServerVoiceId, speechRate: settings.ttsSpeechRate, pitch: settings.ttsPitch, volume: settings.ttsVolume, diff --git a/lib/features/chat/services/text_to_speech_service.dart b/lib/features/chat/services/text_to_speech_service.dart index 56de3b5..0f9ae2e 100644 --- a/lib/features/chat/services/text_to_speech_service.dart +++ b/lib/features/chat/services/text_to_speech_service.dart @@ -16,8 +16,9 @@ class TextToSpeechService { final FlutterTts _tts = FlutterTts(); final AudioPlayer _player = AudioPlayer(); final ApiService? _api; - TtsEngine _engine = TtsEngine.device; + TtsEngine _engine = TtsEngine.auto; String? _preferredVoice; + String? _serverPreferredVoice; bool _initialized = false; bool _available = false; bool _voiceConfigured = false; @@ -41,6 +42,8 @@ class TextToSpeechService { bool get isInitialized => _initialized; bool get isAvailable => _available; + bool get deviceEngineAvailable => _deviceEngineAvailable; + bool get serverEngineAvailable => _api != null; TextToSpeechService({ApiService? api}) : _api = api { // Wire minimal player events to callbacks @@ -59,6 +62,69 @@ class TextToSpeechService { }); } + Future _configureDeviceEngine({ + required String? voice, + required double speechRate, + required double pitch, + required double volume, + }) async { + _deviceEngineAvailable = false; + try { + await _tts.awaitSpeakCompletion(false); + await _tts.setVolume(volume); + await _tts.setSpeechRate(speechRate); + await _tts.setPitch(pitch); + + if (!kIsWeb && Platform.isIOS) { + await _tts.setSharedInstance(true); + await _tts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [ + IosTextToSpeechAudioCategoryOptions.mixWithOthers, + IosTextToSpeechAudioCategoryOptions.defaultToSpeaker, + IosTextToSpeechAudioCategoryOptions.allowBluetooth, + IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, + ]); + } + + if (_engine != TtsEngine.server) { + await _setVoiceByName(_preferredVoice); + } else { + _voiceConfigured = false; + } + + _deviceEngineAvailable = true; + } catch (e) { + _voiceConfigured = false; + _deviceEngineAvailable = false; + rethrow; + } + } + + bool _computeAvailability() { + final serverAvailable = _api != null; + switch (_engine) { + case TtsEngine.device: + return _deviceEngineAvailable; + case TtsEngine.server: + return serverAvailable; + case TtsEngine.auto: + return _deviceEngineAvailable || serverAvailable; + } + } + + bool _shouldUseServer() { + if (_engine == TtsEngine.server) { + return _api != null; + } + if (_engine == TtsEngine.device) { + return false; + } + // Auto: prefer device when available, otherwise fall back to server + if (_deviceEngineAvailable) { + return false; + } + return _api != null; + } + /// Register callbacks for TTS lifecycle events void bindHandlers({ VoidCallback? onStart, @@ -96,56 +162,58 @@ class TextToSpeechService { /// Initialize the native TTS engine lazily Future initialize({ - String? voice, + String? deviceVoice, + String? serverVoice, double speechRate = 0.5, double pitch = 1.0, double volume = 1.0, - TtsEngine engine = TtsEngine.device, + TtsEngine engine = TtsEngine.auto, }) async { if (_initialized) { + _engine = engine; + if (deviceVoice != null) { + _preferredVoice = deviceVoice; + _voiceConfigured = false; + } + if (serverVoice != null) { + _serverPreferredVoice = serverVoice; + } + _available = _computeAvailability(); return _available; } - try { - _engine = engine; - _preferredVoice = voice; - await _tts.awaitSpeakCompletion(false); + _engine = engine; + _preferredVoice = deviceVoice; + _serverPreferredVoice = serverVoice; + _voiceConfigured = false; - // Set volume - await _tts.setVolume(volume); - - // Set speech rate - await _tts.setSpeechRate(speechRate); - - // Set pitch - await _tts.setPitch(pitch); - - if (!kIsWeb && Platform.isIOS) { - await _tts.setSharedInstance(true); - await _tts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [ - IosTextToSpeechAudioCategoryOptions.mixWithOthers, - IosTextToSpeechAudioCategoryOptions.defaultToSpeaker, - IosTextToSpeechAudioCategoryOptions.allowBluetooth, - IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, - ]); + if (_engine != TtsEngine.server || _api == null) { + try { + await _configureDeviceEngine( + voice: deviceVoice, + speechRate: speechRate, + pitch: pitch, + volume: volume, + ); + } catch (e) { + if (_engine == TtsEngine.device) { + _available = false; + _onError?.call(e.toString()); + _initialized = true; + return _available; + } } - - // Set the voice (specific or default) when using device engine - if (_engine == TtsEngine.device) { - await _setVoiceByName(voice); - } - _deviceEngineAvailable = true; - } catch (e) { + } else { _deviceEngineAvailable = false; - if (_engine != TtsEngine.server) { - _available = false; - _onError?.call(e.toString()); - _initialized = true; - return _available; - } + try { + await _tts.awaitSpeakCompletion(false); + await _tts.setVolume(volume); + await _tts.setSpeechRate(speechRate); + await _tts.setPitch(pitch); + } catch (_) {} } - _available = _engine == TtsEngine.server || _deviceEngineAvailable; + _available = _computeAvailability(); _initialized = true; return _available; } @@ -156,10 +224,23 @@ class TextToSpeechService { } if (!_initialized) { - await initialize(voice: _preferredVoice, engine: _engine); + await initialize( + deviceVoice: _preferredVoice, + serverVoice: _serverPreferredVoice, + engine: _engine, + ); } - if (_engine == TtsEngine.server && _api != null) { + final bool useServer = _shouldUseServer(); + + if (useServer) { + if (_api == null) { + if (_deviceEngineAvailable) { + await _speakOnDevice(text); + return; + } + throw StateError('Server text-to-speech is unavailable'); + } // Server-backed TTS with sentence chunking & queued playback try { await _startServerChunkedPlayback(text); @@ -196,7 +277,7 @@ class TextToSpeechService { Future pause() async { if (!_initialized) return; try { - if (_engine == TtsEngine.server) { + if (_shouldUseServer()) { await _player.pause(); _handlePause(); } else if (_deviceEngineAvailable) { @@ -210,7 +291,7 @@ class TextToSpeechService { Future resume() async { if (!_initialized) return; try { - if (_engine == TtsEngine.server) { + if (_shouldUseServer()) { if (_waitingNext && (_currentIndex + 1) < _buffered.length) { _waitingNext = false; await _playNextIfBuffered(_session); @@ -235,7 +316,7 @@ class TextToSpeechService { _expectedChunks = 0; _currentIndex = -1; _waitingNext = false; - if (_engine == TtsEngine.server) { + if (_shouldUseServer()) { await _player.stop(); _handleCancel(); } else { @@ -254,17 +335,23 @@ class TextToSpeechService { /// Update TTS settings on-the-fly Future updateSettings({ Object? voice = const _VoiceNotProvided(), + Object? serverVoice = const _VoiceNotProvided(), double? speechRate, double? pitch, double? volume, TtsEngine? engine, }) async { final voiceProvided = voice is! _VoiceNotProvided; + final serverVoiceProvided = serverVoice is! _VoiceNotProvided; final voiceValue = voiceProvided ? voice as String? : null; + final serverVoiceValue = serverVoiceProvided + ? serverVoice as String? + : null; if (!_initialized || !_available) { // Allow engine and voice to update before init if (engine != null) _engine = engine; if (voiceProvided) _preferredVoice = voiceValue; + if (serverVoiceProvided) _serverPreferredVoice = serverVoiceValue; return; } @@ -275,6 +362,9 @@ class TextToSpeechService { if (voiceProvided) { _preferredVoice = voiceValue; } + if (serverVoiceProvided) { + _serverPreferredVoice = serverVoiceValue; + } if (volume != null) { await _tts.setVolume(volume); } @@ -284,13 +374,15 @@ class TextToSpeechService { if (pitch != null) { await _tts.setPitch(pitch); } - // Set specific voice by name on device engine - if (_engine == TtsEngine.device && voiceProvided) { + // Set specific voice by name on device-capable engines + if (_engine != TtsEngine.server && voiceProvided) { await _setVoiceByName(_preferredVoice); } } catch (e) { _onError?.call(e.toString()); } + + _available = _computeAvailability(); } /// Set voice by name, or use system default if null @@ -343,7 +435,11 @@ class TextToSpeechService { /// Get available voices from the TTS engine Future>> getAvailableVoices() async { if (!_initialized) { - await initialize(voice: _preferredVoice, engine: _engine); + await initialize( + deviceVoice: _preferredVoice, + serverVoice: _serverPreferredVoice, + engine: _engine, + ); } if (_engine == TtsEngine.server && _api != null) { @@ -425,6 +521,10 @@ class TextToSpeechService { } Future _resolveServerVoice() async { + final serverSelected = _serverPreferredVoice?.trim(); + if (serverSelected != null && serverSelected.isNotEmpty) { + return serverSelected; + } final selected = _preferredVoice?.trim(); if (selected != null && selected.isNotEmpty) { return selected; diff --git a/lib/features/chat/services/voice_call_service.dart b/lib/features/chat/services/voice_call_service.dart index f20f356..1fb4123 100644 --- a/lib/features/chat/services/voice_call_service.dart +++ b/lib/features/chat/services/voice_call_service.dart @@ -132,9 +132,8 @@ class VoiceCallService { // Initialize TTS with current app settings (engine/voice/rate/pitch/volume) final settings = _ref.read(appSettingsProvider); await _tts.initialize( - voice: settings.ttsEngine == TtsEngine.server - ? settings.ttsServerVoiceId - : settings.ttsVoice, + deviceVoice: settings.ttsVoice, + serverVoice: settings.ttsServerVoiceId, speechRate: settings.ttsSpeechRate, pitch: settings.ttsPitch, volume: settings.ttsVolume, @@ -587,11 +586,9 @@ VoiceCallService voiceCallService(Ref ref) { // Keep TTS settings in sync with app settings during a call ref.listen(appSettingsProvider, (previous, next) { // Update voice/engine and runtime parameters - final selectedVoice = next.ttsEngine == TtsEngine.server - ? next.ttsServerVoiceId - : next.ttsVoice; service._tts.updateSettings( - voice: selectedVoice, + voice: next.ttsVoice, + serverVoice: next.ttsServerVoiceId, speechRate: next.ttsSpeechRate, pitch: next.ttsPitch, volume: next.ttsVolume, diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index b33b46f..490ea43 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -698,6 +698,35 @@ class AppCustomizationPage extends ConsumerWidget { ) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; + final ttsService = ref.watch(textToSpeechServiceProvider); + 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 = []; + 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); + } + break; + case TtsEngine.server: + if (!serverAvailable) { + warnings.add(l10n.ttsServerUnavailableWarning); + } + break; + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -733,82 +762,160 @@ class AppCustomizationPage extends ConsumerWidget { ) ?? TextStyle(color: theme.sidebarForeground, fontSize: 14), ), - const Spacer(), - Wrap( - spacing: Spacing.sm, - children: [ - ChoiceChip( - label: Text(l10n.ttsEngineDevice), - selected: settings.ttsEngine == TtsEngine.device, - showCheckmark: false, - selectedColor: theme.buttonPrimary, - backgroundColor: theme.cardBackground, - side: BorderSide( - color: settings.ttsEngine == TtsEngine.device - ? theme.buttonPrimary.withValues(alpha: 0.6) - : theme.textPrimary.withValues(alpha: 0.2), - ), - labelStyle: TextStyle( - color: settings.ttsEngine == TtsEngine.device - ? theme.buttonPrimaryText - : theme.textPrimary, - fontWeight: FontWeight.w600, - ), - onSelected: (v) { - if (v) { - final notifier = ref.read( - appSettingsProvider.notifier, - ); - notifier.setTtsEngine(TtsEngine.device); - // Keep previous voice (device voices) + ], + ), + const SizedBox(height: Spacing.sm), + Wrap( + 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); + } } - }, - ), - ChoiceChip( - label: Text(l10n.ttsEngineServer), - selected: settings.ttsEngine == TtsEngine.server, - showCheckmark: false, - selectedColor: theme.buttonPrimary, - backgroundColor: theme.cardBackground, - side: BorderSide( - color: settings.ttsEngine == TtsEngine.server - ? theme.buttonPrimary.withValues(alpha: 0.6) - : theme.textPrimary.withValues(alpha: 0.2), - ), - labelStyle: TextStyle( - color: settings.ttsEngine == TtsEngine.server - ? theme.buttonPrimaryText - : theme.textPrimary, - fontWeight: FontWeight.w600, - ), - onSelected: (v) { - if (v) { - final notifier = ref.read( - appSettingsProvider.notifier, - ); - // Clear device-specific voice so server can default - notifier.setTtsVoice(null); - notifier.setTtsEngine(TtsEngine.server); + : null, + ), + ChoiceChip( + label: Text(l10n.ttsEngineDevice), + selected: settings.ttsEngine == TtsEngine.device, + showCheckmark: false, + selectedColor: theme.buttonPrimary, + backgroundColor: theme.cardBackground, + side: BorderSide( + color: settings.ttsEngine == TtsEngine.device + ? theme.buttonPrimary.withValues(alpha: 0.6) + : theme.textPrimary.withValues( + alpha: deviceSelectable ? 0.2 : 0.12, + ), + ), + labelStyle: TextStyle( + color: settings.ttsEngine == TtsEngine.device + ? theme.buttonPrimaryText + : theme.textPrimary.withValues( + alpha: deviceSelectable ? 1.0 : 0.45, + ), + fontWeight: FontWeight.w600, + ), + onSelected: deviceSelectable + ? (value) { + if (value) { + ref + .read(appSettingsProvider.notifier) + .setTtsEngine(TtsEngine.device); + } } - }, - ), - ], + : null, + ), + ChoiceChip( + label: Text(l10n.ttsEngineServer), + selected: settings.ttsEngine == TtsEngine.server, + showCheckmark: false, + selectedColor: theme.buttonPrimary, + backgroundColor: theme.cardBackground, + side: BorderSide( + color: settings.ttsEngine == TtsEngine.server + ? theme.buttonPrimary.withValues(alpha: 0.6) + : theme.textPrimary.withValues( + alpha: serverSelectable ? 0.2 : 0.12, + ), + ), + labelStyle: TextStyle( + color: settings.ttsEngine == TtsEngine.server + ? theme.buttonPrimaryText + : theme.textPrimary.withValues( + alpha: serverSelectable ? 1.0 : 0.45, + ), + fontWeight: FontWeight.w600, + ), + onSelected: serverSelectable + ? (value) { + if (value) { + final notifier = ref.read( + appSettingsProvider.notifier, + ); + notifier.setTtsVoice(null); + notifier.setTtsEngine(TtsEngine.server); + } + } + : null, ), ], ), + const SizedBox(height: Spacing.sm), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + ttsDescription, + key: ValueKey( + 'tts-desc-${settings.ttsEngine.name}', + ), + style: + theme.bodyMedium?.copyWith( + color: theme.sidebarForeground.withValues( + alpha: 0.9, + ), + ) ?? + TextStyle( + color: theme.sidebarForeground.withValues( + alpha: 0.9, + ), + fontSize: 14, + ), + ), + ), + if (warnings.isNotEmpty) ...[ + const SizedBox(height: Spacing.sm), + ...warnings.map( + (warning) => Padding( + padding: const EdgeInsets.only(top: Spacing.xs), + child: Text( + warning, + style: + theme.bodySmall?.copyWith( + color: theme.error, + fontWeight: FontWeight.w600, + ) ?? + TextStyle( + color: theme.error, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], ], ), ), const SizedBox(height: Spacing.sm), _ExpandableCard( title: l10n.ttsVoice, - subtitle: _getDisplayVoiceName( - settings.ttsEngine == TtsEngine.server - ? ((settings.ttsServerVoiceName ?? settings.ttsServerVoiceId) ?? - '') - : (settings.ttsVoice ?? ''), - l10n.ttsSystemDefault, - ), + subtitle: _ttsVoiceSubtitle(l10n, settings), icon: UiUtils.platformIcon( ios: CupertinoIcons.speaker_3, android: Icons.record_voice_over, @@ -827,14 +934,7 @@ class AppCustomizationPage extends ConsumerWidget { color: theme.buttonPrimary, ), title: l10n.ttsVoice, - subtitle: _getDisplayVoiceName( - settings.ttsEngine == TtsEngine.server - ? ((settings.ttsServerVoiceName ?? - settings.ttsServerVoiceId) ?? - '') - : (settings.ttsVoice ?? ''), - l10n.ttsSystemDefault, - ), + subtitle: _ttsVoiceSubtitle(l10n, settings), onTap: () => _showVoicePickerSheet(context, ref, settings), ), const SizedBox(height: Spacing.md), @@ -928,6 +1028,39 @@ class AppCustomizationPage extends ConsumerWidget { } } + String _ttsPreferenceDescription( + AppLocalizations l10n, + AppSettings settings, + ) { + switch (settings.ttsEngine) { + case TtsEngine.auto: + return l10n.ttsEngineAutoDescription; + case TtsEngine.device: + return l10n.ttsEngineDeviceDescription; + case TtsEngine.server: + return l10n.ttsEngineServerDescription; + } + } + + String _ttsVoiceSubtitle(AppLocalizations l10n, AppSettings settings) { + final deviceName = _getDisplayVoiceName( + settings.ttsVoice, + l10n.ttsSystemDefault, + ); + final serverVoice = + (settings.ttsServerVoiceName ?? settings.ttsServerVoiceId) ?? ''; + 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: + return serverName; + } + } + Widget _buildSliderTile( BuildContext context, WidgetRef ref, { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 84e1e07..19b8782 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Server", "sttEngineAutoDescription": "Verwendet die Erkennung auf dem Gerät, wenn verfügbar, und greift sonst auf deinen Server zurück.", "sttEngineDeviceDescription": "Behält Audio auf diesem Gerät. Spracheingabe funktioniert nicht, wenn das Gerät keine Spracherkennung unterstützt.", - "sttEngineServerDescription": "Sendet Aufnahmen immer an deinen Conduit-Server zur Transkription.", + "sttEngineServerDescription": "Sendet Aufnahmen immer an deinen OpenWebUI-Server zur Transkription.", "sttDeviceUnavailableWarning": "Auf diesem Gerät steht keine Spracherkennung zur Verfügung.", "sttServerUnavailableWarning": "Verbinde dich mit einem Server mit aktivierter Transkription, um diese Option zu nutzen.", "ttsSettings": "Text zu Sprache", + "ttsEngineAuto": "Automatisch", + "ttsEngineAutoDescription": "Verwendet die Sprachausgabe auf dem Gerät, wenn verfügbar, und greift sonst auf deinen Server zurück.", + "ttsEngineDeviceDescription": "Behält die Ausgabe auf diesem Gerät. Sprachausgabe funktioniert nicht, wenn das Gerät keine TTS-Unterstützung bietet.", + "ttsEngineServerDescription": "Sendet die Ausgabe immer an deinen OpenWebUI-Server.", + "ttsDeviceUnavailableWarning": "Sprachausgabe auf dem Gerät steht auf diesem Gerät nicht zur Verfügung.", + "ttsServerUnavailableWarning": "Verbinde dich mit einem Server mit aktivierter Sprachausgabe, um diese Option zu nutzen.", "ttsVoice": "Stimme", "ttsSpeechRate": "Sprechgeschwindigkeit", "ttsPitch": "Tonhöhe", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 686bf81..9fd39b5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1247,7 +1247,7 @@ "@sttEngineDeviceDescription": { "description": "Description shown when on-device speech-to-text preference is active." }, - "sttEngineServerDescription": "Always send recordings to your Conduit server for transcription.", + "sttEngineServerDescription": "Always send recordings to your OpenWebUI server for transcription.", "@sttEngineServerDescription": { "description": "Description shown when server speech-to-text preference is active." }, @@ -1263,6 +1263,10 @@ "@ttsEngineLabel": { "description": "Label for selecting the text-to-speech engine." }, + "ttsEngineAuto": "Auto", + "@ttsEngineAuto": { + "description": "Chip label for automatically selecting the text-to-speech engine." + }, "ttsEngineDevice": "On device", "@ttsEngineDevice": { "description": "Chip label for using on-device text-to-speech." @@ -1271,6 +1275,26 @@ "@ttsEngineServer": { "description": "Chip label for using server-side text-to-speech." }, + "ttsEngineAutoDescription": "Use on-device speech when available and fall back to your server.", + "@ttsEngineAutoDescription": { + "description": "Description shown when automatic text-to-speech preference is active." + }, + "ttsEngineDeviceDescription": "Keep synthesis on this device. Voice playback stops working if on-device TTS isn’t supported.", + "@ttsEngineDeviceDescription": { + "description": "Description shown when on-device text-to-speech preference is active." + }, + "ttsEngineServerDescription": "Always request audio from your OpenWebUI server.", + "@ttsEngineServerDescription": { + "description": "Description shown when server text-to-speech preference is active." + }, + "ttsDeviceUnavailableWarning": "On-device text-to-speech isn’t available on this device.", + "@ttsDeviceUnavailableWarning": { + "description": "Warning shown when on-device text-to-speech is unavailable." + }, + "ttsServerUnavailableWarning": "Connect to a server with text-to-speech enabled to use this option.", + "@ttsServerUnavailableWarning": { + "description": "Warning shown when server text-to-speech is unavailable." + }, "ttsSettings": "Text to Speech", "@ttsSettings": { "description": "Section header for TTS-related customization options." diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6fb7e15..b2329c1 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Servidor", "sttEngineAutoDescription": "Usa el reconocimiento en el dispositivo cuando esté disponible y, si no, recurre a tu servidor.", "sttEngineDeviceDescription": "Mantiene el audio en este dispositivo. La entrada de voz no funciona si el dispositivo no admite reconocimiento de voz.", - "sttEngineServerDescription": "Envía siempre las grabaciones a tu servidor Conduit para la transcripción.", + "sttEngineServerDescription": "Envía siempre las grabaciones a tu servidor OpenWebUI para la transcripción.", "sttDeviceUnavailableWarning": "El reconocimiento de voz en el dispositivo no está disponible en este dispositivo.", "sttServerUnavailableWarning": "Conéctate a un servidor con transcripción habilitada para usar esta opción.", "ttsSettings": "Texto a voz", + "ttsEngineAuto": "Automático", + "ttsEngineAutoDescription": "Usa la síntesis en el dispositivo cuando esté disponible y, si no, recurre a tu servidor.", + "ttsEngineDeviceDescription": "Mantiene la síntesis en este dispositivo. La reproducción de voz no funciona si el dispositivo no admite TTS.", + "ttsEngineServerDescription": "Solicita siempre el audio a tu servidor OpenWebUI.", + "ttsDeviceUnavailableWarning": "La síntesis de voz en el dispositivo no está disponible en este dispositivo.", + "ttsServerUnavailableWarning": "Conéctate a un servidor con texto a voz habilitado para usar esta opción.", "ttsVoice": "Voz", "ttsSpeechRate": "Velocidad de voz", "ttsPitch": "Tono", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a44f277..d6a7dbb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Serveur", "sttEngineAutoDescription": "Utilise la reconnaissance sur l’appareil quand c’est possible, sinon bascule vers votre serveur.", "sttEngineDeviceDescription": "Conserve l’audio sur cet appareil. L’entrée vocale cesse de fonctionner si la reconnaissance vocale n’est pas prise en charge.", - "sttEngineServerDescription": "Envoie toujours les enregistrements à votre serveur Conduit pour transcription.", + "sttEngineServerDescription": "Envoie toujours les enregistrements à votre serveur OpenWebUI pour transcription.", "sttDeviceUnavailableWarning": "La reconnaissance vocale sur l’appareil n’est pas disponible sur cet appareil.", "sttServerUnavailableWarning": "Connectez-vous à un serveur avec la transcription activée pour utiliser cette option.", - "ttsSettings": "Synthèse vocale", +"ttsSettings": "Synthèse vocale", + "ttsEngineAuto": "Auto", + "ttsEngineAutoDescription": "Utilise la synthèse locale quand c’est possible, sinon bascule vers votre serveur.", + "ttsEngineDeviceDescription": "Garde la synthèse sur cet appareil. La lecture vocale ne fonctionne plus si l’appareil n’offre pas la synthèse vocale.", + "ttsEngineServerDescription": "Demande toujours l'audio à votre serveur OpenWebUI.", + "ttsDeviceUnavailableWarning": "La synthèse vocale sur l’appareil n’est pas disponible sur cet appareil.", + "ttsServerUnavailableWarning": "Connectez-vous à un serveur avec la synthèse vocale activée pour utiliser cette option.", "ttsVoice": "Voix", "ttsSpeechRate": "Vitesse de parole", "ttsPitch": "Hauteur", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 9caea28..2c997cd 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Server", "sttEngineAutoDescription": "Usa il riconoscimento sul dispositivo quando disponibile e altrimenti passa al tuo server.", "sttEngineDeviceDescription": "Mantiene l’audio su questo dispositivo. L’input vocale non funziona se il dispositivo non supporta il riconoscimento vocale.", - "sttEngineServerDescription": "Invia sempre le registrazioni al tuo server Conduit per la trascrizione.", + "sttEngineServerDescription": "Invia sempre le registrazioni al tuo server OpenWebUI per la trascrizione.", "sttDeviceUnavailableWarning": "Il riconoscimento vocale sul dispositivo non è disponibile su questo dispositivo.", "sttServerUnavailableWarning": "Collegati a un server con la trascrizione abilitata per usare questa opzione.", - "ttsSettings": "Sintesi vocale", +"ttsSettings": "Sintesi vocale", + "ttsEngineAuto": "Automatico", + "ttsEngineAutoDescription": "Usa la sintesi sul dispositivo quando disponibile e altrimenti passa al tuo server.", + "ttsEngineDeviceDescription": "Mantiene la sintesi su questo dispositivo. La riproduzione vocale non funziona se il dispositivo non supporta il TTS.", + "ttsEngineServerDescription": "Richiede sempre l'audio dal tuo server OpenWebUI.", + "ttsDeviceUnavailableWarning": "La sintesi vocale sul dispositivo non è disponibile su questo dispositivo.", + "ttsServerUnavailableWarning": "Collegati a un server con la sintesi vocale abilitata per usare questa opzione.", "ttsVoice": "Voce", "ttsSpeechRate": "Velocità di sintesi vocale", "ttsPitch": "Tonalità", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7c46858..8d1798c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1829,7 +1829,7 @@ abstract class AppLocalizations { /// Description shown when server speech-to-text preference is active. /// /// In en, this message translates to: - /// **'Always send recordings to your Conduit server for transcription.'** + /// **'Always send recordings to your OpenWebUI server for transcription.'** String get sttEngineServerDescription; /// Warning shown when the user selects on-device speech recognition but it is unavailable. @@ -1850,6 +1850,12 @@ abstract class AppLocalizations { /// **'Engine'** String get ttsEngineLabel; + /// Chip label for automatically selecting the text-to-speech engine. + /// + /// In en, this message translates to: + /// **'Auto'** + String get ttsEngineAuto; + /// Chip label for using on-device text-to-speech. /// /// In en, this message translates to: @@ -1862,6 +1868,36 @@ abstract class AppLocalizations { /// **'Server'** String get ttsEngineServer; + /// Description shown when automatic text-to-speech preference is active. + /// + /// In en, this message translates to: + /// **'Use on-device speech when available and fall back to your server.'** + String get ttsEngineAutoDescription; + + /// Description shown when on-device text-to-speech preference is active. + /// + /// In en, this message translates to: + /// **'Keep synthesis on this device. Voice playback stops working if on-device TTS isn’t supported.'** + String get ttsEngineDeviceDescription; + + /// Description shown when server text-to-speech preference is active. + /// + /// In en, this message translates to: + /// **'Always request audio from your OpenWebUI server.'** + String get ttsEngineServerDescription; + + /// Warning shown when on-device text-to-speech is unavailable. + /// + /// In en, this message translates to: + /// **'On-device text-to-speech isn’t available on this device.'** + String get ttsDeviceUnavailableWarning; + + /// Warning shown when server text-to-speech is unavailable. + /// + /// In en, this message translates to: + /// **'Connect to a server with text-to-speech enabled to use this option.'** + String get ttsServerUnavailableWarning; + /// Section header for TTS-related customization options. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 617bb11..655f1d3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -956,7 +956,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get sttEngineServerDescription => - 'Sendet Aufnahmen immer an deinen Conduit-Server zur Transkription.'; + 'Sendet Aufnahmen immer an deinen OpenWebUI-Server zur Transkription.'; @override String get sttDeviceUnavailableWarning => @@ -969,12 +969,35 @@ class AppLocalizationsDe extends AppLocalizations { @override String get ttsEngineLabel => 'Engine'; + @override + String get ttsEngineAuto => 'Automatisch'; + @override String get ttsEngineDevice => 'Auf dem Gerät'; @override String get ttsEngineServer => 'Server'; + @override + String get ttsEngineAutoDescription => + 'Verwendet die Sprachausgabe auf dem Gerät, wenn verfügbar, und greift sonst auf deinen Server zurück.'; + + @override + String get ttsEngineDeviceDescription => + 'Behält die Ausgabe auf diesem Gerät. Sprachausgabe funktioniert nicht, wenn das Gerät keine TTS-Unterstützung bietet.'; + + @override + String get ttsEngineServerDescription => + 'Sendet die Ausgabe immer an deinen OpenWebUI-Server.'; + + @override + String get ttsDeviceUnavailableWarning => + 'Sprachausgabe auf dem Gerät steht auf diesem Gerät nicht zur Verfügung.'; + + @override + String get ttsServerUnavailableWarning => + 'Verbinde dich mit einem Server mit aktivierter Sprachausgabe, um diese Option zu nutzen.'; + @override String get ttsSettings => 'Text zu Sprache'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0ab2e16..c163b7c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -950,7 +950,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get sttEngineServerDescription => - 'Always send recordings to your Conduit server for transcription.'; + 'Always send recordings to your OpenWebUI server for transcription.'; @override String get sttDeviceUnavailableWarning => @@ -963,12 +963,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get ttsEngineLabel => 'Engine'; + @override + String get ttsEngineAuto => 'Auto'; + @override String get ttsEngineDevice => 'On device'; @override String get ttsEngineServer => 'Server'; + @override + String get ttsEngineAutoDescription => + 'Use on-device speech when available and fall back to your server.'; + + @override + String get ttsEngineDeviceDescription => + 'Keep synthesis on this device. Voice playback stops working if on-device TTS isn’t supported.'; + + @override + String get ttsEngineServerDescription => + 'Always request audio from your OpenWebUI server.'; + + @override + String get ttsDeviceUnavailableWarning => + 'On-device text-to-speech isn’t available on this device.'; + + @override + String get ttsServerUnavailableWarning => + 'Connect to a server with text-to-speech enabled to use this option.'; + @override String get ttsSettings => 'Text to Speech'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c9d0079..9871042 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -965,7 +965,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get sttEngineServerDescription => - 'Envoie toujours les enregistrements à votre serveur Conduit pour transcription.'; + 'Envoie toujours les enregistrements à votre serveur OpenWebUI pour transcription.'; @override String get sttDeviceUnavailableWarning => @@ -978,12 +978,35 @@ class AppLocalizationsFr extends AppLocalizations { @override String get ttsEngineLabel => 'Moteur'; + @override + String get ttsEngineAuto => 'Auto'; + @override String get ttsEngineDevice => 'Sur l\'appareil'; @override String get ttsEngineServer => 'Serveur'; + @override + String get ttsEngineAutoDescription => + 'Utilise la synthèse locale quand c’est possible, sinon bascule vers votre serveur.'; + + @override + String get ttsEngineDeviceDescription => + 'Garde la synthèse sur cet appareil. La lecture vocale ne fonctionne plus si l’appareil n’offre pas la synthèse vocale.'; + + @override + String get ttsEngineServerDescription => + 'Demande toujours l\'audio à votre serveur OpenWebUI.'; + + @override + String get ttsDeviceUnavailableWarning => + 'La synthèse vocale sur l’appareil n’est pas disponible sur cet appareil.'; + + @override + String get ttsServerUnavailableWarning => + 'Connectez-vous à un serveur avec la synthèse vocale activée pour utiliser cette option.'; + @override String get ttsSettings => 'Synthèse vocale'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 6147e1b..5a731ae 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -954,7 +954,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get sttEngineServerDescription => - 'Invia sempre le registrazioni al tuo server Conduit per la trascrizione.'; + 'Invia sempre le registrazioni al tuo server OpenWebUI per la trascrizione.'; @override String get sttDeviceUnavailableWarning => @@ -967,12 +967,35 @@ class AppLocalizationsIt extends AppLocalizations { @override String get ttsEngineLabel => 'Motore'; + @override + String get ttsEngineAuto => 'Automatico'; + @override String get ttsEngineDevice => 'Sul dispositivo'; @override String get ttsEngineServer => 'Server'; + @override + String get ttsEngineAutoDescription => + 'Usa la sintesi sul dispositivo quando disponibile e altrimenti passa al tuo server.'; + + @override + String get ttsEngineDeviceDescription => + 'Mantiene la sintesi su questo dispositivo. La riproduzione vocale non funziona se il dispositivo non supporta il TTS.'; + + @override + String get ttsEngineServerDescription => + 'Richiede sempre l\'audio dal tuo server OpenWebUI.'; + + @override + String get ttsDeviceUnavailableWarning => + 'La sintesi vocale sul dispositivo non è disponibile su questo dispositivo.'; + + @override + String get ttsServerUnavailableWarning => + 'Collegati a un server con la sintesi vocale abilitata per usare questa opzione.'; + @override String get ttsSettings => 'Sintesi vocale'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 548c254..424861f 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Server", "sttEngineAutoDescription": "Gebruikt spraakherkenning op het apparaat wanneer beschikbaar en valt anders terug op je server.", "sttEngineDeviceDescription": "Houdt audio op dit apparaat. Spraakinput werkt niet als het apparaat geen spraakherkenning ondersteunt.", - "sttEngineServerDescription": "Stuurt opnames altijd naar je Conduit-server voor transcriptie.", + "sttEngineServerDescription": "Stuurt opnames altijd naar je OpenWebUI-server voor transcriptie.", "sttDeviceUnavailableWarning": "Spraakherkenning op het apparaat is niet beschikbaar op dit apparaat.", "sttServerUnavailableWarning": "Verbind met een server met transcriptie ingeschakeld om deze optie te gebruiken.", - "ttsSettings": "Tekst naar spraak", +"ttsSettings": "Tekst naar spraak", + "ttsEngineAuto": "Automatisch", + "ttsEngineAutoDescription": "Gebruikt spraaksynthese op het apparaat wanneer beschikbaar en valt anders terug op je server.", + "ttsEngineDeviceDescription": "Houdt de synthese op dit apparaat. Spraakweergave werkt niet als het apparaat geen TTS ondersteunt.", + "ttsEngineServerDescription": "Vraagt altijd audio op bij je OpenWebUI-server.", + "ttsDeviceUnavailableWarning": "Spraaksynthese op het apparaat is niet beschikbaar op dit apparaat.", + "ttsServerUnavailableWarning": "Verbind met een server met tekst-naar-spraak ingeschakeld om deze optie te gebruiken.", "ttsVoice": "Stem", "ttsSpeechRate": "Spraaksnelheid", "ttsPitch": "Toonhoogte", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 332036f..6187548 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -314,10 +314,16 @@ "sttEngineServer": "Сервер", "sttEngineAutoDescription": "Использует распознавание на устройстве, когда это возможно, иначе переключается на ваш сервер.", "sttEngineDeviceDescription": "Оставляет звук на этом устройстве. Голосовой ввод не работает, если устройство не поддерживает распознавание речи.", - "sttEngineServerDescription": "Всегда отправляет записи на сервер Conduit для транскрибации.", + "sttEngineServerDescription": "Всегда отправляет записи на сервер OpenWebUI для транскрибации.", "sttDeviceUnavailableWarning": "Распознавание речи на устройстве недоступно на этом устройстве.", "sttServerUnavailableWarning": "Подключитесь к серверу с включённой транскрибацией, чтобы использовать эту опцию.", - "ttsSettings": "Преобразование текста в речь", +"ttsSettings": "Преобразование текста в речь", + "ttsEngineAuto": "Авто", + "ttsEngineAutoDescription": "Использует синтез речи на устройстве, когда это возможно, иначе переключается на ваш сервер.", + "ttsEngineDeviceDescription": "Оставляет синтез на этом устройстве. Воспроизведение голоса не работает, если устройство не поддерживает синтез речи.", + "ttsEngineServerDescription": "Всегда запрашивает аудио у вашего сервера OpenWebUI.", + "ttsDeviceUnavailableWarning": "Синтез речи на устройстве недоступен на этом устройстве.", + "ttsServerUnavailableWarning": "Подключитесь к серверу с включённым синтезом речи, чтобы использовать эту опцию.", "ttsVoice": "Голос", "ttsSpeechRate": "Скорость речи", "ttsPitch": "Высота тона", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 40e53f4..abc7460 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -314,10 +314,16 @@ "sttEngineServer": "服务器", "sttEngineAutoDescription": "在可用时使用本机识别,否则切换到你的服务器。", "sttEngineDeviceDescription": "音频会保留在此设备上。如果设备不支持语音识别,语音输入将不可用。", - "sttEngineServerDescription": "始终将录音发送到你的 Conduit 服务器进行转写。", + "sttEngineServerDescription": "始终将录音发送到你的 OpenWebUI 服务器进行转写。", "sttDeviceUnavailableWarning": "此设备不支持本机语音识别。", "sttServerUnavailableWarning": "连接到启用转写功能的服务器后才能使用此选项。", - "ttsSettings": "文本转语音", +"ttsSettings": "文本转语音", + "ttsEngineAuto": "自动", + "ttsEngineAutoDescription": "在可用时使用本机合成,否则切换到你的服务器。", + "ttsEngineDeviceDescription": "在此设备上完成合成。如果设备不支持文本转语音,语音播放将不可用。", + "ttsEngineServerDescription": "始终向你的 OpenWebUI 服务器请求音频。", + "ttsDeviceUnavailableWarning": "此设备不支持本机文本转语音。", + "ttsServerUnavailableWarning": "连接到启用文本转语音的服务器后才能使用此选项。", "ttsVoice": "语音", "ttsSpeechRate": "语速", "ttsPitch": "音调",