feat(tts): server-backed TTS engine selection
Introduce server TTS support and engine selection while keeping device TTS as the default. - Add new persistence keys for storing TTS engine and selected server voice (ttsEngine, ttsServerVoiceId, ttsServerVoiceName). - Extend TextToSpeechService to support two engines: TtsEngine.device (FlutterTts) and TtsEngine.server (remote audio). - Wire in an AudioPlayer and optional ApiService to fetch raw audio bytes from the server and play them, with event hooks mapped to existing lifecycle callbacks. - Implement fallback to device TTS on server errors or empty responses, and ensure player lifecycle (pause/stop/dispose) is handled when using server engine. - Allow engine and preferred voice to be configured before initialization and updated at runtime via updateSettings. This enables selecting a server-side voice and using a remote TTS provider while preserving compatibility with the existing device TTS implementation.
This commit is contained in:
@@ -2261,12 +2261,24 @@ class ApiService {
|
||||
}
|
||||
|
||||
// Audio
|
||||
Future<List<String>> getAvailableVoices() async {
|
||||
_traceApi('Fetching available voices');
|
||||
Future<List<Map<String, dynamic>>> getAvailableServerVoices() async {
|
||||
_traceApi('Fetching server TTS voices');
|
||||
final response = await _dio.get('/api/v1/audio/voices');
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final voices = data['voices'];
|
||||
if (voices is List) {
|
||||
return voices
|
||||
.whereType<Map>()
|
||||
.map((e) => e.cast<String, dynamic>())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
if (data is List) {
|
||||
return data.cast<String>();
|
||||
// Fallback: plain list of ids
|
||||
return data
|
||||
.map((e) => {'id': e.toString(), 'name': e.toString()})
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -2279,13 +2291,15 @@ class ApiService {
|
||||
_traceApi('Generating speech for text: $textPreview...');
|
||||
final response = await _dio.post(
|
||||
'/api/v1/audio/speech',
|
||||
data: {'text': text, if (voice != null) 'voice': voice},
|
||||
data: {'input': text, if (voice != null) 'voice': voice},
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
|
||||
// Return audio data as bytes
|
||||
if (response.data is List) {
|
||||
return (response.data as List).cast<int>();
|
||||
}
|
||||
final data = response.data;
|
||||
if (data is List<int>) return data;
|
||||
if (data is Uint8List) return data.toList();
|
||||
if (data is List) return (data).cast<int>();
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user