import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:mymuseum_visitapp/Models/visitContext.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'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/myinfomate_llm_client.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_stt.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/speech_to_text_wake_word.dart'; import 'package:mymuseum_visitapp/Services/Glasses/engines/impl/whisper_stt_engine.dart'; import 'package:mymuseum_visitapp/Services/Glasses/glasses_background_service.dart'; import 'package:mymuseum_visitapp/Services/Glasses/voice_orchestrator.dart'; import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart'; import 'package:mymuseum_visitapp/constants.dart'; enum VoiceMode { none, voiceOnly, glasses } class VoiceController extends ChangeNotifier { VoiceMode _mode = VoiceMode.none; VoiceMode get mode => _mode; bool _isActivating = false; bool get isActivating => _isActivating; VoiceController() { MetaGlassesService.instance.state.addListener(_onGlassesStateChanged); } bool isFeatureAvailable(VisitAppContext ctx) => ctx.applicationInstanceDTO?.isAssistant == true; bool get isGlassesConnected => MetaGlassesService.instance.state.value == GlassesState.connected || MetaGlassesService.instance.state.value == GlassesState.streaming; /// Active le mode demandé. Si un mode est déjà actif, le stoppe d'abord. Future activate(VoiceMode targetMode, VisitAppContext ctx) async { if (_isActivating) return; _isActivating = true; notifyListeners(); try { await _stopOrchestrator(); if (targetMode == VoiceMode.glasses) { await GlassesBackgroundService.initialize(); await GlassesBackgroundService.start(); await MetaGlassesService.instance.connect(); } _buildOrchestrator(ctx); await activeVoiceOrchestrator!.start(); _mode = targetMode; } catch (e) { debugPrint('[VoiceController] activate error: $e'); await _stopOrchestrator(); _mode = VoiceMode.none; } finally { _isActivating = false; notifyListeners(); } } Future deactivate() async { await _stopOrchestrator(); _mode = VoiceMode.none; notifyListeners(); } /// Relance le wake word après un retour en foreground. void restartWakeWordIfNeeded() { final o = activeVoiceOrchestrator; if (o != null && o.isRunning && !o.isInConversation) { o.restartWakeWord(); } } Future _stopOrchestrator() async { if (activeVoiceOrchestrator != null) { await activeVoiceOrchestrator!.stop(); activeVoiceOrchestrator = null; } } void _buildOrchestrator(VisitAppContext ctx) { activeVoiceOrchestrator = VoiceOrchestrator( visitAppContext: ctx, wakeWordEngine: Platform.isAndroid ? NativeWakeWordEngine(modelNames: ['hey_marco', 'hey_alba', 'hey_vasco']) : SpeechToTextWakeWordEngine(keyword: 'visite'), sttEngine: kWhisperApiKey.isNotEmpty ? WhisperSttEngine(apiKey: kWhisperApiKey, endpoint: kWhisperEndpoint) : SpeechToTextSttEngine(), ttsEngine: kGeminiApiKey.isNotEmpty ? GeminiTtsEngine( apiKey: kGeminiApiKey, voiceName: kGeminiTtsVoice, voicePrompt: kGeminiTtsPrompt, ) : FlutterTtsEngine(), llmClient: MyInfoMateLlmClient(visitAppContext: ctx), ); } void _onGlassesStateChanged() { if (_mode == VoiceMode.glasses && MetaGlassesService.instance.state.value == GlassesState.disconnected) { debugPrint('[VoiceController] Glasses disconnected — deactivating'); deactivate(); } } @override void dispose() { MetaGlassesService.instance.state.removeListener(_onGlassesStateChanged); _stopOrchestrator(); super.dispose(); } }