From 72cdd500e0fc1310f48016e2fa74a4415bddf985 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 28 May 2026 22:43:49 +0200 Subject: [PATCH] add feature to clear ml downloads --- .../screens/advanced_settings.dart | 93 +++++++++++++++++++ .../assets/extensions/ml_engine/api/ml.js | 15 ++- .../assets/extensions/ml_engine/background.js | 6 ++ .../assets/extensions/ml_engine/schema.json | 9 +- .../api/GeckoMlApiImpl.kt | 14 ++- .../pigeons/Gecko.g.kt | 18 ++++ .../lib/src/domain/services/gecko_ml.dart | 4 + .../lib/src/pigeons/gecko.g.dart | 18 ++++ .../pigeons/gecko.dart | 2 + 9 files changed, 176 insertions(+), 3 deletions(-) diff --git a/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart index 2967c244..16b1cd26 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/advanced_settings.dart @@ -24,6 +24,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/providers/app_state.dart'; @@ -85,6 +86,12 @@ const List advancedSettingsSections = [ keywords: ['favicons', 'cache'], child: _IconCacheTile(), ), + SettingsEntryDefinition( + title: 'ML Downloads', + subtitle: 'Downloaded AI models and runtime files', + keywords: ['ai', 'ml', 'models', 'onnx', 'cache'], + child: _MlCacheTile(), + ), SettingsEntryDefinition( title: 'Error Logs', subtitle: 'View and copy logs for issue reporting', @@ -294,6 +301,92 @@ class _IconCacheTile extends HookConsumerWidget { } } +class _MlCacheTile extends HookWidget { + const _MlCacheTile(); + + Future _confirmClear(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Clear ML downloads?'), + content: const Text( + 'This clears downloaded AI models and ONNX runtime files for this profile. ' + 'They will be downloaded again when needed. Restart WebLibre before retrying ML features.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Clear'), + ), + ], + ), + ); + + return result ?? false; + } + + @override + Widget build(BuildContext context) { + final isClearing = useState(false); + + return CustomListTile( + title: 'ML Downloads', + subtitle: 'Downloaded AI models and runtime files', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.memory, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: isClearing.value + ? null + : () async { + if (!await _confirmClear(context)) { + return; + } + + isClearing.value = true; + try { + await GeckoMlService().clearMlCache(); + + if (context.mounted) { + showInfoMessage( + context, + 'ML downloads cleared. Restart WebLibre before retrying.', + ); + } + } catch (e) { + if (context.mounted) { + showErrorMessage( + context, + 'Failed to clear ML downloads: $e', + ); + } + } finally { + if (context.mounted) { + isClearing.value = false; + } + } + }, + icon: isClearing.value + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.delete), + label: Text(isClearing.value ? 'Clearing' : 'Clear'), + ), + ); + } +} + class _ErrorLogsTile extends StatelessWidget { const _ErrorLogsTile(); diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js index b0bcd067..bc60424e 100644 --- a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js @@ -6,8 +6,11 @@ const { createEngine, + EngineProcess, FEATURES, } = ChromeUtils.importESModule("chrome://global/content/ml/EngineProcess.sys.mjs"); +const { ModelHub } = ChromeUtils.importESModule("chrome://global/content/ml/ModelHub.sys.mjs"); +const { OPFS } = ChromeUtils.importESModule("chrome://global/content/ml/OPFS.sys.mjs"); const ML_TASK_FEATURE_EXTRACTION = "feature-extraction"; const ML_TASK_TEXT2TEXT = "text2text-generation"; @@ -154,7 +157,7 @@ async function createMlEngine(engineConfig, progressCallback) { numThreads, }; - return await createEngine(initData, progressCallback); + return await createEngine(initData, progressCallback); } this.ml = class extends ExtensionAPI { @@ -234,6 +237,16 @@ this.ml = class extends ExtensionAPI { const generated = cutAtDuplicateWords((res[0]["generated_text"] || "").trim()); return generated; + }, + async clearCache() { + self.embeddingEngine = null; + self.topicEngine = null; + + await EngineProcess.destroyMLEngine(); + await new ModelHub().purgeDatabase(); + await OPFS.remove("mlRuntimeFiles", { recursive: true }); + + return true; } } } diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/background.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/background.js index 24ebbbc5..c340adb6 100644 --- a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/background.js +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/background.js @@ -56,6 +56,12 @@ port.onMessage.addListener(async (message) => { sendJsonResultForRequest(requestId)(result); break; } + case "clearMlCache": { + const result = await browser.experiments.ml.clearCache(); + + sendJsonResultForRequest(requestId)(result); + break; + } default: throw new Error(`Unsupported ML action: ${message["action"]}`); } diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/schema.json b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/schema.json index 0024a374..0b539927 100644 --- a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/schema.json +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/schema.json @@ -85,7 +85,14 @@ "description": "Array of document titles/content" } ] + }, + { + "name": "clearCache", + "type": "function", + "description": "Clear downloaded ML models and runtime files", + "async": true, + "parameters": [] } ] } -] \ No newline at end of file +] diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoMlApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoMlApiImpl.kt index 0b8568b0..373483ea 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoMlApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoMlApiImpl.kt @@ -131,4 +131,16 @@ class GeckoMlApiImpl( }) } -} \ No newline at end of file + override fun clearMlCache(callback: (Result) -> Unit) { + MLEngineFeature.scheduleRequest("clearMlCache", JSONObject(), object : ResultConsumer { + override fun success(result: JSONObject) { + callback(Result.success(Unit)) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails"))) + } + }) + } + +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index c73d2d6c..3dbecd6f 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -8315,6 +8315,7 @@ interface GeckoPrefApi { interface GeckoMlApi { fun predictDocumentTopic(documents: List, callback: (Result) -> Unit) fun generateDocumentEmbeddings(documents: List, callback: (Result>) -> Unit) + fun clearMlCache(callback: (Result) -> Unit) companion object { /** The codec used by GeckoMlApi. */ @@ -8365,6 +8366,23 @@ interface GeckoMlApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.clearMlCache$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.clearMlCache{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + reply.reply(GeckoPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_ml.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_ml.dart index e0c2d427..2917f95e 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_ml.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_ml.dart @@ -28,4 +28,8 @@ class GeckoMlService { .map((values) => (values! as List).cast()) .toList(); } + + Future clearMlCache() { + return _apiInstance.clearMlCache(); + } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 18435536..2cfd21e3 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -8621,6 +8621,24 @@ class GeckoMlApi { ); return pigeonVar_replyValue! as List; } + + Future clearMlCache() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.clearMlCache$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ); + } } class GeckoBrowserExtensionApi { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index f2078909..db9274e5 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1848,6 +1848,8 @@ abstract class GeckoMlApi { String predictDocumentTopic(List documents); @async List generateDocumentEmbeddings(List documents); + @async + void clearMlCache(); } @HostApi()