chromium/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/parse/sandboxed_pumpkin_tagger.js

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as PumpkinConstants from './pumpkin/pumpkin_constants.js';

/**
 * A class that unpacks and loads the pumpkin semantic parser. Runs in a
 * web worker for security purposes.
 */
class SandboxedPumpkinTagger {
  constructor() {
    /** @private {?speech.pumpkin.api.js.PumpkinTagger.PumpkinTagger} */
    this.pumpkinTagger_ = null;
    /** @private {?PumpkinConstants.PumpkinData} */
    this.pumpkinData_ = null;
    this.init_();
  }

  /** @private */
  init_() {
    globalThis.addEventListener(
        'message', (message) => this.onMessage_(message));
    this.sendToBackground_(
        {type: PumpkinConstants.FromPumpkinTaggerCommand.READY});
  }

  /**
   * Called when the background context posts a message to
   * SandboxedPumpkinTagger's web worker.
   * @param {!Event} message
   * @private
   */
  onMessage_(message) {
    const command =
        /** @type {!PumpkinConstants.ToPumpkinTagger} */ (message.data);
    switch (command.type) {
      case PumpkinConstants.ToPumpkinTaggerCommand.LOAD:
        const pumpkinData =
            /** @type {!PumpkinConstants.PumpkinData} */ (command.pumpkinData);
        const locale =
            /** @type {!PumpkinConstants.PumpkinLocale} */ (command.locale);
        this.load_(pumpkinData, locale);
        return;
      case PumpkinConstants.ToPumpkinTaggerCommand.TAG:
        const text = /** @type {string} */ (command.text);
        const numResults = /** @type {number} */ (command.numResults);
        this.tagAndGetNBestHypotheses_(text, numResults);
        return;
      case PumpkinConstants.ToPumpkinTaggerCommand.REFRESH:
        this.refresh_(
            /** @type {!PumpkinConstants.PumpkinLocale} */ (command.locale));
        return;
    }

    throw new Error(`Unrecognized message received in SandboxedPumpkinTagger: ${
        command.type}`);
  }

  /**
   * @param {!PumpkinConstants.FromPumpkinTagger} command
   * @private
   */
  sendToBackground_(command) {
    postMessage(command);
  }

  /**
   * @param {string} text
   * @param {number} numResults
   * @private
   */
  tagAndGetNBestHypotheses_(text, numResults) {
    const results =
        this.pumpkinTagger_.tagAndGetNBestHypotheses(text, numResults);
    this.sendToBackground_(
        {type: PumpkinConstants.FromPumpkinTaggerCommand.TAG_RESULTS, results});
  }

  /**
   * @param {!PumpkinConstants.PumpkinData} data
   * @param {!PumpkinConstants.PumpkinLocale} locale
   * @private
   */
  async load_(data, locale) {
    if (!data) {
      throw new Error(`Can't load pumpkin tagger from empty data`);
    }

    this.pumpkinData_ = data;
    // Unpack the PumpkinTagger JS.
    const pumpkinTaggerBytes = data.js_pumpkin_tagger_bin_js;
    if (!pumpkinTaggerBytes) {
      throw new Error(`Pumpkin tagger bytes must be valid`);
    }
    const pumpkinTaggerFile = new TextDecoder().decode(pumpkinTaggerBytes);
    // Use indirect eval here to ensure the script works in the global scope.
    const indirectEval = eval;
    const pumpkinTaggerModule = indirectEval(pumpkinTaggerFile);
    if (!pumpkinTaggerModule) {
      throw new Error('Failed to eval pumpkin tagger file');
    }
    /**
     * Closure can't recognize pumpkinTaggerModule as a constructor, so suppress
     * the error.
     * @suppress {checkTypes}
     */
    const pumpkinTagger = new pumpkinTaggerModule();

    // The `taggerWasmJsFile` below expects that the corresponding .wasm file
    // lives in the same directory as it. However, since none of these files
    // live in the extension directory, we need to override the fetch method
    // so that it returns the wasm file bytes from `data` when requested.
    globalThis.fetch = async (fileName) => {
      return new Promise(resolve => {
        const response = new Response(null, {
          ok: true,
          status: 200,
        });
        response.arrayBuffer = async () => {
          return new Promise(resolve => {
            resolve(data.tagger_wasm_main_wasm);
          });
        };
        resolve(response);
      });
    };

    const taggerWasmBytes = data.tagger_wasm_main_js;
    if (!taggerWasmBytes) {
      throw new Error(`Pumpkin wasm bytes must be valid`);
    }
    const taggerWasmJsFile = new TextDecoder().decode(taggerWasmBytes);
    // A promise that resolves once the web assembly module loads.
    const wasmLoadPromise = new Promise((resolve) => {
      globalThis['goog']['global']['Module'] = {
        onRuntimeInitialized() {
          resolve();
        },
      };
    });
    // Load the web assembly.
    // Use indirect eval here to ensure the script works in the global scope.
    indirectEval(taggerWasmJsFile);
    await wasmLoadPromise;

    // Initialize from config files.
    const {pumpkinConfig, actionConfig} = this.getConfigsForLocale(locale);
    pumpkinTagger.initializeFromPumpkinConfig(pumpkinConfig);
    pumpkinTagger.loadActionFrame(actionConfig);

    // Save the PumpkinTagger and notify the background context.
    this.pumpkinTagger_ = pumpkinTagger;
    this.sendToBackground_(
        {type: PumpkinConstants.FromPumpkinTaggerCommand.FULLY_INITIALIZED});
  }

