mymuseum-visitapp/lib/Services/glasses_tts_service.dart

118 lines
3.4 KiB
Dart

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<void> 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<void> replay() async {
if (_lastTempPath == null) return;
try {
await _player.seek(Duration.zero);
await _player.play();
} catch (e) {
debugPrint('[GlassesTtsService] replay error: $e');
}
}
Future<void> stop() async {
await _player.stop();
_isSpeaking = false;
}
Future<void> _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<File> _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();
}
}