ml fixes and improvements

This commit is contained in:
Fabian Freund
2026-05-28 21:05:01 +02:00
parent 2d23b9b33e
commit bdca6d78a4
4 changed files with 167 additions and 72 deletions
@@ -202,6 +202,11 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
.toList(), .toList(),
); );
final idsByTitle = <String, List<String>>{};
for (final MapEntry(:key, :value) in unassignedDocumentsInput.entries) {
(idsByTitle[value] ??= []).add(key);
}
final clusterResult = await clusters.mapNotNull( final clusterResult = await clusters.mapNotNull(
(cluster) => Future.wait( (cluster) => Future.wait(
cluster.map((clusterTitles) async { cluster.map((clusterTitles) async {
@@ -224,11 +229,7 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
}, },
), ),
tabIds: originalTitles tabIds: originalTitles
.map( .expand((title) => idsByTitle[title] ?? const <String>[])
(title) => unassignedDocumentsInput.entries
.firstWhere((entry) => entry.value == title)
.key,
)
.toList(), .toList(),
); );
}), }),
@@ -266,7 +267,8 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
await _initialLoadComplete.future; await _initialLoadComplete.future;
final embeddings = await Result.fromAsync( final embeddings = await Result.fromAsync(
() async => await _service.generateDocumentEmbeddings(documents), () async =>
await _service.generateDocumentEmbeddings(embeddingsToGenerate),
); );
return embeddings; return embeddings;
@@ -276,8 +278,25 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
return Result.failure(generatedEmbeddings.error!); return Result.failure(generatedEmbeddings.error!);
} }
final generated = generatedEmbeddings.value;
if (generated.length != embeddingsToGenerate.length) {
return Result.failure(
ErrorMessage(
source: 'Document Embeddings',
message: 'Unexpected embedding count',
details: {
'expected': embeddingsToGenerate.length,
'actual': generated.length,
},
),
);
}
for (var i = 0; i < embeddingsToGenerate.length; i++) { for (var i = 0; i < embeddingsToGenerate.length; i++) {
embeddings[embeddingsToGenerate[i]] = generatedEmbeddings.value[i]; final embedding = generated[i];
final document = embeddingsToGenerate[i];
embeddings[document] = embedding;
_embeddingCache.set(document, embedding);
} }
} }
@@ -289,6 +308,15 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
return Result.failure( return Result.failure(
const ErrorMessage(source: 'Document Embeddings', message: 'Timeout'), const ErrorMessage(source: 'Document Embeddings', message: 'Timeout'),
); );
} catch (e, s) {
return Result.failure(
ErrorMessage(
source: 'Document Embeddings',
message: e.toString(),
details: e,
stackTrace: s,
),
);
} }
} }
@@ -456,15 +484,18 @@ Future<List<String>?> containerTabSuggestions(
.toList(), .toList(),
); );
return suggestedTitles.mapNotNull( final idsByTitle = <String, List<String>>{};
(titles) => titles for (final (id, title) in unassignedTitles.value) {
.map( (idsByTitle[title] ??= []).add(id);
(title) => unassignedTitles.value }
.firstWhere((tab) => tab.$2 == title)
.$1, return suggestedTitles.mapNotNull((titles) {
) final tabIds = titles
.toList(), .expand((title) => idsByTitle[title] ?? const <String>[])
); .toList();
return tabIds.isEmpty ? null : tabIds;
});
} }
} }
@@ -4,7 +4,10 @@
"use strict"; "use strict";
const { createEngine } = ChromeUtils.importESModule("chrome://global/content/ml/EngineProcess.sys.mjs"); const {
createEngine,
FEATURES,
} = ChromeUtils.importESModule("chrome://global/content/ml/EngineProcess.sys.mjs");
const ML_TASK_FEATURE_EXTRACTION = "feature-extraction"; const ML_TASK_FEATURE_EXTRACTION = "feature-extraction";
const ML_TASK_TEXT2TEXT = "text2text-generation"; const ML_TASK_TEXT2TEXT = "text2text-generation";
@@ -15,7 +18,8 @@ const SMART_TAB_GROUPING_CONFIG = {
timeoutMS: 2 * 60 * 1000, // 2 minutes timeoutMS: 2 * 60 * 1000, // 2 minutes
taskName: ML_TASK_FEATURE_EXTRACTION, taskName: ML_TASK_FEATURE_EXTRACTION,
featureId: "smart-tab-embedding", featureId: "smart-tab-embedding",
backend: "onnx", engineId: FEATURES["smart-tab-embedding"].engineId,
backend: "onnx-native",
fallbackBackend: "onnx", fallbackBackend: "onnx",
}, },
topicGeneration: { topicGeneration: {
@@ -23,7 +27,8 @@ const SMART_TAB_GROUPING_CONFIG = {
timeoutMS: 2 * 60 * 1000, // 2 minutes timeoutMS: 2 * 60 * 1000, // 2 minutes
taskName: ML_TASK_TEXT2TEXT, taskName: ML_TASK_TEXT2TEXT,
featureId: "smart-tab-topic", featureId: "smart-tab-topic",
backend: "onnx", engineId: FEATURES["smart-tab-topic"].engineId,
backend: "onnx-native",
fallbackBackend: "onnx", fallbackBackend: "onnx",
}, },
// dataConfig: { // dataConfig: {
@@ -117,6 +122,52 @@ function createProgressCallback(modelType, progressEmitter) {
}; };
} }
async function createMlEngine(engineConfig, progressCallback) {
const {
featureId,
engineId,
dtype,
taskName,
timeoutMS,
modelId,
modelRevision,
backend,
fallbackBackend,
} = engineConfig;
const initData = {
featureId,
engineId,
dtype,
taskName,
timeoutMS,
modelId,
modelRevision,
backend,
};
try {
return await createEngine(initData, progressCallback);
} catch (error) {
if (!fallbackBackend || fallbackBackend === backend) {
throw error;
}
try {
return await createEngine(
{
...initData,
backend: fallbackBackend,
},
progressCallback
);
} catch (fallbackError) {
throw new Error(
`Failed to create ML engine with ${backend} (${error?.message || error}) or ${fallbackBackend} (${fallbackError?.message || fallbackError})`
);
}
}
}
this.ml = class extends ExtensionAPI { this.ml = class extends ExtensionAPI {
constructor(extension) { constructor(extension) {
super(extension); super(extension);
@@ -151,7 +202,7 @@ this.ml = class extends ExtensionAPI {
}; };
if (isEngineClosed(self.embeddingEngine)) { if (isEngineClosed(self.embeddingEngine)) {
self.embeddingEngine = await createEngine( self.embeddingEngine = await createMlEngine(
SMART_TAB_GROUPING_CONFIG.embedding, SMART_TAB_GROUPING_CONFIG.embedding,
createProgressCallback("Embedding Model", self.progressEmitter) createProgressCallback("Embedding Model", self.progressEmitter)
); );
@@ -168,30 +219,8 @@ this.ml = class extends ExtensionAPI {
}, },
async predictTopic(keywords, documents) { async predictTopic(keywords, documents) {
if (isEngineClosed(self.topicEngine)) { if (isEngineClosed(self.topicEngine)) {
const { self.topicEngine = await createMlEngine(
featureId, SMART_TAB_GROUPING_CONFIG.topicGeneration,
engineId,
dtype,
taskName,
timeoutMS,
modelId,
modelRevision,
backend,
} = SMART_TAB_GROUPING_CONFIG.topicGeneration;
let initData = {
featureId,
engineId,
dtype,
taskName,
timeoutMS,
modelId,
modelRevision,
backend,
};
self.topicEngine = await createEngine(
initData,
createProgressCallback("Topic Generation Model", self.progressEmitter) createProgressCallback("Topic Generation Model", self.progressEmitter)
); );
} }
@@ -221,4 +250,4 @@ this.ml = class extends ExtensionAPI {
} }
}; };
} }
}; };
@@ -18,7 +18,7 @@ function sendErrorForRequest(id) {
port.postMessage({ port.postMessage({
"id": id, "id": id,
"status": "error", "status": "error",
"error": error "error": error?.message || String(error)
}); });
} }
} }
@@ -31,27 +31,31 @@ browser.experiments.ml.onProgress.addListener((progressData) => {
}); });
port.onMessage.addListener(async (message) => { port.onMessage.addListener(async (message) => {
let requestId = message["id"] const requestId = message["id"];
switch (message["action"]) {
case "predictDocumentTopic": {
const documents = message["args"];
const keywords = (documents.length > 1)
? await browser.experiments.nlp.extractKeywords([documents.slice(0, 3).join(" ")])
: [[]];
browser.experiments.ml.predictTopic(keywords[0], documents) try {
.then(sendJsonResultForRequest(requestId)) switch (message["action"]) {
.catch(sendErrorForRequest(requestId)); case "predictDocumentTopic": {
const documents = message["args"];
const keywords = (documents.length > 1)
? await browser.experiments.nlp.extractKeywords([documents.slice(0, 3).join(" ")])
: [[]];
const result = await browser.experiments.ml.predictTopic(keywords[0], documents);
break; sendJsonResultForRequest(requestId)(result);
} break;
case "generateDocumentEmbeddings": { }
const documents = message["args"]; case "generateDocumentEmbeddings": {
await browser.experiments.ml.generateEmbeddings(documents) const documents = message["args"];
.then(sendJsonResultForRequest(requestId)) const result = await browser.experiments.ml.generateEmbeddings(documents);
.catch(sendErrorForRequest(requestId));
sendJsonResultForRequest(requestId)(result);
break; break;
}
default:
throw new Error(`Unsupported ML action: ${message["action"]}`);
} }
} catch (error) {
sendErrorForRequest(requestId)(error);
} }
}); });
@@ -7,7 +7,11 @@
package eu.weblibre.flutter_mozilla_components.feature package eu.weblibre.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -26,10 +30,12 @@ object MLEngineFeature {
private const val ML_ENGINE_REPORTER_EXTENSION_ID = "ml-engine@weblibre.eu" private const val ML_ENGINE_REPORTER_EXTENSION_ID = "ml-engine@weblibre.eu"
private const val ML_ENGINE_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/ml_engine/" private const val ML_ENGINE_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/ml_engine/"
private const val ML_ENGINE_REPORTER_MESSAGING_ID = "mlEngine" private const val ML_ENGINE_REPORTER_MESSAGING_ID = "mlEngine"
private const val REQUEST_TIMEOUT_MS = 130_000L
private var nextRequestId: Int = 0 private var nextRequestId: Int = 0
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>() private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
private val mutex = Mutex() private val mutex = Mutex()
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// Progress callback for ML operations // Progress callback for ML operations
var progressCallback: ((JSONObject) -> Unit)? = null var progressCallback: ((JSONObject) -> Unit)? = null
@@ -44,29 +50,54 @@ object MLEngineFeature {
fun scheduleRequest(command: String, args: Any, callback: ResultConsumer<JSONObject>) { fun scheduleRequest(command: String, args: Any, callback: ResultConsumer<JSONObject>) {
val message = JSONObject() val message = JSONObject()
message.put("action", command); message.put("action", command)
message.put("args", args) message.put("args", args)
runBlocking { val requestId = runBlocking {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
mutex.withLock { mutex.withLock {
message.put("id", nextRequestId) val requestId = nextRequestId
message.put("id", requestId)
requestHandlers[nextRequestId] = callback requestHandlers[requestId] = callback
nextRequestId += 1 nextRequestId += 1
extensionController.sendBackgroundMessage(message) try {
extensionController.sendBackgroundMessage(message)
} catch (throwable: Throwable) {
requestHandlers.remove(requestId)
callback.error(
"ML Engine",
"Failed to schedule request",
throwable.message,
)
}
requestId
} }
} }
} }
scope.launch {
delay(REQUEST_TIMEOUT_MS)
val handler = mutex.withLock {
requestHandlers.remove(requestId)
}
handler?.error(
"ML Engine",
"Request timed out",
"No response received for $command",
)
}
} }
private class PrefManagerReporterBackgroundMessageHandler() : MessageHandler { private class PrefManagerReporterBackgroundMessageHandler() : MessageHandler {
override fun onPortMessage(message: Any, port: Port) { override fun onPortMessage(message: Any, port: Port) {
runBlocking { runBlocking {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val messageJSON = message as JSONObject; val messageJSON = message as JSONObject
// Check if this is a progress message // Check if this is a progress message
if (messageJSON.has("type") && messageJSON.getString("type") == "mlProgress") { if (messageJSON.has("type") && messageJSON.getString("type") == "mlProgress") {
@@ -86,7 +117,7 @@ object MLEngineFeature {
handler?.error( handler?.error(
"ML Engine", "ML Engine",
"Failed to perform operation", "Failed to perform operation",
message.getString("error") messageJSON.optString("error", "Unknown ML engine error")
) )
} }
} }