Home Manual Reference Source

src/administrative-sdk/choice-recognition/choice-recognition-controller.js

import Base64Utils from '../utils/base64-utils';
import ChoiceChallenge from '../choice-challenge/choice-challenge';
import ChoiceRecognition from './choice-recognition';
import Connection from '../connection/connection-controller';
import when from 'when';
/**
 * Controller class for the ChoiceRecognition model.
 * @private
 */
export default class ChoiceRecognitionController {
  /**
   * @param {Connection} connection - Object to use for making a connection to the REST API and Websocket server.
   */
  constructor(connection) {
    /**
     * Object to use for making a connection to the REST API and Websocket server.
     * @type {Connection}
     */
    this._connection = connection;
  }

  /**
   * Initialise the choice recognition challenge through RPCs.
   *
   * @param {ChoiceChallenge} challenge - ChoiceChallenge.
   * @private
   */
  choiceRecognitionInitChallenge(challenge) {
    return this._connection.call('choice.init_challenge',
      [this._connection._recognitionId, challenge.id])
      .then(
        // RPC success callback
        recognitionId => {
          console.log('Challenge initialised for recognitionId: ' + this._connection._recognitionId);
          return recognitionId;
        });
  }

  /**
   * Initialise the pronunciation analysis audio specs through RPCs.
   *
   * @param {AudioRecorder} recorder - AudioRecorder.
   * @param {Function} dataavailableCb - Callback.
   * @private
   */
  choiceRecognitionInitAudio(recorder, dataavailableCb) {
    // Indicate to the socket server that we're about to start recording a
    // challenge. This allows the socket server some time to fetch the metadata
    // and reference audio to start the analysis when audio is actually submitted.
    const specs = recorder.getAudioSpecs();
    return this._connection.call('choice.init_audio',
      [this._connection._recognitionId, specs.audioFormat], specs.audioParameters).then(
      // RPC success callback
      recognitionId => {
        console.log('Accepted audio parameters for recognitionId after init_audio: ' + this._connection._recognitionId);
        // Start listening for streaming data.
        recorder.addEventListener('dataavailable', dataavailableCb);
        return recognitionId;
      });
  }

  /**
   * Start a choice recognition from streaming audio.
   *
   * @param {ChoiceChallenge} challenge - The choice challenge to perform.
   * @param {AudioRecorder} recorder - The audio recorder to extract audio from.
   * @param {boolean} [trim=true] - Whether to trim the start and end of recorded audio.
   * @returns {Promise.<ChoiceRecognition>} A {@link https://github.com/cujojs/when} Promise containing a {@link ChoiceRecognition}.
   * @emits {string} 'ReadyToReceive' when the call is made to receive audio. The recorder can now send audio.
   * @throws {Promise.<Error>} challenge parameter of type "ChoiceChallenge" is required.
   * @throws {Promise.<Error>} challenge.id field of type "string" is required.
   * @throws {Promise.<Error>} If the connection is not open.
   * @throws {Promise.<Error>} If the recorder is already recording.
   * @throws {Promise.<Error>} If a recognition session is already in progress.
   * @throws {Promise.<Error>} If something went wrong during analysis.
   */
  startStreamingChoiceRecognition(challenge, recorder, trim) {
    if (!(challenge instanceof ChoiceChallenge)) {
      return Promise.reject(new Error('challenge parameter of type "ChoiceChallenge" is required'));
    }

    if (typeof challenge.id !== 'string') {
      return Promise.reject(new Error('challenge.id field of type "string" is required'));
    }

    // Validate environment prerequisites.
    if (!this._connection._session) {
      return Promise.reject(new Error('WebSocket connection was not open.'));
    }

    if (recorder.isRecording()) {
      return Promise.reject(new Error('Recorder should not yet be recording.'));
    }

    if (this._connection._recognitionId !== null) {
      return Promise.reject(new Error('Session with recognitionId ' + this._connection._recognitionId +
        ' still in progress.'));
    }

    const self = this;
    let trimAudioStart = 0.15;
    const trimAudioEnd = 0.0;
    if (trim === false) {
      trimAudioStart = 0.0;
    }
    return new when.Promise((resolve, reject, notify) => {
      function _cb(data) {
        const recognition = new ChoiceRecognition(
          challenge.id, data.userId, data.id,
          new Date(data.created), new Date(data.updated),
          self._connection.addAccessToken(data.audioUrl), data.recognised);
        resolve({recognitionId: self._connection._recognitionId, recognition});
      }

      function _ecb(data) {
        // There was an unexpected error.
        const recognition = new ChoiceRecognition(
          challenge.id, data.userId, data.id,
          new Date(data.created), new Date(data.updated),
          self._connection.addAccessToken(data.audioUrl), null);
        reject(
          {
            recognition,
            message: data.message
          }
        );
      }

      self._connection._recognitionId = null;

      // Start streaming the binary audio when the user instructs
      // the audio recorder to start recording.
      function dataavailableCb(chunk) {
        const encoded = Base64Utils._arrayBufferToBase64(chunk);
        console.log('Sending audio chunk to websocket for recognitionId: ' +
          self._connection._recognitionId);
        self._connection.call('choice.write',
          [self._connection._recognitionId, encoded, 'base64'])
          .catch(res => {
            console.error('RPC error returned:', res.error);
            _ecb(res);
          });
      }

      function recognitionInitCb(recognitionId) {
        self._connection._recognitionId = recognitionId;
        console.log('Got recognitionId after initialisation: ' + self._connection._recognitionId);
      }
      self._connection.call('choice.init_recognition', [],
        {
          trimStart: trimAudioStart,
          trimEnd: trimAudioEnd
        })
        .then(recognitionInitCb)
        .then(() =>
          self.choiceRecognitionInitChallenge(challenge)
            .then(() => {
              const p = new Promise(resolve_ => {
                if (recorder.hasUserMediaApproval()) {
                  resolve_();
                } else {
                  recorder.addEventListener('ready', resolve_);
                }
              });
              p.then(() => {
                self.choiceRecognitionInitAudio(recorder, dataavailableCb)
                  .catch(reject);
              });
            })
            .then(() => notify('ReadyToReceive'))
        )
        .catch(reject);

      // Stop listening when the audio recorder stopped.
      function recordedCb() {
        // When done, submit any plain text (non-JSON) to start analysing.
        self._connection.call('choice.recognise',
          [self._connection._recognitionId]).then(
          // RPC success callback
          res => {
            // Wait for analysis results to come back.
            _cb(res);
          },
          // RPC error callback
          res => {
            console.error('RPC error returned:', res.error);
            if (res.error === 'nl.itslanguage.recognition_failed') {
              res.kwargs.recognition.message = 'Recognition failed';
            } else {
              res.kwargs.recognition.message = 'Unhandled error';
            }
            _ecb(res.kwargs.analysis);
          });

        recorder.removeEventListener('recorded', recordedCb);
        recorder.removeEventListener('dataavailable', dataavailableCb);
      }
      recorder.addEventListener('recorded', recordedCb);
    })
      .then(res => {
        self._connection._recognitionId = null;
        return Promise.resolve(res);
      })
      .catch(error => {
        self._connection._recognitionId = null;
        Connection.logRPCError(error);
        return Promise.reject(error);
      });
  }

