diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index a241cbe7e..7a0519484 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -47,7 +47,7 @@ android {
defaultConfig {
applicationId "io.ente.photos"
- minSdkVersion 21
+ minSdkVersion 26
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -74,6 +74,10 @@ android {
dimension "default"
applicationIdSuffix ".dev"
}
+ face {
+ dimension "default"
+ applicationIdSuffix ".face"
+ }
playstore {
dimension "default"
}
diff --git a/mobile/android/app/src/face/AndroidManifest.xml b/mobile/android/app/src/face/AndroidManifest.xml
new file mode 100644
index 000000000..cbf1924b2
--- /dev/null
+++ b/mobile/android/app/src/face/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/face/res/values/strings.xml b/mobile/android/app/src/face/res/values/strings.xml
new file mode 100644
index 000000000..4932deb96
--- /dev/null
+++ b/mobile/android/app/src/face/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ ente face
+ backup face
+
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index cf6d6b875..6d2a86940 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -59,6 +59,8 @@ PODS:
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
+ - flutter_isolate (0.0.1):
+ - Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
@@ -197,6 +199,28 @@ PODS:
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
+ - TensorFlowLiteC (2.12.0):
+ - TensorFlowLiteC/Core (= 2.12.0)
+ - TensorFlowLiteC/Core (2.12.0)
+ - TensorFlowLiteC/CoreML (2.12.0):
+ - TensorFlowLiteC/Core
+ - TensorFlowLiteC/Metal (2.12.0):
+ - TensorFlowLiteC/Core
+ - TensorFlowLiteSwift (2.12.0):
+ - TensorFlowLiteSwift/Core (= 2.12.0)
+ - TensorFlowLiteSwift/Core (2.12.0):
+ - TensorFlowLiteC (= 2.12.0)
+ - TensorFlowLiteSwift/CoreML (2.12.0):
+ - TensorFlowLiteC/CoreML (= 2.12.0)
+ - TensorFlowLiteSwift/Core (= 2.12.0)
+ - TensorFlowLiteSwift/Metal (2.12.0):
+ - TensorFlowLiteC/Metal (= 2.12.0)
+ - TensorFlowLiteSwift/Core (= 2.12.0)
+ - tflite_flutter (0.0.1):
+ - Flutter
+ - TensorFlowLiteSwift (= 2.12.0)
+ - TensorFlowLiteSwift/CoreML (= 2.12.0)
+ - TensorFlowLiteSwift/Metal (= 2.12.0)
- Toast (4.1.0)
- uni_links (0.0.1):
- Flutter
@@ -228,6 +252,7 @@ DEPENDENCIES:
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
+ - flutter_isolate (from `.symlinks/plugins/flutter_isolate/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
@@ -257,6 +282,7 @@ DEPENDENCIES:
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
+ - tflite_flutter (from `.symlinks/plugins/tflite_flutter/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
@@ -287,6 +313,8 @@ SPEC REPOS:
- Sentry
- SentryPrivate
- sqlite3
+ - TensorFlowLiteC
+ - TensorFlowLiteSwift
- Toast
EXTERNAL SOURCES:
@@ -314,6 +342,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_image_compress/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
+ flutter_isolate:
+ :path: ".symlinks/plugins/flutter_isolate/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
@@ -372,6 +402,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
+ tflite_flutter:
+ :path: ".symlinks/plugins/tflite_flutter/ios"
uni_links:
:path: ".symlinks/plugins/uni_links/ios"
url_launcher_ios:
@@ -405,6 +437,7 @@ SPEC CHECKSUMS:
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
+ flutter_isolate: 0edf5081826d071adf21759d1eb10ff5c24503b5
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
@@ -449,6 +482,9 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
sqlite3_flutter_libs: aeb4d37509853dfa79d9b59386a2dac5dd079428
+ TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a
+ TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793
+ tflite_flutter: 9433d086a3060431bbc9f3c7c20d017db0e72d08
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index e9cbf0685..c05eaf21d 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -299,6 +299,7 @@
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
"${BUILT_PRODUCTS_DIR}/flutter_image_compress/flutter_image_compress.framework",
"${BUILT_PRODUCTS_DIR}/flutter_inappwebview/flutter_inappwebview.framework",
+ "${BUILT_PRODUCTS_DIR}/flutter_isolate/flutter_isolate.framework",
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
"${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
@@ -382,6 +383,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_isolate.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart
index f82486631..5a3ac03a0 100644
--- a/mobile/lib/core/configuration.dart
+++ b/mobile/lib/core/configuration.dart
@@ -18,6 +18,7 @@ import 'package:photos/db/trash_db.dart';
import 'package:photos/db/upload_locks_db.dart';
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
+import "package:photos/face/db.dart";
import 'package:photos/models/key_attributes.dart';
import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/private_key_attributes.dart';
@@ -164,6 +165,7 @@ class Configuration {
: null;
await CollectionsDB.instance.clearTable();
await MemoriesDB.instance.clearTable();
+ await FaceMLDataDB.instance.clearTable();
await UploadLocksDB.instance.clearTable();
await IgnoredFilesService.instance.reset();
diff --git a/mobile/lib/db/ml_data_db.dart b/mobile/lib/db/ml_data_db.dart
new file mode 100644
index 000000000..07150f09b
--- /dev/null
+++ b/mobile/lib/db/ml_data_db.dart
@@ -0,0 +1,714 @@
+import 'dart:async';
+
+import 'package:logging/logging.dart';
+import 'package:path/path.dart' show join;
+import 'package:path_provider/path_provider.dart';
+import 'package:photos/models/ml/ml_typedefs.dart';
+import "package:photos/services/face_ml/face_feedback.dart/cluster_feedback.dart";
+import "package:photos/services/face_ml/face_feedback.dart/feedback_types.dart";
+import "package:photos/services/face_ml/face_ml_result.dart";
+import 'package:sqflite/sqflite.dart';
+
+/// Stores all data for the ML-related features. The database can be accessed by `MlDataDB.instance.database`.
+///
+/// This includes:
+/// [facesTable] - Stores all the detected faces and its embeddings in the images.
+/// [peopleTable] - Stores all the clusters of faces which are considered to be the same person.
+class MlDataDB {
+ static final Logger _logger = Logger("MlDataDB");
+
+ // TODO: [BOB] put the db in files
+ static const _databaseName = "ente.ml_data.db";
+ static const _databaseVersion = 1;
+
+ static const facesTable = 'faces';
+ static const fileIDColumn = 'file_id';
+ static const faceMlResultColumn = 'face_ml_result';
+ static const mlVersionColumn = 'ml_version';
+
+ static const peopleTable = 'people';
+ static const personIDColumn = 'person_id';
+ static const clusterResultColumn = 'cluster_result';
+ static const centroidColumn = 'cluster_centroid';
+ static const centroidDistanceThresholdColumn = 'centroid_distance_threshold';
+
+ static const feedbackTable = 'feedback';
+ static const feedbackIDColumn = 'feedback_id';
+ static const feedbackTypeColumn = 'feedback_type';
+ static const feedbackDataColumn = 'feedback_data';
+ static const feedbackTimestampColumn = 'feedback_timestamp';
+ static const feedbackFaceMlVersionColumn = 'feedback_face_ml_version';
+ static const feedbackClusterMlVersionColumn = 'feedback_cluster_ml_version';
+
+ static const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
+ $fileIDColumn INTEGER NOT NULL UNIQUE,
+ $faceMlResultColumn TEXT NOT NULL,
+ $mlVersionColumn INTEGER NOT NULL,
+ PRIMARY KEY($fileIDColumn)
+ );
+ ''';
+ static const createPeopleTable = '''CREATE TABLE IF NOT EXISTS $peopleTable (
+ $personIDColumn INTEGER NOT NULL UNIQUE,
+ $clusterResultColumn TEXT NOT NULL,
+ $centroidColumn TEXT NOT NULL,
+ $centroidDistanceThresholdColumn REAL NOT NULL,
+ PRIMARY KEY($personIDColumn)
+ );
+ ''';
+ static const createFeedbackTable =
+ '''CREATE TABLE IF NOT EXISTS $feedbackTable (
+ $feedbackIDColumn TEXT NOT NULL UNIQUE,
+ $feedbackTypeColumn TEXT NOT NULL,
+ $feedbackDataColumn TEXT NOT NULL,
+ $feedbackTimestampColumn TEXT NOT NULL,
+ $feedbackFaceMlVersionColumn INTEGER NOT NULL,
+ $feedbackClusterMlVersionColumn INTEGER NOT NULL,
+ PRIMARY KEY($feedbackIDColumn)
+ );
+ ''';
+ static const _deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
+ static const _deletePeopleTable = 'DROP TABLE IF EXISTS $peopleTable';
+ static const _deleteFeedbackTable = 'DROP TABLE IF EXISTS $feedbackTable';
+
+ MlDataDB._privateConstructor();
+ static final MlDataDB instance = MlDataDB._privateConstructor();
+
+ static Future? _dbFuture;
+ Future get database async {
+ _dbFuture ??= _initDatabase();
+ return _dbFuture!;
+ }
+
+ Future _initDatabase() async {
+ final documentsDirectory = await getApplicationDocumentsDirectory();
+ final String databaseDirectory =
+ join(documentsDirectory.path, _databaseName);
+ return await openDatabase(
+ databaseDirectory,
+ version: _databaseVersion,
+ onCreate: _onCreate,
+ );
+ }
+
+ Future _onCreate(Database db, int version) async {
+ await db.execute(createFacesTable);
+ await db.execute(createPeopleTable);
+ await db.execute(createFeedbackTable);
+ }
+
+ /// WARNING: This will delete ALL data in the database! Only use this for debug/testing purposes!
+ Future cleanTables({
+ bool cleanFaces = false,
+ bool cleanPeople = false,
+ bool cleanFeedback = false,
+ }) async {
+ _logger.fine('`cleanTables()` called');
+ final db = await instance.database;
+
+ if (cleanFaces) {
+ _logger.fine('`cleanTables()`: Cleaning faces table');
+ await db.execute(_deleteFacesTable);
+ }
+
+ if (cleanPeople) {
+ _logger.fine('`cleanTables()`: Cleaning people table');
+ await db.execute(_deletePeopleTable);
+ }
+
+ if (cleanFeedback) {
+ _logger.fine('`cleanTables()`: Cleaning feedback table');
+ await db.execute(_deleteFeedbackTable);
+ }
+
+ if (!cleanFaces && !cleanPeople && !cleanFeedback) {
+ _logger.fine(
+ '`cleanTables()`: No tables cleaned, since no table was specified. Please be careful with this function!',
+ );
+ }
+
+ await db.execute(createFacesTable);
+ await db.execute(createPeopleTable);
+ await db.execute(createFeedbackTable);
+ }
+
+ Future createFaceMlResult(FaceMlResult faceMlResult) async {
+ _logger.fine('createFaceMlResult called');
+
+ final existingResult = await getFaceMlResult(faceMlResult.fileId);
+ if (existingResult != null) {
+ if (faceMlResult.mlVersion <= existingResult.mlVersion) {
+ _logger.fine(
+ 'FaceMlResult with file ID ${faceMlResult.fileId} already exists with equal or higher version. Skipping insert.',
+ );
+ return;
+ }
+ }
+
+ final db = await instance.database;
+ await db.insert(
+ facesTable,
+ {
+ fileIDColumn: faceMlResult.fileId,
+ faceMlResultColumn: faceMlResult.toJsonString(),
+ mlVersionColumn: faceMlResult.mlVersion,
+ },
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+ }
+
+ Future doesFaceMlResultExist(int fileId, {int? mlVersion}) async {
+ _logger.fine('doesFaceMlResultExist called');
+ final db = await instance.database;
+
+ String whereString = '$fileIDColumn = ?';
+ final List whereArgs = [fileId];
+
+ if (mlVersion != null) {
+ whereString += ' AND $mlVersionColumn = ?';
+ whereArgs.add(mlVersion);
+ }
+
+ final result = await db.query(
+ facesTable,
+ where: whereString,
+ whereArgs: whereArgs,
+ limit: 1,
+ );
+ return result.isNotEmpty;
+ }
+
+ Future getFaceMlResult(int fileId, {int? mlVersion}) async {
+ _logger.fine('getFaceMlResult called');
+ final db = await instance.database;
+
+ String whereString = '$fileIDColumn = ?';
+ final List whereArgs = [fileId];
+
+ if (mlVersion != null) {
+ whereString += ' AND $mlVersionColumn = ?';
+ whereArgs.add(mlVersion);
+ }
+
+ final result = await db.query(
+ facesTable,
+ where: whereString,
+ whereArgs: whereArgs,
+ limit: 1,
+ );
+ if (result.isNotEmpty) {
+ return FaceMlResult.fromJsonString(
+ result.first[faceMlResultColumn] as String,
+ );
+ }
+ _logger.fine(
+ 'No faceMlResult found for fileID $fileId and mlVersion $mlVersion (null if not specified)',
+ );
+ return null;
+ }
+
+ /// Returns the faceMlResults for the given [fileIds].
+ Future> getSelectedFaceMlResults(
+ List fileIds,
+ ) async {
+ _logger.fine('getSelectedFaceMlResults called');
+ final db = await instance.database;
+
+ if (fileIds.isEmpty) {
+ _logger.warning('getSelectedFaceMlResults called with empty fileIds');
+ return [];
+ }
+
+ final List