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 _audioCache = {}; static final Map _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 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 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 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'); } }