blob: 75b905d03cb8eac41af97a5eed3bd90739db193b [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Defines a strategy for parsing text that utilizes the pumpkin
* semantic parser.
*/
import {InputController} from '../input_controller.js';
import {LocaleInfo} from '../locale_info.js';
import {DeletePrevSentMacro} from '../macros/delete_prev_sent_macro.js';
import {InputTextViewMacro} from '../macros/input_text_view_macro.js';
import {ListCommandsMacro} from '../macros/list_commands_macro.js';
import {Macro} from '../macros/macro.js';
import {MacroName} from '../macros/macro_names.js';
import {NavNextSentMacro, NavPrevSentMacro} from '../macros/nav_sent_macro.js';
import {RepeatMacro} from '../macros/repeat_macro.js';
import * as RepeatableKeyPressMacro from '../macros/repeatable_key_press_macro.js';
import {SmartDeletePhraseMacro} from '../macros/smart_delete_phrase_macro.js';
import {SmartInsertBeforeMacro} from '../macros/smart_insert_before_macro.js';
import {SmartReplacePhraseMacro} from '../macros/smart_replace_phrase_macro.js';
import {SmartSelectBetweenMacro} from '../macros/smart_select_between_macro.js';
import {StopListeningMacro} from '../macros/stop_listening_macro.js';
import {ParseStrategy} from './parse_strategy.js';
import * as PumpkinConstants from './pumpkin/pumpkin_constants.js';
/** A parsing strategy that utilizes the Pumpkin semantic parser. */
export class PumpkinParseStrategy extends ParseStrategy {
/** @param {!InputController} inputController */
constructor(inputController) {
super(inputController);
/** @private {?PumpkinConstants.PumpkinData} */
this.pumpkinData_ = null;
/** @private {boolean} */
this.pumpkinTaggerReady_ = false;
/** @private {Function} */
this.tagResolver_ = null;
/** @private {?Worker} */
this.worker_ = null;
/** @private {?PumpkinConstants.PumpkinLocale} */
this.locale_ = null;
/** @private {boolean} */
this.requestedPumpkinInstall_ = false;
/** @private {?function(): void} */
this.onPumpkinTaggerReadyChangedForTesting_ = null;
this.init_();
}
/** @private */
init_() {
this.refreshLocale_();
if (!this.locale_) {
return;
}
this.requestedPumpkinInstall_ = true;
chrome.accessibilityPrivate.installPumpkinForDictation(data => {
// TODO(crbug.comg/1258190): Consider retrying installation at a later
// time if it failed.
this.onPumpkinInstalled_(data);
});
}
/**
* @param {PumpkinConstants.PumpkinData} data
* @private
*/
onPumpkinInstalled_(data) {
if (!data) {
console.warn('Pumpkin installed, but data is empty');
return;
}
for (const [key, value] of Object.entries(data)) {
if (!value || value.byteLength === 0) {
throw new Error(`Pumpkin data incomplete, missing data for ${key}`);
}
}
this.refreshLocale_();
if (!this.locale_ || !this.isEnabled()) {
return;
}
// Create SandboxedPumpkinTagger.
this.setPumpkinTaggerReady_(false);
this.pumpkinData_ = data;
this.worker_ = new Worker(
PumpkinConstants.SANDBOXED_PUMPKIN_TAGGER_JS_FILE, {type: 'module'});
this.worker_.onmessage = (message) => this.onMessage_(message);
}
/**
* Called when the SandboxedPumpkinTagger posts a message to the background
* context.
* @param {!Event} message
* @private
*/
onMessage_(message) {
const command =
/** @type {!PumpkinConstants.FromPumpkinTagger} */ (message.data);
switch (command.type) {
case PumpkinConstants.FromPumpkinTaggerCommand.READY:
this.refreshLocale_();
if (!this.locale_) {
throw new Error(
`Can't load SandboxedPumpkinTagger in an unsupported locale ${
LocaleInfo.locale}`);
}
this.sendToSandboxedPumpkinTagger_({
type: PumpkinConstants.ToPumpkinTaggerCommand.LOAD,
locale: this.locale_,
pumpkinData: this.pumpkinData_,
});
this.pumpkinData_ = null;
return;
case PumpkinConstants.FromPumpkinTaggerCommand.FULLY_INITIALIZED:
this.setPumpkinTaggerReady_(true);
this.maybeRefresh_();
return;
case PumpkinConstants.FromPumpkinTaggerCommand.TAG_RESULTS:
this.tagResolver_(command.results);
return;
case PumpkinConstants.FromPumpkinTaggerCommand.REFRESHED:
this.setPumpkinTaggerReady_(true);
this.maybeRefresh_();
return;
}
throw new Error(
`Unrecognized message received from SandboxedPumpkinTagger: ${
command.type}`);
}
/**
* @param {!PumpkinConstants.ToPumpkinTagger} command
* @private
*/
sendToSandboxedPumpkinTagger_(command) {
if (!this.worker_) {
throw new Error(
`Worker not ready, cannot send command to SandboxedPumpkinTagger: ${
command.type}`);
}
this.worker_.postMessage(command);
}
/**
* In Android Voice Access, Pumpkin Hypotheses will be converted to UserIntent
* protos before being passed to Macros.
* @param {proto.speech.pumpkin.HypothesisResult.ObjectFormat} hypothesis
* @return {?Macro} The macro matching the hypothesis if one can be found.
* @private
*/
macroFromPumpkinHypothesis_(hypothesis) {
const numArgs = hypothesis.actionArgumentList.length;
if (!numArgs) {
return null;
}
let repeat = 1;
let text = '';
let tag = '';
let beginPhrase = '';
let endPhrase = '';
for (let i = 0; i < numArgs; i++) {
const argument = hypothesis.actionArgumentList[i];
// See Variable Argument Placeholders in voiceaccess.patterns_template.
if (argument.name === PumpkinConstants.HypothesisArgumentName.SEM_TAG) {
tag = MacroName[argument.value];
} else if (
argument.name === PumpkinConstants.HypothesisArgumentName.NUM_ARG) {
repeat = argument.value;
} else if (
argument.name ===
PumpkinConstants.HypothesisArgumentName.OPEN_ENDED_TEXT) {
text = argument.value;
} else if (
argument.name ===
PumpkinConstants.HypothesisArgumentName.BEGIN_PHRASE) {
beginPhrase = argument.value;
} else if (
argument.name ===
PumpkinConstants.HypothesisArgumentName.END_PHRASE) {
endPhrase = argument.value;
}
}
switch (tag) {
case MacroName.INPUT_TEXT_VIEW:
return new InputTextViewMacro(text, this.getInputController());
case MacroName.DELETE_PREV_CHAR:
return new RepeatableKeyPressMacro.DeletePreviousCharacterMacro(repeat);
case MacroName.NAV_PREV_CHAR:
return new RepeatableKeyPressMacro.NavPreviousCharMacro(repeat);
case MacroName.NAV_NEXT_CHAR:
return new RepeatableKeyPressMacro.NavNextCharMacro(repeat);
case MacroName.NAV_PREV_LINE:
return new RepeatableKeyPressMacro.NavPreviousLineMacro(repeat);
case MacroName.NAV_NEXT_LINE:
return new RepeatableKeyPressMacro.NavNextLineMacro(repeat);
case MacroName.COPY_SELECTED_TEXT:
return new RepeatableKeyPressMacro.CopySelectedTextMacro();
case MacroName.PASTE_TEXT:
return new RepeatableKeyPressMacro.PasteTextMacro();
case MacroName.CUT_SELECTED_TEXT:
return new RepeatableKeyPressMacro.CutSelectedTextMacro();
case MacroName.UNDO_TEXT_EDIT:
return new RepeatableKeyPressMacro.UndoTextEditMacro();
case MacroName.REDO_ACTION:
return new RepeatableKeyPressMacro.RedoActionMacro();
case MacroName.SELECT_ALL_TEXT:
return new RepeatableKeyPressMacro.SelectAllTextMacro();
case MacroName.UNSELECT_TEXT:
return new RepeatableKeyPressMacro.UnselectTextMacro(
this.getInputController());
case MacroName.LIST_COMMANDS:
return new ListCommandsMacro();
case MacroName.STOP_LISTENING:
return new StopListeningMacro();
case MacroName.DELETE_PREV_WORD:
return new RepeatableKeyPressMacro.DeletePrevWordMacro(repeat);
case MacroName.DELETE_PREV_SENT:
return new DeletePrevSentMacro(this.getInputController());
case MacroName.NAV_NEXT_WORD:
return new RepeatableKeyPressMacro.NavNextWordMacro(repeat);
case MacroName.NAV_PREV_WORD:
return new RepeatableKeyPressMacro.NavPrevWordMacro(repeat);
case MacroName.SMART_DELETE_PHRASE:
return new SmartDeletePhraseMacro(this.getInputController(), text);
case MacroName.SMART_REPLACE_PHRASE:
return new SmartReplacePhraseMacro(
this.getInputController(), beginPhrase, text);
case MacroName.SMART_INSERT_BEFORE:
return new SmartInsertBeforeMacro(
this.getInputController(), text, endPhrase);
case MacroName.SMART_SELECT_BTWN_INCL:
return new SmartSelectBetweenMacro(
this.getInputController(), beginPhrase, endPhrase);
case MacroName.NAV_NEXT_SENT:
return new NavNextSentMacro(this.getInputController());
case MacroName.NAV_PREV_SENT:
return new NavPrevSentMacro(this.getInputController());
case MacroName.DELETE_ALL_TEXT:
return new RepeatableKeyPressMacro.DeleteAllText();
case MacroName.NAV_START_TEXT:
return new RepeatableKeyPressMacro.NavStartText();
case MacroName.NAV_END_TEXT:
return new RepeatableKeyPressMacro.NavEndText();
case MacroName.SELECT_PREV_WORD:
return new RepeatableKeyPressMacro.SelectPrevWord(repeat);
case MacroName.SELECT_NEXT_WORD:
return new RepeatableKeyPressMacro.SelectNextWord(repeat);
case MacroName.SELECT_NEXT_CHAR:
return new RepeatableKeyPressMacro.SelectNextChar(repeat);
case MacroName.SELECT_PREV_CHAR:
return new RepeatableKeyPressMacro.SelectPrevChar(repeat);
case MacroName.REPEAT:
return new RepeatMacro();
default:
// Every hypothesis is guaranteed to include a semantic tag due to the
// way Voice Access set up its grammars. Not all tags are supported in
// Dictation yet.
console.log('Unsupported Pumpkin action: ', tag);
return null;
}
}
/** @private */
refreshLocale_() {
this.locale_ =
PumpkinConstants.SUPPORTED_LOCALES[LocaleInfo.locale] || null;
}
/**
* Refreshes SandboxedPumpkinTagger if the Dictation locale differs from
* the pumpkin locale.
* @private
*/
maybeRefresh_() {
const dictationLocale =
PumpkinConstants.SUPPORTED_LOCALES[LocaleInfo.locale];
if (dictationLocale !== this.locale_) {
this.refresh();
}
}
/** @override */
refresh() {
this.refreshLocale_();
this.enabled = Boolean(this.locale_) && LocaleInfo.areCommandsSupported();
if (!this.requestedPumpkinInstall_) {
this.init_();
return;
}
if (!this.isEnabled() || !this.locale_ || !this.pumpkinTaggerReady_) {
return;
}
this.setPumpkinTaggerReady_(false);
this.sendToSandboxedPumpkinTagger_({
type: PumpkinConstants.ToPumpkinTaggerCommand.REFRESH,
locale: this.locale_,
});
}
/** @override */
async parse(text) {
if (!this.isEnabled() || !this.pumpkinTaggerReady_) {
return null;
}
this.tagResolver_ = null;
// Get results from Pumpkin.
// TODO(crbug.com/1264544): Could increase the hypotheses count from 1
// when we are ready to implement disambiguation.
this.sendToSandboxedPumpkinTagger_({
type: PumpkinConstants.ToPumpkinTaggerCommand.TAG,
text,
numResults: 1,
});
const taggerResults = await new Promise(resolve => {
this.tagResolver_ = resolve;
});
if (!taggerResults || taggerResults.hypothesisList.length === 0) {
return null;
}
return this.macroFromPumpkinHypothesis_(taggerResults.hypothesisList[0]);
}
/** @override */
isEnabled() {
return this.enabled;
}
/**
* @param {boolean} ready
* @private
*/
setPumpkinTaggerReady_(ready) {
this.pumpkinTaggerReady_ = ready;
if (this.onPumpkinTaggerReadyChangedForTesting_) {
this.onPumpkinTaggerReadyChangedForTesting_();
}
}
}