feat(tts): add auto mode for text-to-speech engine selection

This commit is contained in:
cogwheel0
2025-11-02 21:31:13 +05:30
parent da249eaa31
commit cfadeffd24
19 changed files with 579 additions and 154 deletions

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

View File

@@ -12,7 +12,7 @@ part 'settings_service.g.dart';
enum SttPreference { auto, deviceOnly, serverOnly } enum SttPreference { auto, deviceOnly, serverOnly }
/// TTS engine selection /// TTS engine selection
enum TtsEngine { device, server } enum TtsEngine { auto, device, server }
/// Service for managing app-wide settings including accessibility preferences /// Service for managing app-wide settings including accessibility preferences
class SettingsService { class SettingsService {
@@ -223,11 +223,15 @@ class SettingsService {
static TtsEngine _parseTtsEngine(String? raw) { static TtsEngine _parseTtsEngine(String? raw) {
switch ((raw ?? '').toLowerCase()) { switch ((raw ?? '').toLowerCase()) {
case 'auto':
case '':
return TtsEngine.auto;
case 'server': case 'server':
return TtsEngine.server; return TtsEngine.server;
case 'device': case 'device':
default:
return TtsEngine.device; return TtsEngine.device;
default:
return TtsEngine.auto;
} }
} }
@@ -409,7 +413,7 @@ class AppSettings {
this.ttsSpeechRate = 0.5, this.ttsSpeechRate = 0.5,
this.ttsPitch = 1.0, this.ttsPitch = 1.0,
this.ttsVolume = 1.0, this.ttsVolume = 1.0,
this.ttsEngine = TtsEngine.device, this.ttsEngine = TtsEngine.auto,
this.ttsServerVoiceId, this.ttsServerVoiceId,
this.ttsServerVoiceName, this.ttsServerVoiceName,
}); });

View File

