261 lines
11 KiB
Dart

//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<AppBootstrap> {
VisitAppContext? _ctx;
@override
void initState() {
super.initState();
_initApp();
}
Future<void> _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<SectionRead> articleReadTest = List<SectionRead>.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<SectionRead> articleReadTest = List<SectionRead>.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<MyApp> 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<AppContext>(
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(),
}
),
);
}
}