diff --git a/mobile/lib/models/search/hierarchical/face_filter.dart b/mobile/lib/models/search/hierarchical/face_filter.dart index 5521473bc2..d399f0776a 100644 --- a/mobile/lib/models/search/hierarchical/face_filter.dart +++ b/mobile/lib/models/search/hierarchical/face_filter.dart @@ -1,4 +1,4 @@ -import "package:flutter/widgets.dart"; +import "package:flutter/material.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/search/hierarchical/hierarchical_search_filter.dart"; import "package:photos/models/search/search_types.dart"; @@ -7,8 +7,8 @@ class FaceFilter extends HierarchicalSearchFilter { final String? personId; final String? clusterId; - ///If name is not available, use string of memories count instead. It should be - ///of the same format as SearchResult.name(); + ///Since name is not available when personID is null, use clusterId instead + ///as name. final String faceName; final EnteFile faceFile; final int occurrence; @@ -36,9 +36,10 @@ class FaceFilter extends HierarchicalSearchFilter { @override IconData? icon() { - throw UnimplementedError( - "FaceFilter does not need an icon, the face crop should be used instead", - ); + return Icons.face; + // throw UnimplementedError( + // "FaceFilter does not need an icon, the face crop should be used instead", + // ); } @override diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index c75f2cf6e2..b862cf02c9 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -864,7 +864,7 @@ class SearchService { facesResult.add( GenericSearchResult( ResultType.faces, - "", + clusterName, files, params: { kClusterParamId: clusterId, diff --git a/mobile/lib/utils/hierarchical_search_util.dart b/mobile/lib/utils/hierarchical_search_util.dart index 62da2e6a77..3a3de6594a 100644 --- a/mobile/lib/utils/hierarchical_search_util.dart +++ b/mobile/lib/utils/hierarchical_search_util.dart @@ -4,17 +4,22 @@ import "package:flutter/material.dart"; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; import "package:photos/db/files_db.dart"; +import "package:photos/db/ml/db.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/file/file_type.dart"; import "package:photos/models/location_tag/location_tag.dart"; +import "package:photos/models/ml/face/person.dart"; import "package:photos/models/search/hierarchical/album_filter.dart"; import "package:photos/models/search/hierarchical/contacts_filter.dart"; +import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/models/search/hierarchical/file_type_filter.dart"; import "package:photos/models/search/hierarchical/hierarchical_search_filter.dart"; import "package:photos/models/search/hierarchical/location_filter.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/location_service.dart"; +import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; +import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/search_service.dart"; import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; @@ -85,9 +90,11 @@ void curateFilters( ); final contactsFilters = _curateContactsFilter(searchFilterDataProvider, files); + final faceFilters = await curateFaceFilters(files); searchFilterDataProvider.clearAndAddRecommendations( [ + ...faceFilters, ...fileTypeFilters, ...contactsFilters, ...albumFilters, @@ -237,3 +244,95 @@ List _curateContactsFilter( return contactsFilters; } + +Future> curateFaceFilters( + List files, +) async { + try { + final faceFilters = []; + final Map> fileIdToClusterID = + await MLDataDB.instance.getFileIdToClusterIds(); + final Map personIdToPerson = + await PersonService.instance.getPersonsMap(); + final clusterIDToPersonID = + await MLDataDB.instance.getClusterIDToPersonID(); + + final Map> clusterIdToFiles = {}; + final Map> personIdToFiles = {}; + + for (final f in files) { + if (!fileIdToClusterID.containsKey(f.uploadedFileID ?? -1)) { + continue; + } + final clusterIds = fileIdToClusterID[f.uploadedFileID ?? -1]!; + for (final cluster in clusterIds) { + final PersonEntity? p = + personIdToPerson[clusterIDToPersonID[cluster] ?? ""]; + if (p != null) { + if (personIdToFiles.containsKey(p.remoteID)) { + personIdToFiles[p.remoteID]!.add(f); + } else { + personIdToFiles[p.remoteID] = [f]; + } + } else { + if (clusterIdToFiles.containsKey(cluster)) { + clusterIdToFiles[cluster]!.add(f); + } else { + clusterIdToFiles[cluster] = [f]; + } + } + } + } + + for (final personID in personIdToFiles.keys) { + final files = personIdToFiles[personID]!; + if (files.isEmpty) { + continue; + } + final PersonEntity p = personIdToPerson[personID]!; + if (p.data.isIgnored) continue; + + faceFilters.add( + FaceFilter( + personId: personID, + clusterId: null, + faceName: p.data.name, + faceFile: files.first, + occurrence: files.length, + ), + ); + } + + for (final clusterId in clusterIdToFiles.keys) { + final files = clusterIdToFiles[clusterId]!; + final String clusterName = clusterId; + + if (clusterIDToPersonID[clusterId] != null) { + // This should not happen, means a faceID is assigned to multiple persons. + Logger("hierarchical_search_util").severe( + "`getAllFace`: Cluster $clusterId should not have person id ${clusterIDToPersonID[clusterId]}", + ); + } + if (files.length < kMinimumClusterSizeSearchResult && + clusterIdToFiles.keys.length > 3) { + continue; + } + + faceFilters.add( + FaceFilter( + personId: null, + clusterId: clusterId, + faceName: clusterName, + faceFile: files.first, + occurrence: files.length, + ), + ); + } + + return faceFilters; + } catch (e, s) { + Logger("hierarchical_search_util") + .severe("Error in curating face filters", e, s); + rethrow; + } +}