branch backend + ajout du serveur backend, next, et sqlite
This commit is contained in:
146
lib/services/ai_export_service.dart
Normal file
146
lib/services/ai_export_service.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import '../data/models/shot.dart';
|
||||
import '../data/models/target_type.dart';
|
||||
import 'wallet_identity_service.dart';
|
||||
|
||||
class AiExportService {
|
||||
// Utilise 10.0.2.2 pour l'émulateur Android, sinon localhost.
|
||||
// Pour un appareil physique, il faudra utiliser l'IP locale du PC (ex: 192.168.1.X).
|
||||
static String get _defaultApiUrl {
|
||||
if (Platform.isAndroid) {
|
||||
return 'http://10.0.2.2:3000/api/upload';
|
||||
}
|
||||
return 'http://localhost:3000/api/upload';
|
||||
}
|
||||
|
||||
/// Extrait les informations de l'appareil
|
||||
Future<Map<String, dynamic>> _getDeviceInfo() async {
|
||||
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||
Map<String, dynamic> deviceData = {'model': 'Unknown', 'os': 'Unknown'};
|
||||
|
||||
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) {
|
||||
print('Erreur lors de la récupération des infos appareil: $e');
|
||||
}
|
||||
|
||||
return deviceData;
|
||||
}
|
||||
|
||||
/// Exporte l'image et les données de plotting vers le serveur
|
||||
Future<bool> exportData({
|
||||
required String imagePath,
|
||||
required String sessionId,
|
||||
required TargetType targetType,
|
||||
required double targetCenterX,
|
||||
required double targetCenterY,
|
||||
required double targetRadius,
|
||||
required List<Shot> shots,
|
||||
String? apiUrl,
|
||||
}) async {
|
||||
try {
|
||||
final url = Uri.parse(apiUrl ?? _defaultApiUrl);
|
||||
final request = http.MultipartRequest('POST', url);
|
||||
|
||||
// 1. Prepare image
|
||||
final file = File(imagePath);
|
||||
if (!await file.exists()) {
|
||||
throw Exception('Le fichier image n\'existe pas');
|
||||
}
|
||||
|
||||
// Read image metadata (approximate dimensions since decoding image can be heavy)
|
||||
// On the frontend we usually have aspectRatio, here we use generic values if not available.
|
||||
final deviceData = await _getDeviceInfo();
|
||||
|
||||
// We approximate the target corners from center and radius
|
||||
// radius is relative (0 to 1). We need image width/height to get pixels.
|
||||
// But we can just pass relative corners as well, or a normalized bounding box.
|
||||
// Let's create normalized corners (0 to 1).
|
||||
final corners = [
|
||||
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY - targetRadius},
|
||||
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY - targetRadius},
|
||||
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY + targetRadius},
|
||||
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY + targetRadius},
|
||||
];
|
||||
|
||||
// Format the impacts
|
||||
final formattedImpacts = shots.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final shot = entry.value;
|
||||
return {
|
||||
"id": index + 1,
|
||||
"label": "bullet_hole",
|
||||
"score": shot.score,
|
||||
"coords": {
|
||||
"norm_x": shot.x,
|
||||
"norm_y": shot.y
|
||||
}
|
||||
};
|
||||
}).toList();
|
||||
|
||||
// Get and hash the wallet identity
|
||||
final walletService = WalletIdentityService();
|
||||
final phrase = await walletService.getIdentityPhrase();
|
||||
final phraseBytes = utf8.encode(phrase);
|
||||
final walletHash = sha256.convert(phraseBytes).toString();
|
||||
|
||||
// Build JSON payload
|
||||
final plottingJson = {
|
||||
"session_id": sessionId,
|
||||
"wallet_hash": walletHash,
|
||||
"timestamp": DateTime.now().toIso8601String(),
|
||||
"device_info": deviceData,
|
||||
"target_metadata": {
|
||||
"type": targetType.name,
|
||||
"distance_meters": 25, // Default/placeholder
|
||||
"weapon": "Unknown", // Default/placeholder
|
||||
// The backend could extract exact width/height from the image.
|
||||
},
|
||||
"plotting": {
|
||||
"target_corners": corners,
|
||||
"impacts": formattedImpacts
|
||||
}
|
||||
};
|
||||
|
||||
// Add fields to request
|
||||
request.fields['plotting'] = jsonEncode(plottingJson);
|
||||
|
||||
// Add file
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('photo', imagePath),
|
||||
);
|
||||
|
||||
// Send request
|
||||
final response = await request.send();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = await response.stream.bytesToString();
|
||||
print('Export réussi: $responseData');
|
||||
return true;
|
||||
} else {
|
||||
final errorData = await response.stream.bytesToString();
|
||||
print('Erreur d\'export: ${response.statusCode} - $errorData');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Exception lors de l\'export: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user