chore(ml,android): retire react-native-fast-tflite + nitro, mock JS only

Contexte
- Build Android C++ instable sur Windows (CMake/Ninja path too long, Nitro
  headers manquants au clean). Modèle .tflite final pas encore prêt.
- Désinstall temporaire des deux libs natives, le mock JS dans model.ts
  continue de servir les détections simulées pondérées.

Changements
- package.json : retire react-native-fast-tflite (3.0.1)
- pnpm-lock.yaml : régénéré, -72 packages dont nitro-modules
- src/services/tflite/model.ts : refactor pur mock, interface publique
  inchangée (loadModel + runInference), procédure de réintégration
  documentée en tête du fichier
- plugins/withCmakeFix.js : plugin Expo config qui injecte les flags
  CMake (response files + ninja 1.12.1 + OBJECT_PATH_MAX) à chaque
  prebuild — dormant tant que fast-tflite n'est pas réintégré
- app.json : référence le plugin
- CLAUDE.md + .claude/notes/android-build : doc de l'état actuel et
  des étapes de réintégration (idéalement via EAS Build)

Reste
- src/assets/models/grapevine_v1.tflite conservé pour la réintégration
- metro.config.js continue de déclarer .tflite dans assetExts
- TypeScript check: 1 erreur préexistante (homeheader.tsx, palette[50]),
  non liée à ce changement

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-04-30 21:14:38 +02:00
parent a8b84472e6
commit 6232068208
7 changed files with 123 additions and 115 deletions

View file

