fix(mobile): dictée validée sur iPhone physique — 2 bugs réels corrigés

setAudioModeAsync({allowsRecording:true}) manquant avant recorder.record()
(RecordingDisabledException iOS) ; EXPO_USE_PRECOMPILED_MODULES/
RCT_USE_PREBUILT_RNCORE jamais persistés dans .env (documentés dans ADR-005
mais seulement exportés en shell ad-hoc) — corrigé pour que tout rebuild
depuis zéro ne reproduise plus le crash dyld. Recette terrain complète
vérifiée : dictée → note → clôture → réindexation → recherche sémantique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-08-01 22:09:04 +01:00
parent 59ed6f3952
commit 4af3f5668a
5 changed files with 68 additions and 5 deletions

View File

@@ -68,5 +68,6 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS),
- 🏁 **R5 CLOSE (17/07/2026, tag `release/r5`)** : recettée par le référent (revue pixel + arbitrage tableau), recette passée SANS clé API, durcissement répété en local conteneurisé. **R0 → R5 : périmètre v1 couvert.** - 🏁 **R5 CLOSE (17/07/2026, tag `release/r5`)** : recettée par le référent (revue pixel + arbitrage tableau), recette passée SANS clé API, durcissement répété en local conteneurisé. **R0 → R5 : périmètre v1 couvert.**
- **Recette terrain mobile sur iPhone 15 Pro — 7/7 validés (19/07, ADR-005)** : Expo Go bloqué par le retard d'approbation Apple (SDK 54 vs notre SDK 57) **décision : builds natifs locaux (Xcode/Android Studio, signature gratuite) pour la vraie recette, Expo Go conservé pour l'aperçu sans installation**. 5 obstacles techniques réels corrigés et documentés (UDID plutôt que nom, ne jamais contourner `expo run:*` par un xcodebuild manuel, modules Expo/RN précompilés SDK 56/57 incompatibles avec la liaison statique du projet, `fetch` global incompatible avec l'upload multipart natif fix permanent `EXPO_PUBLIC_USE_RN_FETCH=1` dans `apps/mobile/.env`, découverte réseau du dev client peu fiable). Recette iOS : connexion, scan caméra réel (résolution + rejet QR étranger), vrai mode avion, conflit D2 tranché par l'humain avec photo réellement téléversée, persistance à travers fermeture/reconstruction de l'app, suggestions R5 au vrai modèle, purge de sécurité au changement de compte tout validé. Android : build natif sur émulateur, passage santé complet. - **Recette terrain mobile sur iPhone 15 Pro — 7/7 validés (19/07, ADR-005)** : Expo Go bloqué par le retard d'approbation Apple (SDK 54 vs notre SDK 57) **décision : builds natifs locaux (Xcode/Android Studio, signature gratuite) pour la vraie recette, Expo Go conservé pour l'aperçu sans installation**. 5 obstacles techniques réels corrigés et documentés (UDID plutôt que nom, ne jamais contourner `expo run:*` par un xcodebuild manuel, modules Expo/RN précompilés SDK 56/57 incompatibles avec la liaison statique du projet, `fetch` global incompatible avec l'upload multipart natif fix permanent `EXPO_PUBLIC_USE_RN_FETCH=1` dans `apps/mobile/.env`, découverte réseau du dev client peu fiable). Recette iOS : connexion, scan caméra réel (résolution + rejet QR étranger), vrai mode avion, conflit D2 tranché par l'humain avec photo réellement téléversée, persistance à travers fermeture/reconstruction de l'app, suggestions R5 au vrai modèle, purge de sécurité au changement de compte tout validé. Android : build natif sur émulateur, passage santé complet.
- **Dictée implémentée (22/07, ADR-004 §5)** : écran Voix R5 (jamais construit) `faster-whisper` local (CTranslate2, CPU, opt-in `AI_TRANSCRIPTION=off|locale`, défaut off), audio jamais persisté (purge serveur ET mobile quoi qu'il arrive), transcription relecture humaine suggestion immédiate et/ou sauvegarde dans `InterventionReport.note` (champ existant depuis R2, jamais eu d'écran jusqu'ici) `note` rejoint le corpus à la clôture comme les bilans codés. Mobile : `expo-audio`/`expo-file-system`, bouton dicter sur l'écran de clôture. Docker `siop2-ai` embarque le modèle (1,542,19 Go). Vérifié réellement (voix de synthèse texte fidèle, bout en bout API, conteneur Docker construit, chaîne corpus complète noteclôtureréindexationrecherche). 80 tests API, 29 pytest ai, mobile vert. - **Dictée implémentée (22/07, ADR-004 §5)** : écran Voix R5 (jamais construit) `faster-whisper` local (CTranslate2, CPU, opt-in `AI_TRANSCRIPTION=off|locale`, défaut off), audio jamais persisté (purge serveur ET mobile quoi qu'il arrive), transcription relecture humaine suggestion immédiate et/ou sauvegarde dans `InterventionReport.note` (champ existant depuis R2, jamais eu d'écran jusqu'ici) `note` rejoint le corpus à la clôture comme les bilans codés. Mobile : `expo-audio`/`expo-file-system`, bouton dicter sur l'écran de clôture. Docker `siop2-ai` embarque le modèle (1,542,19 Go). Vérifié réellement (voix de synthèse texte fidèle, bout en bout API, conteneur Docker construit, chaîne corpus complète noteclôtureréindexationrecherche). 80 tests API, 29 pytest ai, mobile vert.
- 🔄 **Reprise ici** : test tactile de la dictée sur iPhone (bloqué par une connexion USB qui ne s'est pas rétablie malgré câble/port/redémarrage reporté comme la recette Android). Redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer runbook §2 puis « Réindexer tout »). Restes : recette Android sur appareil physique, calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`, production client SPELEV (attend les accès serveur du partenaire). - **Dictée validée sur iPhone physique (01/08, ADR-004 §5)** : le blocage USB du 22/07 était un faux négatif de `system_profiler` (outil de diagnostic en panne, pas le câble). Deux bugs réels trouvés et corrigés : `expo-audio` iOS exige `setAudioModeAsync({ allowsRecording: true })` avant `record()` (absent, jamais détecté hors appareil réel) ; le correctif des modules précompilés (ADR-005) n'avait jamais été rendu permanent dans `apps/mobile/.env` corrigé. Chaîne complète vérifiée en vrai : micro dictée transcription fidèle note clôture réindexation **retrouvé par la recherche sémantique**.
- 🔄 **Reprise ici** : redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer runbook §2 puis « Réindexer tout »). Restes : recette Android sur appareil physique, calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`, production client SPELEV (attend les accès serveur du partenaire).
- Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0R5. - Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0R5.

View File

@@ -6,3 +6,11 @@
# boucle de retry silencieuse — trouvé en recette terrain iOS (17/07/2026). # boucle de retry silencieuse — trouvé en recette terrain iOS (17/07/2026).
# Voir ADR-005 et docs/journal/journal.md. # Voir ADR-005 et docs/journal/journal.md.
EXPO_PUBLIC_USE_RN_FETCH=1 EXPO_PUBLIC_USE_RN_FETCH=1
# Modules Expo/RN précompilés (XCFrameworks, SDK 56/57) incompatibles avec la
# liaison statique de ce projet (pas de use_frameworks!) : sans ce réglage,
# l'app compile mais CRASHE au lancement
# (dyld: Library not loaded: @rpath/React.framework/React, signal 6).
# Force la recompilation depuis les sources — voir ADR-005.
EXPO_USE_PRECOMPILED_MODULES=0
RCT_USE_PREBUILT_RNCORE=0

View File

@@ -2,7 +2,12 @@ import * as FileSystem from 'expo-file-system';
import { router, useLocalSearchParams } from 'expo-router'; import { router, useLocalSearchParams } from 'expo-router';
import { useState } from 'react'; import { useState } from 'react';
import { Pressable, ScrollView, Text, TextInput, View } from 'react-native'; import { Pressable, ScrollView, Text, TextInput, View } from 'react-native';
import { RecordingPresets, requestRecordingPermissionsAsync, useAudioRecorder } from 'expo-audio'; import {
RecordingPresets,
requestRecordingPermissionsAsync,
setAudioModeAsync,
useAudioRecorder,
} from 'expo-audio';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { import {
BILAN_FIELD_LABELS, BILAN_FIELD_LABELS,
@@ -197,6 +202,9 @@ function CarteSuggestion({
const demarrerDictee = async () => { const demarrerDictee = async () => {
const { granted } = await requestRecordingPermissionsAsync(); const { granted } = await requestRecordingPermissionsAsync();
if (!granted) return; if (!granted) return;
// iOS refuse recorder.record() tant que la session audio n'a pas été
// explicitement autorisée à enregistrer (RecordingDisabledException).
await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
await recorder.prepareToRecordAsync(); await recorder.prepareToRecordAsync();
recorder.record(); recorder.record();
setEnregistrement(true); setEnregistrement(true);
@@ -205,6 +213,7 @@ function CarteSuggestion({
const terminerDictee = async () => { const terminerDictee = async () => {
await recorder.stop(); await recorder.stop();
await setAudioModeAsync({ allowsRecording: false }); // n'arme le micro que le temps de dicter
setEnregistrement(false); setEnregistrement(false);
const uri = recorder.uri; const uri = recorder.uri;
if (!uri) return; if (!uri) return;

View File

@@ -80,6 +80,18 @@
- Implémentée et vérifiée le 22/07/2026 : transcription réelle (français, voix de - Implémentée et vérifiée le 22/07/2026 : transcription réelle (français, voix de
synthèse) juste et rapide en local, bout en bout via l'API NestJS, et dans l'image synthèse) juste et rapide en local, bout en bout via l'API NestJS, et dans l'image
Docker de production construite pour l'occasion (modèle embarqué, aucun téléchargement Docker de production construite pour l'occasion (modèle embarqué, aucun téléchargement
au démarrage). **Reste** : le test tactile sur iPhone physique (bouton dicter, permission au démarrage). Le test tactile sur iPhone physique n'avait pas pu se jouer ce jour-là
micro) n'a pas pu se jouer — blocage USB persistant malgré câble/port/redémarrage (blocage USB), reporté.
multiples, reporté comme la recette Android (attend un accès matériel qui fonctionne). - **Recette terrain sur iPhone physique complétée le 01/08/2026** : micro, dictée réelle,
transcription fidèle, sauvegarde dans le bilan, clôture, puis retrouvée par la recherche
sémantique après réindexation — la chaîne complète (D5) est validée sur vrai matériel.
Deux bugs réels trouvés et corrigés en route :
1. `expo-audio` sur iOS refuse `recorder.record()` tant que la session audio n'a pas
été explicitement autorisée à enregistrer — `RecordingDisabledException`, corrigée
par un appel à `setAudioModeAsync({ allowsRecording: true })` avant l'enregistrement
(et remis à `false` après, par hygiène — le micro ne reste pas « armé »).
2. Le contournement des modules Expo/RN précompilés (`EXPO_USE_PRECOMPILED_MODULES=0`,
`RCT_USE_PREBUILT_RNCORE=0` — ADR-005) n'avait **jamais été rendu permanent** dans
`apps/mobile/.env`, contrairement au correctif FormData voisin : une reconstruction
ultérieure sans ces variables aurait reproduit le crash au lancement déjà documenté.
Désormais committé aux côtés de `EXPO_PUBLIC_USE_RN_FETCH`.

View File

@@ -4,6 +4,39 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
--- ---
## 2026-08-01 — Pr. Daaif (+ Claude) — Dictée validée sur iPhone physique (ADR-004 §5)
**Actions**
- Reprise du reste laissé le 22/07 : le test tactile de la dictée sur iPhone, alors bloqué par une connexion
USB qui ne se rétablissait pas. Diagnostic cette fois : `system_profiler` (l'outil utilisé pour vérifier la
détection USB) était lui-même en panne et renvoyait un faux négatif silencieux — `ioreg` a révélé que
l'iPhone était en réalité détecté. Le vrai câble/port n'a jamais été le problème.
- Une fois l'app reconstruite (certificat gratuit expiré après plus d'une semaine — régénéré via le pipeline
officiel `expo run:ios`, jamais par un contournement manuel), **deux bugs réels trouvés et corrigés** :
1. `expo-audio` sur iOS refuse d'enregistrer sans un appel explicite à `setAudioModeAsync({ allowsRecording:
true })` avant `recorder.record()` — absent du code initial, jamais détecté hors d'un vrai appareil.
2. Le correctif des modules Expo/RN précompilés (ADR-005, `EXPO_USE_PRECOMPILED_MODULES=0` /
`RCT_USE_PREBUILT_RNCORE=0`) n'avait **jamais été committé** dans `apps/mobile/.env`, contrairement au
correctif FormData voisin déjà permanent — un vrai trou qui aurait refait planter l'app à la prochaine
reconstruction depuis zéro. Corrigé : les deux variables vivent maintenant côte à côte dans `.env`.
- **Recette réelle bout en bout** : micro → dictée d'une phrase → transcription fidèle (`faster-whisper` réel)
→ relecture → sauvegarde dans le bilan → clôture de l'OT → réindexation du corpus → **le contenu dicté
retrouvé par la recherche sémantique** (score 0,476). La boucle complète de l'idée d'origine (« cette
transcription devrait alimenter le corpus ») est validée sur vrai matériel, vraie voix, vrai réseau.
- Infra locale relancée depuis zéro après une semaine d'inactivité (Docker, conteneurs `siop2-*`, API,
service IA) — occasion de vérifier que le redémarrage à froid fonctionne proprement.
**Décisions**
- Aucune — cette session referme un reste déjà décidé, sans nouvelle décision de conception.
**Prochaine étape** : recette Android sur appareil physique (toujours en attente d'un appareil) ; redéploiement
Dokploy (`AI_SERVICE_TOKEN`) ; calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel ; secret
`DOKPLOY_WEBHOOK_URL`.
---
## 2026-07-22 — Pr. Daaif (+ Claude) — Dictée implémentée : audio → transcription locale → corpus (ADR-004 §5) ## 2026-07-22 — Pr. Daaif (+ Claude) — Dictée implémentée : audio → transcription locale → corpus (ADR-004 §5)
**Actions** **Actions**