  /**
   * Refreshes SandboxedPumpkinTagger in a new locale.
   * @param {!PumpkinConstants.PumpkinLocale} locale
   */
  refresh_(locale) {
    if (!this.pumpkinTagger_) {
      throw new Error(
          'SandboxedPumpkinTagger must be initialized before calling refresh');
    }

    const {pumpkinConfig, actionConfig} = this.getConfigsForLocale(locale);
    this.pumpkinTagger_.initializeFromPumpkinConfig(pumpkinConfig);
    this.pumpkinTagger_.loadActionFrame(actionConfig);
    this.sendToBackground_({
      type: PumpkinConstants.FromPumpkinTaggerCommand.REFRESHED,
    });
  }

  /**
   * @param {!PumpkinConstants.PumpkinLocale} locale
   * @return {!{pumpkinConfig: !ArrayBuffer, actionConfig: !ArrayBuffer}}
   */
  getConfigsForLocale(locale) {
    let pumpkinConfig;
    let actionConfig;
    switch (locale) {
      case PumpkinConstants.PumpkinLocale.EN_US:
        pumpkinConfig = this.pumpkinData_.en_us_pumpkin_config_binarypb;
        actionConfig = this.pumpkinData_.en_us_action_config_binarypb;
        break;
      case PumpkinConstants.PumpkinLocale.FR_FR:
        pumpkinConfig = this.pumpkinData_.fr_fr_pumpkin_config_binarypb;
        actionConfig = this.pumpkinData_.fr_fr_action_config_binarypb;
        break;
      case PumpkinConstants.PumpkinLocale.IT_IT:
        pumpkinConfig = this.pumpkinData_.it_it_pumpkin_config_binarypb;
        actionConfig = this.pumpkinData_.it_it_action_config_binarypb;
        break;
      case PumpkinConstants.PumpkinLocale.DE_DE:
        pumpkinConfig = this.pumpkinData_.de_de_pumpkin_config_binarypb;
        actionConfig = this.pumpkinData_.de_de_action_config_binarypb;
        break;
      case PumpkinConstants.PumpkinLocale.ES_ES:
        pumpkinConfig = this.pumpkinData_.es_es_pumpkin_config_binarypb;
        actionConfig = this.pumpkinData_.es_es_action_config_binarypb;
        break;
      default:
        throw new Error(
            `Can't initialize Pumpkin in unsupported locale: ${locale}`);
    }

    if (!pumpkinConfig || !actionConfig) {
      throw new Error(
          `Either pumpkinConfig or actionConfig is invalid for locale: ${
              locale}`);
    }

    return {pumpkinConfig, actionConfig};
  }
}

new SandboxedPumpkinTagger();