Skip to content

Commit 255db86

Browse files
committed
Bug 1940906 - Select the speech recognition model per locale. r=media-playback-reviewers,ai-platform-reviewers,alwu,valentinp
Add a models.yaml manifest (gen_speech_models.py) so LanguagesToSpeechModel (SpeechRecognitionModelMapping.{h,cpp}) picks a model per locale prefix, overridable via media.webspeech.recognition.model.<prefix> prefs, instead of always using a single hardcoded parakeet model. Model size will be used in later patches. Differential Revision: https://phabricator.services.mozilla.com/D311112
1 parent f1fee4e commit 255db86

7 files changed

Lines changed: 409 additions & 39 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2+
/* vim: set ts=8 et sw=2 tw=80: */
3+
/* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
#include "SpeechRecognitionModelMapping.h"
8+
9+
#include "SpeechRecognitionModels.h"
10+
#include "mozilla/Assertions.h"
11+
#include "mozilla/Preferences.h"
12+
#include "nsFmtString.h"
13+
#include "nsReadableUtils.h"
14+
15+
namespace mozilla::dom {
16+
17+
nsCString SpeechModelIdentifier::ToString() const {
18+
return nsFmtCString("{}/{}/{}", mModelName.get(), mFileName.get(),
19+
mRevision.get());
20+
}
21+
22+
nsCString LanguagesToSpeechModelId(const nsTArray<nsCString>& aLanguages) {
23+
// Determine the primary-subtag locale prefix (e.g. "en" from "en-US"). This
24+
// discards script/region subtags, so e.g. zh-Hans and zh-Hant both collapse
25+
// to zh; proper BCP-47 canonicalization is tracked in bug 2060247.
26+
nsCString prefix;
27+
if (!aLanguages.IsEmpty()) {
28+
prefix = aLanguages[0];
29+
int32_t dash = prefix.FindChar('-');
30+
if (dash != kNotFound) {
31+
prefix.Truncate(dash);
32+
}
33+
}
34+
35+
// A pref may override the default model for a locale prefix:
36+
// media.webspeech.recognition.model.<prefix> (or .multilingual for the
37+
// fallback). Empty prefix uses the multilingual fallback.
38+
nsAutoCString prefKey("media.webspeech.recognition.model.");
39+
prefKey.Append(prefix.IsEmpty() ? "multilingual"_ns : prefix);
40+
nsAutoCString prefModelId;
41+
Preferences::GetCString(prefKey.get(), prefModelId);
42+
43+
if (!prefModelId.IsEmpty()) {
44+
for (const auto& m : kSpeechRecognitionModels) {
45+
if (m.id && prefModelId.Equals(m.id)) {
46+
return nsCString(m.id);
47+
}
48+
}
49+
}
50+
51+
// No usable pref: pick the default model whose locale list matches the
52+
// prefix, falling back to the default fallback model (empty locale list).
53+
const SpeechRecognitionModelInfo* fallback = nullptr;
54+
for (const auto& m : kSpeechRecognitionModels) {
55+
if (!m.id) {
56+
break;
57+
}
58+
if (!m.locales[0]) {
59+
if (m.is_default && !fallback) {
60+
fallback = &m;
61+
}
62+
continue;
63+
}
64+
for (const char* const* l = m.locales; *l; ++l) {
65+
if (!prefix.IsEmpty() &&
66+
StringBeginsWith(prefix, nsDependentCString(*l))) {
67+
if (m.is_default) {
68+
return nsCString(m.id);
69+
}
70+
}
71+
}
72+
}
73+
74+
if (fallback) {
75+
return nsCString(fallback->id);
76+
}
77+
78+
MOZ_ASSERT_UNREACHABLE("No default model found in kSpeechRecognitionModels");
79+
return {};
80+
}
81+
82+
bool ResolveSpeechModelId(const nsACString& aId, SpeechModelIdentifier& aOut) {
83+
for (const auto& m : kSpeechRecognitionModels) {
84+
if (!m.id) {
85+
break;
86+
}
87+
if (aId.Equals(m.id)) {
88+
aOut = {nsCString(m.repo), nsCString(m.filename), nsCString(m.revision),
89+
m.size_mb};
90+
return true;
91+
}
92+
}
93+
return false;
94+
}
95+
96+
uint32_t SpeechModelSizeMB(const nsACString& aModel,
97+
const nsACString& aRevision,
98+
const nsACString& aFilename) {
99+
for (const auto& m : kSpeechRecognitionModels) {
100+
if (!m.id) {
101+
break;
102+
}
103+
if (aModel.Equals(m.repo) && aRevision.Equals(m.revision) &&
104+
aFilename.Equals(m.filename)) {
105+
return m.size_mb;
106+
}
107+
}
108+
return 0;
109+
}
110+
111+
} // namespace mozilla::dom
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2+
/* vim: set ts=8 et sw=2 tw=80: */
3+
/* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
#ifndef DOM_MEDIA_WEBSPEECH_RECOGNITION_SPEECHRECOGNITIONMODELMAPPING_H_
8+
#define DOM_MEDIA_WEBSPEECH_RECOGNITION_SPEECHRECOGNITIONMODELMAPPING_H_
9+
10+
#include "nsString.h"
11+
#include "nsTArray.h"
12+
13+
namespace mozilla::dom {
14+
15+
// ModelHub engine id and task used for every speech recognition model. Shared
16+
// so the download path in the parent process and the read-only availability
17+
// queries in the utility process agree on them.
18+
inline constexpr auto kSpeechRecognitionEngineId = "parakeet-gguf"_ns;
19+
inline constexpr auto kSpeechRecognitionTask = "speech-recognition"_ns;
20+
21+
// The concrete ModelHub artifact a set of requested languages maps to.
22+
struct SpeechModelIdentifier {
23+
nsCString mModelName;
24+
nsCString mFileName;
25+
nsCString mRevision = "main"_ns;
26+
uint32_t mSizeMB = 0;
27+
nsCString ToString() const;
28+
};
29+
30+
// Maps a set of requested BCP-47 languages to a model id (the model table's
31+
// own "id" field, e.g. "english_tdt_ctc_q6_k"), honoring the
32+
// media.webspeech.recognition.model.<prefix> pref override. The id, not the
33+
// artifact itself, is what the utility process sends onward: it can only ever
34+
// select among the ids this function can return, never name a
35+
// model/revision/filename directly. The model table (generated from
36+
// models.yaml) lives only in this translation unit's .cpp.
37+
nsCString LanguagesToSpeechModelId(const nsTArray<nsCString>& aLanguages);
38+
39+
// Expands aId (as returned by LanguagesToSpeechModelId) to the concrete
40+
// ModelHub artifact it names. Returns false if aId is unknown. Used trusted-
41+
// side (see SpeechModelResolver) to validate an id supplied by the utility
42+
// process before it is passed to ModelHub.
43+
bool ResolveSpeechModelId(const nsACString& aId, SpeechModelIdentifier& aOut);
44+
45+
// Approximate download size in MB of the model artifact identified by
46+
// aModel/aRevision/aFilename, for display in the download permission prompt.
47+
// Returns 0 for an unknown artifact. Keyed by the artifact (not languages) so
48+
// the download gate, which only sees the generic model coordinates, can look
49+
// it up. The model table lives only in this translation unit's .cpp.
50+
uint32_t SpeechModelSizeMB(const nsACString& aModel,
51+
const nsACString& aRevision,
52+
const nsACString& aFilename);
53+
54+
} // namespace mozilla::dom
55+
56+
#endif // DOM_MEDIA_WEBSPEECH_RECOGNITION_SPEECHRECOGNITIONMODELMAPPING_H_

‎dom/media/webspeech/recognition/SpeechRecognitionParent.cpp‎

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <chrono>
1111
#include <thread>
1212

13+
#include "SpeechRecognitionModelMapping.h"
1314
#include "mozilla/Logging.h"
1415
#include "mozilla/Mutex.h"
1516
#include "mozilla/Preferences.h"
@@ -44,29 +45,9 @@ static LazyLogModule gSpeechRecognitionParentLog("SpeechRecognitionParent");
4445
#define LOGE(fmt, ...) \
4546
MOZ_LOG_FMT(gSpeechRecognitionParentLog, LogLevel::Error, fmt, ##__VA_ARGS__)
4647

47-
// Task name the model hub files speech recognition models under.
48-
static constexpr auto kSpeechRecognitionTask = "speech-recognition"_ns;
49-
5048
// Sample rate the Parakeet models operate at.
5149
static constexpr int32_t PARAKEET_SAMPLE_RATE = 16000;
5250

53-
SpeechRecognitionParent::ModelIdentifier
54-
SpeechRecognitionParent::LanguagesToModelIdentifier(
55-
const nsTArray<nsCString>&) {
56-
// For now this ignores the requested languages and always returns a single
57-
// hardcoded model. Per-language selection and generalization to more models
58-
// land in a later patch in this stack.
59-
// mudler/parakeet.cpp cache-aware streaming GGUF, hosted on the Mozilla
60-
// model hub under asr-test/parakeet.
61-
return {"asr-test/parakeet"_ns, "realtime_eou_120m-v1-q5_k.gguf"_ns,
62-
"main"_ns};
63-
}
64-
65-
nsCString SpeechRecognitionParent::ModelIdentifier::ToString() const {
66-
return nsFmtCString("{}/{}/{}", mModelName.get(), mFileName.get(),
67-
mRevision.get());
68-
}
69-
7051
void SpeechRecognitionParent::ResolveOrRejectInitOnIPCThread(
7152
InitResolver&& aResolver, bool aSuccess) {
7253
if (GetActorEventTarget()->IsOnCurrentThread()) {
@@ -133,14 +114,14 @@ mozilla::ipc::IPCResult SpeechRecognitionParent::RecvIsModelAvailable(
133114
"RecvIsModelAvailable requires at least one language");
134115
}
135116

136-
nsCString modelId = LanguagesToModelIdentifier(aLanguages).ToString();
117+
nsCString modelId = dom::LanguagesToSpeechModelId(aLanguages);
137118
LOGD("{} languages: {} mapped to id={}", __func__,
138119
fmt::join(aLanguages, ", "), modelId.get());
139120

140121
return RunHWInferenceBoolQuery(
141122
__func__,
142123
[modelId](hwinference::HWInferenceChild* aChild) {
143-
return aChild->SendIsModelAvailable(nsCString(kSpeechRecognitionTask),
124+
return aChild->SendIsModelAvailable(dom::kSpeechRecognitionTask,
144125
modelId);
145126
},
146127
std::move(aResolver), mIsModelAvailableRequest);
@@ -152,7 +133,7 @@ mozilla::ipc::IPCResult SpeechRecognitionParent::RecvInstallModels(
152133
return IPC_FAIL(this, "RecvInstallModels requires at least one language");
153134
}
154135

155-
nsCString modelId = LanguagesToModelIdentifier(aLanguages).ToString();
136+
nsCString modelId = dom::LanguagesToSpeechModelId(aLanguages);
156137
LOGD("{} languages: {} mapped to id={}", __func__,
157138
fmt::join(aLanguages, ", "), modelId.get());
158139

@@ -162,7 +143,7 @@ mozilla::ipc::IPCResult SpeechRecognitionParent::RecvInstallModels(
162143
return RunHWInferenceBoolQuery(
163144
__func__,
164145
[modelId, contentId](hwinference::HWInferenceChild* aChild) {
165-
return aChild->SendInstallModel(kSpeechRecognitionTask, modelId, 0,
146+
return aChild->SendInstallModel(dom::kSpeechRecognitionTask, modelId, 0,
166147
contentId);
167148
},
168149
std::move(aResolver), mInstallModelRequest);
@@ -211,7 +192,7 @@ void SpeechRecognitionParent::RetrieveModel(InitResolver&& aResolver) {
211192
nsCString modelId;
212193
{
213194
MutexAutoLock lock(mLock);
214-
modelId = LanguagesToModelIdentifier(nsTArray{mLanguage}).ToString();
195+
modelId = dom::LanguagesToSpeechModelId(nsTArray{mLanguage});
215196
}
216197

217198
LOGD("{} Checking model is installed: id={}", __func__, modelId.get());
@@ -220,8 +201,7 @@ void SpeechRecognitionParent::RetrieveModel(InitResolver&& aResolver) {
220201
// permission doorhanger. start() requires the model to already be
221202
// installed, so check that before FetchModelFile() rather than letting
222203
// GetModelFile download it on demand.
223-
hwInferenceChild
224-
->SendIsModelInstalled(nsCString(kSpeechRecognitionTask), modelId)
204+
hwInferenceChild->SendIsModelInstalled(dom::kSpeechRecognitionTask, modelId)
225205
->Then(GetCurrentSerialEventTarget(), __func__,
226206
[self = RefPtr{this}, aResolver = std::move(aResolver),
227207
modelId](hwinference::PHWInferenceChild::IsModelInstalledPromise::
@@ -256,8 +236,7 @@ void SpeechRecognitionParent::FetchModelFile(const nsCString& aModelId,
256236

257237
LOGD("{} Requesting model: id={}", __func__, aModelId.get());
258238

259-
hwInferenceChild
260-
->SendGetModelFile(nsCString(kSpeechRecognitionTask), aModelId)
239+
hwInferenceChild->SendGetModelFile(dom::kSpeechRecognitionTask, aModelId)
261240
->Then(
262241
GetCurrentSerialEventTarget(), __func__,
263242
[self = RefPtr{this}, aResolver = std::move(aResolver)](

‎dom/media/webspeech/recognition/SpeechRecognitionParent.h‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,6 @@ class SpeechRecognitionParent final : public PSpeechRecognitionParent {
6060

6161
void ActorDestroy(ActorDestroyReason aReason) override;
6262

63-
struct ModelIdentifier {
64-
nsCString mModelName;
65-
nsCString mFileName;
66-
nsCString mRevision = "main"_ns;
67-
nsCString ToString() const;
68-
};
69-
70-
ModelIdentifier LanguagesToModelIdentifier(
71-
const nsTArray<nsCString>& aLanguages);
72-
7363
void ResolveOrRejectInitOnIPCThread(InitResolver&& aResolver, bool aSuccess)
7464
MOZ_EXCLUDES(mLock);
7565

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
"""Generate SpeechRecognitionModels.h and SpeechRecognitionModelDisplayInfo.sys.mjs
6+
from models.yaml."""
7+
8+
import json
9+
10+
import yaml
11+
12+
13+
def gen_models_header(output, input):
14+
with open(input) as f:
15+
data = yaml.safe_load(f)
16+
17+
models = data["models"]
18+
19+
output.write(
20+
"/* This file is generated by gen_speech_models.py. Do not edit. */\n"
21+
"#pragma once\n\n"
22+
"#include <stdint.h>\n\n"
23+
"namespace mozilla::dom {\n\n"
24+
"struct SpeechRecognitionModelInfo {\n"
25+
" const char* id;\n"
26+
" const char* const* locales; // null-terminated; empty (first==nullptr) = fallback\n"
27+
" const char* repo;\n"
28+
" const char* filename;\n"
29+
" const char* revision;\n"
30+
" uint32_t size_mb;\n"
31+
" const char* quant;\n"
32+
" uint32_t latency_ms;\n"
33+
" bool is_default;\n"
34+
" bool is_streaming; // true = streaming RNN-T, false = offline CTC batch\n"
35+
"};\n\n"
36+
)
37+
38+
# Emit per-model locale arrays.
39+
for m in models:
40+
locales = m["locales"]
41+
entries = (
42+
", ".join(f'"{l}"' for l in locales) + (", " if locales else "") + "nullptr"
43+
)
44+
output.write(
45+
f"static const char* const kSpeechModelLocales_{m['id']}[] = {{{entries}}};\n"
46+
)
47+
48+
output.write("\n")
49+
50+
# Emit the table, sentinel-terminated.
51+
output.write(
52+
"static const SpeechRecognitionModelInfo kSpeechRecognitionModels[] = {\n"
53+
)
54+
for m in models:
55+
is_default = "true" if m.get("default", False) else "false"
56+
is_streaming = "true" if m.get("streaming", True) else "false"
57+
output.write(
58+
f' {{"{m["id"]}", kSpeechModelLocales_{m["id"]},\n'
59+
f' "{m["repo"]}", "{m["filename"]}", "{m["revision"]}",\n'
60+
f' {m["size_mb"]}, "{m["quant"]}", {m["latency_ms"]}, {is_default}, {is_streaming}}},\n'
61+
)
62+
output.write(
63+
" {nullptr, nullptr, nullptr, nullptr, nullptr, 0, nullptr, 0, false}\n"
64+
)
65+
output.write("};\n\n")
66+
67+
output.write("} // namespace mozilla::dom\n")
68+
69+
70+
def gen_models_display_info(output, input):
71+
"""Generate a JS module mapping each model's downloaded file name to the
72+
human-readable name and upstream Hugging Face model card shown in
73+
about:addons' "Manage On-Device AI Models" panel."""
74+
with open(input) as f:
75+
data = yaml.safe_load(f)
76+
77+
info = {
78+
m["filename"]: {"name": m["display_name"], "hfUrl": m["hf_url"]}
79+
for m in data["models"]
80+
}
81+
82+
output.write(
83+
"/* This file is generated by gen_speech_models.py. Do not edit. */\n\n"
84+
"export default " + json.dumps(info, indent=2) + ";\n"
85+
)

0 commit comments

Comments
 (0)