Home | History | Annotate | Download | only in speech_rules
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview An interface definition of a speech rule.
      7  *
      8  * A speech rule is a data structure along with supporting methods that
      9  * stipulates how to transform a tree structure such as XML, a browser DOM, or
     10  * HTML into a format (usually strings) suitable for rendering by a
     11  * text-to-speech engine.
     12  *
     13  * Speech rules consists of a variable number of speech rule components. Each
     14  * component describes how to construct a single utterance. Text-to-speech
     15  * renders the components in order.
     16  */
     17 
     18 goog.provide('cvox.SpeechRule');
     19 goog.provide('cvox.SpeechRule.Action');
     20 goog.provide('cvox.SpeechRule.Component');
     21 goog.provide('cvox.SpeechRule.DynamicCstr');
     22 goog.provide('cvox.SpeechRule.Precondition');
     23 goog.provide('cvox.SpeechRule.Type');
     24 
     25 
     26 /**
     27  * Creates a speech rule with precondition, actions and admin information.
     28  * @constructor
     29  * @param {string} name The name of the rule.
     30  * @param {cvox.SpeechRule.DynamicCstr} dynamic Dynamic constraint annotations
     31  *     of the rule.
     32  * @param {cvox.SpeechRule.Precondition} prec Precondition of the rule.
     33  * @param {cvox.SpeechRule.Action} action Action of the speech rule.
     34  */
     35 cvox.SpeechRule = function(name, dynamic, prec, action) {
     36   /** @type {string} */
     37   this.name = name;
     38   /** @type {cvox.SpeechRule.DynamicCstr} */
     39   this.dynamicCstr = dynamic;
     40   /** @type {cvox.SpeechRule.Precondition} */
     41   this.precondition = prec;
     42   /** @type {cvox.SpeechRule.Action} */
     43   this.action = action;
     44 };
     45 
     46 
     47 /**
     48  *
     49  * @override
     50  */
     51 cvox.SpeechRule.prototype.toString = function() {
     52   var cstrStrings = [];
     53   for (var key in this.dynamicCstr) {
     54     cstrStrings.push(this.dynamicCstr[key]);
     55   }
     56   return this.name + ' | ' + cstrStrings.join('.') + ' | ' +
     57     this.precondition.toString() + ' ==> ' +
     58     this.action.toString();
     59 };
     60 
     61 
     62 /**
     63  * Mapping for types of speech rule components.
     64  * @enum {string}
     65  */
     66 cvox.SpeechRule.Type = {
     67   NODE: 'NODE',
     68   MULTI: 'MULTI',
     69   TEXT: 'TEXT',
     70   PERSONALITY: 'PERSONALITY'
     71 };
     72 
     73 
     74 /**
     75  * Maps a string to a valid speech rule type.
     76  * @param {string} str Input string.
     77  * @return {cvox.SpeechRule.Type}
     78  */
     79 cvox.SpeechRule.Type.fromString = function(str) {
     80   switch (str) {
     81     case '[n]': return cvox.SpeechRule.Type.NODE;
     82     case '[m]': return cvox.SpeechRule.Type.MULTI;
     83     case '[t]': return cvox.SpeechRule.Type.TEXT;
     84     case '[p]': return cvox.SpeechRule.Type.PERSONALITY;
     85     default: throw 'Parse error: ' + str;
     86   }
     87 };
     88 
     89 
     90 /**
     91  * Maps a speech rule type to a human-readable string.
     92  * @param {cvox.SpeechRule.Type} speechType
     93  * @return {string} Output string.
     94  */
     95 cvox.SpeechRule.Type.toString = function(speechType) {
     96   switch (speechType) {
     97     case cvox.SpeechRule.Type.NODE: return '[n]';
     98     case cvox.SpeechRule.Type.MULTI: return '[m]';
     99     case cvox.SpeechRule.Type.TEXT: return '[t]';
    100     case cvox.SpeechRule.Type.PERSONALITY: return '[p]';
    101     default: throw 'Unknown type error: ' + speechType;
    102   }
    103 };
    104 
    105 
    106 /**
    107  * Defines a component within a speech rule.
    108  * @param {{type: cvox.SpeechRule.Type, content: string}} kwargs The input
    109  * component in JSON format.
    110  * @constructor
    111  */
    112 cvox.SpeechRule.Component = function(kwargs) {
    113   /** @type {cvox.SpeechRule.Type} */
    114   this.type = kwargs.type;
    115 
    116   /** @type {string} */
    117   this.content = kwargs.content;
    118 };
    119 
    120 
    121 /**
    122  * Parses a valid string representation of a speech component into a Component
    123  * object.
    124  * @param {string} input The input string.
    125  * @return {cvox.SpeechRule.Component} The resulting component.
    126  */
    127 cvox.SpeechRule.Component.fromString = function(input) {
    128   // The output JSON.
    129   var output = {};
    130 
    131   // Parse the type.
    132   output.type = cvox.SpeechRule.Type.fromString(input.substring(0, 3));
    133 
    134   // Prep the rest of the parsing.
    135   var rest = input.slice(3).trimLeft();
    136   if (!rest) {
    137     throw new cvox.SpeechRule.OutputError('Missing content.');
    138   }
    139 
    140   switch (output.type) {
    141     case cvox.SpeechRule.Type.TEXT:
    142       if (rest[0] == '"') {
    143         var quotedString = cvox.SpeechRule.splitString_(rest, '\\(')[0].trim();
    144         if (quotedString.slice(-1) != '"') {
    145           throw new cvox.SpeechRule.OutputError('Invalid string syntax.');
    146         }
    147         output.content = quotedString;
    148         rest = rest.slice(quotedString.length).trim();
    149         if (rest.indexOf('(') == -1) {
    150           rest = '';
    151         }
    152         // This break is conditional. If the content is not an explicit string,
    153         // it can be treated like node and multi type.
    154         break;
    155       }
    156     case cvox.SpeechRule.Type.NODE:
    157     case cvox.SpeechRule.Type.MULTI:
    158       var bracket = rest.indexOf(' (');
    159       if (bracket == -1) {
    160         output.content = rest.trim();
    161         rest = '';
    162         break;
    163       }
    164       output.content = rest.substring(0, bracket).trim();
    165       rest = rest.slice(bracket).trimLeft();
    166     break;
    167   }
    168   output = new cvox.SpeechRule.Component(output);
    169   if (rest) {
    170     output.addAttributes(rest);
    171   }
    172   return output;
    173 };
    174 
    175 
    176 /**
    177  * @override
    178  */
    179 cvox.SpeechRule.Component.prototype.toString = function() {
    180   var strs = '';
    181   strs += cvox.SpeechRule.Type.toString(this.type);
    182   strs += this.content ? ' ' +  this.content : '';
    183   var attribs = this.getAttributes();
    184   if (attribs.length > 0) {
    185     strs += ' (' + attribs.join(', ') + ')';
    186   }
    187   return strs;
    188 };
    189 
    190 
    191 /**
    192  * Adds a single attribute to the component.
    193  * @param {string} attr String representation of an attribute.
    194  */
    195 cvox.SpeechRule.Component.prototype.addAttribute = function(attr) {
    196   var colon = attr.indexOf(':');
    197   if (colon == -1) {
    198     this[attr.trim()] = 'true';
    199   } else {
    200     this[attr.substring(0, colon).trim()] = attr.slice(colon + 1).trim();
    201   }
    202 };
    203 
    204 
    205 /**
    206  * Adds a list of attributes to the component.
    207  * @param {string} attrs String representation of attribute list.
    208  */
    209 cvox.SpeechRule.Component.prototype.addAttributes = function(attrs) {
    210   if (attrs[0] != '(' || attrs.slice(-1) != ')') {
    211     throw new cvox.SpeechRule.OutputError(
    212         'Invalid attribute expression: ' + attrs);
    213   }
    214   var attribs = cvox.SpeechRule.splitString_(attrs.slice(1, -1), ',');
    215   for (var i = 0; i < attribs.length; i++) {
    216     this.addAttribute(attribs[i]);
    217   }
    218 };
    219 
    220 
    221 /**
    222  * Transforms the attributes of an object into a list of strings.
    223  * @return {Array.<string>} List of translated attribute:value strings.
    224  */
    225 cvox.SpeechRule.Component.prototype.getAttributes = function() {
    226   var attribs = [];
    227   for (var key in this) {
    228     if (key != 'content' && key != 'type' && typeof(this[key]) != 'function') {
    229       attribs.push(key + ':' + this[key]);
    230     }
    231   }
    232   return attribs;
    233 };
    234 
    235 
    236 /**
    237  * A speech rule is a collection of speech components.
    238  * @param {Array.<cvox.SpeechRule.Component>} components The input rule.
    239  * @constructor
    240  */
    241 cvox.SpeechRule.Action = function(components) {
    242   /** @type {Array.<cvox.SpeechRule.Component>} */
    243   this.components = components;
    244 };
    245 
    246 
    247 /**
    248  * Parses an input string into a speech rule class object.
    249  * @param {string} input The input string.
    250  * @return {cvox.SpeechRule.Action} The resulting object.
    251  */
    252 cvox.SpeechRule.Action.fromString = function(input) {
    253   var comps = cvox.SpeechRule.splitString_(input, ';')
    254       .filter(function(x) {return x.match(/\S/);})
    255       .map(function(x) {return x.trim();});
    256   var newComps = [];
    257   for (var i = 0; i < comps.length; i++) {
    258     var comp = cvox.SpeechRule.Component.fromString(comps[i]);
    259     if (comp) {
    260       newComps.push(comp);
    261     }
    262   }
    263 return new cvox.SpeechRule.Action(newComps);
    264 };
    265 
    266 
    267 /**
    268  * @override
    269  */
    270 cvox.SpeechRule.Action.prototype.toString = function() {
    271   var comps = this.components.map(function(c) { return c.toString(); });
    272   return comps.join('; ');
    273 };
    274 
    275 
    276 // TODO (sorge) Separatation of xpath expressions and custom functions.
    277 // Also test validity of xpath expressions.
    278 /**
    279  * Constructs a valid precondition for a speech rule.
    280  * @param {string} query A node selector function or xpath expression.
    281  * @param {Array.<string>=} opt_constraints A list of constraint functions.
    282  * @constructor
    283  */
    284 cvox.SpeechRule.Precondition = function(query, opt_constraints) {
    285   /** @type {string} */
    286   this.query = query;
    287 
    288   /** @type {!Array.<string>} */
    289   this.constraints = opt_constraints || [];
    290 };
    291 
    292 
    293 /**
    294  * @override
    295  */
    296 cvox.SpeechRule.Precondition.prototype.toString = function() {
    297   var constrs = this.constraints.join(', ');
    298   return this.query + ', ' + constrs;
    299 };
    300 
    301 
    302 /**
    303  * Split a string wrt. a given separator symbol while not splitting inside of a
    304  * double quoted string. For example, splitting
    305  * '[t] "matrix; 3 by 3"; [n] ./*[1]' with separators ';' would yield
    306  * ['[t] "matrix; 3 by 3"', ' [n] ./*[1]'].
    307  * @param {string} str String to be split.
    308  * @param {string} sep Separator symbol.
    309  * @return {Array.<string>} A list of single component strings.
    310  * @private
    311  */
    312 cvox.SpeechRule.splitString_ = function(str, sep) {
    313   var strList = [];
    314   var prefix = '';
    315 
    316   while (str != '') {
    317     var sepPos = str.search(sep);
    318     if (sepPos == -1) {
    319       if ((str.match(/"/g) || []).length % 2 != 0) {
    320         throw new cvox.SpeechRule.OutputError(
    321             'Invalid string in expression: ' + str);
    322       }
    323       strList.push(prefix + str);
    324       prefix = '';
    325       str = '';
    326     } else if (
    327         (str.substring(0, sepPos).match(/"/g) || []).length % 2 == 0) {
    328       strList.push(prefix + str.substring(0, sepPos));
    329       prefix = '';
    330       str = str.substring(sepPos + 1);
    331     } else {
    332       var nextQuot = str.substring(sepPos).search('"');
    333       if (nextQuot == -1) {
    334         throw new cvox.SpeechRule.OutputError(
    335             'Invalid string in expression: ' + str);
    336       } else {
    337         prefix = prefix + str.substring(0, sepPos + nextQuot + 1);
    338         str = str.substring(sepPos + nextQuot + 1);
    339       }
    340     }
    341   }
    342   if (prefix) {
    343     strList.push(prefix);
    344   }
    345   return strList;
    346 };
    347 
    348 
    349 /**
    350  * Attributes for dynamic constraints.
    351  * We define one default attribute as style. Speech rule stores can add other
    352  * attributes later.
    353  * @enum {string}
    354  */
    355 cvox.SpeechRule.DynamicCstrAttrib =
    356 {
    357   STYLE: 'style'
    358 };
    359 
    360 
    361 /**
    362  * Dynamic constraints are a means to specialize rules that can be changed
    363  * dynamically by the user, for example by choosing different styles, etc.
    364  * @typedef {!Object.<cvox.SpeechRule.DynamicCstrAttrib, string>}
    365  */
    366 cvox.SpeechRule.DynamicCstr;
    367 
    368 
    369 /**
    370  * Error object for signaling parsing errors.
    371  * @param {string} msg The error message.
    372  * @constructor
    373  * @extends {Error}
    374  */
    375 cvox.SpeechRule.OutputError = function(msg) {
    376   this.name = 'RuleError';
    377   this.message = msg || '';
    378 };
    379 goog.inherits(cvox.SpeechRule.OutputError, Error);
    380