119 lines
4.3 KiB
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');
|
|
}
|
|
}
|
|
}
|