blob: 302b81fee926529abc2c80add7d3a192a35b89e8 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/live_caption/translation_dispatcher.h"
#include <memory>
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "components/live_caption/translation_util.h"
#include "components/soda/constants.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "media/mojo/mojom/speech_recognition_result.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/header_util.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace captions {
// Request constants.
const size_t kMaxMessageSize = 1024 * 1024; // 1MB
constexpr char kTranslateBodyRequestTemplate[] =
"{"
"\"q\":\"%s\","
"\"source\":\"%s\","
"\"target\":\"%s\","
"\"format\":\"text\""
"}";
constexpr char kTranslateUrl[] =
"https://translation.googleapis.com/language/translate/v2?key=%s";
constexpr char kUploadContentType[] = "application/json";
constexpr char kHttpErrorMessageTemplate[] =
"Failed to recieve response, got errror: %s";
// Response constants.
constexpr char kDataKey[] = "data";
constexpr char kTranslationsKey[] = "translations";
constexpr char kTranslatedTextKey[] = "translatedText";
// Histogram names.
constexpr char kTranslationDispatcherLoadResultHistogram[] =
"Accessibility.LiveTranslate.TranslationDispatcher.LoadResult";
constexpr char kTranslationDispatcherParseResultHistogram[] =
"Accessibility.LiveTranslate.TranslationDispatcher.ParseResult";
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(TranslationDispatcherLoadResult)
enum class TranslationDispatcherLoadResult {
kSuccess = 0,
kNetworkError = 1,
kHttpError = 2,
kEmptyResponse = 3,
kMaxValue = kEmptyResponse,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/accessibility/enums.xml:TranslationDispatcherLoadResult)
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(TranslationDispatcherParseResult)
enum class TranslationDispatcherParseResult {
kSuccess = 0,
kJsonParseError = 1,
kValueNotDict = 2,
kDataDictNotFound = 3,
kTranslationsListNotFound = 4,
kTranslatedTextNotDict = 5,
kTranslatedTextNotFound = 6,
kMaxValue = kTranslatedTextNotFound,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/accessibility/enums.xml:TranslationDispatcherParseResult)
TranslationDispatcher::TranslationDispatcher(
std::string api_key,
content::BrowserContext* browser_context)
: api_key_(api_key), browser_context_(browser_context) {}
TranslationDispatcher::~TranslationDispatcher() = default;
void TranslationDispatcher::GetTranslation(const std::string& result,
std::string source_language,
std::string target_language,
TranslateEventCallback callback) {
if (!url_loader_factory_.is_bound() || !url_loader_factory_.is_connected()) {
ResetURLLoaderFactory();
}
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url =
GURL(base::StringPrintf(kTranslateUrl, api_key_.c_str()));
resource_request->method = net::HttpRequestHeaders::kPostMethod;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("cloud_speech_recognition",
R"(
semantics {
sender: "Live Translate"
description:
"Chrome provides the ability to translate captions generated from "
"output audio by using the Google Cloud Translate web service. "
"Captions are sent to Google's servers and translated captions are "
"returned."
trigger:
"Triggered in direct response to a user playing a media with audio "
"with the Live Translate feature enabled."
data: "Captions to be translated by Google's Cloud Translate web "
"service."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "evanliu@google.com"
}
}
user_data {
type: OTHER
}
last_reviewed: "2024-10-11"
}
policy {
cookies_allowed: NO
setting:
"You can enable or disable this feature via the caption settings "
"page."
chrome_policy {
TranslateEnabled {
TranslateEnabled: false
}
}
})");
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
url_loader_->AttachStringForUpload(
base::StringPrintf(kTranslateBodyRequestTemplate, result.c_str(),
source_language.c_str(), target_language.c_str()),
kUploadContentType);
// Unretained is safe because |this| owns |url_loader_|.
url_loader_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&TranslationDispatcher::OnURLLoadComplete,
base::Unretained(this), std::move(callback)),
kMaxMessageSize);
base::UmaHistogramSparse("Accessibility.LiveTranslate.TargetLanguage",
base::HashMetricName(target_language));
base::UmaHistogramSparse("Accessibility.LiveTranslate.SourceLanguage",
base::HashMetricName(source_language));
}
void TranslationDispatcher::ResetURLLoaderFactory() {
network::mojom::URLLoaderFactoryParamsPtr params =
network::mojom::URLLoaderFactoryParams::New();
params->process_id = network::mojom::kBrowserProcessId;
params->is_trusted = false;
params->automatically_assign_isolation_info = true;
network::mojom::NetworkContext* network_context =
browser_context_->GetDefaultStoragePartition()->GetNetworkContext();
network_context->CreateURLLoaderFactory(
url_loader_factory_.BindNewPipeAndPassReceiver(), std::move(params));
}
void TranslationDispatcher::OnURLLoadComplete(
TranslateEventCallback callback,
std::optional<std::string> response_body) {
// Check that the request succeeded. First with Network Errors...
if (static_cast<net::Error>(url_loader_->NetError()) != net::Error::OK) {
base::UmaHistogramEnumeration(
kTranslationDispatcherLoadResultHistogram,
TranslationDispatcherLoadResult::kNetworkError);
EmitError(
std::move(callback),
base::StringPrintf(kHttpErrorMessageTemplate,
net::ErrorToShortString(url_loader_->NetError())));
return;
}
// and then check HTTP errors.
if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers &&
!network::IsSuccessfulStatus(
url_loader_->ResponseInfo()->headers->response_code())) {
base::UmaHistogramEnumeration(kTranslationDispatcherLoadResultHistogram,
TranslationDispatcherLoadResult::kHttpError);
EmitError(std::move(callback),
base::StringPrintf(
kHttpErrorMessageTemplate,
base::NumberToString(
url_loader_->ResponseInfo()->headers->response_code())));
return;
}
// Somehow the request succeeded but the body is empty.
if (!response_body.has_value()) {
base::UmaHistogramEnumeration(
kTranslationDispatcherLoadResultHistogram,
TranslationDispatcherLoadResult::kEmptyResponse);
EmitError(std::move(callback),
"Error parsing response: Translation dispatcher recieved a 2XX "
"response, but the body was empty");
return;
}
base::UmaHistogramEnumeration(kTranslationDispatcherLoadResultHistogram,
TranslationDispatcherLoadResult::kSuccess);
// Parse the response in a utility process.
data_decoder_.ParseJson(
*response_body,
base::BindOnce(&TranslationDispatcher::OnResponseJsonParsed,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void TranslationDispatcher::EmitError(TranslateEventCallback callback,
const std::string& message) const {
std::move(callback).Run(base::unexpected<std::string>(message));
}
void TranslationDispatcher::OnResponseJsonParsed(
TranslateEventCallback callback,
data_decoder::DataDecoder::ValueOrError result) {
if (!result.has_value()) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kJsonParseError);
EmitError(std::move(callback), "Error parsing response: value null");
return;
}
if (!result.value().is_dict()) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kValueNotDict);
EmitError(std::move(callback),
"Error parsing response: result value is not a dictionary");
return;
}
const base::Value::Dict* data_dict =
result.value().GetDict().FindDict(kDataKey);
if (!data_dict) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kDataDictNotFound);
EmitError(std::move(callback),
"Error parsing response: dictionary not found");
return;
}
const base::Value::List* translations_list =
data_dict->FindList(kTranslationsKey);
if (!translations_list || translations_list->empty()) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kTranslationsListNotFound);
EmitError(std::move(callback),
"Error parsing response: translations not found");
return;
}
const base::Value::Dict* translated_text =
(*translations_list)[0].GetIfDict();
if (!translated_text) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kTranslatedTextNotDict);
EmitError(std::move(callback),
"Error parsing response: translated list entry not found");
return;
}
const std::string* value = translated_text->FindString(kTranslatedTextKey);
if (!value) {
base::UmaHistogramEnumeration(
kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kTranslatedTextNotFound);
EmitError(std::move(callback),
"Error parsing response: translated text not found");
return;
}
base::UmaHistogramEnumeration(kTranslationDispatcherParseResultHistogram,
TranslationDispatcherParseResult::kSuccess);
std::move(callback).Run(TranslateEvent(*value));
}
} // namespace captions