fix(ai-export): strip photo metadata and minimize the training payload

This commit is contained in:
qlionbleusam
2026-08-29 09:39:31 +02:00
parent eeb857d452
commit 0ab9043d82
6 changed files with 122 additions and 33 deletions
+44 -28
View File
@@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:image/image.dart' as img;
import 'package:crypto/crypto.dart';
import '../data/models/shot.dart';
import '../data/models/target_type.dart';
@@ -64,30 +64,37 @@ class AiExportResult {
}
class AiExportService {
/// Extrait les informations de l'appareil
Future<Map<String, dynamic>> _getDeviceInfo() async {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
Map<String, dynamic> deviceData = {'model': 'Unknown', 'os': 'Unknown'};
/// Retire toutes les metadonnees de la photo avant l'envoi.
///
/// Une photo de cible prise au telephone embarque un bloc EXIF qui contient
/// typiquement la position GPS du stand de tir, la date exacte et le modele
/// d'appareil. Rien de tout cela n'est utile au modele de detection.
///
/// L'orientation est d'abord appliquee physiquement aux pixels : les
/// coordonnees d'impact sont normalisees sur l'image telle qu'elle est
/// affichee dans l'app (Flutter applique l'orientation EXIF), donc supprimer
/// le tag sans redresser l'image ferait pivoter la photo par rapport a ses
/// propres annotations.
///
/// Retourne null si l'image est illisible.
@visibleForTesting
Uint8List? stripMetadata(Uint8List originalBytes) {
try {
if (Platform.isAndroid) {
final androidInfo = await deviceInfoPlugin.androidInfo;
deviceData['model'] = '${androidInfo.brand} ${androidInfo.model}';
deviceData['os'] = 'Android ${androidInfo.version.release}';
} else if (Platform.isIOS) {
final iosInfo = await deviceInfoPlugin.iosInfo;
deviceData['model'] = iosInfo.name;
deviceData['os'] = '${iosInfo.systemName} ${iosInfo.systemVersion}';
} else if (Platform.isWindows) {
final windowsInfo = await deviceInfoPlugin.windowsInfo;
deviceData['model'] = 'Windows PC';
deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
}
} catch (e) {
debugPrint('Erreur lors de la récupération des infos appareil: $e');
}
final decoded = img.decodeImage(originalBytes);
if (decoded == null) return null;
return deviceData;
final baked = img.bakeOrientation(decoded);
// bakeOrientation recopie tout l'EXIF sauf l'orientation : sans ce reset,
// le GPS survivrait au reencodage.
baked.exif = img.ExifData();
return img.encodeJpg(baked, quality: 90);
} catch (e) {
// Sur un fichier tronque, decodeImage leve au lieu de retourner null.
debugPrint('Photo illisible, export annule: $e');
return null;
}
}
/// Exporte l'image et les données de plotting vers le serveur
@@ -100,7 +107,7 @@ class AiExportService {
required double targetRadius,
required List<Shot> shots,
int distanceMeters = 25,
String weaponName = 'Unknown',
String caliber = 'unknown',
String? apiUrl,
}) async {
try {
@@ -121,7 +128,13 @@ class AiExportService {
);
}
final deviceData = await _getDeviceInfo();
final sanitizedPhoto = stripMetadata(await file.readAsBytes());
if (sanitizedPhoto == null) {
return AiExportResult.error(
code: 'INVALID_IMAGE',
message: 'La photo de la cible est illisible et n\'a pas pu être envoyée.',
);
}
final corners = [
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY - targetRadius},
@@ -155,11 +168,10 @@ class AiExportService {
"session_id": sessionId,
"wallet_hash": walletHash,
"timestamp": DateTime.now().toIso8601String(),
"device_info": deviceData,
"target_metadata": {
"type": targetType.name,
"distance_meters": distanceMeters,
"weapon": weaponName,
"caliber": caliber,
},
"plotting": {
"target_corners": corners,
@@ -169,7 +181,11 @@ class AiExportService {
request.fields['plotting'] = jsonEncode(plottingJson);
request.files.add(
await http.MultipartFile.fromPath('photo', imagePath),
http.MultipartFile.fromBytes(
'photo',
sanitizedPhoto,
filename: 'target.jpg',
),
);
final streamedResponse = await request.send().timeout(