Thomas Fransolet e9e68942c9 Proactif câblé, M3 fermé, voix du CMS honorée, et trois services morts supprimés
Déclenchement proactif (lot F) — quatre branchements : la garde, le cycle de
vie accroché à VoiceController, les points GPS peuplés depuis la configuration,
et un réglage visiteur.

⚠️ La garde recommandée par le plan aurait coûté de l'argent. _trigger appelle
le LLM d'abord et ne parle qu'ensuite via activeVoiceOrchestrator?.ttsEngine,
dont le ?. avale le cas « aucun mode vocal actif ». Avec proactiveModeEnabled
seul, un visiteur traversant une zone avec l'app en poche et le vocal éteint
consommait des jetons Gemini facturés au client pour une phrase que personne
n'entend. La garde interroge l'orchestrateur : la vraie condition n'est pas
« quel matériel » mais « y a-t-il quelqu'un pour écouter ».

Il n'y avait pas de troisième mode à inventer : VoiceMode.voiceOnly existe déjà
et construit le même orchestrateur que le mode lunettes. Le proactif est donc un
sous-réglage, pas une tuile. glassesEnabled est supprimé, ses trois usages
étaient morts — dont une pastille « Lunettes / Déconnecté » jamais affichée.

M3 — meterZoneGPS devient le rayon par section, la constante n'étant plus qu'un
défaut. Deux pièges de format vérifiés : currentSections porte des maps JSON
brutes, pas des DTO, et latitude/longitude y sont des chaînes.

Voix du guide — le sélecteur Viva/Marco de manager-app écrivait dans le vide :
Instance.GuideVoiceId n'était lu nulle part ici, l'app utilisant une constante
de build dont le défaut était Algieba, une voix que personne n'a écoutée. Même
forme que W1. Et aucun APK ne parlait avec la voix du produit : GeminiTtsEngine
n'est choisi que si GEMINI_API_KEY est injectée, ce qui n'était fait nulle part,
donc repli silencieux sur la voix système. Le repli reste le défaut — les tests
ne coûtent alors ni jetons ni argent — mais il s'annonce dans les logs.

Code mort supprimé : wake_word_service.dart, glasses_qr_scanner_service.dart et
glasses_tts_service.dart, les ancêtres de Services/Glasses/. Ils portaient les 4
erreurs kElevenLabs* et l'APK se construisait quand même, personne ne les
important. flutter analyze lib rend désormais zéro erreur.
⚠️ android/.../WakeWordService.kt est un homonyme bien vivant, non touché.
impl/elevenlabs_tts_engine.dart est conservé comme option.

flutter build apk --debug --flavor dev : vert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 11:57:44 +02:00

220 lines
8.1 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/voice_orchestrator.dart';
import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart';
import 'package:mymuseum_visitapp/Services/voice_controller.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 (si pas encore en mémoire) et la voix du guide choisie par
// le client. La condition portait sur `apiKey == null` : une fois la clé en base, la
// réponse n'était plus jamais lue, et `guideVoiceId` avec elle. Elle porte désormais
// sur l'instance — un appel au démarrage, déjà non bloquant en cas d'échec réseau.
if (localContext.instanceId != null) {
try {
final instanceDto = await localContext.clientAPI.instanceApi!
.instanceGetDetail(localContext.instanceId!);
if (localContext.apiKey == null && 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');
}
localContext.guideVoiceId = instanceDto?.guideVoiceId;
} catch (e) {
// Hors ligne : la voix retombe sur le repli du build, le reste de l'app tourne.
print('Could not load instance detail: $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 SDK init — toujours initialisé pour détecter la connexion BT.
// L'orchestrateur vocal ne démarre PAS automatiquement — c'est VoiceController qui gère ça.
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);
// Le mode vocal ne démarre plus automatiquement — l'utilisateur choisit via VoiceModeSheet.
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
final o = activeVoiceOrchestrator;
if (o != null && o.isRunning && !o.isInConversation) {
o.restartWakeWord();
}
}
}
@override
Widget build(BuildContext context) {
Get.put(RequirementStateController());
return MultiProvider(
providers: [
ChangeNotifierProvider<AppContext>(
create: (_) => AppContext(widget.visitAppContext),
),
ChangeNotifierProvider<VoiceController>(
create: (_) => VoiceController(),
),
],
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(),
}
),
);
}
}