190 lines
5.2 KiB
Dart
190 lines
5.2 KiB
Dart
import 'package:web/web.dart' as web;
|
|
import 'package:em2rp/config/api_config.dart';
|
|
import 'package:em2rp/utils/debug_log.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
/// Service de Text-to-Speech utilisant Google Cloud TTS via Cloud Function
|
|
/// Avec système de cache pour optimiser les performances et réduire les coûts
|
|
class CloudTextToSpeechService {
|
|
static final Map<String, String> _audioCache = {};
|
|
static final Map<String, web.HTMLAudioElement> _audioPlayers = {};
|
|
|
|
/// Générer l'audio TTS via Cloud Function
|
|
/// Retourne l'URL de l'audio (mise en cache automatiquement côté serveur)
|
|
static Future<String?> generateAudio(String text) async {
|
|
try {
|
|
// Vérifier le cache local d'abord
|
|
if (_audioCache.containsKey(text)) {
|
|
DebugLog.info('[CloudTTS] ✓ Local cache HIT: "${text.substring(0, 30)}..."');
|
|
return _audioCache[text];
|
|
}
|
|
|
|
DebugLog.info('[CloudTTS] Generating audio for: "$text"');
|
|
|
|
// Récupérer le token d'authentification
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
if (user == null) {
|
|
DebugLog.error('[CloudTTS] User not authenticated');
|
|
return null;
|
|
}
|
|
|
|
final token = await user.getIdToken();
|
|
if (token == null) {
|
|
DebugLog.error('[CloudTTS] Failed to get auth token');
|
|
return null;
|
|
}
|
|
|
|
// Préparer la requête
|
|
final url = '${ApiConfig.baseUrl}/generateTTSV2';
|
|
final headers = {
|
|
'Authorization': 'Bearer $token',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
final body = json.encode({
|
|
'data': {
|
|
'text': text,
|
|
'voiceConfig': {
|
|
'languageCode': 'fr-FR',
|
|
'name': 'fr-FR-Standard-B', // Voix masculine gratuite
|
|
'ssmlGender': 'MALE',
|
|
},
|
|
},
|
|
});
|
|
|
|
DebugLog.info('[CloudTTS] Calling Cloud Function...');
|
|
final startTime = DateTime.now();
|
|
|
|
// Appeler la Cloud Function
|
|
final response = await http.post(
|
|
Uri.parse(url),
|
|
headers: headers,
|
|
body: body,
|
|
);
|
|
|
|
final duration = DateTime.now().difference(startTime).inMilliseconds;
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = json.decode(response.body);
|
|
final audioUrl = data['audioUrl'] as String?;
|
|
final cached = data['cached'] as bool? ?? false;
|
|
|
|
if (audioUrl != null) {
|
|
// Mettre en cache localement
|
|
_audioCache[text] = audioUrl;
|
|
|
|
DebugLog.info('[CloudTTS] ✓ Audio generated - cached: $cached, duration: ${duration}ms');
|
|
|
|
return audioUrl;
|
|
}
|
|
}
|
|
|
|
DebugLog.error('[CloudTTS] Failed to generate audio', {
|
|
'status': response.statusCode,
|
|
'body': response.body,
|
|
});
|
|
|
|
return null;
|
|
} catch (e) {
|
|
DebugLog.error('[CloudTTS] Exception:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Lire un audio depuis une URL
|
|
static void playAudio(String audioUrl) {
|
|
try {
|
|
DebugLog.info('[CloudTTS] Playing audio...');
|
|
|
|
// Créer ou réutiliser un HTMLAudioElement
|
|
final player = _audioPlayers[audioUrl] ?? web.HTMLAudioElement();
|
|
if (!_audioPlayers.containsKey(audioUrl)) {
|
|
player.src = audioUrl;
|
|
_audioPlayers[audioUrl] = player;
|
|
}
|
|
|
|
// Configurer le volume
|
|
player.volume = 1.0;
|
|
|
|
// Écouter les événements
|
|
player.onEnded.listen((_) {
|
|
DebugLog.info('[CloudTTS] ✓ Playback finished');
|
|
});
|
|
|
|
player.onError.listen((event) {
|
|
DebugLog.error('[CloudTTS] ✗ Playback error:', event);
|
|
});
|
|
|
|
// Lire l'audio (pas de await avec package:web)
|
|
player.play();
|
|
|
|
DebugLog.info('[CloudTTS] ✓ Playback started');
|
|
} catch (e) {
|
|
DebugLog.error('[CloudTTS] Error playing audio:', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Générer et lire l'audio en une seule opération
|
|
static Future<void> speak(String text) async {
|
|
try {
|
|
final audioUrl = await generateAudio(text);
|
|
|
|
if (audioUrl != null) {
|
|
playAudio(audioUrl);
|
|
} else {
|
|
DebugLog.error('[CloudTTS] Failed to generate audio for speech');
|
|
}
|
|
} catch (e) {
|
|
DebugLog.error('[CloudTTS] Error in speak:', e);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Arrêter tous les audios en cours
|
|
static void stopAll() {
|
|
for (final player in _audioPlayers.values) {
|
|
try {
|
|
player.pause();
|
|
player.currentTime = 0;
|
|
} catch (e) {
|
|
// Ignorer les erreurs de pause
|
|
}
|
|
}
|
|
DebugLog.info('[CloudTTS] All players stopped');
|
|
}
|
|
|
|
/// Nettoyer le cache
|
|
static void clearCache() {
|
|
_audioCache.clear();
|
|
_audioPlayers.clear();
|
|
DebugLog.info('[CloudTTS] Cache cleared');
|
|
}
|
|
|
|
/// Pré-charger des audios fréquemment utilisés
|
|
static Future<void> preloadCommonPhrases() async {
|
|
final phrases = [
|
|
'Équipement scanné',
|
|
'Flight case',
|
|
'Conteneur',
|
|
'Validé',
|
|
'Erreur',
|
|
];
|
|
|
|
DebugLog.info('[CloudTTS] Preloading ${phrases.length} common phrases...');
|
|
|
|
for (final phrase in phrases) {
|
|
try {
|
|
await generateAudio(phrase);
|
|
} catch (e) {
|
|
DebugLog.warning('[CloudTTS] Failed to preload: $phrase - $e');
|
|
}
|
|
}
|
|
|
|
DebugLog.info('[CloudTTS] ✓ Preload complete');
|
|
}
|
|
}
|
|
|