//import 'package:audioplayers/audioplayers.dart'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // TODO // import 'package:flutter_beacon/flutter_beacon.dart'; import 'package:get/get.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart'; import 'package:mymuseum_visitapp/Models/articleRead.dart'; import 'package:mymuseum_visitapp/Screens/Home/home_3.0.dart'; import 'package:mymuseum_visitapp/Screens/splash_screen.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/flutter_tts_engine.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/gemini_tts_engine.dart'; // ElevenLabsTtsEngine disponible dans impl/ mais retiré du pipeline — trop cher import 'package:mymuseum_visitapp/Services/Glasses/glasses_background_service.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/myinfomate_llm_client.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/speech_to_text_stt.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/whisper_stt_engine.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/native_wake_word_engine.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/speech_to_text_wake_word.dart'; import 'package:mymuseum_visitapp/Services/Glasses/glasses_orchestrator.dart'; import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart'; import 'package:mymuseum_visitapp/Services/pushNotificationService.dart'; import 'package:mymuseum_visitapp/l10n/app_localizations.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'Helpers/DatabaseHelper.dart'; import 'Models/visitContext.dart'; import 'app_context.dart'; import 'client.dart'; import 'constants.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const AppBootstrap()); } class AppBootstrap extends StatefulWidget { const AppBootstrap({Key? key}) : super(key: key); @override _AppBootstrapState createState() => _AppBootstrapState(); } class _AppBootstrapState extends State { VisitAppContext? _ctx; @override void initState() { super.initState(); _initApp(); } Future _initApp() async { // Firebase init (requires google-services.json on Android, GoogleService-Info.plist on iOS) if (!Platform.isWindows) { await Firebase.initializeApp(); } VisitAppContext? localContext = await DatabaseHelper.instance.getData(DatabaseTableType.main); Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); String localPath = appDocumentsDirectory!.path; MapboxOptions.setAccessToken("pk.eyJ1IjoidGZyYW5zb2xldCIsImEiOiJjbHRpcGNvZDYwYWhkMnFxdmF0ampveW10In0.7xHN0NGvUfQu5ThS3RGJRw"); // TODO put in json file or resource file if (localContext != null) { print("we've got an local db !"); print(localContext); List articleReadTest = List.from(await DatabaseHelper.instance.getData(DatabaseTableType.articleRead)); localContext.readSections = articleReadTest; } else { localContext = VisitAppContext(language: "FR", id: "UserId_Init", instanceId: kInstanceId, isAdmin: false, isAllLanguages: false); DatabaseHelper.instance.insert(DatabaseTableType.main, localContext.toMap()); List articleReadTest = List.from(await DatabaseHelper.instance.getData(DatabaseTableType.articleRead)); localContext.readSections = articleReadTest; print(localContext.readSections); print("NO LOCAL DB !"); } localContext.clientAPI = Client(kApiBaseUrl, apiKey: localContext.apiKey ?? (kApiKey.isNotEmpty ? kApiKey : null)); // Récupère publicApiKey depuis le backend si pas encore en mémoire if (localContext.apiKey == null && localContext.instanceId != null) { try { final instanceDto = await localContext.clientAPI.instanceApi! .instanceGetDetail(localContext.instanceId!); if (instanceDto?.publicApiKey != null) { localContext.apiKey = instanceDto!.publicApiKey; localContext.clientAPI = Client(kApiBaseUrl, apiKey: localContext.apiKey); DatabaseHelper.instance.updateTableMain(DatabaseTableType.main, localContext); print('publicApiKey loaded and saved'); } } catch (e) { print('Could not load publicApiKey: $e'); } } // Push notifications — subscribe to instance topic if enabled if (!Platform.isWindows && localContext.instanceId != null) { try { await PushNotificationService.initialize(localContext.instanceId!); } catch (e) { print('PushNotification init failed: $e'); } } localContext.localPath = localPath; print("Local path $localPath"); // Glasses init — initialize only here, startSession() est déclenché dans _MyAppState // TODO: remplacer glassesEnabled par un vrai toggle UI localContext.glassesEnabled = true; if (!Platform.isWindows && (Platform.isAndroid || Platform.isIOS)) { await MetaGlassesService.instance.initialize(); } SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( systemNavigationBarColor: Colors.transparent, statusBarColor: Colors.transparent, )); if (mounted) setState(() => _ctx = localContext); } @override Widget build(BuildContext context) { if (_ctx == null) { return const MaterialApp( debugShowCheckedModeBanner: false, home: SplashScreen(), ); } return MyApp(visitAppContext: _ctx!, initialRoute: '/home'); } } class MyApp extends StatefulWidget { String initialRoute = ""; VisitAppContext visitAppContext; MyApp({Key? key, required this.initialRoute, required this.visitAppContext}) : super(key: key); @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); if (widget.visitAppContext.glassesEnabled && !Platform.isWindows && (Platform.isAndroid || Platform.isIOS)) { // Activity complètement attachée — démarrer le pipeline lunettes final ctx = widget.visitAppContext; WidgetsBinding.instance.addPostFrameCallback((_) async { // Foreground service Android — garde le main isolate vivant en background await GlassesBackgroundService.initialize(); await GlassesBackgroundService.start(); // connect() = enregistrement DAT + HFP audio routing (micro lunettes actif) await MetaGlassesService.instance.connect(); // Orchestrateur avec implémentations swappables // Pour passer à ElevenLabs TTS : remplacer FlutterTtsEngine par ElevenLabsTtsEngine // Pour passer à Porcupine wake word : remplacer SpeechToTextWakeWordEngine par PorcupineWakeWordEngine final orchestrator = GlassesOrchestrator( visitAppContext: ctx, // OpenWakeWord natif Android (TFLite, background, écran éteint) // Changer modelName: 'hey_viva' pour l'autre wakeword disponible wakeWordEngine: Platform.isAndroid ? NativeWakeWordEngine(modelName: 'hey_viva') : SpeechToTextWakeWordEngine(keyword: 'visite'), // fallback iOS // STT : WhisperSttEngine si clé configurée, sinon SpeechToTextSttEngine // OpenAI : --dart-define=WHISPER_API_KEY=sk-xxx // Auto-hébergé : --dart-define=WHISPER_API_KEY=xxx --dart-define=WHISPER_ENDPOINT=http://ton-serveur:8000/v1/audio/transcriptions sttEngine: kWhisperApiKey.isNotEmpty ? WhisperSttEngine(apiKey: kWhisperApiKey, endpoint: kWhisperEndpoint) : SpeechToTextSttEngine(), // TTS : Gemini 2.5 Flash si clé configurée, sinon flutter_tts on-device (tests) ttsEngine: kGeminiApiKey.isNotEmpty ? GeminiTtsEngine( apiKey: kGeminiApiKey, voiceName: kGeminiTtsVoice, voicePrompt: kGeminiTtsPrompt, ) : FlutterTtsEngine(), llmClient: MyInfoMateLlmClient(visitAppContext: ctx), ); activeOrchestrator = orchestrator; await orchestrator.start(); }); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } /// Relance l'écoute wake word quand l'app repasse en foreground (déverrouillage). /// SpeechRecognizer se suspend quand l'écran est verrouillé — on le redémarre. @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { final o = activeOrchestrator; // Relance uniquement si l'orchestrateur tourne ET qu'aucune conversation // n'est en cours (sinon on interromprait une question/réponse active) if (o != null && o.isRunning && !o.isInConversation) { o.restartWakeWord(); } } } @override Widget build(BuildContext context) { Get.put(RequirementStateController()); return ChangeNotifierProvider( create: (_) => AppContext(widget.visitAppContext), child: MaterialApp( debugShowCheckedModeBanner: false, title: 'Carnaval de Marche', //'Musée de la fraise' // Autres // 'Fort Saint Héribert' initialRoute: widget.initialRoute, localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const[ Locale('en', ''), Locale('fr', ''), ], theme: ThemeData( primarySwatch: Colors.blue, scaffoldBackgroundColor: kBackgroundColor, //fontFamily: "Vollkorn", textTheme: TextTheme(bodyLarge: TextStyle(color: widget.visitAppContext.configuration != null ? widget.visitAppContext.configuration!.primaryColor != null ? Color(int.parse(widget.visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kMainColor1 : kMainColor1)), visualDensity: VisualDensity.adaptivePlatformDensity, appBarTheme: const AppBarTheme( iconTheme: IconThemeData(color: Colors.white), // Change the color here ), ), routes: { '/home': (context) => const HomePage3(), } ), ); } }