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 Implementation of the speech rule engine.
      7  *
      8  * The speech rule engine chooses and applies speech rules. Rules are chosen
      9  * from a set of rule stores wrt. their applicability to a node in a particular
     10  * markup type such as MathML or HTML. Rules are dispatched either by
     11  * recursively computing new nodes and applicable rules or, if no further rule
     12  * is applicable to a current node, by computing a speech object in the form of
     13  * an array of navigation descriptions.
     14  *
     15  * Consequently the rule engine is parameterisable wrt. rule stores and
     16  * evaluator function.
     17  */
     18 
     19 goog.provide('cvox.SpeechRuleEngine');
     20 
     21 goog.require('cvox.BaseRuleStore');
     22 goog.require('cvox.NavDescription');
     23 goog.require('cvox.NavMathDescription');
     24 goog.require('cvox.SpeechRule');
     25 
     26 
     27 /**
     28  * @constructor
     29  */
     30 cvox.SpeechRuleEngine = function() {
     31   /**
     32    * The currently active speech rule store.
     33    * @type {cvox.BaseRuleStore}
     34    * @private
     35    */
     36   this.activeStore_ = null;
     37 
     38   /**
     39    * Dynamic constraint annotation.
     40    * @type {!cvox.SpeechRule.DynamicCstr}
     41    */
     42   this.dynamicCstr = {};
     43   this.dynamicCstr[cvox.SpeechRule.DynamicCstrAttrib.STYLE] = 'short';
     44 };
     45 goog.addSingletonGetter(cvox.SpeechRuleEngine);
     46 
     47 
     48 /**
     49  * Parameterizes the speech rule engine.
     50  * @param {cvox.BaseRuleStore} store A speech rule store.
     51  */
     52 cvox.SpeechRuleEngine.prototype.parameterize = function(store) {
     53   try {
     54     store.initialize();
     55   } catch (err) {
     56     if (err.name == 'StoreError') {
     57       console.log('Store Error:', err.message);
     58     }
     59     else {
     60       throw err;
     61     }
     62   }
     63   this.activeStore_ = store;
     64 };
     65 
     66 
     67 /**
     68  * Parameterizes the dynamic constraint annotation for the speech rule
     69  * engine. This is a separate function as this can be done interactively, while
     70  * a particular speech rule store is active.
     71  * @param {cvox.SpeechRule.DynamicCstr} dynamic The new dynamic constraint.
     72  */
     73 cvox.SpeechRuleEngine.prototype.setDynamicConstraint = function(dynamic) {
     74   if (dynamic) {
     75     this.dynamicCstr = dynamic;
     76   }
     77 };
     78 
     79 
     80 /**
     81  * Constructs a string from the node and the given expression.
     82  * @param {!Node} node The initial node.
     83  * @param {string} expr An Xpath expression string, a name of a custom
     84  *     function or a string.
     85  * @return {string} The result of applying expression to node.
     86  */
     87 cvox.SpeechRuleEngine.prototype.constructString = function(node, expr) {
     88   if (!expr) {
     89     return '';
     90   }
     91   if (expr.charAt(0) == '"') {
     92     return expr.slice(1, -1);
     93   }
     94   var func = this.activeStore_.customStrings.lookup(expr);
     95   if (func) {
     96     // We always return the result of the custom function, in case it
     97     // deliberately computes the empty string!
     98     return func(node);
     99   }
    100   // Finally we assume expr to be an xpath expression and calculate a string
    101   // value from the node.
    102   return cvox.XpathUtil.evaluateString(expr, node);
    103 };
    104 
    105 
    106 // Dispatch functionality.
    107 /**
    108  * Computes a speech object for a given node. Returns the empty list if
    109  * no node is given.
    110  * @param {Node} node The node to be evaluated.
    111  * @return {!Array.<cvox.NavDescription>} A list of navigation descriptions for
    112  *   that node.
    113  */
    114 cvox.SpeechRuleEngine.prototype.evaluateNode = function(node) {
    115   if (!node) {
    116     return [];
    117   }
    118   return this.evaluateTree_(node);
    119 };
    120 
    121 
    122 /**
    123  * Applies rules recursively to compute the final speech object.
    124  * @param {!Node} node Node to apply the speech rule to.
    125  * @return {!Array.<cvox.NavDescription>} A list of Navigation descriptions.
    126  * @private
    127  */
    128 cvox.SpeechRuleEngine.prototype.evaluateTree_ = function(node) {
    129   var rule = this.activeStore_.lookupRule(node, this.dynamicCstr);
    130   if (!rule) {
    131     return this.activeStore_.evaluateDefault(node);
    132   }
    133   var components = rule.action.components;
    134   var result = [];
    135   for (var i = 0, component; component = components[i]; i++) {
    136     var navs = [];
    137     var content = component['content'] || '';
    138     switch (component.type) {
    139       case cvox.SpeechRule.Type.NODE:
    140         var selected = this.activeStore_.applyQuery(node, content);
    141         if (selected) {
    142           navs = this.evaluateTree_(selected);
    143         }
    144         break;
    145       case cvox.SpeechRule.Type.MULTI:
    146         selected = this.activeStore_.applySelector(node, content);
    147         if (selected.length > 0) {
    148           navs = this.evaluateNodeList_(
    149               selected,
    150               component['sepFunc'],
    151               this.constructString(node, component['separator']),
    152               component['ctxtFunc'],
    153               this.constructString(node, component['context']));
    154         }
    155         break;
    156       case cvox.SpeechRule.Type.TEXT:
    157         selected = this.constructString(node, content);
    158         if (selected) {
    159           navs = [new cvox.NavDescription({text: selected})];
    160         }
    161         break;
    162       case cvox.SpeechRule.Type.PERSONALITY:
    163       default:
    164         navs = [new cvox.NavDescription({text: content})];
    165     }
    166     // Adding overall context if it exists.
    167     if (navs[0] && component['context'] &&
    168         component.type != cvox.SpeechRule.Type.MULTI) {
    169       navs[0]['context'] =
    170           this.constructString(node, component['context']) +
    171               (navs[0]['context'] || '');
    172     }
    173     // Adding personality to the nav descriptions.
    174     result = result.concat(this.addPersonality_(navs, component));
    175   }
    176   return result;
    177 };
    178 
    179 
    180 /**
    181  * Evaluates a list of nodes into a list of navigation descriptions.
    182  * @param {!Array.<Node>} nodes Array of nodes.
    183  * @param {string} sepFunc Name of a function used to compute a separator
    184  *     between every element.
    185  * @param {string} separator A string that is used as argument to the sepFunc or
    186  *     interspersed directly between each node if sepFunc is not supplied.
    187  * @param {string} ctxtFunc Name of a function applied to compute the context
    188  *     for every element in the list.
    189  * @param {string} context Additional context string that is given to the
    190  *     ctxtFunc function or used directly if ctxtFunc is not supplied.
    191  * @return {Array.<cvox.NavDescription>} A list of Navigation descriptions.
    192  * @private
    193  */
    194 cvox.SpeechRuleEngine.prototype.evaluateNodeList_ = function(
    195     nodes, sepFunc, separator, ctxtFunc, context) {
    196   if (nodes == []) {
    197     return [];
    198   }
    199   var sep = separator || '';
    200   var cont = context || '';
    201   var cFunc = this.activeStore_.contextFunctions.lookup(ctxtFunc);
    202   var ctxtClosure = cFunc ? cFunc(nodes, cont) : function() {return cont;};
    203   var sFunc = this.activeStore_.contextFunctions.lookup(sepFunc);
    204   var sepClosure = sFunc ? sFunc(nodes, sep) : function() {return sep;};
    205   var result = [];
    206   for (var i = 0, node; node = nodes[i]; i++) {
    207     var navs = this.evaluateTree_(node);
    208     if (navs.length > 0) {
    209       navs[0]['context'] = ctxtClosure() + (navs[0]['context'] || '');
    210       result = result.concat(navs);
    211       if (i < nodes.length - 1) {
    212         var text = sepClosure();
    213         if (text) {
    214           result.push(new cvox.NavDescription({text: text}));
    215         }
    216       }
    217     }
    218   }
    219   return result;
    220 };
    221 
    222 
    223 /**
    224  * Maps properties in speech rules to personality properties.
    225  * @type {{pitch : string,
    226  *         rate: string,
    227  *         volume: string,
    228  *         pause: string}}
    229  * @const
    230  */
    231 cvox.SpeechRuleEngine.propMap = {'pitch': cvox.AbstractTts.RELATIVE_PITCH,
    232                                  'rate': cvox.AbstractTts.RELATIVE_RATE,
    233                                  'volume': cvox.AbstractTts.RELATIVE_VOLUME,
    234                                  'pause': cvox.AbstractTts.PAUSE
    235                                 };
    236 
    237 
    238 /**
    239  * Adds personality to every Navigation Descriptions in input list.
    240  * @param {Array.<cvox.NavDescription>} navs A list of Navigation descriptions.
    241  * @param {Object} props Property dictionary.
    242  * TODO (sorge) Fully specify, when we have finalised the speech rule
    243  * format.
    244  * @return {Array.<cvox.NavDescription>} The modified array.
    245  * @private
    246  */
    247 cvox.SpeechRuleEngine.prototype.addPersonality_ = function(navs, props) {
    248   var personality = {};
    249   for (var key in cvox.SpeechRuleEngine.propMap) {
    250     var value = parseFloat(props[key]);
    251     if (!isNaN(value)) {
    252       personality[cvox.SpeechRuleEngine.propMap[key]] = value;
    253     }
    254   }
    255   navs.forEach(goog.bind(function(nav) {
    256     this.addRelativePersonality_(nav, personality);
    257     this.resetPersonality_(nav);
    258   }, this));
    259   return navs;
    260 };
    261 
    262 
    263 /**
    264  * Adds relative personality entries to the personality of a Navigation
    265  * Description.
    266  * @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description.
    267  * @param {!Object} personality Dictionary with relative personality entries.
    268  * @return {cvox.NavDescription|cvox.NavMathDescription} Updated description.
    269  * @private
    270  */
    271 cvox.SpeechRuleEngine.prototype.addRelativePersonality_ = function(
    272     nav, personality) {
    273   if (!nav['personality']) {
    274     nav['personality'] = personality;
    275     return nav;
    276   }
    277   var navPersonality = nav['personality'];
    278   for (var p in personality) {
    279     // Although values could exceed boundaries, they will be limited to the
    280     // correct interval via the call to
    281     // cvox.AbstractTts.prototype.mergeProperties in
    282     // cvox.TtsBackground.prototype.speak
    283     if (navPersonality[p] && typeof(navPersonality[p]) == 'number') {
    284       navPersonality[p] = navPersonality[p] + personality[p];
    285     } else {
    286       navPersonality[p] = personality[p];
    287     }
    288   }
    289   return nav;
    290 };
    291 
    292 
    293 /**
    294  * Resets personalities to default values if necessary.
    295  * @param {cvox.NavDescription|cvox.NavMathDescription} nav Nav Description.
    296  * @private
    297  */
    298 cvox.SpeechRuleEngine.prototype.resetPersonality_ = function(nav) {
    299   if (this.activeStore_.defaultTtsProps) {
    300     for (var i = 0, prop; prop = this.activeStore_.defaultTtsProps[i]; i++) {
    301       nav.personality[prop] = cvox.ChromeVox.tts.getDefaultProperty(prop);
    302     }
    303   }
    304 };
    305 
    306 
    307 /**
    308  * Flag for the debug mode of the speech rule engine.
    309  * @type {boolean}
    310  */
    311 cvox.SpeechRuleEngine.debugMode = false;
    312 
    313 
    314 /**
    315  * Give debug output.
    316  * @param {...*} output Rest elements of debug output.
    317  */
    318 cvox.SpeechRuleEngine.outputDebug = function(output) {
    319   if (cvox.SpeechRuleEngine.debugMode) {
    320     var outputList = Array.prototype.slice.call(arguments, 0);
    321     console.log.apply(console,
    322                       ['Speech Rule Engine Debugger:'].concat(outputList));
    323   }
    324 };
    325 
    326 
    327 /**
    328  * Prints the list of all current rules in ChromeVox to the console.
    329  * @return {string} A textual representation of all rules in the speech rule
    330  *     engine.
    331  */
    332 cvox.SpeechRuleEngine.prototype.toString = function() {
    333   var allRules = this.activeStore_.findAllRules(function(x) {return true;});
    334   return allRules.map(function(rule) {return rule.toString();}).
    335     join('\n');
    336 };
    337 
    338 
    339 /**
    340  * Test the precondition of a speech rule in debugging mode.
    341  * @param {cvox.SpeechRule} rule A speech rule.
    342  * @param {!Node} node DOM node to test applicability of the rule.
    343  */
    344 cvox.SpeechRuleEngine.debugSpeechRule = function(rule, node) {
    345   var store = cvox.SpeechRuleEngine.getInstance().activeStore_;
    346   if (store) {
    347     var prec = rule.precondition;
    348     cvox.SpeechRuleEngine.outputDebug(
    349         prec.query, store.applyQuery(node, prec.query));
    350     prec.constraints.forEach(
    351         function(cstr) {
    352           cvox.SpeechRuleEngine.outputDebug(
    353               cstr, store.applyConstraint(node, cstr));});
    354   }
    355 };
    356 
    357 
    358 /**
    359  * Test the precondition of a speech rule in debugging mode.
    360  * @param {string} name Rule to debug.
    361  * @param {!Node} node DOM node to test applicability of the rule.
    362  */
    363 cvox.SpeechRuleEngine.debugNamedSpeechRule = function(name, node) {
    364   var store = cvox.SpeechRuleEngine.getInstance().activeStore_;
    365   var allRules = store.findAllRules(
    366     function(rule) {return rule.name == name;});
    367   for (var i = 0, rule; rule = allRules[i]; i++) {
    368     cvox.SpeechRuleEngine.outputDebug('Rule', name, 'number', i);
    369     cvox.SpeechRuleEngine.debugSpeechRule(rule, node);
    370   }
    371 };
    372