Skip to content

Commit c924fce

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 9cb3cb4 commit c924fce

17 files changed

Lines changed: 1183 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+
component_updater, global_prefs,
62+
base::BindOnce(&BraveSodaInstaller::OnComponentUpdaterResult,
63+
weak_factory_.GetWeakPtr()));
64+
}
65+
66+
void BraveSodaInstaller::OnEvent(const update_client::CrxUpdateItem& item) {
67+
if (item.id != local_ai::kOnDeviceSpeechModelsComponentId) {
68+
return;
69+
}
70+
71+
// Only handle failure case here, success case is handled via
72+
// `ComponentReady`. In-progress events are ignored as we don't report
73+
// downloading status.
74+
if (item.state != update_client::ComponentState::kUpdateError) {
75+
return;
76+
}
77+
78+
NotifyOnSodaInstallError(LanguageCode::kEnUs, ErrorCode::kUnspecifiedError);
79+
}
80+
81+
void BraveSodaInstaller::OnComponentUpdaterResult(update_client::Error error) {
82+
// Success needs no action, because `ComponentReady` publishes the install dir
83+
// before this runs and that is what reports the model as installed.
84+
auto* state = local_ai::OnDeviceSpeechModelsState::GetInstance();
85+
if (error == update_client::Error::NONE && state->IsModelInstalled()) {
86+
return;
87+
}
88+
89+
// Not a failure. A download for the component is already running, so this
90+
// request was refused as a duplicate. That download reports its own outcome,
91+
// success through `ComponentReady` and failure through `OnEvent`.
92+
if (error == update_client::Error::UPDATE_IN_PROGRESS) {
93+
return;
94+
}
95+
96+
NotifyOnSodaInstallError(LanguageCode::kEnUs, ErrorCode::kUnspecifiedError);
97+
}
98+
99+
std::vector<std::string> BraveSodaInstaller::GetLiveCaptionEnabledLanguages()
100+
const {
101+
// Brave's model serves English only. `install()` rejects anything outside
102+
// this list before it reaches `InstallLanguage`, which is what keeps a
103+
// request for another language from installing an English model.
104+
return {GetLanguageName(LanguageCode::kEnUs)};
105+
}
106+
107+
std::vector<std::string> BraveSodaInstaller::GetAvailableLanguages() const {
108+
return GetLiveCaptionEnabledLanguages();
109+
}
110+
111+
base::FilePath BraveSodaInstaller::GetSodaBinaryPath() const {
112+
return base::FilePath();
113+
}
114+
115+
base::FilePath BraveSodaInstaller::GetLanguagePath(
116+
std::string_view language) const {
117+
return base::FilePath();
118+
}
119+
120+
void BraveSodaInstaller::OnSpeechModelInstalledChanged(bool installed) {
121+
if (!installed) {
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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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()` is left alone: it parks the reply,
33+
// calls `InstallSoda` then `InstallLanguage`, and settles it from this class's
34+
// `OnSodaInstalled` / `OnSodaInstallError`, so only where the bytes come from
35+
// changes.
36+
//
37+
// Availability is not answered here. It stays with
38+
// `GetBraveOnDeviceSpeechAvailability`.
39+
//
40+
// The base class drives SODA's own install and uninstall lifecycle on SODA's
41+
// component ids, directories and language pack prefs, none of which Brave has,
42+
// so those overrides no-op and the whole install runs from `InstallLanguage`.
43+
class BraveSodaInstaller
44+
: public SodaInstaller,
45+
public component_updater::ServiceObserver,
46+
public local_ai::OnDeviceSpeechModelsState::Observer {
47+
public:
48+
BraveSodaInstaller();
49+
~BraveSodaInstaller() override;
50+
BraveSodaInstaller(const BraveSodaInstaller&) = delete;
51+
BraveSodaInstaller& operator=(const BraveSodaInstaller&) = delete;
52+
53+
// SodaInstaller:
54+
void InstallLanguage(std::string_view language,
55+
PrefService* global_prefs) override;
56+
std::vector<std::string> GetLiveCaptionEnabledLanguages() const override;
57+
std::vector<std::string> GetAvailableLanguages() const override;
58+
base::FilePath GetSodaBinaryPath() const override;
59+
base::FilePath GetLanguagePath(std::string_view language) const override;
60+
void Init(PrefService* profile_prefs, PrefService* global_prefs) override;
61+
void UninstallLanguage(std::string_view language,
62+
PrefService* global_prefs) override;
63+
void RegisterLanguage(std::string_view language,
64+
PrefService* global_prefs) override;
65+
void UnregisterLanguage(std::string_view language,
66+
PrefService* global_prefs) override;
67+
68+
protected:
69+
// SodaInstaller:
70+
void InstallSoda(PrefService* global_prefs) override;
71+
void UninstallSoda(PrefService* global_prefs) override;
72+
73+
private:
74+
// local_ai::OnDeviceSpeechModelsState::Observer:
75+
void OnSpeechModelInstalledChanged(bool installed) override;
76+
77+
// component_updater::ServiceObserver:
78+
// Reports a failed download, including one this installer did not start.
79+
// `OnComponentUpdaterResult` answers only for its own request, so a download
80+
// already in flight is heard about here alone.
81+
void OnEvent(const update_client::CrxUpdateItem& item) override;
82+
83+
// Outcome of the request `InstallLanguage` made. Only a failure is reported,
84+
// because a success arrives as the install dir being published.
85+
void OnComponentUpdaterResult(update_client::Error error);
86+
87+
base::ScopedObservation<component_updater::ComponentUpdateService,
88+
component_updater::ComponentUpdateService::Observer>
89+
component_updater_observation_{this};
90+
91+
base::WeakPtrFactory<BraveSodaInstaller> weak_factory_{this};
92+
};
93+
94+
} // namespace speech
95+
96+
#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)