Skip to content

Commit f464aed

Browse files
committed
Bug 1940906 - Allow looking up per-file display info for on-device models in about:addons. r=extension-reviewers,ai-platform-reviewers,rpl,valentinp
Currently, about:addons relies on a specific way to organize ModelHub and also how Hugging Face works: ModelHub slugs mirror HuggingFace slugs (same `org/model`, a bit like on github). For SpeechRecognition, it's a little bit different: we take multiple models that come from different Hugging Face pages from one Hugging Face repo (https://huggingface.co/mudler/parakeet-cpp-gguf/). They are conversions of models from Nvidia. We'd like to link to the original repo (that has much more informative info), and we also want better names, so we'll be in a position to say "Speech Recognition model (multilingual)" instead of the inscrutable model name (with a link to go further for the nerds / interested people). This is a simple idirection layer that any developer of a feature that uses AI models can use to display things better. For Speech Recognition, the information comes from a small js module generated from a yaml. This yaml also generates some C++ for other things, so we can't risk being out of sync here. Differential Revision: https://phabricator.services.mozilla.com/D316337
1 parent a3092f2 commit f464aed

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

‎toolkit/components/ml/content/Utils.sys.mjs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,38 @@ export function featureEngineIdToFluentId(engineId) {
10861086
return null;
10871087
}
10881088

