import "dart:io" show File; import "package:flutter/foundation.dart"; import "package:logging/logging.dart"; import "package:photos/core/cache/lru_map.dart"; import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/extensions/stop_watch.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/file/file_type.dart"; import "package:photos/models/ml/face/box.dart"; import "package:photos/models/ml/face/face.dart"; import "package:photos/services/machine_learning/ml_computer.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/thumbnail_util.dart"; import "package:pool/pool.dart"; void resetPool({required bool fullFile}) { if (fullFile) { _poolFullFileFaceGenerations = Pool(20, timeout: const Duration(seconds: 15)); } else { _poolThumbnailFaceGenerations = Pool(100, timeout: const Duration(seconds: 15)); } } final _logger = Logger("FaceCropUtils"); const int _retryLimit = 3; final LRUMap _faceCropCache = LRUMap(1000); final LRUMap _faceCropThumbnailCache = LRUMap(1000); Pool _poolFullFileFaceGenerations = Pool(20, timeout: const Duration(seconds: 15)); Pool _poolThumbnailFaceGenerations = Pool(100, timeout: const Duration(seconds: 15)); Future checkGetCachedCropForFaceID(String faceID) async { final Uint8List? cachedCover = _faceCropCache.get(faceID); return cachedCover; } Future putCachedCropForFaceID( String faceID, Uint8List data, ) async { _faceCropCache.put(faceID, data); } Future?> getCachedFaceCrops( EnteFile enteFile, Iterable faces, { int fetchAttempt = 1, bool useFullFile = true, }) async { try { final faceIdToCrop = {}; final facesWithoutCrops = {}; for (final face in faces) { final Uint8List? cachedFace = _faceCropCache.get(face.faceID); if (cachedFace != null) { faceIdToCrop[face.faceID] = cachedFace; } else { final faceCropCacheFile = cachedFaceCropPath(face.faceID); if ((await faceCropCacheFile.exists())) { final data = await faceCropCacheFile.readAsBytes(); _faceCropCache.put(face.faceID, data); faceIdToCrop[face.faceID] = data; } else { facesWithoutCrops[face.faceID] = face.detection.box; } } } if (facesWithoutCrops.isEmpty) { return faceIdToCrop; } if (!useFullFile) { for (final face in faces) { if (facesWithoutCrops.containsKey(face.faceID)) { final Uint8List? cachedFaceThumbnail = _faceCropThumbnailCache.get(face.faceID); if (cachedFaceThumbnail != null) { faceIdToCrop[face.faceID] = cachedFaceThumbnail; facesWithoutCrops.remove(face.faceID); } } } if (facesWithoutCrops.isEmpty) { return faceIdToCrop; } } late final Pool relevantResourcePool; if (useFullFile) { relevantResourcePool = _poolFullFileFaceGenerations; } else { relevantResourcePool = _poolThumbnailFaceGenerations; } final result = await relevantResourcePool.withResource( () async => await _getFaceCrops( enteFile, facesWithoutCrops, useFullFile: useFullFile, ), ); if (result == null) { return (faceIdToCrop.isEmpty) ? null : faceIdToCrop; } for (final entry in result.entries) { final Uint8List? computedCrop = result[entry.key]; if (computedCrop != null) { faceIdToCrop[entry.key] = computedCrop; if (useFullFile) { _faceCropCache.put(entry.key, computedCrop); final faceCropCacheFile = cachedFaceCropPath(entry.key); faceCropCacheFile.writeAsBytes(computedCrop).ignore(); } else { _faceCropThumbnailCache.put(entry.key, computedCrop); } } } return faceIdToCrop.isEmpty ? null : faceIdToCrop; } catch (e, s) { _logger.severe( "Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}", e, s, ); resetPool(fullFile: useFullFile); if (fetchAttempt <= _retryLimit) { return getCachedFaceCrops( enteFile, faces, fetchAttempt: fetchAttempt + 1, useFullFile: useFullFile, ); } return null; } } Future precomputeClusterFaceCrop( file, clusterID, { required bool useFullFile, }) async { try { final w = (kDebugMode ? EnteWatch('precomputeClusterFaceCrop') : null)?..start(); final Face? face = await MLDataDB.instance.getCoverFaceForPerson( recentFileID: file.uploadedFileID!, clusterID: clusterID, ); w?.log('getCoverFaceForPerson'); if (face == null) { debugPrint( "No cover face for cluster $clusterID and recentFile ${file.uploadedFileID}", ); return null; } EnteFile? fileForFaceCrop = file; if (face.fileID != file.uploadedFileID!) { fileForFaceCrop = await FilesDB.instance.getAnyUploadedFile(face.fileID); w?.log('getAnyUploadedFile'); } if (fileForFaceCrop == null) { return null; } final cropMap = await getCachedFaceCrops( fileForFaceCrop, [face], useFullFile: useFullFile, ); w?.logAndReset('getCachedFaceCrops'); return cropMap?[face.faceID]; } catch (e, s) { _logger.severe( "Error getting cover face for cluster $clusterID", e, s, ); return null; } } Future?> _getFaceCrops( EnteFile file, Map faceBoxeMap, { bool useFullFile = true, }) async { late String? imagePath; if (useFullFile && file.fileType != FileType.video) { final File? ioFile = await getFile(file); if (ioFile == null) { return null; } imagePath = ioFile.path; } else { final thumbnail = await getThumbnailForUploadedFile(file); if (thumbnail == null) { return null; } imagePath = thumbnail.path; } final List faceIds = []; final List faceBoxes = []; for (final e in faceBoxeMap.entries) { faceIds.add(e.key); faceBoxes.add(e.value); } final List faceCrop = await MLComputer.instance.generateFaceThumbnails( // await generateJpgFaceThumbnails( imagePath, faceBoxes, ); final Map result = {}; for (int i = 0; i < faceCrop.length; i++) { result[faceIds[i]] = faceCrop[i]; } return result; }