blob: cc48679c45654ced7f834343b49dc31969aa62ad [file]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/services/speech/speech_recognition_recognizer_impl.h"
#include <algorithm>
#include <string>
#include <utility>
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/services/speech/soda/proto/soda_api.pb.h"
#include "chrome/services/speech/soda/soda_client.h"
#include "components/soda/constants.h"
#include "google_apis/google_api_keys.h"
#include "media/base/audio_buffer.h"
#include "media/base/audio_sample_types.h"
#include "media/base/audio_timestamp_helper.h"
#include "media/base/limits.h"
#include "media/base/media_switches.h"
#include "media/mojo/common/media_type_converters.h"
#include "media/mojo/mojom/audio_data.mojom.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
namespace speech {
constexpr char kInvalidAudioDataError[] = "Invalid audio data received.";
// static
const char
SpeechRecognitionRecognizerImpl::kCaptionBubbleVisibleHistogramName[] =
"Accessibility.LiveCaption.Duration.CaptionBubbleVisible3";
// static
const char
SpeechRecognitionRecognizerImpl::kCaptionBubbleHiddenHistogramName[] =
"Accessibility.LiveCaption.Duration.CaptionBubbleHidden3";
constexpr char kLiveCaptionLanguageCountHistogramName[] =
"Accessibility.LiveCaption.LanguageCount";
namespace {
// Callback executed by the SODA library on a speech recognition event. The
// callback handle is a void pointer to the SpeechRecognitionRecognizerImpl that
// owns the SODA instance. SpeechRecognitionRecognizerImpl owns the SodaClient
// which owns the instance of SODA and their sequential destruction order
// ensures that this callback will never be called with an invalid callback
// handle to the SpeechRecognitionRecognizerImpl.
void OnSodaResponse(const char* serialized_proto,
int length,
void* callback_handle) {
DCHECK(callback_handle);
soda::chrome::SodaResponse response;
if (!response.ParseFromArray(serialized_proto, length)) {
LOG(ERROR) << "Unable to parse result from SODA.";
return;
}
if (response.soda_type() == soda::chrome::SodaResponse::RECOGNITION) {
soda::chrome::SodaRecognitionResult result = response.recognition_result();
DCHECK(result.hypothesis_size());
static_cast<SpeechRecognitionRecognizerImpl*>(callback_handle)
->recognition_event_callback()
.Run(media::SpeechRecognitionResult(
result.hypothesis(0),
result.result_type() ==
soda::chrome::SodaRecognitionResult::FINAL));
}
if (response.soda_type() == soda::chrome::SodaResponse::LANGID) {
// TODO(crbug.com/40167928): Use the langid event to prompt users to switch
// languages.
soda::chrome::SodaLangIdEvent event = response.langid_event();
if (event.confidence_level() >
static_cast<int>(media::mojom::ConfidenceLevel::kHighlyConfident) ||
event.confidence_level() <
static_cast<int>(media::mojom::ConfidenceLevel::kUnknown)) {
LOG(ERROR) << "Invalid confidence level returned by SODA: "
<< event.confidence_level();
return;
}
static_cast<SpeechRecognitionRecognizerImpl*>(callback_handle)
->language_identification_event_callback()
.Run(std::string(event.language()),
static_cast<media::mojom::ConfidenceLevel>(
event.confidence_level()),
static_cast<media::mojom::AsrSwitchResult>(
event.asr_switch_result()));
}
if (response.soda_type() == soda::chrome::SodaResponse::STOP) {
static_cast<SpeechRecognitionRecognizerImpl*>(callback_handle)
->speech_recognition_stopped_callback()
.Run();
}
}
speech::soda::chrome::ExtendedSodaConfigMsg::RecognitionMode
GetSodaSpeechRecognitionMode(
media::mojom::SpeechRecognitionMode recognition_mode) {
switch (recognition_mode) {
case media::mojom::SpeechRecognitionMode::kUnknown:
return soda::chrome::ExtendedSodaConfigMsg::UNKNOWN;
case media::mojom::SpeechRecognitionMode::kIme:
return soda::chrome::ExtendedSodaConfigMsg::IME;
case media::mojom::SpeechRecognitionMode::kCaption:
return soda::chrome::ExtendedSodaConfigMsg::CAPTION;
}
}
} // namespace
SpeechRecognitionRecognizerImpl::~SpeechRecognitionRecognizerImpl() {
base::UmaHistogramBoolean(
base::StrCat({"Accessibility.LiveCaption.", primary_language_name_,
".SessionContainsRecognizedSpeech"}),
session_contains_speech_);
RecordDuration();
soda_client_.reset();
if (speech_recognition_service_) {
speech_recognition_service_->RemoveObserver(this);
}
}
void SpeechRecognitionRecognizerImpl::OnLanguagePackInstalled(
base::flat_map<std::string, base::FilePath> config_paths) {
config_paths_ = config_paths;
ResetSoda();
}
void SpeechRecognitionRecognizerImpl::Create(
mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizer> receiver,
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
media::mojom::SpeechRecognitionOptionsPtr options,
const base::FilePath& binary_path,
const base::flat_map<std::string, base::FilePath>& config_paths,
const std::string& primary_language_name,
const bool mask_offensive_words,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<SpeechRecognitionRecognizerImpl>(
std::move(remote), std::move(options), binary_path, config_paths,
primary_language_name, mask_offensive_words,
speech_recognition_service),
std::move(receiver));
}
bool SpeechRecognitionRecognizerImpl::IsMultichannelSupported() {
return false;
}
void SpeechRecognitionRecognizerImpl::OnRecognitionEvent(
media::SpeechRecognitionResult event) {
if (!event.transcription.empty()) {
session_contains_speech_ = true;
}
if (!client_remote_.is_bound())
return;
client_remote_->OnSpeechRecognitionRecognitionEvent(
std::move(event),
base::BindOnce(&SpeechRecognitionRecognizerImpl::
OnSpeechRecognitionRecognitionEventCallback,
weak_factory_.GetWeakPtr()));
}
void SpeechRecognitionRecognizerImpl::
OnSpeechRecognitionRecognitionEventCallback(bool success) {
is_client_requesting_speech_recognition_ = success;
}
void SpeechRecognitionRecognizerImpl::OnLanguageIdentificationEvent(
const std::string& language,
const media::mojom::ConfidenceLevel confidence_level,
const media::mojom::AsrSwitchResult asr_switch_result) {
if (client_remote_.is_bound()) {
client_remote_->OnLanguageIdentificationEvent(
media::mojom::LanguageIdentificationEvent::New(
language, confidence_level, asr_switch_result));
}
}
void SpeechRecognitionRecognizerImpl::OnRecognitionStoppedCallback() {
if (client_remote_.is_bound()) {
client_remote_->OnSpeechRecognitionStopped();
}
}
SpeechRecognitionRecognizerImpl::SpeechRecognitionRecognizerImpl(
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
media::mojom::SpeechRecognitionOptionsPtr options,
const base::FilePath& binary_path,
const base::flat_map<std::string, base::FilePath>& config_paths,
const std::string& primary_language_name,
const bool mask_offensive_words,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service)
: options_(std::move(options)),
client_remote_(std::move(remote)),
config_paths_(config_paths),
primary_language_name_(primary_language_name),
mask_offensive_words_(mask_offensive_words),
speech_recognition_service_(speech_recognition_service) {
recognition_event_callback_ = base::BindPostTaskToCurrentDefault(
base::BindRepeating(&SpeechRecognitionRecognizerImpl::OnRecognitionEvent,
weak_factory_.GetWeakPtr()));
language_identification_event_callback_ =
base::BindPostTaskToCurrentDefault(base::BindRepeating(
&SpeechRecognitionRecognizerImpl::OnLanguageIdentificationEvent,
weak_factory_.GetWeakPtr()));
speech_recognition_stopped_callback_ =
base::BindPostTaskToCurrentDefault(base::BindRepeating(
&SpeechRecognitionRecognizerImpl::OnRecognitionStoppedCallback,
weak_factory_.GetWeakPtr()));
client_remote_.set_disconnect_handler(
base::BindOnce(&SpeechRecognitionRecognizerImpl::OnClientHostDisconnected,
weak_factory_.GetWeakPtr()));
if (speech_recognition_service_) {
speech_recognition_service_->AddObserver(this);
}
#if !BUILDFLAG(IS_CHROMEOS_ASH)
// On Chrome OS Ash, soda_client_ is not used, so don't try to create it
// here because it exists at a different location. Instead,
// CrosSpeechRecognitionRecognizerImpl has its own CrosSodaClient.
DCHECK(base::PathExists(binary_path));
soda_client_ = std::make_unique<::soda::SodaClient>(binary_path);
if (!soda_client_->BinaryLoadedSuccessfully()) {
OnSpeechRecognitionError();
}
#endif
}
void SpeechRecognitionRecognizerImpl::OnClientHostDisconnected() {
is_client_requesting_speech_recognition_ = false;
}
void SpeechRecognitionRecognizerImpl::SendAudioToSpeechRecognitionService(
media::mojom::AudioDataS16Ptr buffer) {
int channel_count = buffer->channel_count;
int frame_count = buffer->frame_count;
int sample_rate = buffer->sample_rate;
size_t num_samples = 0;
size_t buffer_size = 0;
// Update watch time durations.
if (options_->recognizer_client_type ==
media::mojom::RecognizerClientType::kLiveCaption) {
base::TimeDelta duration =
media::AudioTimestampHelper::FramesToTime(frame_count, sample_rate);
if (is_client_requesting_speech_recognition_) {
caption_bubble_visible_duration_ += duration;
} else {
caption_bubble_hidden_duration_ += duration;
return;
}
}
// Verify the channel count.
if (channel_count <= 0 || channel_count > media::limits::kMaxChannels) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
// Verify and calculate the number of samples.
if (sample_rate <= 0 || frame_count <= 0 ||
!base::CheckMul(frame_count, channel_count).AssignIfValid(&num_samples) ||
num_samples != buffer->data.size()) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
// Verify and calculate the buffer size.
if (!base::CheckMul(buffer->data.size(), sizeof(buffer->data[0]))
.AssignIfValid(&buffer_size)) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
// Skip this buffer if there has been no nonzero data for several seconds.
if (options_->skip_continuously_empty_audio) {
const bool buffer_is_zero =
std::all_of(buffer->data.begin(), buffer->data.end(),
[](int16_t x) { return x == 0; });
const base::Time now = base::Time::Now();
if (!buffer_is_zero) {
last_non_empty_audio_time_ = now;
}
if (now - last_non_empty_audio_time_ > base::Seconds(10)) {
// No nonzero data for several seconds. Don't send this buffer of zeroes.
return;
}
}
// OK, everything is verified, let's send the audio.
SendAudioToSpeechRecognitionServiceInternal(std::move(buffer));
}
void SpeechRecognitionRecognizerImpl::OnSpeechRecognitionError() {
if (client_remote_.is_bound()) {
client_remote_->OnSpeechRecognitionError();
}
}
void SpeechRecognitionRecognizerImpl::MarkDone() {
soda_client_->MarkDone();
}
void SpeechRecognitionRecognizerImpl::AddAudio(
media::mojom::AudioDataS16Ptr buffer) {
SendAudioToSpeechRecognitionService(std::move(buffer));
}
void SpeechRecognitionRecognizerImpl::OnAudioCaptureEnd() {
MarkDone();
}
void SpeechRecognitionRecognizerImpl::OnAudioCaptureError() {
OnSpeechRecognitionError();
}
void SpeechRecognitionRecognizerImpl::
SendAudioToSpeechRecognitionServiceInternal(
media::mojom::AudioDataS16Ptr buffer) {
channel_count_ = buffer->channel_count;
sample_rate_ = buffer->sample_rate;
size_t buffer_size = 0;
// Verify and calculate the buffer size.
if (!base::CheckMul(buffer->data.size(), sizeof(buffer->data[0]))
.AssignIfValid(&buffer_size)) {
mojo::ReportBadMessage(kInvalidAudioDataError);
return;
}
CHECK(soda_client_);
if (!soda_client_->IsInitialized() ||
soda_client_->DidAudioPropertyChange(sample_rate_, channel_count_)) {
ResetSoda();
}
soda_client_->AddAudio(reinterpret_cast<char*>(buffer->data.data()),
buffer_size);
}
void SpeechRecognitionRecognizerImpl::OnLanguageChanged(
const std::string& language) {
std::optional<speech::SodaLanguagePackComponentConfig>
language_component_config = GetLanguageComponentConfig(language);
if (!language_component_config.has_value())
return;
// Only reset SODA if the language changed.
if (language_component_config.value().language_name ==
primary_language_name_ ||
language_component_config.value().language_code == LanguageCode::kNone) {
return;
}
if (!task_runner_) {
task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});
}
// Changing the language requires a blocking call to check if the language
// pack exists on the device.
scoped_refptr<base::SequencedTaskRunner> current_task_runner =
base::SequencedTaskRunner::GetCurrentDefault();
task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](const std::string& language) {
base::FilePath config_file_path =
GetLatestSodaLanguagePackDirectory(language);
return std::make_pair(config_file_path,
base::PathExists(config_file_path));
},
language),
base::BindOnce(&SpeechRecognitionRecognizerImpl::ResetSodaWithNewLanguage,
weak_factory_.GetWeakPtr(),
language_component_config.value().language_name));
}
void SpeechRecognitionRecognizerImpl::OnMaskOffensiveWordsChanged(
bool mask_offensive_words) {
mask_offensive_words_ = mask_offensive_words;
ResetSoda();
}
void SpeechRecognitionRecognizerImpl::ResetSodaWithNewLanguage(
std::string language_name,
std::pair<base::FilePath, bool> config_and_exists) {
if (config_and_exists.second) {
config_paths_[language_name] = config_and_exists.first;
primary_language_name_ = language_name;
ResetSoda();
}
}
void SpeechRecognitionRecognizerImpl::RecordDuration() {
if (options_->recognizer_client_type !=
media::mojom::RecognizerClientType::kLiveCaption) {
return;
}
// TODO(b:245620092) Create metrics for other features using speech
// recognition.
if (caption_bubble_visible_duration_.is_positive()) {
base::UmaHistogramLongTimes100(kCaptionBubbleVisibleHistogramName,
caption_bubble_visible_duration_);
}
if (caption_bubble_hidden_duration_.is_positive()) {
base::UmaHistogramLongTimes100(kCaptionBubbleHiddenHistogramName,
caption_bubble_hidden_duration_);
}
}
void SpeechRecognitionRecognizerImpl::ResetSoda() {
// Initialize the SODA instance.
auto api_key = google_apis::GetSodaAPIKey();
// TODO(crbug.com/40162502): Use language from SpeechRecognitionOptions
// to determine the appropriate language pack path. Note that
// SodaInstaller::GetLanguagePath() is not implemented outside of Chrome OS,
// and options_->language is not set for Live Caption.
std::string language_pack_directory =
config_paths_[primary_language_name_].AsUTF8Unsafe();
// Initialize the SODA instance with the serialized config.
soda::chrome::ExtendedSodaConfigMsg config_msg;
config_msg.set_channel_count(channel_count_);
config_msg.set_sample_rate(sample_rate_);
config_msg.set_api_key(api_key);
config_msg.set_language_pack_directory(language_pack_directory);
config_msg.set_simulate_realtime_testonly(false);
config_msg.set_enable_lang_id(false);
config_msg.set_recognition_mode(
GetSodaSpeechRecognitionMode(options_->recognition_mode));
config_msg.set_enable_formatting(options_->enable_formatting);
config_msg.set_enable_speaker_change_detection(
base::FeatureList::IsEnabled(media::kSpeakerChangeDetection));
config_msg.set_mask_offensive_words(mask_offensive_words_);
if (base::FeatureList::IsEnabled(media::kLiveCaptionMultiLanguage) &&
config_paths_.size() > 0) {
auto* multilang_config = config_msg.mutable_multilang_config();
multilang_config->set_rewind_when_switching_language(true);
auto& multilang_language_pack_directory =
*(multilang_config->mutable_multilang_language_pack_directory());
for (const auto& config : config_paths_) {
multilang_language_pack_directory[base::ToLowerASCII(config.first)] =
config.second.AsUTF8Unsafe();
}
base::UmaHistogramCounts100(kLiveCaptionLanguageCountHistogramName,
config_paths_.size());
}
auto serialized = config_msg.SerializeAsString();
SerializedSodaConfig config;
config.soda_config = serialized.c_str();
config.soda_config_size = serialized.size();
config.callback = &OnSodaResponse;
config.callback_handle = this;
CHECK(soda_client_);
soda_client_->Reset(config, sample_rate_, channel_count_);
}
} // namespace speech