Ml fixesv148 (#3028)

## Description

- Don't process ML on mobile for large files
- ML user developer options to clear local data
This commit is contained in:
Laurens Priem
2024-08-29 11:52:13 +02:00
committed by GitHub
9 changed files with 174 additions and 25 deletions

View File

@@ -668,6 +668,22 @@ class MLDataDB {
return maps.first['count'] as int;
}
Future<Set<int>> getErroredFileIDs() async {
final db = await instance.asyncDB;
final List<Map<String, dynamic>> maps = await db.getAll(
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore < 0',
);
return maps.map((e) => e[fileIDColumn] as int).toSet();
}
Future<void> deleteFaceIndexForFiles(List<int> fileIDs) async {
final db = await instance.asyncDB;
final String sql = '''
DELETE FROM $facesTable WHERE $fileIDColumn IN (${fileIDs.join(", ")})
''';
await db.execute(sql);
}
Future<int> getClusteredOrFacelessFileCount() async {
final db = await instance.asyncDB;
final List<Map<String, dynamic>> clustered = await db.getAll(

View File

@@ -7,3 +7,4 @@ const minimumClusterSize = 2;
const embeddingFetchLimit = 200;
final fileDownloadMlLimit = Platform.isIOS ? 5 : 10;
const maxFileDownloadSize = 100000000;

View File

@@ -189,9 +189,8 @@ class MLIndexingIsolate {
/// Analyzes the given image data by running the full pipeline for faces, using [_analyzeImageSync] in the isolate.
Future<MLResult?> analyzeImage(
FileMLInstruction instruction,
String filePath,
) async {
final String filePath = await getImagePathForML(instruction.file);
final Stopwatch stopwatch = Stopwatch()..start();
late MLResult result;

View File

@@ -391,8 +391,11 @@ class MLService {
bool actuallyRanML = false;
try {
final String filePath = await getImagePathForML(instruction.file);
final MLResult? result = await MLIndexingIsolate.instance.analyzeImage(
instruction,
filePath,
);
// Check if there's no result simply because MLController paused indexing
if (result == null) {
@@ -467,25 +470,20 @@ class MLService {
_logger.info("Results for file ${result.fileId} stored locally");
return actuallyRanML;
} catch (e, s) {
bool acceptedIssue = false;
final String errorString = e.toString();
if (errorString.contains('ThumbnailRetrievalException')) {
_logger.severe(
'ThumbnailRetrievalException while processing image with ID ${instruction.file.uploadedFileID}, storing empty results so indexing does not get stuck',
e,
s,
);
acceptedIssue = true;
}
if (errorString.contains('InvalidImageFormatException')) {
_logger.severe(
'$errorString with ID ${instruction.file.uploadedFileID}, storing empty results so indexing does not get stuck',
e,
s,
);
acceptedIssue = true;
}
final String format = instruction.file.displayName.split('.').last;
final int? size = instruction.file.fileSize;
final fileType = instruction.file.fileType;
final bool acceptedIssue =
errorString.contains('ThumbnailRetrievalException') ||
errorString.contains('InvalidImageFormatException') ||
errorString.contains('FileSizeTooLargeForMobileIndexing');
if (acceptedIssue) {
_logger.severe(
'$errorString with ID ${instruction.file.uploadedFileID} (format $format, type $fileType, size $size), storing empty results so indexing does not get stuck',
e,
s,
);
await MLDataDB.instance.bulkInsertFaces(
[Face.empty(instruction.file.uploadedFileID!, error: true)],
);
@@ -495,7 +493,7 @@ class MLService {
return true;
}
_logger.severe(
"Failed to analyze using FaceML for image with ID: ${instruction.file.uploadedFileID} and format ${instruction.file.displayName.split('.').last} (${instruction.file.fileType}). Not storing any results locally, which means it will be automatically retried later.",
"Failed to index file with ID: ${instruction.file.uploadedFileID} (format $format, type $fileType, size $size). Not storing any results locally, which means it will be automatically retried later.",
e,
s,
);

View File

@@ -13,7 +13,7 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/components/title_bar_widget.dart';
import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/ui/settings/machine_learning_settings_page.dart";
import "package:photos/ui/settings/ml/machine_learning_settings_page.dart";
import 'package:photos/ui/tools/debug/app_storage_viewer.dart';
import 'package:photos/ui/viewer/gallery/photo_grid_size_picker_page.dart';
import 'package:photos/utils/navigation_util.dart';

View File

@@ -26,6 +26,7 @@ import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/ui/components/toggle_switch_widget.dart";
import "package:photos/ui/settings/ml/enable_ml_consent.dart";
import "package:photos/ui/settings/ml/ml_user_dev_screen.dart";
import "package:photos/utils/ml_util.dart";
import "package:photos/utils/network_util.dart";
import "package:photos/utils/wakelock_util.dart";
@@ -42,6 +43,8 @@ class _MachineLearningSettingsPageState
extends State<MachineLearningSettingsPage> {
final EnteWakeLock _wakeLock = EnteWakeLock();
Timer? _timer;
int _titleTapCount = 0;
Timer? _advancedOptionsTimer;
@override
void initState() {
@@ -55,6 +58,9 @@ class _MachineLearningSettingsPageState
}
});
}
_advancedOptionsTimer = Timer.periodic(const Duration(seconds: 7), (timer) {
_titleTapCount = 0;
});
}
@override
@@ -63,6 +69,7 @@ class _MachineLearningSettingsPageState
_wakeLock.disable();
MachineLearningController.instance.forceOverrideML(turnOn: false);
_timer?.cancel();
_advancedOptionsTimer?.cancel();
}
@override
@@ -73,8 +80,26 @@ class _MachineLearningSettingsPageState
primary: false,
slivers: <Widget>[
TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: S.of(context).machineLearning,
flexibleSpaceTitle: GestureDetector(
child: TitleBarTitleWidget(
title: S.of(context).machineLearning,
),
onTap: () {
setState(() {
_titleTapCount++;
if (_titleTapCount >= 7) {
_titleTapCount = 0;
// showShortToast(context, "Advanced options enabled");
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const MLUserDeveloperOptions();
},
),
).ignore();
}
});
},
),
actionIcons: [
IconButtonWidget(

View File

@@ -0,0 +1,103 @@
import "package:flutter/material.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/db/ml/clip_db.dart";
import "package:photos/db/ml/db.dart";
import "package:photos/events/people_changed_event.dart";
import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/components/title_bar_widget.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/toast_util.dart";
class MLUserDeveloperOptions extends StatelessWidget {
const MLUserDeveloperOptions({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
const TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: "ML debug options",
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(delegateBuildContext, index) => Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(
children: [
Text(
"Only use if you know what you're doing",
textAlign: TextAlign.left,
style: getEnteTextTheme(context).body.copyWith(
color: getEnteColorScheme(context).textMuted,
),
),
const SizedBox(height: 48),
ButtonWidget(
buttonType: ButtonType.neutral,
labelText: "Purge empty indices",
onTap: () async {
await deleteEmptyIndices(context);
},
),
const SizedBox(height: 24),
ButtonWidget(
buttonType: ButtonType.neutral,
labelText: "Reset all local ML",
onTap: () async {
await deleteAllLocalML(context);
},
),
const SafeArea(
child: SizedBox(
height: 12,
),
),
],
),
),
childCount: 1,
),
),
],
),
);
}
Future<void> deleteEmptyIndices(BuildContext context) async {
try {
final Set<int> emptyFileIDs = await MLDataDB.instance.getErroredFileIDs();
await MLDataDB.instance.deleteFaceIndexForFiles(emptyFileIDs.toList());
await MLDataDB.instance.deleteEmbeddings(emptyFileIDs.toList());
showShortToast(context, "Deleted ${emptyFileIDs.length} entries");
} catch (e) {
// ignore: unawaited_futures
showGenericErrorDialog(
context: context,
error: e,
);
}
}
Future<void> deleteAllLocalML(BuildContext context) async {
try {
await MLDataDB.instance.dropClustersAndPersonTable(faces: true);
await SemanticSearchService.instance.clearIndexes();
Bus.instance.fire(PeopleChangedEvent());
showShortToast(context, "All local ML cleared");
} catch (e) {
// ignore: unawaited_futures
showGenericErrorDialog(
context: context,
error: e,
);
}
}
}

View File

@@ -13,7 +13,7 @@ import "package:photos/models/search/search_constants.dart";
import "package:photos/models/search/search_result.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/settings/machine_learning_settings_page.dart";
import "package:photos/ui/settings/ml/machine_learning_settings_page.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/people/add_person_action_sheet.dart";

View File

@@ -153,7 +153,8 @@ Future<List<FileMLInstruction>> getFilesForMlIndexing() async {
}
Stream<List<FileMLInstruction>> fetchEmbeddingsAndInstructions(
int yieldSize,) async* {
int yieldSize,
) async* {
final List<FileMLInstruction> filesToIndex = await getFilesForMlIndexing();
final List<List<FileMLInstruction>> chunks =
filesToIndex.chunks(embeddingFetchLimit);
@@ -312,6 +313,12 @@ Future<String> getImagePathForML(EnteFile enteFile) async {
throw ThumbnailRetrievalException(e.toString(), s);
}
} else {
// Don't process the file if it's too large (more than 100MB)
if (enteFile.fileSize != null && enteFile.fileSize! > maxFileDownloadSize) {
throw Exception(
"FileSizeTooLargeForMobileIndexing: size is ${enteFile.fileSize}",
);
}
try {
file = await getFile(enteFile, isOrigin: true);
} catch (e, s) {