mymuseum-visitapp/lib/Services/voice_controller.dart
2026-07-17 15:23:12 +02:00

120 lines
4.1 KiB
Dart

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<void> 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<void> 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<void> _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();
}
}