DOCS/rayban-meta-integration.md
Thomas Fransolet 4206fde7f0 Ray-Ban Meta : le SDK DAT tel qu'il est réellement intégré
flutter_meta_wearables_dat 0.9.1 et app_links remplacent les
dépendances « à décommenter quand publiées ». Le SDK natif vit sur
GitHub Packages : sans PAT read:packages dans local.properties, le
build tombe en 401 — et un token expiré peut passer inaperçu, Gradle
servant son cache.

Note ce que le SDK ne donne pas (ni micro, ni haut-parleur, ni bouton :
le vocal passe par le routage Bluetooth) et pourquoi iOS n'est pas
activé.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:05:06 +02:00

306 lines
13 KiB
Markdown

# Intégration Ray-Ban Meta — mymuseum-visitapp
## Vue d'ensemble
L'intégration Ray-Ban Meta transforme `mymuseum-visitapp` en guide audio mains-libres. Le visiteur porte les lunettes ; son téléphone est en poche. Il peut :
- Poser une question à voix haute à l'assistant sans toucher le téléphone
- Scanner un QR code en appuyant sur le bouton des lunettes → entendre l'explication
- Recevoir automatiquement des informations contextuelles en approchant d'une œuvre (beacon ou géolocalisation)
L'architecture existante (AssistantService, QR scanner, beacons, géoloc) est **inchangée**. Les lunettes sont une nouvelle surface d'entrée/sortie branchée dessus.
---
## Prérequis
### Matériel & apps
- Ray-Ban Meta Gen 2 (~380€, 8h autonomie)
- **App Meta AI installée sur le téléphone** — bridge Bluetooth obligatoire
- Compte Meta connecté dans l'app Meta AI
### Comptes & clés API
| Service | Usage | Où obtenir |
|---|---|---|
| **Picovoice Console** | Générer le wake word .ppn | https://console.picovoice.ai/ — gratuit jusqu'à 3 keywords |
| **ElevenLabs** | Synthèse vocale TTS | https://elevenlabs.io — tier gratuit ou payant selon volume |
### SDK Flutter (developer preview)
```yaml
flutter_meta_wearables_dat: ^0.9.1 # Android API 29+ et iOS 17.2+ — SDK DAT 0.9.0
app_links: ^6.4.1 # retour du deep link Meta AI → handleUrl()
```
Le SDK natif est publié sur **GitHub Packages**, pas sur Maven Central. Il faut un
PAT GitHub avec le scope `read:packages`, dans `android/local.properties` :
```properties
github_token=ghp_xxxx
```
Sans lui, le build échoue en `401 Unauthorized` sur `maven.pkg.github.com`. Un build
peut sembler passer alors que le token est expiré : Gradle sert les artefacts depuis
son cache local. Vérifier le token, pas le build.
**iOS n'est pas activé** malgré le support du package. Le coût réel :
- deployment target à **17.2** pour toute l'app visiteur (Podfile est à 12.0)
- dictionnaire `MWDAT` dans Info.plist (`MetaAppID`, `ClientToken`, `TeamID`) — le
mode développeur `"0"` d'Android ne suffit pas
- choix d'un transport caméra : Wi-Fi (entitlements Hotspot Configuration + Wi-Fi
Information, `NSLocalNetworkUsageDescription`, +10 s à la 1re connexion) ou
Bluetooth Classic (`UISupportedExternalAccessoryProtocols`, bande passante moindre)
- la publication publique reste limitée aux partenaires Meta sous developer preview
Le SDK DAT n'expose **ni micro, ni haut-parleur, ni bouton** des lunettes. Tout le
pipeline vocal passe hors SDK (routing Bluetooth HFP/A2DP via `AudioRoutingChannel`).
---
## Setup initial
### 1. Générer les fichiers wake word (.ppn)
1. Aller sur https://console.picovoice.ai/
2. Créer un compte → **Wake Word****Create a keyword**
3. Keyword : `Hey MyVisit` (ou autre phrase de votre choix)
4. Exporter pour **Android** → sauvegarder dans `assets/wake_words/hey_myvisit_android.ppn`
5. Exporter pour **iOS** → sauvegarder dans `assets/wake_words/hey_myvisit_ios.ppn`
6. Copier l'**Access Key** affiché dans le dashboard
### 2. Configurer les variables de build (--dart-define)
Toutes les clés et URLs sont injectées via `--dart-define` au moment du build/run. Aucune clé en dur dans le code.
#### Référence complète des `--dart-define`
| Variable | Obligatoire | Exemple | Description |
|---|---|---|---|
| `API_BASE_URL` | ✅ prod | `https://api.mymuseum.be` | URL du backend MyInfoMate |
| `API_KEY` | ✅ | `abc123` | Clé API MyInfoMate (bootstrappe la clé depuis le backend au 1er run) |
| `INSTANCE_ID` | ✅ | `63514fd67ed8c735aaa4b8f2` | ID de l'instance (fixe par flavor) |
| `WHISPER_API_KEY` | ⚡ recommandé | `sk-xxxx` | Clé OpenAI Whisper **ou** n'importe quelle valeur si endpoint auto-hébergé |
| `WHISPER_ENDPOINT` | ⚡ si auto-hébergé | `http://192.168.1.x:8000/v1/audio/transcriptions` | Endpoint Whisper compatible OpenAI (défaut: api.openai.com) |
| `ELEVENLABS_API_KEY` | ❌ optionnel | `sk_xxxx` | TTS ElevenLabs — si vide, utilise flutter_tts (on-device gratuit) |
| `ELEVENLABS_VOICE_ID` | ❌ optionnel | `EXAVITQu4vr4xnSDxMaL` | ID voix ElevenLabs |
| `PICOVOICE_ACCESS_KEY` | ❌ optionnel | `xxxx` | Wake word Porcupine — si vide, utilise speech_to_text |
| `FLAVOR` | ✅ build | `dev` / `mdlf` / `fortsaintheribert` | Détermine les couleurs et l'app name |
#### Commandes de lancement
**Dev minimal (speech_to_text + flutter_tts) :**
```bash
flutter run --flavor dev \
--dart-define=API_BASE_URL=http://192.168.x.x:5000 \
--dart-define=API_KEY=ta_clé \
--dart-define=INSTANCE_ID=63514fd67ed8c735aaa4b8f2
```
**Dev avec Whisper OpenAI :**
```bash
flutter run --flavor dev \
--dart-define=API_BASE_URL=https://api.mymuseum.be \
--dart-define=API_KEY=ta_clé \
--dart-define=INSTANCE_ID=63514fd67ed8c735aaa4b8f2 \
--dart-define=WHISPER_API_KEY=sk-xxxx
```
**Dev avec Whisper auto-hébergé (faster-whisper-server Docker) :**
```bash
flutter run --flavor dev \
--dart-define=API_BASE_URL=https://api.mymuseum.be \
--dart-define=API_KEY=ta_clé \
--dart-define=INSTANCE_ID=63514fd67ed8c735aaa4b8f2 \
--dart-define=WHISPER_API_KEY=anykey \
--dart-define=WHISPER_ENDPOINT=http://192.168.x.x:8000/v1/audio/transcriptions
```
**Production complète (Porcupine + Whisper auto-hébergé + flutter_tts) :**
```bash
flutter build appbundle --flavor mdlf \
--dart-define=API_BASE_URL=https://api.mymuseum.be \
--dart-define=API_KEY=ta_clé \
--dart-define=INSTANCE_ID=xxx \
--dart-define=FLAVOR=mdlf \
--dart-define=PICOVOICE_ACCESS_KEY=xxx \
--dart-define=WHISPER_API_KEY=anykey \
--dart-define=WHISPER_ENDPOINT=https://whisper.myinfomate.be/v1/audio/transcriptions
```
#### Démarrer faster-whisper-server (Docker)
```bash
docker run -p 8000:8000 fedirz/faster-whisper-server:latest-cpu
# Avec GPU :
docker run --gpus all -p 8000:8000 fedirz/faster-whisper-server:latest-cuda
```
### 3. Activer le mode lunettes dans l'app
Dans `VisitAppContext`, passer `glassesEnabled = true`. Pour l'instant c'est à faire dans le code ou via un toggle UI à créer dans les paramètres de la configuration.
### 4. Lancer l'app sur le device
```bash
flutter run --dart-define=ELEVENLABS_API_KEY=xxx --dart-define=PICOVOICE_ACCESS_KEY=xxx
```
---
## Architecture technique
```
Lunettes Ray-Ban Meta
│ Bluetooth (HFP mic, A2DP speakers, camera stream, button)
[MetaGlassesService] ← SDK DAT lifecycle + capture photo
├── [WakeWordService] ← Porcupine détecte "Hey MyVisit" (on-device)
│ └── speech_to_text ← transcrit la commande après wake word
│ ├── "scanne ce QR" → MetaGlassesService.requestPhotoCapture()
│ ├── "répète" → GlassesTtsService.replay()
│ └── autre → AssistantService.chat() → GlassesTtsService
├── [GlassesQrScannerService] ← photo → mobile_scanner → regex URL → AssistantService
├── [GeoBeaconTriggerService] ← geolocator GPS + beacon_scanner BLE → AssistantService
└── [GlassesTtsService] ← ElevenLabs API → just_audio → AudioRoutingChannel
└── [AudioRoutingPlugin] ← natif iOS (AVAudioSession) / Android (AudioManager)
force sortie A2DP vers lunettes
```
### Flux de données type (question vocale)
1. Porcupine détecte "Hey MyVisit"
2. `speech_to_text` transcrit : "Qui a peint ce tableau ?"
3. `AssistantService.chat("Qui a peint ce tableau ?")` → backend MyInfoMate
4. Réponse texte → `GlassesTtsService.speak()`
5. ElevenLabs → MP3 → `just_audio` → AudioRoutingPlugin → lunettes Ray-Ban
---
## Fonctionnalités
### 1. Wake word + conversation vocale
**Déclencheur :** "Hey MyVisit"
**Commandes reconnues :**
| Phrase | Action |
|---|---|
| "Hey MyVisit" + question | Pose la question à l'assistant, entend la réponse |
| "Hey MyVisit, scanne ce QR" | Déclenche la capture photo → pipeline QR |
| "Hey MyVisit, répète" | Rejoue la dernière synthèse vocale |
Le wake word fonctionne en permanence en background (Foreground Service sur Android).
### 2. Scanner QR via lunettes
**Déclencheur :** commande vocale "Hey MyVisit, scanne ce QR"
⚠️ Le bouton hardware des lunettes n'est **pas** un déclencheur : le SDK DAT ne
l'expose pas, et rien n'est implémenté pour lui côté app.
**Fonctionnement :**
1. Les lunettes capturent une photo
2. `GlassesQrScannerService` décode le QR (même regex que le scanner téléphone)
3. Si la section appartient à la configuration courante, l'assistant la résume en 3 phrases
4. La réponse est lue dans les lunettes
**Anti-spam :** 10s de cooldown entre deux scans du même QR.
### 3. Géolocalisation → TTS automatique (mode proactif)
Activer `VisitAppContext.proactiveModeEnabled = true` pour que l'assistant parle spontanément.
**GPS :** Le service surveille en continu la position GPS. Quand le visiteur entre dans le rayon d'un `GeoTriggerPoint` (défini par lat/lng + rayon en mètres), l'assistant propose une présentation du lieu.
**Beacons BLE :** Quand la précision d'un beacon connu passe sous `3m`, l'assistant présente l'œuvre ou l'espace associé.
Les deux fonctionnent en parallèle, avec un cooldown de 30s par point pour éviter le spam.
### 4. Navigation guidée mains-libres
Dans un `GuidedPath`, chaque étape peut déclencher un TTS automatique à l'entrée de sa zone GPS ou à la détection de son beacon. Le visiteur progresse sans jamais toucher son téléphone.
### 5. Mode "Quiet"
La sortie audio est forcée sur les lunettes via `AudioRoutingPlugin`, pas sur le haut-parleur téléphone. Respecte le silence des musées.
---
## Mode proactif — comportement
| `proactiveModeEnabled` | `glassesEnabled` | Comportement |
|---|---|---|
| `false` | `false` | Comportement app normal, pas de lunettes |
| `false` | `true` | Lunettes actives, TTS sur questions vocales et QR, **pas** de déclenchements auto |
| `true` | `true` | Tout actif : wake word + QR + géoloc/beacon auto |
Recommandation : proposer le toggle du mode proactif à l'écran de démarrage de visite.
---
## Multi-visiteur broadcast (V2)
Plusieurs visiteurs avec des lunettes dans la même instance peuvent recevoir simultanément un message du guide via Firebase.
**Comment ça marchera :**
1. Le guide envoie un message depuis `manager-app` → push notification Firebase
2. `PushNotificationService` reçoit la notification
3. Un handler à ajouter dans `initialize()` pipe le corps du message vers `GlassesTtsService`
4. Tous les visiteurs connectés entendent le message dans leurs lunettes
Le topic Firebase `instance_{instanceId}` est déjà en place.
---
## Limitations SDK (developer preview, mai 2026)
| Limitation | Détail |
|---|---|
| **App Meta AI requise** | Doit être installée et active — bridge Bluetooth obligatoire |
| **Résolution caméra** | Max 720p/30fps via Bluetooth (contrainte protocole, pas hardware) |
| **Publication App Store/Play Store** | Réservée aux partenaires Meta sélectionnés pour l'instant — distribution interne possible |
| **Android uniquement** pour `meta_wearables_dat` | Le plugin Flutter iOS (`meta_wearables`) est en cours |
| **iOS 26.0+** pour le SDK natif | Si implémentation Swift directe (comme OpenGlasses) |
---
## Troubleshooting
**Son dans le téléphone au lieu des lunettes**
- Vérifier que les lunettes sont bien en Bluetooth actif et appairées
- Sur Android API < 31 : vérifier que `startBluetoothSco()` ne génère pas d'erreur de permission
- Sur iOS : vérifier que `AVAudioSession.setCategory` est appelé avant `just_audio.play()`
**Wake word ne répond pas**
- Vérifier que `kPicovoiceAccessKey` est bien injecté (non vide)
- Vérifier que les fichiers `.ppn` sont bien dans `assets/wake_words/` et déclarés dans `pubspec.yaml`
- Les fichiers `.ppn` sont spécifiques Android/iOS ne pas les inverser
**Faux positifs wake word fréquents**
- Télécharger un modèle plus sensible (paramètre `sensitivity` de `PorcupineManager`)
- Choisir une phrase wake word plus longue sur Picovoice Console
**Scan QR échoue depuis les lunettes**
- Vérifier que le QR code est bien visible (lumière suffisante)
- Le SDK DAT doit être en état `connected` ou `streaming` vérifier `MetaGlassesService.instance.state`
- Vérifier que la section scannée appartient bien à la configuration active (`visitContext.sectionIds`)
**ElevenLabs retourne 401**
- Clé API expirée ou quota dépassé vérifier sur https://elevenlabs.io/speech-synthesis
---
## Fichiers clés
| Fichier | Rôle |
|---|---|
| `lib/Services/meta_glasses_service.dart` | SDK DAT lifecycle, photo capture |
| `lib/Services/glasses_tts_service.dart` | ElevenLabs audio Bluetooth |
| `lib/Services/wake_word_service.dart` | Porcupine + dispatch commandes vocales |
| `lib/Services/geo_beacon_trigger_service.dart` | Géofence GPS + beacons TTS auto |
| `lib/Services/glasses_qr_scanner_service.dart` | QR decode depuis photo capture lunettes |
| `lib/PlatformChannels/audio_routing_channel.dart` | Canal Dart natif pour routage Bluetooth |
| `android/…/MainActivity.kt` | Plugin Android : AudioManager A2DP |
| `ios/Runner/AudioRoutingPlugin.swift` | Plugin iOS : AVAudioSession Bluetooth |
| `assets/wake_words/` | Fichiers .ppn Porcupine (à générer) |