diff --git a/third_party/blink/renderer/modules/ai/ai_mojo_client.h b/third_party/blink/renderer/modules/ai/ai_mojo_client.h index 111b8b4318b42..886f3fab16154 100644 --- a/third_party/blink/renderer/modules/ai/ai_mojo_client.h +++ b/third_party/blink/renderer/modules/ai/ai_mojo_client.h @@ -52,6 +52,7 @@ class AIMojoClient : public ContextLifecycleObserver { ~AIMojoClient() override = default; protected: + ScriptState* GetScriptState() { return script_state_; } ScriptPromiseResolver<V8SessionObjectType>* GetResolver() { return resolver_; } diff --git a/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.cc b/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.cc index 557c84caff5e5..96c7ea3066e71 100644 --- a/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.cc +++ b/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.cc @@ -7,10 +7,13 @@ #include "third_party/blink/renderer/bindings/core/v8/script_promise.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_ai_create_monitor_callback.h" +#include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/modules/ai/ai.h" #include "third_party/blink/renderer/modules/ai/ai_create_monitor.h" #include "third_party/blink/renderer/modules/ai/ai_mojo_client.h" +#include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/mojo/heap_mojo_receiver.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" namespace blink { namespace { @@ -18,20 +21,37 @@ namespace { const char kExceptionMessageUnableToCreateTranslator[] = "Unable to create translator for the given source and target language."; +bool RequiresUserActivation(mojom::blink::CanCreateTranslatorResult result) { + switch (result) { + case mojom::blink::CanCreateTranslatorResult::kAfterDownloadLibraryNotReady: + case mojom::blink::CanCreateTranslatorResult:: + kAfterDownloadLanguagePackNotReady: + case mojom::blink::CanCreateTranslatorResult:: + kAfterDownloadLibraryAndLanguagePackNotReady: + return true; + case mojom::blink::CanCreateTranslatorResult::kReadily: + case mojom::blink::CanCreateTranslatorResult::kNoNotSupportedLanguage: + case mojom::blink::CanCreateTranslatorResult::kNoAcceptLanguagesCheckFailed: + case mojom::blink::CanCreateTranslatorResult:: + kNoExceedsLanguagePackCountLimitation: + case mojom::blink::CanCreateTranslatorResult::kNoServiceCrashed: + case mojom::blink::CanCreateTranslatorResult::kNoDisallowedByPolicy: + case mojom::blink::CanCreateTranslatorResult:: + kNoExceedsServiceCountLimitation: + return false; + } +} + class CreateTranslatorClient : public GarbageCollected<CreateTranslatorClient>, public mojom::blink::TranslationManagerCreateTranslatorClient, public AIMojoClient<AITranslator> { public: - CreateTranslatorClient( - ScriptState* script_state, - AITranslatorFactory* translation, - AITranslatorCreateOptions* options, - scoped_refptr<base::SequencedTaskRunner> task_runner, - ScriptPromiseResolver<AITranslator>* resolver, - mojo::PendingReceiver< - mojom::blink::TranslationManagerCreateTranslatorClient> - pending_receiver) + CreateTranslatorClient(ScriptState* script_state, + AITranslatorFactory* translation, + AITranslatorCreateOptions* options, + scoped_refptr<base::SequencedTaskRunner> task_runner, + ScriptPromiseResolver<AITranslator>* resolver) : AIMojoClient(script_state, translation, resolver, @@ -41,7 +61,6 @@ class CreateTranslatorClient target_language_(options->targetLanguage()), receiver_(this, translation_->GetExecutionContext()), task_runner_(task_runner) { - receiver_.Bind(std::move(pending_receiver), task_runner); if (options->hasMonitor()) { monitor_ = MakeGarbageCollected<AICreateMonitor>( translation_->GetExecutionContext(), task_runner); @@ -86,6 +105,30 @@ class CreateTranslatorClient Cleanup(); } + void OnGotAvailability(mojom::blink::CanCreateTranslatorResult result) { + LocalDOMWindow* const window = LocalDOMWindow::From(GetScriptState()); + + if (RuntimeEnabledFeatures::TranslationAPIV1Enabled() && + RequiresUserActivation(result) && + !LocalFrame::ConsumeTransientUserActivation(window->GetFrame())) { + GetResolver()->RejectWithDOMException( + DOMExceptionCode::kNotAllowedError, + "Requires handling a user gesture when availability is " + "\"after-download\"."); + return; + } + mojo::PendingRemote<mojom::blink::TranslationManagerCreateTranslatorClient> + client; + + receiver_.Bind(client.InitWithNewPipeAndPassReceiver(), task_runner_); + + translation_->GetTranslationManagerRemote()->CreateTranslator( + std::move(client), + mojom::blink::TranslatorCreateOptions::New( + mojom::blink::TranslatorLanguageCode::New(source_language_), + mojom::blink::TranslatorLanguageCode::New(target_language_))); + } + void ResetReceiver() override { receiver_.reset(); } private: @@ -111,14 +154,15 @@ ScriptPromise<AITranslator> AITranslatorFactory::create( ScriptState* script_state, AITranslatorCreateOptions* options, ExceptionState& exception_state) { + // If `sourceLanguage` and `targetLanguage` are not passed, A TypeError should + // be thrown before we get here. + CHECK(options && options->sourceLanguage() && options->targetLanguage()); + if (!script_state->ContextIsValid()) { exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, "The execution context is not valid."); return EmptyPromise(); } - // If `sourceLanguage` and `targetLanguage` are not passed, A TypeError should - // be thrown before we get here. - CHECK(options && options->sourceLanguage() && options->targetLanguage()); AbortSignal* signal = options->getSignalOr(nullptr); if (HandleAbortSignal(signal, script_state, exception_state)) { @@ -128,17 +172,15 @@ ScriptPromise<AITranslator> AITranslatorFactory::create( auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<AITranslator>>(script_state); - mojo::PendingRemote<mojom::blink::TranslationManagerCreateTranslatorClient> - client; - MakeGarbageCollected<CreateTranslatorClient>( - script_state, this, options, task_runner_, resolver, - client.InitWithNewPipeAndPassReceiver()); - GetTranslationManagerRemote()->CreateTranslator( - std::move(client), - mojom::blink::TranslatorCreateOptions::New( - mojom::blink::TranslatorLanguageCode::New(options->sourceLanguage()), - mojom::blink::TranslatorLanguageCode::New( - options->targetLanguage()))); + CreateTranslatorClient* create_translator_client = + MakeGarbageCollected<CreateTranslatorClient>(script_state, this, options, + task_runner_, resolver); + + GetTranslationManagerRemote()->CanCreateTranslator( + mojom::blink::TranslatorLanguageCode::New(options->sourceLanguage()), + mojom::blink::TranslatorLanguageCode::New(options->targetLanguage()), + WTF::BindOnce(&CreateTranslatorClient::OnGotAvailability, + WrapPersistent(create_translator_client))); return resolver->Promise(); } diff --git a/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.h b/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.h index a45250ede093c..0dc723c3b4ae0 100644 --- a/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.h +++ b/third_party/blink/renderer/modules/ai/on_device_translation/ai_translator_factory.h @@ -34,12 +34,12 @@ class AITranslatorFactory final : public ScriptWrappable, ScriptState* script_state, ExceptionState& exception_state); - void Trace(Visitor* visitor) const override; - - private: HeapMojoRemote<mojom::blink::TranslationManager>& GetTranslationManagerRemote(); + void Trace(Visitor* visitor) const override; + + private: scoped_refptr<base::SequencedTaskRunner> task_runner_; HeapMojoRemote<mojom::blink::TranslationManager> translation_manager_remote_{ nullptr}; diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5 index 6c727b2c6a2d2..ca9d96369af5a 100644 --- a/third_party/blink/renderer/platform/runtime_enabled_features.json5 +++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5 @@ -4498,6 +4498,7 @@ origin_trial_feature_name: "TranslationAPI", base_feature_status: "enabled", copied_from_base_feature_if: "overridden", + implied_by: ["TranslationAPIV1"], }, { name: "TranslationAPIEntryPoint", @@ -4505,6 +4506,11 @@ origin_trial_feature_name: "TranslationAPIEntryPoint", implied_by: ["TranslationAPI", "LanguageDetectionAPI"], }, + { + name: "TranslationAPIV1", + status: "test", + copied_from_base_feature_if: "overridden", + }, { name: "TrustedTypeBeforePolicyCreationEvent", status: "experimental", diff --git a/third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js b/third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js index 2041ad82b5aad..5cd8cb86b22b8 100644 --- a/third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js +++ b/third_party/blink/web_tests/external/wpt/ai/translator/ai_translator_translate.tentative.https.any.js @@ -1,24 +1,54 @@ // META: title=Translate from English to Japanese -// META: global=window,worker +// META: global=window // META: timeout=long // META: script=../resources/util.js +// META: script=../resources/language_codes.js +// META: script=/resources/testdriver.js // // Setting `timeout=long` as this test may require downloading the translation // library and the language models. 'use strict'; +async function createTranslator(options) { + return await test_driver.bless('Create translator', async () => { + return await ai.translator.create(options); + }); +} + promise_test(async t => { - const translatorFactory = ai.translator; - assert_not_equals(translatorFactory, null); - const translator = await translatorFactory.create( - {sourceLanguage: 'en', targetLanguage: 'ja'}); + const languagePair = {sourceLanguage: 'en', targetLanguage: 'ja'}; + + // Creating the translator without user activation rejects with + // NotAllowedError. + const createPromise = ai.translator.create(languagePair); + await promise_rejects_dom(t, 'NotAllowedError', createPromise); + + // Creating the translator with user activation succeeds. + await createTranslator(languagePair); + + // TODO(crbug.com/390459310): Replace with availability. + // + // Creating it should have switched it to readily. + const capabilities = await ai.translator.capabilities(); + const {sourceLanguage, targetLanguage} = languagePair; + assert_equals( + capabilities.languagePairAvailable(sourceLanguage, targetLanguage), + 'readily'); + + // Now that it is readily, we should no longer need user activation. + await ai.translator.create(languagePair); +}, 'AITranslator.create() requires user activation when availability is "after-download".'); + +promise_test(async t => { + const translator = + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); assert_equals(await translator.translate('hello'), 'こんにちは'); }, 'Simple AITranslator.translate() call'); promise_test(async () => { const translator = - await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); const streamingResponse = translator.translateStreaming('hello'); assert_equals( Object.prototype.toString.call(streamingResponse), @@ -32,14 +62,14 @@ promise_test(async () => { promise_test(async t => { const translator = - await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); assert_equals(translator.sourceLanguage, 'en'); assert_equals(translator.targetLanguage, 'ja'); }, 'AITranslator: sourceLanguage and targetLanguage are equal to their respective option passed in to AITranslatorFactory.create.') promise_test(async (t) => { const translator = - await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); translator.destroy(); await promise_rejects_dom( t, 'InvalidStateError', translator.translate('hello')); @@ -49,7 +79,7 @@ promise_test(async t => { const controller = new AbortController(); controller.abort(); - const createPromise = ai.translator.create( + const createPromise = createTranslator( {signal: controller.signal, sourceLanguage: 'en', targetLanguage: 'ja'}); await promise_rejects_dom(t, 'AbortError', createPromise); @@ -57,7 +87,7 @@ promise_test(async t => { promise_test(async t => { await testAbortPromise(t, signal => { - return ai.translator.create( + return createTranslator( {signal, sourceLanguage: 'en', targetLanguage: 'ja'}); }); }, 'Aborting AITranslatorFactory.create().'); @@ -67,7 +97,7 @@ promise_test(async t => { controller.abort(); const translator = - await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); const translatePromise = translator.translate('hello', {signal: controller.signal}); @@ -76,7 +106,7 @@ promise_test(async t => { promise_test(async t => { const translator = - await ai.translator.create({sourceLanguage: 'en', targetLanguage: 'ja'}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja'}); await testAbortPromise(t, signal => { return translator.translate('hello', {signal}); }); @@ -93,8 +123,7 @@ promise_test(async t => { }); } - await ai.translator.create( - {sourceLanguage: 'en', targetLanguage: 'ja', monitor}); + await createTranslator({sourceLanguage: 'en', targetLanguage: 'ja', monitor}); // Monitor callback must be called. assert_true(monitorCalled);