blob: db205242046fbbc3d021c64930aa28528afe8b71 [file]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 <string>
#include <utility>
#include "base/bind.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.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/bind_to_current_loop.h"
#include "media/base/limits.h"
#include "media/base/media_switches.h"
#include "media/mojo/common/media_type_converters.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.CaptionBubbleVisible2";
// static
const char
SpeechRecognitionRecognizerImpl::kCaptionBubbleHiddenHistogramName[] =
"Accessibility.LiveCaption.Duration.CaptionBubbleHidden2";
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/1175357): 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()));
}
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() {
RecordDuration();
soda_client_.reset();
}
void SpeechRecognitionRecognizerImpl::Create(
mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizer> receiver,
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service_impl,
media::mojom::SpeechRecognitionOptionsPtr options,
const base::FilePath& binary_path,
const base::FilePath& config_path) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<SpeechRecognitionRecognizerImpl>(
std::move(remote), std::move(speech_recognition_service_impl),
std::move(options), binary_path, config_path),
std::move(receiver));
}
bool SpeechRecognitionRecognizerImpl::IsMultichannelSupported() {
return false;
}
void SpeechRecognitionRecognizerImpl::OnRecognitionEvent(
media::SpeechRecognitionResult event) {
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) {
if (client_remote_.is_bound()) {
client_remote_->OnLanguageIdentificationEvent(
media::mojom::LanguageIdentificationEvent::New(language,
confidence_level));
}
}
void SpeechRecognitionRecognizerImpl::OnRecognitionStoppedCallback() {
if (client_remote_.is_bound()) {
client_remote_->OnSpeechRecognitionStopped();
}
}
SpeechRecognitionRecognizerImpl::SpeechRecognitionRecognizerImpl(
mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> remote,
base::WeakPtr<SpeechRecognitionServiceImpl> speech_recognition_service_impl,
media::mojom::SpeechRecognitionOptionsPtr options,
const base::FilePath& binary_path,
const base::FilePath& config_path)
: options_(std::move(options)),
client_remote_(std::move(remote)),
config_path_(config_path) {
recognition_event_callback_ = media::BindToCurrentLoop(
base::BindRepeating(&SpeechRecognitionRecognizerImpl::OnRecognitionEvent,
weak_factory_.GetWeakPtr()));
language_identification_event_callback_ =
media::BindToCurrentLoop(base::BindRepeating(
&SpeechRecognitionRecognizerImpl::OnLanguageIdentificationEvent,
weak_factory_.GetWeakPtr()));
speech_recognition_stopped_callback_ =
media::BindToCurrentLoop(base::BindRepeating(
&SpeechRecognitionRecognizerImpl::OnRecognitionStoppedCallback,
weak_factory_.GetWeakPtr()));
// Unretained is safe because |this| owns the mojo::Remote.
client_remote_.set_disconnect_handler(
base::BindOnce(&SpeechRecognitionRecognizerImpl::OnClientHostDisconnected,
weak_factory_.GetWeakPtr()));
#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.
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;
}
// 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::
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_);
DCHECK(base::PathExists(config_path_));
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) {
absl::optional<speech::SodaLanguagePackComponentConfig>
language_component_config = GetLanguageComponentConfig(language);
if (!language_component_config.has_value())
return;
// Only reset SODA if the language changed.
LanguageCode language_code = language_component_config.value().language_code;
if (language_code == language_ || language_code == LanguageCode::kNone)
return;
language_ = language_component_config.value().language_code;
base::FilePath config_path = GetLatestSodaLanguagePackDirectory(language);
if (base::PathExists(config_path)) {
config_path_ = config_path;
ResetSoda();
} else {
NOTREACHED();
}
}
void SpeechRecognitionRecognizerImpl::RecordDuration() {
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/1161569): 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_path_.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));
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