Émission des événements vocaux — le vocal est un attribut, pas un canal
StatisticsService.track porte isVoice, qui pose "voice": true dans VisitEvent.Metadata. Le flux lunettes n'émettait aucun VisitEvent : une visite vocale ne produisait ni session, ni section vue, ni durée. Le sectionView vocal part après la synthèse, pas avant : un déclenchement dont le TTS échoue n'a rien fait entendre, le compter serait faux. Aucun événement par question posée — elles sont déjà dans VisitorQuestion et remontent dans l'onglet Guide IA ; les compter ici serait le même fait dans deux écrans. isAutoTriggered suit le renommage serveur en isVisitorQuestion. flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
591775cd30
commit
e967678dd2
@ -102,29 +102,29 @@ class AssistantService extends ChangeNotifier {
|
|||||||
required String message,
|
required String message,
|
||||||
String? configurationId,
|
String? configurationId,
|
||||||
bool isVoice = false,
|
bool isVoice = false,
|
||||||
bool isAutoTriggered = false,
|
bool isVisitorQuestion = true,
|
||||||
}) => chatWithAppType(
|
}) => chatWithAppType(
|
||||||
message: message,
|
message: message,
|
||||||
configurationId: configurationId,
|
configurationId: configurationId,
|
||||||
isVoice: isVoice,
|
isVoice: isVoice,
|
||||||
isAutoTriggered: isAutoTriggered,
|
isVisitorQuestion: isVisitorQuestion,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// [isAutoTriggered] : le tour vient du mode proactif, pas d'une question du visiteur.
|
/// [isVisitorQuestion] à false : le tour vient du mode proactif, pas d'une question
|
||||||
/// Le serveur compte les jetons mais ne le journalise pas dans `VisitorQuestion`.
|
/// posée. Le serveur compte les jetons mais ne le journalise pas dans `VisitorQuestion`.
|
||||||
Future<AssistantResponse> chatWithAppType({
|
Future<AssistantResponse> chatWithAppType({
|
||||||
required String message,
|
required String message,
|
||||||
String? configurationId,
|
String? configurationId,
|
||||||
AppType appType = AppType.Mobile,
|
AppType appType = AppType.Mobile,
|
||||||
bool isVoice = false,
|
bool isVoice = false,
|
||||||
bool isAutoTriggered = false,
|
bool isVisitorQuestion = true,
|
||||||
}) async {
|
}) async {
|
||||||
_resetInactivityTimer();
|
_resetInactivityTimer();
|
||||||
|
|
||||||
// Le prompt d'un déclenchement proactif est une consigne machine : il ne
|
// Le prompt d'un déclenchement proactif est une consigne machine : il ne
|
||||||
// s'affiche pas. La réponse, elle, s'affichera — c'est ce que le visiteur a
|
// s'affiche pas. La réponse, elle, s'affichera — c'est ce que le visiteur a
|
||||||
// entendu, et c'est tout l'objet du miroir.
|
// entendu, et c'est tout l'objet du miroir.
|
||||||
if (!isAutoTriggered) {
|
if (isVisitorQuestion) {
|
||||||
_turns.add(AssistantTurn(text: message, isUser: true, isVoice: isVoice));
|
_turns.add(AssistantTurn(text: message, isUser: true, isVoice: isVoice));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ class AssistantService extends ChangeNotifier {
|
|||||||
conversationId: _conversationId,
|
conversationId: _conversationId,
|
||||||
history: List.from(_history),
|
history: List.from(_history),
|
||||||
isVoice: isVoice,
|
isVoice: isVoice,
|
||||||
isAutoTriggered: isAutoTriggered,
|
isVisitorQuestion: isVisitorQuestion,
|
||||||
);
|
);
|
||||||
|
|
||||||
final AiChatResponse? response;
|
final AiChatResponse? response;
|
||||||
@ -179,7 +179,7 @@ class AssistantService extends ChangeNotifier {
|
|||||||
// modèle n'a pas à relire « Tu es un guide audio de musée… » au tour suivant.
|
// modèle n'a pas à relire « Tu es un guide audio de musée… » au tour suivant.
|
||||||
// Sa réponse, si — c'est ce que le visiteur a entendu, et ça rend « tu peux
|
// Sa réponse, si — c'est ce que le visiteur a entendu, et ça rend « tu peux
|
||||||
// répéter ? » compréhensible depuis le chat comme depuis la voix.
|
// répéter ? » compréhensible depuis le chat comme depuis la voix.
|
||||||
if (!isAutoTriggered) {
|
if (isVisitorQuestion) {
|
||||||
_history.add(AiChatMessage(role: 'user', content: message));
|
_history.add(AiChatMessage(role: 'user', content: message));
|
||||||
}
|
}
|
||||||
_history.add(AiChatMessage(role: 'assistant', content: result.reply));
|
_history.add(AiChatMessage(role: 'assistant', content: result.reply));
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:beacon_scanner/beacon_scanner.dart';
|
import 'package:beacon_scanner/beacon_scanner.dart';
|
||||||
|
import 'package:manager_api_new/api.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||||
@ -123,8 +124,11 @@ class GeoBeaconTriggerService {
|
|||||||
point.longitude,
|
point.longitude,
|
||||||
);
|
);
|
||||||
if (dist <= point.radiusMeters && _canTrigger(point.id)) {
|
if (dist <= point.radiusMeters && _canTrigger(point.id)) {
|
||||||
|
// L'id d'un point GPS **est** l'id de la section dont il vient (`buildGeoPoints`),
|
||||||
|
// donc la consultation est attribuable.
|
||||||
_trigger(
|
_trigger(
|
||||||
triggerId: point.id,
|
triggerId: point.id,
|
||||||
|
sectionId: point.id,
|
||||||
prompt: 'Tu es un guide audio de musée. '
|
prompt: 'Tu es un guide audio de musée. '
|
||||||
'Le visiteur vient d\'entrer dans la zone "${point.id}". '
|
'Le visiteur vient d\'entrer dans la zone "${point.id}". '
|
||||||
'Accueille-le et donne-lui une brève information de bienvenue en 2 phrases.',
|
'Accueille-le et donne-lui une brève information de bienvenue en 2 phrases.',
|
||||||
@ -177,6 +181,7 @@ class GeoBeaconTriggerService {
|
|||||||
if (match?.sectionId != null && _canTrigger(match!.sectionId!)) {
|
if (match?.sectionId != null && _canTrigger(match!.sectionId!)) {
|
||||||
_trigger(
|
_trigger(
|
||||||
triggerId: match.sectionId!,
|
triggerId: match.sectionId!,
|
||||||
|
sectionId: match.sectionId,
|
||||||
prompt: 'Tu es un guide audio de musée. '
|
prompt: 'Tu es un guide audio de musée. '
|
||||||
'Le visiteur est juste devant l\'œuvre ou l\'espace lié au point d\'intérêt "${match.sectionId}". '
|
'Le visiteur est juste devant l\'œuvre ou l\'espace lié au point d\'intérêt "${match.sectionId}". '
|
||||||
'Donne une présentation courte et engageante en 2-3 phrases.',
|
'Donne une présentation courte et engageante en 2-3 phrases.',
|
||||||
@ -238,23 +243,26 @@ class GeoBeaconTriggerService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [sectionId] : renseigné quand le déclencheur correspond à une section (beacon ou
|
||||||
|
/// zone GPS d'un point d'intérêt). C'est lui qui permet de compter la consultation.
|
||||||
Future<void> _trigger({
|
Future<void> _trigger({
|
||||||
required String triggerId,
|
required String triggerId,
|
||||||
required String prompt,
|
required String prompt,
|
||||||
|
String? sectionId,
|
||||||
}) async {
|
}) async {
|
||||||
_lastTriggerTime[triggerId] = DateTime.now().millisecondsSinceEpoch;
|
_lastTriggerTime[triggerId] = DateTime.now().millisecondsSinceEpoch;
|
||||||
debugPrint('[GeoBeaconTriggerService] Trigger: $triggerId');
|
debugPrint('[GeoBeaconTriggerService] Trigger: $triggerId');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ⚠️ `isAutoTriggered` n'est pas cosmétique : sans lui, ce prompt — une consigne que
|
// ⚠️ `isVisitorQuestion: false` n'est pas cosmétique : sans lui, ce prompt — une
|
||||||
// le système s'écrit à lui-même — atterrissait dans `VisitorQuestion`, donc dans
|
// consigne que le système s'écrit à lui-même — atterrissait dans `VisitorQuestion`,
|
||||||
// l'onglet « Ce que demandent vos visiteurs », et gonflait le bloc des questions
|
// donc dans l'onglet « Ce que demandent vos visiteurs », et gonflait le bloc des
|
||||||
// sans réponse. Les jetons, eux, restent comptés côté serveur.
|
// questions sans réponse. Les jetons, eux, restent comptés côté serveur.
|
||||||
final response = await _assistantService!.chat(
|
final response = await _assistantService!.chat(
|
||||||
message: prompt,
|
message: prompt,
|
||||||
configurationId: _visitAppContext?.configuration?.id,
|
configurationId: _visitAppContext?.configuration?.id,
|
||||||
isVoice: true,
|
isVoice: true,
|
||||||
isAutoTriggered: true,
|
isVisitorQuestion: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.reply.isNotEmpty) {
|
if (response.reply.isNotEmpty) {
|
||||||
@ -263,6 +271,21 @@ class GeoBeaconTriggerService {
|
|||||||
response.reply,
|
response.reply,
|
||||||
languageCode: _toLangCode(lang),
|
languageCode: _toLangCode(lang),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// La section a réellement été lue au visiteur : c'est une consultation, au même
|
||||||
|
// titre qu'un écran ouvert au doigt. Émis **après** la synthèse, pas avant — un
|
||||||
|
// déclenchement dont le TTS échoue n'a rien fait entendre.
|
||||||
|
//
|
||||||
|
// ⛔ Aucun événement par question posée : elles sont déjà dans `VisitorQuestion`
|
||||||
|
// et remontent dans l'onglet Guide IA. Les compter ici serait le même fait dans
|
||||||
|
// deux écrans.
|
||||||
|
if (sectionId != null) {
|
||||||
|
_visitAppContext?.statisticsService?.track(
|
||||||
|
VisitEventType.sectionView,
|
||||||
|
sectionId: sectionId,
|
||||||
|
isVoice: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[GeoBeaconTriggerService] Trigger error for $triggerId: $e');
|
debugPrint('[GeoBeaconTriggerService] Trigger error for $triggerId: $e');
|
||||||
|
|||||||
@ -26,12 +26,28 @@ class StatisticsService {
|
|||||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [isVoice] marque un événement **consommé à la voix** — le guide a lu la section
|
||||||
|
/// à haute voix, par déclenchement proactif ou en réponse à une question.
|
||||||
|
///
|
||||||
|
/// ⚠️ C'est un attribut de l'événement, **pas un `appType`**. Un visiteur passe des
|
||||||
|
/// lunettes à l'écran dans une même session — c'est tout l'objet du miroir — et
|
||||||
|
/// `AppTypeDistribution` ne compte qu'une entrée par session, tranchée sur
|
||||||
|
/// l'événement le plus ancien : commencer au doigt puis mettre les lunettes aurait
|
||||||
|
/// rendu le vocal invisible, l'inverse aurait compté la navigation tactile comme
|
||||||
|
/// vocale. Le KPI serveur devient « part des sessions ayant utilisé le vocal ».
|
||||||
|
///
|
||||||
|
/// Même conception que `VisitorQuestion`, qui porte un booléen `IsVoice` **à côté**
|
||||||
|
/// de son `AppType`.
|
||||||
Future<void> track(
|
Future<void> track(
|
||||||
String eventType, {
|
String eventType, {
|
||||||
String? sectionId,
|
String? sectionId,
|
||||||
int? durationSeconds,
|
int? durationSeconds,
|
||||||
Map<String, dynamic>? metadata,
|
Map<String, dynamic>? metadata,
|
||||||
|
bool isVoice = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
final payload = isVoice
|
||||||
|
? {...?metadata, 'voice': true}
|
||||||
|
: metadata;
|
||||||
try {
|
try {
|
||||||
await clientAPI.statsApi!.statsTrackEvent(VisitEventDTO(
|
await clientAPI.statsApi!.statsTrackEvent(VisitEventDTO(
|
||||||
instanceId: instanceId ?? '',
|
instanceId: instanceId ?? '',
|
||||||
@ -42,7 +58,7 @@ class StatisticsService {
|
|||||||
appType: appType,
|
appType: appType,
|
||||||
language: language,
|
language: language,
|
||||||
durationSeconds: durationSeconds,
|
durationSeconds: durationSeconds,
|
||||||
metadata: metadata != null ? jsonEncode(metadata) : null,
|
metadata: payload != null ? jsonEncode(payload) : null,
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
));
|
));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user