  /**
   * Get a choice recognition in a choice challenge from the current active {@link Organisation} derived from
   * the OAuth2 scope.
   *
   * @param {string} challengeId - Specify a choice challenge identifier.
   * @param {string} recognitionId - Specify a choice recognition identifier.
   * @returns {Promise.<ChoiceRecognition>} Promise containing a ChoiceRecognition.
   * @throws {Promise.<Error>} challengeId parameter of type "string" is required.
   * @throws {Promise.<Error>} recognitionId parameter of type "string" is required.
   * @throws {Promise.<Error>} If no result could not be found.
   */
  getChoiceRecognition(challengeId, recognitionId) {
    if (typeof challengeId !== 'string') {
      return Promise.reject(new Error('challengeId parameter of type "string" is required'));
    }

    if (typeof recognitionId !== 'string') {
      return Promise.reject(new Error('recognitionId parameter of type "string" is required'));
    }
    const url = this._connection._settings.apiUrl + '/challenges/choice/' +
      challengeId + '/recognitions/' + recognitionId;

    return this._connection._secureAjaxGet(url)
      .then(datum => new ChoiceRecognition(challengeId, datum.userId,
        datum.id, new Date(datum.created), new Date(datum.updated),
        datum.audioUrl, datum.recognised));
  }

  /**
   * Get and return all choice recognitions in a specific {@link ChoiceChallenge} from the current active
   * {@link Organisation} derived from the OAuth2 scope.
   *
   * @param {string} challengeId - Specify a choice challenge to list speech recognitions for.
   * @returns {Promise.<ChoiceRecognition[]>} Promise containing an array of ChoiceRecognitions.
   * @throws {Promise.<Error>} challengeId parameter of type "string" is required.
   * @throws {Promise.<Error>} If no result could not be found.
   */
  getChoiceRecognitions(challengeId) {
    if (typeof challengeId !== 'string') {
      return Promise.reject(new Error('challengeId parameter of type "string" is required'));
    }
    const url = this._connection._settings.apiUrl + '/challenges/choice/' +
      challengeId + '/recognitions';
    return this._connection._secureAjaxGet(url)
      .then(data => data.map(datum => {
        const recognition = new ChoiceRecognition(challengeId, datum.userId,
          datum.id, new Date(datum.created), new Date(datum.updated),
          datum.audioUrl, datum.recognised);
        return recognition;
      }));
  }
}