261 lines
11 KiB
Dart
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(),
|
|
}
|
|
),
|
|
);
|
|
}
|
|
}
|