@ -14,9 +14,21 @@
### Solution appliquée ### Solution appliquée
#### 1. `VinEye/android/app/build.gradle` — bloc `externalNativeBuild` > ⚠️ `android/` est gitignored (régénéré par `expo prebuild`). On ne peut PAS éditer `android/app/build.gradle` à la main et le commiter — la modification serait perdue au prochain prebuild. La solution est un **plugin Expo config** (`plugins/withCmakeFix.js`) qui ré-injecte le bloc à chaque prebuild.
Ajouté dans `android.defaultConfig` : #### 1. Plugin Expo config — `VinEye/plugins/withCmakeFix.js`
Plugin qui utilise `withAppBuildGradle` pour injecter le bloc `externalNativeBuild` dans `defaultConfig`. Idempotent grâce au marker `// CMAKE_FIX_INJECTED`.
Référencé dans `app.json` :
```json
"plugins": [
"./plugins/withCmakeFix",
...
]
```
#### 2. Bloc injecté dans `android/app/build.gradle` (généré)
```gradle ```gradle
externalNativeBuild { externalNativeBuild {
@ -58,7 +70,17 @@ Pas besoin de télécharger — on pointe `CMAKE_MAKE_PROGRAM` directement dessu
--- ---
## Fix #2`react-native-nitro-modules` headers manquants (2026-04-30, en cours) ## Fix #2`react-native-nitro-modules` headers manquants (2026-04-30, ✅ contourné)
> **Décision finale** : `react-native-fast-tflite` et `react-native-nitro-modules` ont été **désinstallés**. Le mock JS dans `src/services/tflite/model.ts` continue de fournir des résultats simulés. Le `.tflite` reste dans `src/assets/models/` pour la réintégration future. Procédure de réintégration documentée en tête de `model.ts`.
### Pourquoi ce contournement
- Le modèle ML n'est pas encore prêt pour la prod (~30% précision sur dataset terrain)
- Les builds Android C++ Nitro/fast-tflite sont fragiles sur Windows
- Le mock TS suffit pour le développement UI/UX
- Quand le `.tflite` sera prêt → réintégrer via **EAS Build** pour éviter les builds locaux Windows
### Erreur historique (avant désinstall)
### Symptômes ### Symptômes
``` ```
@ -79,7 +101,7 @@ CMake Error in CMakeLists.txt:
4. **Vérifier la version** — incompatibilité possible entre `react-native-fast-tflite` et `react-native-nitro-modules` (vérifier les peerDependencies) 4. **Vérifier la version** — incompatibilité possible entre `react-native-fast-tflite` et `react-native-nitro-modules` (vérifier les peerDependencies)
### Statut ### Statut
🟡 **En cours** — fix CMake/ninja passé, ce nouveau problème est sur la chaîne de dépendances Gradle. **Résolu par contournement** — fast-tflite désinstallé, mock JS en place, builds C++ Nitro plus nécessaires.
--- ---

View file

@ -15,7 +15,7 @@ Cible des amateurs de vin/jardinage. Scan par camera, identification de maladies
| Styling | **NativeWind v4** (Tailwind) prioritaire, StyleSheet pour ombres/gradients | | Styling | **NativeWind v4** (Tailwind) prioritaire, StyleSheet pour ombres/gradients |
| Icones | **lucide-react-native** (bottom bar) + **Ionicons** (reste de l'app) | | Icones | **lucide-react-native** (bottom bar) + **Ionicons** (reste de l'app) |
| Animations | React Native Reanimated v4 | | Animations | React Native Reanimated v4 |
| IA | **react-native-fast-tflite** + MobileNetV2 (.tflite, 9.4 MB, 4 classes) | | IA | Mock JS pondéré (random 4 classes) — `react-native-fast-tflite` désinstallé temporairement, voir `services/tflite/model.ts` pour la procédure de réintégration |
| Persistance | AsyncStorage | | Persistance | AsyncStorage |
| i18n | i18next + react-i18next (FR + EN) | | i18n | i18next + react-i18next (FR + EN) |
| Camera | expo-camera | | Camera | expo-camera |
@ -193,8 +193,11 @@ pnpm ios # Build iOS
## ML / inference on-device ## ML / inference on-device
Le modele MobileNetV2 (val_accuracy 99.93% — voir `docs/paper.md`) est embarque > ⚠️ **2026-04-30** : `react-native-fast-tflite` et `react-native-nitro-modules` ont été **désinstallés temporairement**. Le service `services/tflite/model.ts` retourne actuellement un **mock JS pondéré** (random sur les 4 classes). Raisons : modèle pas encore exporté en `.tflite` final + builds Android C++ instables sur Windows (CMake/Nitro headers). Procédure de réintégration documentée en tête de `services/tflite/model.ts`.
dans le bundle et execute en local via `react-native-fast-tflite`.
Le modele MobileNetV2 (val_accuracy 99.93% — voir `docs/paper.md`) est destiné
à être embarqué dans le bundle et exécuté en local via `react-native-fast-tflite`
une fois la lib réintégrée.
### Pipeline ### Pipeline
@ -270,5 +273,11 @@ le dev sans device natif.
Détail complet : [`.claude/notes/android-build/README.md`](.claude/notes/android-build/README.md) Détail complet : [`.claude/notes/android-build/README.md`](.claude/notes/android-build/README.md)
- ✅ **CMake/Ninja path too long** — résolu via `externalNativeBuild.cmake.arguments` dans `android/app/build.gradle` (response files + ninja 1.12.1 + `CMAKE_OBJECT_PATH_MAX=1024`) - ✅ **CMake/Ninja path too long** — résolu via plugin Expo config `plugins/withCmakeFix.js` (référencé dans `app.json`) qui injecte response files + ninja 1.12.1 + `CMAKE_OBJECT_PATH_MAX=1024` à chaque prebuild
- 🟡 **`react-native-nitro-modules` headers manquants** — survient au clean ; corriger en buildant Nitro avant fast-tflite, ou via `pnpm dlx expo prebuild --clean` - ✅ **`react-native-nitro-modules` headers manquants** — contourné en désinstallant `react-native-fast-tflite` (qui dépendait de Nitro). Mock JS en place. À réintégrer quand le `.tflite` sera prêt et idéalement via EAS Build pour éviter les soucis Windows.
### Setup dev Windows recommandé
- **Chemin court** : placer le projet dans `C:\dev\vineye\` plutôt que `C:\Users\Client\projet_web\...\VinEye\` — réduit ~50 chars sur tous les chemins de build CMake
- **`LongPathsEnabled` registre** : `HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1` (déjà actif sur ce poste)
- **Git long paths** : `git config --system core.longpaths true` (en PowerShell admin)

View file

@ -41,6 +41,7 @@
"favicon": "./src/assets/images/icon.png" "favicon": "./src/assets/images/icon.png"
}, },
"plugins": [ "plugins": [
"./plugins/withCmakeFix",
"expo-localization", "expo-localization",
[ [
"expo-camera", "expo-camera",

View file

@ -44,7 +44,6 @@
"react-i18next": "^17.0.1", "react-i18next": "^17.0.1",
"react-lucid": "^0.0.1", "react-lucid": "^0.0.1",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-fast-tflite": "^3.0.1",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",

View file

@ -0,0 +1,42 @@
const { withAppBuildGradle } = require("expo/config-plugins");
const NINJA_PATH =
"C:\\\\Users\\\\Client\\\\AppData\\\\Local\\\\Android\\\\Sdk\\\\cmake\\\\4.1.2\\\\bin\\\\ninja.exe";
const CMAKE_BLOCK = `
externalNativeBuild {
cmake {
arguments "-DCMAKE_MAKE_PROGRAM=${NINJA_PATH}",
"-DCMAKE_OBJECT_PATH_MAX=1024",
"-DCMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS=1",
"-DCMAKE_CXX_USE_RESPONSE_FILE_FOR_LIBRARIES=1",
"-DCMAKE_CXX_RESPONSE_FILE_LINK_FLAG=@",
"-DCMAKE_NINJA_FORCE_RESPONSE_FILE=1"
}
}
`;
const MARKER = "// CMAKE_FIX_INJECTED";
function injectCmakeFix(buildGradle) {
if (buildGradle.includes(MARKER)) return buildGradle;
const defaultConfigRegex = /(defaultConfig\s*\{)([\s\S]*?)(\n\s*\})/m;
const match = buildGradle.match(defaultConfigRegex);
if (!match) {
throw new Error(
"[withCmakeFix] Impossible de trouver le bloc defaultConfig dans build.gradle"
);
}
const [, openTag, body, closeTag] = match;
const newBlock = `${openTag}${body}\n ${MARKER}${CMAKE_BLOCK}${closeTag}`;
return buildGradle.replace(defaultConfigRegex, newBlock);
}
module.exports = function withCmakeFix(config) {
return withAppBuildGradle(config, (config) => {
config.modResults.contents = injectCmakeFix(config.modResults.contents);
return config;
});
};

View file

@ -113,9 +113,6 @@ importers:
react-native: react-native:
specifier: 0.81.5 specifier: 0.81.5
version: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) version: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-fast-tflite:
specifier: ^3.0.1
version: 3.0.1(react-native-nitro-modules@0.35.6(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-gesture-handler: react-native-gesture-handler:
specifier: ~2.28.0 specifier: ~2.28.0
version: 2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) version: 2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@ -2948,14 +2945,6 @@ packages:
react-native-svg: react-native-svg:
optional: true optional: true
react-native-fast-tflite@3.0.1:
resolution: {integrity: sha512-88wNR/4iR8X0zuQtrpb1jRbF+X+hUqrD8cER4DhNJnbhA+3PuGz8SoP3n8WEhjYWDkGqTme2Ezk+mbeLiiE+6w==}
engines: {node: '>= 18'}
peerDependencies:
react: '*'
react-native: '*'
react-native-nitro-modules: '*'
react-native-gesture-handler@2.28.0: react-native-gesture-handler@2.28.0:
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==} resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies: peerDependencies:
@ -2968,12 +2957,6 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-nitro-modules@0.35.6:
resolution: {integrity: sha512-3Cb7s+O5tpZ6RdIiPOB/wi3IMfBxD6tl6VDF8gJ5zvM/BEGTWxwMMLjzmWmsYPKekdbYBznF6qp2d2SxixPy8g==}
peerDependencies:
react: '*'
react-native: '*'
react-native-reanimated@4.1.7: react-native-reanimated@4.1.7:
resolution: {integrity: sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==} resolution: {integrity: sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==}
peerDependencies: peerDependencies:
@ -4846,7 +4829,9 @@ snapshots:
metro-runtime: 0.83.5 metro-runtime: 0.83.5
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- bufferutil
- supports-color - supports-color
- utf-8-validate
optional: true optional: true
'@react-native/normalize-colors@0.74.89': {} '@react-native/normalize-colors@0.74.89': {}
@ -6985,12 +6970,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
react-native-fast-tflite@3.0.1(react-native-nitro-modules@0.35.6(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-nitro-modules: 0.35.6(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
'@egjs/hammerjs': 2.0.17 '@egjs/hammerjs': 2.0.17
@ -7004,11 +6983,6 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-nitro-modules@0.35.6(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies: dependencies:
react: 19.1.0 react: 19.1.0

View file

@ -1,79 +1,47 @@
/**
* MOCK TFLite Service
*
* Ce service retourne actuellement des résultats simulés (random pondéré).
* Les libs `react-native-fast-tflite` et `react-native-nitro-modules` ont é
* désinstallées temporairement car :
* - Le modèle ML n'est pas encore exporté en .tflite final (précision insuffisante)
* - Les builds Android C++ (CMake/Ninja + Nitro headers) étaient instables sur Windows
*
* L'interface publique reste identique :
* - `loadModel(): Promise<boolean>` retourne false (pas de modèle chargé)
* - `runInference(imageUri?: string): Promise<Detection>` renvoie un mock pondéré
*
* RÉINTÉGRATION DU VRAI MODÈLE (quand le .tflite sera prêt) :
* 1. pnpm add react-native-fast-tflite react-native-nitro-modules
* 2. Vérifier que `src/assets/models/grapevine_v1.tflite` est présent
* 3. Remplacer `runInference` ci-dessous par l'implémentation native :
* const tflite = require('react-native-fast-tflite');
* const asset = require('@/assets/models/grapevine_v1.tflite');
* const model = await tflite.loadTensorflowModel(asset);
* const input = await preprocessImage(imageUri); // depuis services/ml/preprocessing
* const outputs = model.runSync([input]);
* // ... softmax/argmax → buildDetection
* 4. pnpm dlx expo prebuild --clean
* 5. pnpm dlx expo run:android (ou EAS Build pour éviter les soucis CMake Windows)
*
* Documentation : https://github.com/mrousavy/react-native-fast-tflite
*/
import type { Detection, DiseaseClass, ClassProbability } from '@/types/detection'; import type { Detection, DiseaseClass, ClassProbability } from '@/types/detection';
import { ML_CLASSES, CLASS_TO_SLUG, CONFIDENCE_THRESHOLD_VINE, CONFIDENCE_THRESHOLD_UNCERTAIN } from '@/services/ml/classes'; import {
import { preprocessImage, argmax, softmax } from '@/services/ml/preprocessing'; ML_CLASSES,
CLASS_TO_SLUG,
type FastTfliteModel = { CONFIDENCE_THRESHOLD_VINE,
runSync: (inputs: (Float32Array | Int32Array | Uint8Array)[]) => (Float32Array | Int32Array | Uint8Array)[]; CONFIDENCE_THRESHOLD_UNCERTAIN,
}; } from '@/services/ml/classes';
import { argmax } from '@/services/ml/preprocessing';
let cachedModel: FastTfliteModel | null = null;
let modelLoadFailed = false;
async function getModel(): Promise<FastTfliteModel | null> {
if (cachedModel) return cachedModel;
if (modelLoadFailed) return null;
try {
const tflite = require('react-native-fast-tflite');
const asset = require('@/assets/models/grapevine_v1.tflite');
cachedModel = await tflite.loadTensorflowModel(asset);
return cachedModel;
} catch (err) {
if (__DEV__) {
console.warn('[TFLite] Failed to load model — falling back to mock:', err);
}
modelLoadFailed = true;
return null;
}
}
export async function loadModel(): Promise<boolean> { export async function loadModel(): Promise<boolean> {
const m = await getModel(); return false;
return m !== null;
} }
export async function runInference(imageUri?: string): Promise<Detection> { export async function runInference(imageUri?: string): Promise<Detection> {
const timestamp = Date.now(); return mockDetection(Date.now(), imageUri);
if (!imageUri) {
return mockDetection(timestamp);
}
const model = await getModel();
if (!model) {
return mockDetection(timestamp, imageUri);
}
try {
const input = await preprocessImage(imageUri);
const outputs = model.runSync([input]);
const raw = outputs[0];
const rawArr = raw instanceof Float32Array ? Array.from(raw) : Array.from(raw as ArrayLike<number>);
const probs = isProbabilityVector(rawArr) ? rawArr : softmax(rawArr);
const idx = argmax(probs);
const topClass = ML_CLASSES[idx];
const topProb = probs[idx];
const allProbabilities: ClassProbability[] = ML_CLASSES.map((cls, i) => ({
class: cls,
probability: probs[i],
}));
return buildDetection({
timestamp,
imageUri,
topClass,
topProb,
allProbabilities,
});
} catch (err) {
if (__DEV__) {
console.warn('[TFLite] Inference failed — falling back to mock:', err);
}
return mockDetection(timestamp, imageUri);
}
} }
function buildDetection(args: { function buildDetection(args: {
@ -104,13 +72,6 @@ function buildDetection(args: {
}; };
} }
function isProbabilityVector(values: number[]): boolean {
if (values.length === 0) return false;
const sum = values.reduce((a, b) => a + b, 0);
if (Math.abs(sum - 1) > 0.05) return false;
return values.every((v) => v >= 0 && v <= 1);
}
function mockDetection(timestamp: number, imageUri?: string): Detection { function mockDetection(timestamp: number, imageUri?: string): Detection {
const probs = randomProbabilities(); const probs = randomProbabilities();
const idx = argmax(probs); const idx = argmax(probs);