1089+
/**
1090+
* Looks up display info (a human-readable name and a homepage URL) for one
1091+
* of a downloaded model's files, from the feature registered for one of
1092+
* engineIds. Features register their file display info via
1093+
* FEATURES[...].fileDisplayInfoModule, a module whose default export maps
1094+
* file name to {name, hfUrl}.
1095+
*
1096+
* @param {Array<string>} [engineIds] A model with no recorded engineIds has no
1097+
* feature to look up, and gets no display info.
1098+
* @param {Array<string>} files
1099+
* @returns {Promise<{name: string, hfUrl: string}|null>}
1100+
*/
1101+
export async function fileDisplayInfoForEngineIds(engineIds, files) {
1102+
for (const config of Object.values(lazy.FEATURES)) {
1103+
if (
1104+
!config.fileDisplayInfoModule ||
1105+
!engineIds?.includes(config.engineId)
1106+
) {
1107+
continue;
1108+
}
1109+
const { default: info } = await ChromeUtils.importESModule(
1110+
config.fileDisplayInfoModule
1111+
);
1112+
for (const file of files) {
1113+
if (info[file]) {
1114+
return info[file];
1115+
}
1116+
}
1117+
}
1118+
return null;
1119+
}
1120+
10891121
/**
10901122
* Generates a random uuid to use where Services.uuid is not available,
10911123
* for instance pipelines

‎toolkit/mozapps/extensions/internal/ModelHubProvider.sys.mjs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
1313
ModelHub: "chrome://global/content/ml/ModelHub.sys.mjs",
1414
isAddonEngineId: "chrome://global/content/ml/Utils.sys.mjs",
1515
engineIdToAddonId: "chrome://global/content/ml/Utils.sys.mjs",
16+
fileDisplayInfoForEngineIds: "chrome://global/content/ml/Utils.sys.mjs",
1617
});
1718

1819
XPCOMUtils.defineLazyPreferenceGetter(
@@ -36,6 +37,7 @@ export class ModelHubAddonWrapper {
3637
updateDate;
3738
modelIconURL;
3839
engineIds;
40+
displayInfo;
3941

4042
constructor(params) {
4143
this.#provider = params.provider;
@@ -47,6 +49,7 @@ export class ModelHubAddonWrapper {
4749
this.updateDate = params.updateDate;
4850
this.modelIconURL = params.modelIconURL;
4951
this.engineIds = params.engineIds ?? [];
52+
this.displayInfo = params.displayInfo ?? null;
5053
}
5154

5255
async uninstall() {
@@ -70,11 +73,17 @@ export class ModelHubAddonWrapper {
7073
}
7174

7275
get name() {
76+
if (this.displayInfo) {
77+
return this.displayInfo.name;
78+
}
7379
const parts = this.model.split("/");
7480
return parts.slice(2).join("/");
7581
}
7682

7783
get modelHomepageURL() {
84+
if (this.displayInfo) {
85+
return this.displayInfo.hfUrl;
86+
}
7887
// Model card URL for models downloaded from "model-hub.mozilla.org" should point to the corresponding "https://huggingface.co" url.
7988
return `https://${this.model}/`.replace(
8089
"https://model-hub.mozilla.org/",
@@ -193,13 +202,17 @@ export const ModelHubProvider = {
193202
const models = await this.modelHub.listModels();
194203

195204
for (const model of models) {
196-
const { metadata } = await this.modelHub.listFiles({
205+
const { files, metadata } = await this.modelHub.listFiles({
197206
model: model.name,
198207
revision: model.revision,
199208
});
200209

201210
const modelIconURL = await this.modelHub.getOwnerIcon(model.name);
202211
const id = this.getWrapperIdForModel(model);
212+
const displayInfo = await lazy.fileDisplayInfoForEngineIds(
213+
metadata.engineIds,
214+
files.map(file => file.path)
215+
);
203216

204217
const wrapper = new ModelHubAddonWrapper({
205218
provider: this,
@@ -211,6 +224,7 @@ export const ModelHubProvider = {
211224
updateDate: new Date(metadata.updateDate),
212225
modelIconURL,
213226
engineIds: metadata.engineIds,
227+
displayInfo,
214228
});
215229
this.cache.set(wrapper.id, wrapper);
216230
}

‎toolkit/mozapps/extensions/test/xpcshell/test_ModelHubProvider.js‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { ModelHubProvider } = ChromeUtils.importESModule(
66
"resource://gre/modules/addons/ModelHubProvider.sys.mjs"
77
);
88
ChromeUtils.defineESModuleGetters(this, {
9+
FEATURES: "chrome://global/content/ml/EngineProcess.sys.mjs",
910
addonIdToEngineId: "chrome://global/content/ml/Utils.sys.mjs",
1011
engineIdToAddonId: "chrome://global/content/ml/Utils.sys.mjs",
1112
isAddonEngineId: "chrome://global/content/ml/Utils.sys.mjs",
@@ -88,6 +89,9 @@ add_task(
8889
},
8990
];
9091
const mockListFilesResult = {
92+
// No display info is registered for these, so the wrappers fall back to
93+
// deriving their name and homepage from the model string.
94+
files: [{ path: "model.onnx" }, { path: "config.json" }],
9195
metadata: {
9296
totalSize: 2048,
9397
lastUsed: new Date("2023-10-01T12:00:00Z"),
@@ -120,6 +124,7 @@ add_task(
120124
.resolves(mockListFilesResult)
121125
.onSecondCall()
122126
.resolves({
127+
files: mockListFilesResult.files,
123128
metadata: {
124129
...mockListFilesResult.metadata,
125130
// Setting engineIds to undefined to confirm that it is
@@ -375,6 +380,7 @@ add_task(
375380
];
376381

377382
const mockListFilesResult = {
383+
files: [{ path: "model.onnx" }],
378384
metadata: {
379385
totalSize: 2048,
380386
lastUsed: new Date("2023-10-01T12:00:00Z"),
@@ -396,6 +402,7 @@ add_task(
396402
.resolves(mockListFilesResult)
397403
.onSecondCall()
398404
.resolves({
405+
files: mockListFilesResult.files,
399406
metadata: {
400407
...mockListFilesResult.metadata,
401408
// Setting engineIds to undefined to confirm that it is
@@ -432,3 +439,55 @@ add_task(
432439
sandbox.restore();
433440
}
434441
);
442+
443+
add_task(
444+
{
445+
pref_set: [[LOCAL_MODEL_MANAGEMENT_ENABLED_PREF, true]],
446+
},
447+
async function test_modelhub_provider_file_display_info() {
448+
let sandbox = sinon.createSandbox();
449+
ModelHubProvider.clearAddonCache();
450+
451+
// Speech recognition registers display info for its model files, keyed by
452+
// file name. Drive the lookup off the registry itself rather than repeating
453+
// its contents here.
454+
const { default: displayInfoByFile } = ChromeUtils.importESModule(
455+
"resource://gre/modules/SpeechRecognitionModelDisplayInfo.sys.mjs"
456+
);
457+
const [fileName, displayInfo] = Object.entries(displayInfoByFile)[0];
458+
459+
const mockModel = {
460+
name: "model-hub.mozilla.org/mudler/parakeet-cpp-gguf",
461+
revision: "mockRevision",
462+
engineIds: [],
463+
};
464+
465+
sandbox.stub(ModelHubProvider.modelHub, "listModels").resolves([mockModel]);
466+
sandbox.stub(ModelHubProvider.modelHub, "listFiles").resolves({
467+
files: [{ path: fileName }],
468+
metadata: {
469+
totalSize: 2048,
470+
lastUsed: new Date("2023-10-01T12:00:00Z"),
471+
updateDate: 0,
472+
engineIds: [FEATURES["speech-recognition"].engineId],
473+
},
474+
});
475+
sandbox.stub(ModelHubProvider.modelHub, "getOwnerIcon").resolves(null);
476+
477+
const [modelWrapper] = await AddonManager.getAddonsByTypes(["mlmodel"]);
478+
Assert.ok(modelWrapper, "Got a model AddonWrapper instance");
479+
480+
Assert.equal(
481+
modelWrapper.name,
482+
displayInfo.name,
483+
"Expect the name to come from the registered display info"
484+
);
485+
Assert.equal(
486+
modelWrapper.modelHomepageURL,
487+
displayInfo.hfUrl,
488+
"Expect the homepage URL to come from the registered display info"
489+
);
490+
491+
sandbox.restore();
492+
}
493+
);

0 commit comments

Comments
 (0)