Skip to content

Commit e8c7a7b

Browse files
committed
Install Brave's own model for on-device speech recognition
Availability and install stay with upstream's `OnDeviceSpeechRecognitionImpl`, so the pieces it consults are replaced instead. Availability answers terminally because Brave ships no SODA and no optimization guide model, and the requested quality is normalised so both entry points take the SODA branch. The global `SodaInstaller` is replaced by a subclass installing Brave's component. It derives from `SodaInstaller`, not the desktop implementation whose delivery acts on SODA's own ids, directories and preferences, so a new pure virtual becomes a compile error rather than inherited behaviour. `install()` parks its reply so every path reports one. The component updater is watched too, because startup registration asks for the same download, drops its callback, and its failure is reported nowhere else. Only `kUpdateError` is taken: success arrives via `ComponentReady`, and intermediate states stay unreported so `IsSodaLanguageDownloading` stays false.
1 parent c799ee5 commit e8c7a7b

17 files changed

Lines changed: 1169 additions & 1 deletion

browser/sources.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ if (enable_local_ai) {
410410
brave_chrome_browser_deps += [
411411
"//brave/browser/history_embeddings",
412412
"//brave/browser/speech",
413+
"//brave/browser/speech:chromium_impl",
413414
"//brave/browser/ui/webui/local_ai",
414415
"//brave/components/local_ai/core",
415416
"//brave/components/local_ai/core:features",

browser/speech/BUILD.gn

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,33 @@ import("//brave/components/local_ai/buildflags/buildflags.gni")
77

88
assert(enable_local_ai)
99

10+
# Implements the hooks that the chromium_src overrides of
11+
# chrome/browser/speech/on_device_speech_recognition_util.cc and
12+
# chrome/browser/browser_process_impl.cc forward declare.
13+
source_set("chromium_impl") {
14+
sources = [
15+
"brave_soda_installer_factory.cc",
16+
"on_device_speech_recognition_util.cc",
17+
]
18+
19+
deps = [
20+
":speech",
21+
"//base",
22+
"//brave/components/local_ai/core",
23+
"//brave/components/local_ai/core:features",
24+
"//brave/components/local_ai/core:utils",
25+
"//chrome/browser:browser_process",
26+
"//components/prefs",
27+
"//components/soda",
28+
"//media/mojo/mojom:web_speech_recognition",
29+
"//ui/base",
30+
]
31+
}
32+
1033
static_library("speech") {
1134
sources = [
35+
"brave_soda_installer.cc",
36+
"brave_soda_installer.h",
1237
"on_device_speech_recognition_controller.cc",
1338
"on_device_speech_recognition_controller.h",
1439
]
@@ -23,6 +48,12 @@ static_library("speech") {
2348
"//chrome/browser:browser_process",
2449
"//chrome/browser/profiles:profile",
2550
"//chrome/browser/profiles:profile_manager",
51+
"//components/component_updater",
52+
"//components/prefs",
53+
"//components/soda",
54+
"//components/soda:constants",
55+
"//components/update_client",
56+
"//content/public/browser",
2657
"//mojo/public/cpp/base",
2758
"//mojo/public/cpp/bindings",
2859
"//services/network/public/mojom",
@@ -34,19 +65,36 @@ static_library("speech") {
3465
source_set("unit_tests") {
3566
testonly = true
3667

37-
sources = [ "on_device_speech_recognition_controller_unittest.cc" ]
68+
sources = [
69+
"brave_soda_installer_unittest.cc",
70+
"on_device_speech_recognition_controller_unittest.cc",
71+
"on_device_speech_recognition_util_unittest.cc",
72+
]
3873

3974
deps = [
75+
# The availability policy has no header, so its test forward-declares it
76+
# and relies on this target for the definition.
77+
":chromium_impl",
4078
":speech",
4179
"//base",
4280
"//base/test:test_support",
81+
"//brave/components/brave_component_updater/browser:test_support",
4382
"//brave/components/local_ai/core",
83+
"//brave/components/local_ai/core:features",
4484
"//brave/components/local_ai/core:speech_recognition_mojom",
4585
"//chrome/browser/profiles:profile",
4686
"//chrome/test:test_support",
87+
"//components/component_updater",
88+
"//components/component_updater:test_support",
89+
"//components/prefs",
90+
"//components/soda",
91+
"//components/soda:constants",
92+
"//components/update_client",
4793
"//content/test:test_support",
94+
"//media/mojo/mojom:web_speech_recognition",
4895
"//mojo/public/cpp/bindings",
4996
"//services/on_device_model/public/mojom",
97+
"//testing/gmock",
5098
"//testing/gtest",
5199
"//url",
52100
]

browser/speech/DEPS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
specific_include_rules = {
2+
".*_unittest\.cc": [
3+
"+brave/components/brave_component_updater/browser/mock_on_demand_updater.h",
4+
],
5+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at https://mozilla.org/MPL/2.0/. */
5+
6+
#include "brave/browser/speech/brave_soda_installer.h"
7+
8+
#include <string>
9+
#include <string_view>
10+
#include <vector>
11+
12+
#include "base/files/file_path.h"
13+
#include "base/functional/bind.h"
14+
#include "brave/components/local_ai/core/on_device_speech_models_component_installer.h"
15+
#include "chrome/browser/browser_process.h"
16+
#include "components/component_updater/component_updater_service.h"
17+
#include "components/soda/constants.h"
18+
#include "components/update_client/crx_update_item.h"
19+
#include "components/update_client/update_client_errors.h"
20+
21+
namespace speech {
22+
23+
BraveSodaInstaller::BraveSodaInstaller() {
24+
local_ai::OnDeviceSpeechModelsState::GetInstance()->AddObserver(this);
25+
}
26+
27+
BraveSodaInstaller::~BraveSodaInstaller() {
28+
local_ai::OnDeviceSpeechModelsState::GetInstance()->RemoveObserver(this);
29+
}
30+
31+
void BraveSodaInstaller::Init(PrefService* profile_prefs,
32+
PrefService* global_prefs) {}
33+
34+
void BraveSodaInstaller::InstallSoda(PrefService* global_prefs) {}
35+
36+
void BraveSodaInstaller::UninstallSoda(PrefService* global_prefs) {}
37+
38+
void BraveSodaInstaller::UninstallLanguage(std::string_view language,
39+
PrefService* global_prefs) {}
40+
41+
void BraveSodaInstaller::RegisterLanguage(std::string_view language,
42+
PrefService* global_prefs) {}
43+
44+
void BraveSodaInstaller::UnregisterLanguage(std::string_view language,
45+
PrefService* global_prefs) {}
46+
47+
void BraveSodaInstaller::InstallLanguage(std::string_view language,
48+
PrefService* global_prefs) {
49+
auto* state = local_ai::OnDeviceSpeechModelsState::GetInstance();
50+
if (state->IsModelInstalled()) {
51+
return;
52+
}
53+
54+
auto* component_updater = g_browser_process->component_updater();
55+
if (component_updater &&
56+
!component_updater_observation_.IsObservingSource(component_updater)) {
57+
component_updater_observation_.Observe(component_updater);
58+
}
59+
60+
local_ai::MaybeRegisterOnDeviceSpeechModelsComponent(
61+
base::BindOnce(&BraveSodaInstaller::OnComponentUpdaterResult,
62+
weak_factory_.GetWeakPtr()));
63+
}
64+
65+
void BraveSodaInstaller::OnEvent(const update_client::CrxUpdateItem& item) {
66+
if (item.id != local_ai::kOnDeviceSpeechModelsComponentId) {
67+
return;
68+
}
69+
70+
// Only handle failure case here, success case is handled via
71+
// `ComponentReady`. In-progress events are ignored as we don't report
72+
// downloading status.
73+
if (item.state != update_client::ComponentState::kUpdateError) {
74+
return;
75+
}
76+
77+
NotifyOnSodaInstallError(LanguageCode::kEnUs, ErrorCode::kUnspecifiedError);
78+
}
79+
80+
void BraveSodaInstaller::OnComponentUpdaterResult(update_client::Error error) {
81+
// Success needs no action, because `ComponentReady` publishes the install dir
82+
// before this runs and that is what reports the model as installed.
83+
auto* state = local_ai::OnDeviceSpeechModelsState::GetInstance();
84+
if (error == update_client::Error::NONE && state->IsModelInstalled()) {
85+
return;
86+
}
87+
88+
// Not a failure. A download for the component is already running, so this
89+
// request was refused as a duplicate. That download reports its own outcome,
90+
// success through `ComponentReady` and failure through `OnEvent`.
91+
if (error == update_client::Error::UPDATE_IN_PROGRESS) {
92+
return;
93+
}
94+
95+
NotifyOnSodaInstallError(LanguageCode::kEnUs, ErrorCode::kUnspecifiedError);
96+
}
97+
98+
std::vector<std::string> BraveSodaInstaller::GetLiveCaptionEnabledLanguages()
99+
const {
100+
// Brave's model serves English only. `install()` rejects anything outside
101+
// this list before it reaches `InstallLanguage`, which is what keeps a
102+
// request for another language from installing an English model.
103+
return {GetLanguageName(LanguageCode::kEnUs)};
104+
}
105+
106+
std::vector<std::string> BraveSodaInstaller::GetAvailableLanguages() const {
107+
return GetLiveCaptionEnabledLanguages();
108+
}
109+
110+
base::FilePath BraveSodaInstaller::GetSodaBinaryPath() const {
111+
return base::FilePath();
112+
}
113+
114+
base::FilePath BraveSodaInstaller::GetLanguagePath(
115+
std::string_view language) const {
116+
return base::FilePath();
117+
}
118+
119+
void BraveSodaInstaller::OnSpeechModelDirChanged(
120+
const base::FilePath& model_dir) {
121+
if (model_dir.empty()) {
122+
soda_binary_installed_ = false;
123+
installed_languages_.erase(LanguageCode::kEnUs);
124+
return;
125+
}
126+
127+
// Brave's model covers what upstream splits into a binary and a language
128+
// pack, so both flip together.
129+
soda_binary_installed_ = true;
130+
installed_languages_.insert(LanguageCode::kEnUs);
131+
NotifyOnSodaInstalled(LanguageCode::kEnUs);
132+
}
133+
134+
} // namespace speech
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at https://mozilla.org/MPL/2.0/. */
5+
6+
#ifndef BRAVE_BROWSER_SPEECH_BRAVE_SODA_INSTALLER_H_
7+
#define BRAVE_BROWSER_SPEECH_BRAVE_SODA_INSTALLER_H_
8+
9+
#include <string>
10+
#include <string_view>
11+
#include <vector>
12+
13+
#include "base/files/file_path.h"
14+
#include "base/memory/weak_ptr.h"
15+
#include "base/scoped_observation.h"
16+
#include "brave/components/local_ai/core/on_device_speech_models_state.h"
17+
#include "components/component_updater/component_updater_service.h"
18+
#include "components/soda/soda_installer.h"
19+
#include "components/update_client/update_client_errors.h"
20+
21+
class PrefService;
22+
23+
namespace update_client {
24+
struct CrxUpdateItem;
25+
} // namespace update_client
26+
27+
namespace speech {
28+
29+
// The global `SodaInstaller`, replaced so that installing on-device speech
30+
// recognition installs Brave's own model rather than SODA.
31+
//
32+
// `OnDeviceSpeechRecognitionImpl::Install()` parks its reply, calls
33+
// `InstallSoda` then `InstallLanguage`, and settles the reply from
34+
// `OnSodaInstalled` / `OnSodaInstallError`. Availability is answered elsewhere,
35+
// by `GetBraveOnDeviceSpeechAvailability`.
36+
//
37+
// The base class drives SODA's own install and uninstall lifecycle on component
38+
// ids, directories and language pack prefs that Brave does not have, so those
39+
// overrides no-op and the whole install runs from `InstallLanguage`.
40+
class BraveSodaInstaller
41+
: public SodaInstaller,
42+
public component_updater::ServiceObserver,
43+
public local_ai::OnDeviceSpeechModelsState::Observer {
44+
public:
45+
BraveSodaInstaller();
46+
~BraveSodaInstaller() override;
47+
BraveSodaInstaller(const BraveSodaInstaller&) = delete;
48+
BraveSodaInstaller& operator=(const BraveSodaInstaller&) = delete;
49+
50+
// SodaInstaller:
51+
void InstallLanguage(std::string_view language,
52+
PrefService* global_prefs) override;
53+
std::vector<std::string> GetLiveCaptionEnabledLanguages() const override;
54+
std::vector<std::string> GetAvailableLanguages() const override;
55+
base::FilePath GetSodaBinaryPath() const override;
56+
base::FilePath GetLanguagePath(std::string_view language) const override;
57+
void Init(PrefService* profile_prefs, PrefService* global_prefs) override;
58+
void UninstallLanguage(std::string_view language,
59+
PrefService* global_prefs) override;
60+
void RegisterLanguage(std::string_view language,
61+
PrefService* global_prefs) override;
62+
void UnregisterLanguage(std::string_view language,
63+
PrefService* global_prefs) override;
64+
65+
protected:
66+
// SodaInstaller:
67+
void InstallSoda(PrefService* global_prefs) override;
68+
void UninstallSoda(PrefService* global_prefs) override;
69+
70+
private:
71+
// local_ai::OnDeviceSpeechModelsState::Observer:
72+
void OnSpeechModelDirChanged(const base::FilePath& model_dir) override;
73+
74+
// component_updater::ServiceObserver:
75+
// Reports a failed download, including one this installer did not start.
76+
// `OnComponentUpdaterResult` answers only for its own request, so a download
77+
// already in flight is heard about here alone.
78+
void OnEvent(const update_client::CrxUpdateItem& item) override;
79+
80+
// Outcome of the request `InstallLanguage` made. Only a failure is reported,
81+
// because a success arrives as the install dir being published.
82+
void OnComponentUpdaterResult(update_client::Error error);
83+
84+
base::ScopedObservation<component_updater::ComponentUpdateService,
85+
component_updater::ComponentUpdateService::Observer>
86+
component_updater_observation_{this};
87+
88+
base::WeakPtrFactory<BraveSodaInstaller> weak_factory_{this};
89+
};
90+
91+
} // namespace speech
92+
93+
#endif // BRAVE_BROWSER_SPEECH_BRAVE_SODA_INSTALLER_H_
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
* You can obtain one at https://mozilla.org/MPL/2.0/. */
5+
6+
// Implements the hook that the chromium_src override of
7+
// `chrome/browser/browser_process_impl.cc` forward declares.
8+
9+
#include <memory>
10+
11+
#include "brave/browser/speech/brave_soda_installer.h"
12+
#include "components/soda/soda_installer.h"
13+
14+
namespace speech {
15+
16+
std::unique_ptr<SodaInstaller> CreateBraveSodaInstaller() {
17+
return std::make_unique<BraveSodaInstaller>();
18+
}
19+
20+
} // namespace speech

0 commit comments

Comments
 (0)