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/Glasses/engines/tts_engine.dart'; /// TTS via ElevenLabs API — voix naturelle, payant. /// Upgrade par rapport à FlutterTtsEngine quand la qualité vocale prime. class ElevenLabsTtsEngine implements TtsEngine { static const String _baseUrl = 'https://api.elevenlabs.io/v1'; final String apiKey; final String voiceId; final AudioPlayer _player = AudioPlayer(); bool _speaking = false; String? _lastTempPath; ElevenLabsTtsEngine({required this.apiKey, required this.voiceId}); @override bool get isSpeaking => _speaking; @override Future speak(String text, {String languageCode = 'fr-FR'}) async { if (text.isEmpty) return; try { _speaking = true; if (!Platform.isWindows) await AudioRoutingChannel.enableBluetoothOutput(); await _speakElevenLabs(text); } catch (e) { debugPrint('[ElevenLabsTtsEngine] speak error: $e'); } finally { _speaking = false; } } @override Future stop() async { await _player.stop(); _speaking = false; } @override Future replay() async { if (_lastTempPath == null) return; await _player.seek(Duration.zero); await _player.play(); } Future _speakElevenLabs(String text) async { 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: jsonEncode({ 'text': text, 'model_id': 'eleven_multilingual_v2', 'voice_settings': {'stability': 0.5, 'similarity_boost': 0.75}, }), ); if (response.statusCode != 200) { throw Exception('ElevenLabs ${response.statusCode}'); } final dir = await getTemporaryDirectory(); final file = File('${dir.path}/glasses_tts.mp3'); await file.writeAsBytes(response.bodyBytes, flush: true); _lastTempPath = file.path; await _player.setFilePath(file.path); await _player.play(); } void dispose() => _player.dispose(); }