@@ -107,11 +107,9 @@ class TextToSpeechController extends Notifier<TextToSpeechState> {
// Listen to settings changes and update TTS when initialized // Listen to settings changes and update TTS when initialized
ref.listen<AppSettings>(appSettingsProvider, (previous, next) { ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
if (_service.isInitialized && _service.isAvailable) { if (_service.isInitialized && _service.isAvailable) {
final selectedVoice = next.ttsEngine == TtsEngine.server
? next.ttsServerVoiceId
: next.ttsVoice;
_service.updateSettings( _service.updateSettings(
voice: selectedVoice, voice: next.ttsVoice,
serverVoice: next.ttsServerVoiceId,
speechRate: next.ttsSpeechRate, speechRate: next.ttsSpeechRate,
pitch: next.ttsPitch, pitch: next.ttsPitch,
volume: next.ttsVolume, volume: next.ttsVolume,
@@ -137,9 +135,8 @@ class TextToSpeechController extends Notifier<TextToSpeechState> {
final settings = ref.read(appSettingsProvider); final settings = ref.read(appSettingsProvider);
final future = _service final future = _service
.initialize( .initialize(
voice: settings.ttsEngine == TtsEngine.server deviceVoice: settings.ttsVoice,
? settings.ttsServerVoiceId serverVoice: settings.ttsServerVoiceId,
: settings.ttsVoice,
speechRate: settings.ttsSpeechRate, speechRate: settings.ttsSpeechRate,
pitch: settings.ttsPitch, pitch: settings.ttsPitch,
volume: settings.ttsVolume, volume: settings.ttsVolume,

View File

@@ -16,8 +16,9 @@ class TextToSpeechService {
final FlutterTts _tts = FlutterTts(); final FlutterTts _tts = FlutterTts();
final AudioPlayer _player = AudioPlayer(); final AudioPlayer _player = AudioPlayer();
final ApiService? _api; final ApiService? _api;
TtsEngine _engine = TtsEngine.device; TtsEngine _engine = TtsEngine.auto;
String? _preferredVoice; String? _preferredVoice;
String? _serverPreferredVoice;
bool _initialized = false; bool _initialized = false;
bool _available = false; bool _available = false;
bool _voiceConfigured = false; bool _voiceConfigured = false;
@@ -41,6 +42,8 @@ class TextToSpeechService {
bool get isInitialized => _initialized; bool get isInitialized => _initialized;
bool get isAvailable => _available; bool get isAvailable => _available;
bool get deviceEngineAvailable => _deviceEngineAvailable;
bool get serverEngineAvailable => _api != null;
TextToSpeechService({ApiService? api}) : _api = api { TextToSpeechService({ApiService? api}) : _api = api {
// Wire minimal player events to callbacks // Wire minimal player events to callbacks
@@ -59,6 +62,69 @@ class TextToSpeechService {
}); });
} }
Future<void> _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 /// Register callbacks for TTS lifecycle events
void bindHandlers({ void bindHandlers({
VoidCallback? onStart, VoidCallback? onStart,
@@ -96,56 +162,58 @@ class TextToSpeechService {
/// Initialize the native TTS engine lazily /// Initialize the native TTS engine lazily
Future<bool> initialize({ Future<bool> initialize({
String? voice, String? deviceVoice,
String? serverVoice,
double speechRate = 0.5, double speechRate = 0.5,
double pitch = 1.0, double pitch = 1.0,
double volume = 1.0, double volume = 1.0,
TtsEngine engine = TtsEngine.device, TtsEngine engine = TtsEngine.auto,
}) async { }) async {
if (_initialized) { if (_initialized) {
_engine = engine;
if (deviceVoice != null) {
_preferredVoice = deviceVoice;
_voiceConfigured = false;
}
if (serverVoice != null) {
_serverPreferredVoice = serverVoice;
}
_available = _computeAvailability();
return _available; return _available;
} }
try { _engine = engine;
_engine = engine; _preferredVoice = deviceVoice;
_preferredVoice = voice; _serverPreferredVoice = serverVoice;
await _tts.awaitSpeakCompletion(false); _voiceConfigured = false;
// Set volume if (_engine != TtsEngine.server || _api == null) {
await _tts.setVolume(volume); try {
await _configureDeviceEngine(
// Set speech rate voice: deviceVoice,
await _tts.setSpeechRate(speechRate); speechRate: speechRate,
pitch: pitch,
// Set pitch volume: volume,
await _tts.setPitch(pitch); );
} catch (e) {
if (!kIsWeb && Platform.isIOS) { if (_engine == TtsEngine.device) {
await _tts.setSharedInstance(true); _available = false;
await _tts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [ _onError?.call(e.toString());
IosTextToSpeechAudioCategoryOptions.mixWithOthers, _initialized = true;
IosTextToSpeechAudioCategoryOptions.defaultToSpeaker, return _available;
IosTextToSpeechAudioCategoryOptions.allowBluetooth, }
IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP,
]);
} }
} else {
// Set the voice (specific or default) when using device engine
if (_engine == TtsEngine.device) {
await _setVoiceByName(voice);
}
_deviceEngineAvailable = true;
} catch (e) {
_deviceEngineAvailable = false; _deviceEngineAvailable = false;
if (_engine != TtsEngine.server) { try {
_available = false; await _tts.awaitSpeakCompletion(false);
_onError?.call(e.toString()); await _tts.setVolume(volume);
_initialized = true; await _tts.setSpeechRate(speechRate);
return _available; await _tts.setPitch(pitch);
} } catch (_) {}
} }
_available = _engine == TtsEngine.server || _deviceEngineAvailable; _available = _computeAvailability();
_initialized = true; _initialized = true;
return _available; return _available;
} }
@@ -156,10 +224,23 @@ class TextToSpeechService {
} }
if (!_initialized) { 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 // Server-backed TTS with sentence chunking & queued playback
try { try {
await _startServerChunkedPlayback(text); await _startServerChunkedPlayback(text);
@@ -196,7 +277,7 @@ class TextToSpeechService {
Future<void> pause() async { Future<void> pause() async {
if (!_initialized) return; if (!_initialized) return;
try { try {
if (_engine == TtsEngine.server) { if (_shouldUseServer()) {
await _player.pause(); await _player.pause();
_handlePause(); _handlePause();
} else if (_deviceEngineAvailable) { } else if (_deviceEngineAvailable) {
@@ -210,7 +291,7 @@ class TextToSpeechService {
Future<void> resume() async { Future<void> resume() async {
if (!_initialized) return; if (!_initialized) return;
try { try {
if (_engine == TtsEngine.server) { if (_shouldUseServer()) {
if (_waitingNext && (_currentIndex + 1) < _buffered.length) { if (_waitingNext && (_currentIndex + 1) < _buffered.length) {
_waitingNext = false; _waitingNext = false;
await _playNextIfBuffered(_session); await _playNextIfBuffered(_session);
@@ -235,7 +316,7 @@ class TextToSpeechService {
_expectedChunks = 0; _expectedChunks = 0;
_currentIndex = -1; _currentIndex = -1;
_waitingNext = false; _waitingNext = false;
if (_engine == TtsEngine.server) { if (_shouldUseServer()) {
await _player.stop(); await _player.stop();
_handleCancel(); _handleCancel();
} else { } else {
@@ -254,17 +335,23 @@ class TextToSpeechService {
/// Update TTS settings on-the-fly /// Update TTS settings on-the-fly
Future<void> updateSettings({ Future<void> updateSettings({
Object? voice = const _VoiceNotProvided(), Object? voice = const _VoiceNotProvided(),
Object? serverVoice = const _VoiceNotProvided(),
double? speechRate, double? speechRate,
double? pitch, double? pitch,
double? volume, double? volume,
TtsEngine? engine, TtsEngine? engine,
}) async { }) async {
final voiceProvided = voice is! _VoiceNotProvided; final voiceProvided = voice is! _VoiceNotProvided;
final serverVoiceProvided = serverVoice is! _VoiceNotProvided;
final voiceValue = voiceProvided ? voice as String? : null; final voiceValue = voiceProvided ? voice as String? : null;
final serverVoiceValue = serverVoiceProvided
? serverVoice as String?
: null;
if (!_initialized || !_available) { if (!_initialized || !_available) {
// Allow engine and voice to update before init // Allow engine and voice to update before init
if (engine != null) _engine = engine; if (engine != null) _engine = engine;
if (voiceProvided) _preferredVoice = voiceValue; if (voiceProvided) _preferredVoice = voiceValue;
if (serverVoiceProvided) _serverPreferredVoice = serverVoiceValue;
return; return;
} }
@@ -275,6 +362,9 @@ class TextToSpeechService {
if (voiceProvided) { if (voiceProvided) {
_preferredVoice = voiceValue; _preferredVoice = voiceValue;
} }
if (serverVoiceProvided) {
_serverPreferredVoice = serverVoiceValue;
}
if (volume != null) { if (volume != null) {
await _tts.setVolume(volume); await _tts.setVolume(volume);
} }
@@ -284,13 +374,15 @@ class TextToSpeechService {
if (pitch != null) { if (pitch != null) {
await _tts.setPitch(pitch); await _tts.setPitch(pitch);
} }
// Set specific voice by name on device engine // Set specific voice by name on device-capable engines
if (_engine == TtsEngine.device && voiceProvided) { if (_engine != TtsEngine.server && voiceProvided) {
await _setVoiceByName(_preferredVoice); await _setVoiceByName(_preferredVoice);
} }
} catch (e) { } catch (e) {
_onError?.call(e.toString()); _onError?.call(e.toString());
} }
_available = _computeAvailability();
} }
/// Set voice by name, or use system default if null /// Set voice by name, or use system default if null
@@ -343,7 +435,11 @@ class TextToSpeechService {
/// Get available voices from the TTS engine /// Get available voices from the TTS engine
Future<List<Map<String, dynamic>>> getAvailableVoices() async { Future<List<Map<String, dynamic>>> getAvailableVoices() async {
if (!_initialized) { if (!_initialized) {
await initialize(voice: _preferredVoice, engine: _engine); await initialize(
deviceVoice: _preferredVoice,
serverVoice: _serverPreferredVoice,
engine: _engine,
);
} }
if (_engine == TtsEngine.server && _api != null) { if (_engine == TtsEngine.server && _api != null) {
@@ -425,6 +521,10 @@ class TextToSpeechService {
} }
Future<String?> _resolveServerVoice() async { Future<String?> _resolveServerVoice() async {
final serverSelected = _serverPreferredVoice?.trim();
if (serverSelected != null && serverSelected.isNotEmpty) {
return serverSelected;
}
final selected = _preferredVoice?.trim(); final selected = _preferredVoice?.trim();
if (selected != null && selected.isNotEmpty) { if (selected != null && selected.isNotEmpty) {
return selected; return selected;

View File

@@ -132,9 +132,8 @@ class VoiceCallService {
// Initialize TTS with current app settings (engine/voice/rate/pitch/volume) // Initialize TTS with current app settings (engine/voice/rate/pitch/volume)
final settings = _ref.read(appSettingsProvider); final settings = _ref.read(appSettingsProvider);
await _tts.initialize( await _tts.initialize(
voice: settings.ttsEngine == TtsEngine.server deviceVoice: settings.ttsVoice,
? settings.ttsServerVoiceId serverVoice: settings.ttsServerVoiceId,
: settings.ttsVoice,
speechRate: settings.ttsSpeechRate, speechRate: settings.ttsSpeechRate,
pitch: settings.ttsPitch, pitch: settings.ttsPitch,
volume: settings.ttsVolume, volume: settings.ttsVolume,
@@ -587,11 +586,9 @@ VoiceCallService voiceCallService(Ref ref) {
// Keep TTS settings in sync with app settings during a call // Keep TTS settings in sync with app settings during a call
ref.listen<AppSettings>(appSettingsProvider, (previous, next) { ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
// Update voice/engine and runtime parameters // Update voice/engine and runtime parameters
final selectedVoice = next.ttsEngine == TtsEngine.server
? next.ttsServerVoiceId
: next.ttsVoice;
service._tts.updateSettings( service._tts.updateSettings(
voice: selectedVoice, voice: next.ttsVoice,
serverVoice: next.ttsServerVoiceId,
speechRate: next.ttsSpeechRate, speechRate: next.ttsSpeechRate,
pitch: next.ttsPitch, pitch: next.ttsPitch,
volume: next.ttsVolume, volume: next.ttsVolume,

View File

@@ -698,6 +698,35 @@ class AppCustomizationPage extends ConsumerWidget {
) { ) {
final theme = context.conduitTheme; final theme = context.conduitTheme;
final l10n = AppLocalizations.of(context)!; 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 = <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);
}
break;
case TtsEngine.server:
if (!serverAvailable) {
warnings.add(l10n.ttsServerUnavailableWarning);
}
break;
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -733,82 +762,160 @@ class AppCustomizationPage extends ConsumerWidget {
) ?? ) ??
TextStyle(color: theme.sidebarForeground, fontSize: 14), TextStyle(color: theme.sidebarForeground, fontSize: 14),
), ),
const Spacer(), ],
Wrap( ),
spacing: Spacing.sm, const SizedBox(height: Spacing.sm),
children: [ Wrap(
ChoiceChip( spacing: Spacing.sm,
label: Text(l10n.ttsEngineDevice), runSpacing: Spacing.sm,
selected: settings.ttsEngine == TtsEngine.device, children: [
showCheckmark: false, ChoiceChip(
selectedColor: theme.buttonPrimary, label: Text(l10n.ttsEngineAuto),
backgroundColor: theme.cardBackground, selected: settings.ttsEngine == TtsEngine.auto,
side: BorderSide( showCheckmark: false,
color: settings.ttsEngine == TtsEngine.device selectedColor: theme.buttonPrimary,
? theme.buttonPrimary.withValues(alpha: 0.6) backgroundColor: theme.cardBackground,
: theme.textPrimary.withValues(alpha: 0.2), side: BorderSide(
), color: settings.ttsEngine == TtsEngine.auto
labelStyle: TextStyle( ? theme.buttonPrimary.withValues(alpha: 0.6)
color: settings.ttsEngine == TtsEngine.device : theme.textPrimary.withValues(
? theme.buttonPrimaryText alpha: autoSelectable ? 0.2 : 0.12,
: theme.textPrimary, ),
fontWeight: FontWeight.w600, ),
), labelStyle: TextStyle(
onSelected: (v) { color: settings.ttsEngine == TtsEngine.auto
if (v) { ? theme.buttonPrimaryText
final notifier = ref.read( : theme.textPrimary.withValues(
appSettingsProvider.notifier, alpha: autoSelectable ? 1.0 : 0.45,
); ),
notifier.setTtsEngine(TtsEngine.device); fontWeight: FontWeight.w600,
// Keep previous voice (device voices) ),
onSelected: autoSelectable
? (value) {
if (value) {
ref
.read(appSettingsProvider.notifier)
.setTtsEngine(TtsEngine.auto);
}
} }
}, : null,
), ),
ChoiceChip( ChoiceChip(
label: Text(l10n.ttsEngineServer), label: Text(l10n.ttsEngineDevice),
selected: settings.ttsEngine == TtsEngine.server, selected: settings.ttsEngine == TtsEngine.device,
showCheckmark: false, showCheckmark: false,
selectedColor: theme.buttonPrimary, selectedColor: theme.buttonPrimary,
backgroundColor: theme.cardBackground, backgroundColor: theme.cardBackground,
side: BorderSide( side: BorderSide(
color: settings.ttsEngine == TtsEngine.server color: settings.ttsEngine == TtsEngine.device
? theme.buttonPrimary.withValues(alpha: 0.6) ? theme.buttonPrimary.withValues(alpha: 0.6)
: theme.textPrimary.withValues(alpha: 0.2), : theme.textPrimary.withValues(
), alpha: deviceSelectable ? 0.2 : 0.12,
labelStyle: TextStyle( ),
color: settings.ttsEngine == TtsEngine.server ),
? theme.buttonPrimaryText labelStyle: TextStyle(
: theme.textPrimary, color: settings.ttsEngine == TtsEngine.device
fontWeight: FontWeight.w600, ? theme.buttonPrimaryText
), : theme.textPrimary.withValues(
onSelected: (v) { alpha: deviceSelectable ? 1.0 : 0.45,
if (v) { ),
final notifier = ref.read( fontWeight: FontWeight.w600,
appSettingsProvider.notifier, ),
); onSelected: deviceSelectable
// Clear device-specific voice so server can default ? (value) {
notifier.setTtsVoice(null); if (value) {
notifier.setTtsEngine(TtsEngine.server); 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<String>(
'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), const SizedBox(height: Spacing.sm),
_ExpandableCard( _ExpandableCard(
title: l10n.ttsVoice, title: l10n.ttsVoice,
subtitle: _getDisplayVoiceName( subtitle: _ttsVoiceSubtitle(l10n, settings),
settings.ttsEngine == TtsEngine.server
? ((settings.ttsServerVoiceName ?? settings.ttsServerVoiceId) ??
'')
: (settings.ttsVoice ?? ''),
l10n.ttsSystemDefault,
),
icon: UiUtils.platformIcon( icon: UiUtils.platformIcon(
ios: CupertinoIcons.speaker_3, ios: CupertinoIcons.speaker_3,
android: Icons.record_voice_over, android: Icons.record_voice_over,
@@ -827,14 +934,7 @@ class AppCustomizationPage extends ConsumerWidget {
color: theme.buttonPrimary, color: theme.buttonPrimary,
), ),
title: l10n.ttsVoice, title: l10n.ttsVoice,
subtitle: _getDisplayVoiceName( subtitle: _ttsVoiceSubtitle(l10n, settings),
settings.ttsEngine == TtsEngine.server
? ((settings.ttsServerVoiceName ??
settings.ttsServerVoiceId) ??
'')
: (settings.ttsVoice ?? ''),
l10n.ttsSystemDefault,
),
onTap: () => _showVoicePickerSheet(context, ref, settings), onTap: () => _showVoicePickerSheet(context, ref, settings),
), ),
const SizedBox(height: Spacing.md), 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( Widget _buildSliderTile(
BuildContext context, BuildContext context,
WidgetRef ref, { WidgetRef ref, {

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Server", "sttEngineServer": "Server",
"sttEngineAutoDescription": "Verwendet die Erkennung auf dem Gerät, wenn verfügbar, und greift sonst auf deinen Server zurück.", "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.", "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.", "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.", "sttServerUnavailableWarning": "Verbinde dich mit einem Server mit aktivierter Transkription, um diese Option zu nutzen.",
"ttsSettings": "Text zu Sprache", "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", "ttsVoice": "Stimme",
"ttsSpeechRate": "Sprechgeschwindigkeit", "ttsSpeechRate": "Sprechgeschwindigkeit",
"ttsPitch": "Tonhöhe", "ttsPitch": "Tonhöhe",

View File

@@ -1247,7 +1247,7 @@
"@sttEngineDeviceDescription": { "@sttEngineDeviceDescription": {
"description": "Description shown when on-device speech-to-text preference is active." "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": { "@sttEngineServerDescription": {
"description": "Description shown when server speech-to-text preference is active." "description": "Description shown when server speech-to-text preference is active."
}, },
@@ -1263,6 +1263,10 @@
"@ttsEngineLabel": { "@ttsEngineLabel": {
"description": "Label for selecting the text-to-speech engine." "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": "On device",
"@ttsEngineDevice": { "@ttsEngineDevice": {
"description": "Chip label for using on-device text-to-speech." "description": "Chip label for using on-device text-to-speech."
@@ -1271,6 +1275,26 @@
"@ttsEngineServer": { "@ttsEngineServer": {
"description": "Chip label for using server-side text-to-speech." "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 isnt 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 isnt 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": "Text to Speech",
"@ttsSettings": { "@ttsSettings": {
"description": "Section header for TTS-related customization options." "description": "Section header for TTS-related customization options."

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Servidor", "sttEngineServer": "Servidor",
"sttEngineAutoDescription": "Usa el reconocimiento en el dispositivo cuando esté disponible y, si no, recurre a tu 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.", "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.", "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.", "sttServerUnavailableWarning": "Conéctate a un servidor con transcripción habilitada para usar esta opción.",
"ttsSettings": "Texto a voz", "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", "ttsVoice": "Voz",
"ttsSpeechRate": "Velocidad de voz", "ttsSpeechRate": "Velocidad de voz",
"ttsPitch": "Tono", "ttsPitch": "Tono",

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Serveur", "sttEngineServer": "Serveur",
"sttEngineAutoDescription": "Utilise la reconnaissance sur lappareil quand cest possible, sinon bascule vers votre serveur.", "sttEngineAutoDescription": "Utilise la reconnaissance sur lappareil quand cest possible, sinon bascule vers votre serveur.",
"sttEngineDeviceDescription": "Conserve laudio sur cet appareil. Lentrée vocale cesse de fonctionner si la reconnaissance vocale nest pas prise en charge.", "sttEngineDeviceDescription": "Conserve laudio sur cet appareil. Lentrée vocale cesse de fonctionner si la reconnaissance vocale nest 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 lappareil nest pas disponible sur cet appareil.", "sttDeviceUnavailableWarning": "La reconnaissance vocale sur lappareil nest pas disponible sur cet appareil.",
"sttServerUnavailableWarning": "Connectez-vous à un serveur avec la transcription activée pour utiliser cette option.", "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 cest possible, sinon bascule vers votre serveur.",
"ttsEngineDeviceDescription": "Garde la synthèse sur cet appareil. La lecture vocale ne fonctionne plus si lappareil noffre pas la synthèse vocale.",
"ttsEngineServerDescription": "Demande toujours l'audio à votre serveur OpenWebUI.",
"ttsDeviceUnavailableWarning": "La synthèse vocale sur lappareil nest pas disponible sur cet appareil.",
"ttsServerUnavailableWarning": "Connectez-vous à un serveur avec la synthèse vocale activée pour utiliser cette option.",
"ttsVoice": "Voix", "ttsVoice": "Voix",
"ttsSpeechRate": "Vitesse de parole", "ttsSpeechRate": "Vitesse de parole",
"ttsPitch": "Hauteur", "ttsPitch": "Hauteur",

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Server", "sttEngineServer": "Server",
"sttEngineAutoDescription": "Usa il riconoscimento sul dispositivo quando disponibile e altrimenti passa al tuo server.", "sttEngineAutoDescription": "Usa il riconoscimento sul dispositivo quando disponibile e altrimenti passa al tuo server.",
"sttEngineDeviceDescription": "Mantiene laudio su questo dispositivo. Linput vocale non funziona se il dispositivo non supporta il riconoscimento vocale.", "sttEngineDeviceDescription": "Mantiene laudio su questo dispositivo. Linput 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.", "sttDeviceUnavailableWarning": "Il riconoscimento vocale sul dispositivo non è disponibile su questo dispositivo.",
"sttServerUnavailableWarning": "Collegati a un server con la trascrizione abilitata per usare questa opzione.", "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", "ttsVoice": "Voce",
"ttsSpeechRate": "Velocità di sintesi vocale", "ttsSpeechRate": "Velocità di sintesi vocale",
"ttsPitch": "Tonalità", "ttsPitch": "Tonalità",

View File

@@ -1829,7 +1829,7 @@ abstract class AppLocalizations {
/// Description shown when server speech-to-text preference is active. /// Description shown when server speech-to-text preference is active.
/// ///
/// In en, this message translates to: /// 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; String get sttEngineServerDescription;
/// Warning shown when the user selects on-device speech recognition but it is unavailable. /// Warning shown when the user selects on-device speech recognition but it is unavailable.
@@ -1850,6 +1850,12 @@ abstract class AppLocalizations {
/// **'Engine'** /// **'Engine'**
String get ttsEngineLabel; 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. /// Chip label for using on-device text-to-speech.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1862,6 +1868,36 @@ abstract class AppLocalizations {
/// **'Server'** /// **'Server'**
String get ttsEngineServer; 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 isnt 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 isnt 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. /// Section header for TTS-related customization options.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -956,7 +956,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get sttEngineServerDescription => String get sttEngineServerDescription =>
'Sendet Aufnahmen immer an deinen Conduit-Server zur Transkription.'; 'Sendet Aufnahmen immer an deinen OpenWebUI-Server zur Transkription.';
@override @override
String get sttDeviceUnavailableWarning => String get sttDeviceUnavailableWarning =>
@@ -969,12 +969,35 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get ttsEngineLabel => 'Engine'; String get ttsEngineLabel => 'Engine';
@override
String get ttsEngineAuto => 'Automatisch';
@override @override
String get ttsEngineDevice => 'Auf dem Gerät'; String get ttsEngineDevice => 'Auf dem Gerät';
@override @override
String get ttsEngineServer => 'Server'; 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 @override
String get ttsSettings => 'Text zu Sprache'; String get ttsSettings => 'Text zu Sprache';

View File

@@ -950,7 +950,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get sttEngineServerDescription => String get sttEngineServerDescription =>
'Always send recordings to your Conduit server for transcription.'; 'Always send recordings to your OpenWebUI server for transcription.';
@override @override
String get sttDeviceUnavailableWarning => String get sttDeviceUnavailableWarning =>
@@ -963,12 +963,35 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get ttsEngineLabel => 'Engine'; String get ttsEngineLabel => 'Engine';
@override
String get ttsEngineAuto => 'Auto';
@override @override
String get ttsEngineDevice => 'On device'; String get ttsEngineDevice => 'On device';
@override @override
String get ttsEngineServer => 'Server'; 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 isnt supported.';
@override
String get ttsEngineServerDescription =>
'Always request audio from your OpenWebUI server.';
@override
String get ttsDeviceUnavailableWarning =>
'On-device text-to-speech isnt available on this device.';
@override
String get ttsServerUnavailableWarning =>
'Connect to a server with text-to-speech enabled to use this option.';
@override @override
String get ttsSettings => 'Text to Speech'; String get ttsSettings => 'Text to Speech';

View File

@@ -965,7 +965,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get sttEngineServerDescription => String get sttEngineServerDescription =>
'Envoie toujours les enregistrements à votre serveur Conduit pour transcription.'; 'Envoie toujours les enregistrements à votre serveur OpenWebUI pour transcription.';
@override @override
String get sttDeviceUnavailableWarning => String get sttDeviceUnavailableWarning =>
@@ -978,12 +978,35 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get ttsEngineLabel => 'Moteur'; String get ttsEngineLabel => 'Moteur';
@override
String get ttsEngineAuto => 'Auto';
@override @override
String get ttsEngineDevice => 'Sur l\'appareil'; String get ttsEngineDevice => 'Sur l\'appareil';
@override @override
String get ttsEngineServer => 'Serveur'; String get ttsEngineServer => 'Serveur';
@override
String get ttsEngineAutoDescription =>
'Utilise la synthèse locale quand cest possible, sinon bascule vers votre serveur.';
@override
String get ttsEngineDeviceDescription =>
'Garde la synthèse sur cet appareil. La lecture vocale ne fonctionne plus si lappareil noffre 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 lappareil nest 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 @override
String get ttsSettings => 'Synthèse vocale'; String get ttsSettings => 'Synthèse vocale';

View File

@@ -954,7 +954,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get sttEngineServerDescription => 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 @override
String get sttDeviceUnavailableWarning => String get sttDeviceUnavailableWarning =>
@@ -967,12 +967,35 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get ttsEngineLabel => 'Motore'; String get ttsEngineLabel => 'Motore';
@override
String get ttsEngineAuto => 'Automatico';
@override @override
String get ttsEngineDevice => 'Sul dispositivo'; String get ttsEngineDevice => 'Sul dispositivo';
@override @override
String get ttsEngineServer => 'Server'; 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 @override
String get ttsSettings => 'Sintesi vocale'; String get ttsSettings => 'Sintesi vocale';

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Server", "sttEngineServer": "Server",
"sttEngineAutoDescription": "Gebruikt spraakherkenning op het apparaat wanneer beschikbaar en valt anders terug op je 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.", "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.", "sttDeviceUnavailableWarning": "Spraakherkenning op het apparaat is niet beschikbaar op dit apparaat.",
"sttServerUnavailableWarning": "Verbind met een server met transcriptie ingeschakeld om deze optie te gebruiken.", "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", "ttsVoice": "Stem",
"ttsSpeechRate": "Spraaksnelheid", "ttsSpeechRate": "Spraaksnelheid",
"ttsPitch": "Toonhoogte", "ttsPitch": "Toonhoogte",

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "Сервер", "sttEngineServer": "Сервер",
"sttEngineAutoDescription": "Использует распознавание на устройстве, когда это возможно, иначе переключается на ваш сервер.", "sttEngineAutoDescription": "Использует распознавание на устройстве, когда это возможно, иначе переключается на ваш сервер.",
"sttEngineDeviceDescription": "Оставляет звук на этом устройстве. Голосовой ввод не работает, если устройство не поддерживает распознавание речи.", "sttEngineDeviceDescription": "Оставляет звук на этом устройстве. Голосовой ввод не работает, если устройство не поддерживает распознавание речи.",
"sttEngineServerDescription": "Всегда отправляет записи на сервер Conduit для транскрибации.", "sttEngineServerDescription": "Всегда отправляет записи на сервер OpenWebUI для транскрибации.",
"sttDeviceUnavailableWarning": "Распознавание речи на устройстве недоступно на этом устройстве.", "sttDeviceUnavailableWarning": "Распознавание речи на устройстве недоступно на этом устройстве.",
"sttServerUnavailableWarning": "Подключитесь к серверу с включённой транскрибацией, чтобы использовать эту опцию.", "sttServerUnavailableWarning": "Подключитесь к серверу с включённой транскрибацией, чтобы использовать эту опцию.",
"ttsSettings": "Преобразование текста в речь", "ttsSettings": "Преобразование текста в речь",
"ttsEngineAuto": "Авто",
"ttsEngineAutoDescription": "Использует синтез речи на устройстве, когда это возможно, иначе переключается на ваш сервер.",
"ttsEngineDeviceDescription": "Оставляет синтез на этом устройстве. Воспроизведение голоса не работает, если устройство не поддерживает синтез речи.",
"ttsEngineServerDescription": "Всегда запрашивает аудио у вашего сервера OpenWebUI.",
"ttsDeviceUnavailableWarning": "Синтез речи на устройстве недоступен на этом устройстве.",
"ttsServerUnavailableWarning": "Подключитесь к серверу с включённым синтезом речи, чтобы использовать эту опцию.",
"ttsVoice": "Голос", "ttsVoice": "Голос",
"ttsSpeechRate": "Скорость речи", "ttsSpeechRate": "Скорость речи",
"ttsPitch": "Высота тона", "ttsPitch": "Высота тона",

View File

@@ -314,10 +314,16 @@
"sttEngineServer": "服务器", "sttEngineServer": "服务器",
"sttEngineAutoDescription": "在可用时使用本机识别,否则切换到你的服务器。", "sttEngineAutoDescription": "在可用时使用本机识别,否则切换到你的服务器。",
"sttEngineDeviceDescription": "音频会保留在此设备上。如果设备不支持语音识别,语音输入将不可用。", "sttEngineDeviceDescription": "音频会保留在此设备上。如果设备不支持语音识别,语音输入将不可用。",
"sttEngineServerDescription": "始终将录音发送到你的 Conduit 服务器进行转写。", "sttEngineServerDescription": "始终将录音发送到你的 OpenWebUI 服务器进行转写。",
"sttDeviceUnavailableWarning": "此设备不支持本机语音识别。", "sttDeviceUnavailableWarning": "此设备不支持本机语音识别。",
"sttServerUnavailableWarning": "连接到启用转写功能的服务器后才能使用此选项。", "sttServerUnavailableWarning": "连接到启用转写功能的服务器后才能使用此选项。",
"ttsSettings": "文本转语音", "ttsSettings": "文本转语音",
"ttsEngineAuto": "自动",
"ttsEngineAutoDescription": "在可用时使用本机合成,否则切换到你的服务器。",
"ttsEngineDeviceDescription": "在此设备上完成合成。如果设备不支持文本转语音,语音播放将不可用。",
"ttsEngineServerDescription": "始终向你的 OpenWebUI 服务器请求音频。",
"ttsDeviceUnavailableWarning": "此设备不支持本机文本转语音。",
"ttsServerUnavailableWarning": "连接到启用文本转语音的服务器后才能使用此选项。",
"ttsVoice": "语音", "ttsVoice": "语音",
"ttsSpeechRate": "语速", "ttsSpeechRate": "语速",
"ttsPitch": "音调", "ttsPitch": "音调",