mymuseum-visitapp/lib/Services/glasses_qr_scanner_service.dart

119 lines
4.3 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Services/assistantService.dart';
import 'package:mymuseum_visitapp/Services/glasses_tts_service.dart';
import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart';
import 'package:mymuseum_visitapp/constants.dart';
/// Décode les QR codes depuis les photos capturées par les lunettes.
///
/// Déclencheur : bouton hardware des lunettes → MetaGlassesService.requestPhotoCapture()
/// → capturePhoto() retourne un chemin fichier → analyzeImage() → stream barcodes
///
/// Sur QR valide (format web.mymuseum.be ou web.myinfomate.be) :
/// - Appelle AssistantService avec un prompt de résumé de la section
/// - Joue la réponse via GlassesTtsService
///
/// Anti-spam : 10s de cooldown entre deux traitements du même QR.
class GlassesQrScannerService {
GlassesQrScannerService._();
static final GlassesQrScannerService instance = GlassesQrScannerService._();
static const int _cooldownMs = 10000;
static final RegExp _urlPattern1 =
RegExp(r'https://web\.mymuseum\.be/([^/]+)/([^/]+)/([^/\s]+)');
static final RegExp _urlPattern2 =
RegExp(r'https://web\.myinfomate\.be/([^/]+)/([^/]+)/([^/\s]+)');
VisitAppContext? _visitAppContext;
AssistantService? _assistantService;
MobileScannerController? _scannerController;
final Map<String, int> _lastScanTime = {};
void start({required VisitAppContext visitAppContext}) {
_visitAppContext = visitAppContext;
_assistantService = AssistantService(visitAppContext: visitAppContext);
_scannerController = MobileScannerController();
_scannerController!.barcodes.listen((capture) {
for (final barcode in capture.barcodes) {
final raw = barcode.rawValue;
if (raw != null) _processRawValue(raw);
}
});
// meta_wearables_dat : capturePhoto() retourne un CapturedPhoto avec .path
MetaGlassesService.instance.onPhotoCaptured = _onPhotoCaptured;
debugPrint('[GlassesQrScannerService] Started');
}
void stop() {
MetaGlassesService.instance.onPhotoCaptured = null;
_scannerController?.dispose();
_scannerController = null;
debugPrint('[GlassesQrScannerService] Stopped');
}
Future<void> _onPhotoCaptured(String photoPath) async {
if (_visitAppContext == null || _scannerController == null) return;
try {
// analyzeImage déclenche le stream .barcodes si un QR est trouvé
await _scannerController!.analyzeImage(photoPath);
} catch (e) {
debugPrint('[GlassesQrScannerService] analyzeImage error: $e');
}
}
void _processRawValue(String raw) {
final sectionId = _extractSectionId(raw);
if (sectionId == null) {
debugPrint('[GlassesQrScannerService] QR not recognized: $raw');
return;
}
final ctx = _visitAppContext!;
if (ctx.sectionIds != null && !ctx.sectionIds!.contains(sectionId)) {
debugPrint('[GlassesQrScannerService] Section $sectionId not in current config');
return;
}
final now = DateTime.now().millisecondsSinceEpoch;
if ((now - (_lastScanTime[sectionId] ?? 0)) < _cooldownMs) return;
_lastScanTime[sectionId] = now;
_explainSection(sectionId);
}
String? _extractSectionId(String raw) {
final m1 = _urlPattern1.firstMatch(raw);
if (m1 != null) return m1.group(3);
final m2 = _urlPattern2.firstMatch(raw);
if (m2 != null) return m2.group(3);
if (_visitAppContext?.sectionIds?.contains(raw) == true) return raw;
return null;
}
Future<void> _explainSection(String sectionId) async {
debugPrint('[GlassesQrScannerService] Explaining section: $sectionId');
try {
final response = await _assistantService!.chat(
message: 'Le visiteur vient de scanner le QR code de la section "$sectionId". '
'Présente-lui ce contenu de façon engageante en 3 phrases maximum.',
configurationId: _visitAppContext?.configuration?.id,
);
if (response.reply.isNotEmpty) {
await GlassesTtsService.instance.speak(
response.reply,
apiKey: kElevenLabsApiKey,
voiceId: kElevenLabsVoiceId,
);
}
} catch (e) {
debugPrint('[GlassesQrScannerService] explainSection error: $e');
}
}
}