import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:mymuseum_visitapp/PlatformChannels/audio_routing_channel.dart'; import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart'; /// Service TTS qui synthétise du texte via ElevenLabs /// et joue l'audio directement dans les lunettes via Bluetooth A2DP. /// /// Si les lunettes ne sont pas connectées, le son joue sur le haut-parleur. /// Si la clé ElevenLabs n'est pas configurée, le TTS est silencieux. class GlassesTtsService { GlassesTtsService._(); static final GlassesTtsService instance = GlassesTtsService._(); static const String _baseUrl = 'https://api.elevenlabs.io/v1'; final AudioPlayer _player = AudioPlayer(); String? _lastSpokenText; String? _lastTempPath; bool _isSpeaking = false; bool get isSpeaking => _isSpeaking; /// Synthétise [text] et joue l'audio sur les lunettes (ou haut-parleur en fallback). /// /// [apiKey] : clé ElevenLabs (typiquement kElevenLabsApiKey de constants.dart) /// [voiceId] : ID de voix ElevenLabs (kElevenLabsVoiceId) Future speak( String text, { required String apiKey, required String voiceId, }) async { if (text.isEmpty) return; _lastSpokenText = text; try { _isSpeaking = true; if (MetaGlassesService.instance.isConnected && !Platform.isWindows) { await AudioRoutingChannel.enableBluetoothOutput(); } if (apiKey.isNotEmpty) { await _speakElevenLabs(text, apiKey, voiceId); } else { debugPrint('[GlassesTtsService] No ElevenLabs API key — TTS skipped'); } } catch (e) { debugPrint('[GlassesTtsService] speak error: $e'); } finally { _isSpeaking = false; } } /// Rejoue la dernière synthèse (commande vocale "répète"). Future replay() async { if (_lastTempPath == null) return; try { await _player.seek(Duration.zero); await _player.play(); } catch (e) { debugPrint('[GlassesTtsService] replay error: $e'); } } Future stop() async { await _player.stop(); _isSpeaking = false; } Future _speakElevenLabs( String text, String apiKey, String voiceId) async { final body = jsonEncode({ 'text': text, 'model_id': 'eleven_multilingual_v2', 'voice_settings': {'stability': 0.5, 'similarity_boost': 0.75}, }); final response = await http.post( Uri.parse('$_baseUrl/text-to-speech/$voiceId'), headers: { 'xi-api-key': apiKey, 'Content-Type': 'application/json', 'Accept': 'audio/mpeg', }, body: body, ); if (response.statusCode != 200) { throw Exception( 'ElevenLabs error ${response.statusCode}: ${response.body}'); } final file = await _writeTempMp3(response.bodyBytes); _lastTempPath = file.path; await _player.setFilePath(file.path); await _player.play(); } Future _writeTempMp3(Uint8List bytes) async { final dir = await getTemporaryDirectory(); final file = File('${dir.path}/glasses_tts.mp3'); await file.writeAsBytes(bytes, flush: true); return file; } void dispose() { _player.dispose(); AudioRoutingChannel.restoreDefaultOutput(); } }