blob: e0a744dcabad89649afecb887331e7eb86278f4d [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Base class for Text-to-Speech engines that actually transform
* text to speech.
*
*/
goog.provide('cvox.AbstractTts');
goog.require('cvox.TtsInterface');
goog.require('goog.i18n.MessageFormat');
/**
* Creates a new instance.
* @constructor
* @implements {cvox.TtsInterface}
*/
cvox.AbstractTts = function() {
this.ttsProperties = new Object();
/**
* Default value for TTS properties.
* Note that these as well as the subsequent properties might be different
* on different host platforms (like Chrome, Android, etc.).
* @type {{pitch : number,
* rate: number,
* volume: number}}
* @protected
*/
this.propertyDefault = {
'rate': 0.5,
'pitch': 0.5,
'volume': 0.5
};
/**
* Min value for TTS properties.
* @type {{pitch : number,
* rate: number,
* volume: number}}
* @protected
*/
this.propertyMin = {
'rate': 0.0,
'pitch': 0.0,
'volume': 0.0
};
/**
* Max value for TTS properties.
* @type {{pitch : number,
* rate: number,
* volume: number}}
* @protected
*/
this.propertyMax = {
'rate': 1.0,
'pitch': 1.0,
'volume': 1.0
};
/**
* Step value for TTS properties.
* @type {{pitch : number,
* rate: number,
* volume: number}}
* @protected
*/
this.propertyStep = {
'rate': 0.1,
'pitch': 0.1,
'volume': 0.1
};
/** @private */
if (cvox.AbstractTts.pronunciationDictionaryRegexp_ == undefined) {
// Create an expression that matches all words in the pronunciation
// dictionary on word boundaries, ignoring case.
var words = [];
for (var word in cvox.AbstractTts.PRONUNCIATION_DICTIONARY) {
words.push(word);
}
var expr = '\\b(' + words.join('|') + ')\\b';
cvox.AbstractTts.pronunciationDictionaryRegexp_ = new RegExp(expr, 'ig');
}
if (cvox.AbstractTts.substitutionDictionaryRegexp_ == undefined) {
// Create an expression that matches all words in the substitution
// dictionary.
var symbols = [];
for (var symbol in cvox.AbstractTts.SUBSTITUTION_DICTIONARY) {
symbols.push(symbol);
}
var expr = '(' + symbols.join('|') + ')';
cvox.AbstractTts.substitutionDictionaryRegexp_ = new RegExp(expr, 'ig');
}
};
/**
* Default TTS properties for this TTS engine.
* @type {Object}
* @protected
*/
cvox.AbstractTts.prototype.ttsProperties;
/** @override */
cvox.AbstractTts.prototype.speak = function(textString, queueMode, properties) {
return this;
};
/** @override */
cvox.AbstractTts.prototype.isSpeaking = function() {
return false;
};
/** @override */
cvox.AbstractTts.prototype.stop = function() {
};
/** @override */
cvox.AbstractTts.prototype.addCapturingEventListener = function(listener) { };
/** @override */
cvox.AbstractTts.prototype.increaseOrDecreaseProperty =
function(propertyName, increase) {
var min = this.propertyMin[propertyName];
var max = this.propertyMax[propertyName];
var step = this.propertyStep[propertyName];
var current = this.ttsProperties[propertyName];
current = increase ? current + step : current - step;
this.ttsProperties[propertyName] = Math.max(Math.min(current, max), min);
};
/**
* Merges the given properties with the default ones. Always returns a
* new object, so that you can safely modify the result of mergeProperties
* without worrying that you're modifying an object used elsewhere.
* @param {Object=} properties The properties to merge with the current ones.
* @return {Object} The merged properties.
* @protected
*/
cvox.AbstractTts.prototype.mergeProperties = function(properties) {
var mergedProperties = new Object();
var p;
if (this.ttsProperties) {
for (p in this.ttsProperties) {
mergedProperties[p] = this.ttsProperties[p];
}
}
if (properties) {
var tts = cvox.AbstractTts;
if (typeof(properties[tts.VOLUME]) == 'number') {
mergedProperties[tts.VOLUME] = properties[tts.VOLUME];
}
if (typeof(properties[tts.PITCH]) == 'number') {
mergedProperties[tts.PITCH] = properties[tts.PITCH];
}
if (typeof(properties[tts.RATE]) == 'number') {
mergedProperties[tts.RATE] = properties[tts.RATE];
}
if (typeof(properties[tts.LANG]) == 'string') {
mergedProperties[tts.LANG] = properties[tts.LANG];
}
var context = this;
var mergeRelativeProperty = function(abs, rel) {
if (typeof(properties[rel]) == 'number' &&
typeof(mergedProperties[abs]) == 'number') {
mergedProperties[abs] += properties[rel];
var min = context.propertyMin[abs];
var max = context.propertyMax[abs];
if (mergedProperties[abs] > max) {
mergedProperties[abs] = max;
} else if (mergedProperties[abs] < min) {
mergedProperties[abs] = min;
}
}
};
mergeRelativeProperty(tts.VOLUME, tts.RELATIVE_VOLUME);
mergeRelativeProperty(tts.PITCH, tts.RELATIVE_PITCH);
mergeRelativeProperty(tts.RATE, tts.RELATIVE_RATE);
}
return mergedProperties;
};
/**
* Method to preprocess text to be spoken properly by a speech
* engine.
*
* 1. Replace any single character with a description of that character.
*
* 2. Convert all-caps words to lowercase if they don't look like an
* acronym / abbreviation.
*
* @param {string} text A text string to be spoken.
* @param {Object= } properties Out parameter populated with how to speak the
* string.
* @return {string} The text formatted in a way that will sound better by
* most speech engines.
* @protected
*/
cvox.AbstractTts.prototype.preprocess = function(text, properties) {
if (text.length == 1 && text >= 'A' && text <= 'Z') {
for (var prop in cvox.AbstractTts.PERSONALITY_CAPITAL)
properties[prop] = cvox.AbstractTts.PERSONALITY_CAPITAL[prop];
}
// Substitute all symbols in the substitution dictionary. This is pretty
// efficient because we use a single regexp that matches all symbols
// simultaneously.
text = text.replace(
cvox.AbstractTts.substitutionDictionaryRegexp_,
function(symbol) {
return ' ' + cvox.AbstractTts.SUBSTITUTION_DICTIONARY[symbol] + ' ';
});
// Handle single characters that we want to make sure we pronounce.
if (text.length == 1) {
return cvox.AbstractTts.CHARACTER_DICTIONARY[text] ?
(new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg(
cvox.AbstractTts.CHARACTER_DICTIONARY[text])))
.format({'COUNT': 1}) :
text.toUpperCase();
}
// Substitute all words in the pronunciation dictionary. This is pretty
// efficient because we use a single regexp that matches all words
// simultaneously, and it calls a function with each match, which we can
// use to look up the replacement in our dictionary.
text = text.replace(
cvox.AbstractTts.pronunciationDictionaryRegexp_,
function(word) {
return cvox.AbstractTts.PRONUNCIATION_DICTIONARY[word.toLowerCase()];
});
// Special case for google+, where the punctuation must be pronounced.
text = text.replace(/google\+/ig, 'google plus');
// Expand all repeated characters.
text = text.replace(
cvox.AbstractTts.repetitionRegexp_, cvox.AbstractTts.repetitionReplace_);
// If there's no lower case letters, and at least two spaces, skip spacing
// text.
var skipSpacing = false;
if (!text.match(/[a-z]+/) && text.indexOf(' ') != text.lastIndexOf(' ')) {
skipSpacing = true;
}
// Convert all-caps words to lowercase if they don't look like acronyms,
// otherwise add a space before all-caps words so that all-caps words in
// the middle of camelCase will be separated.
text = text.replace(/[A-Z]+/g, function(word) {
// If a word contains vowels and is more than 3 letters long, it is
// probably a real word and not just an abbreviation. Convert it to lower
// case and speak it normally.
if ((word.length > 3) && word.match(/([AEIOUY])/g)) {
return word.toLowerCase();
} else if (!skipSpacing) {
// Builds spaced-out camelCased/all CAPS words so they sound better when
// spoken by TTS engines.
return ' ' + word.split('').join(' ');
} else {
return word;
}
});
return text;
};
/** TTS rate property. @type {string} */
cvox.AbstractTts.RATE = 'rate';
/** TTS pitch property. @type {string} */
cvox.AbstractTts.PITCH = 'pitch';
/** TTS volume property. @type {string} */
cvox.AbstractTts.VOLUME = 'volume';
/** TTS language property. @type {string} */
cvox.AbstractTts.LANG = 'lang';
/** TTS relative rate property. @type {string} */
cvox.AbstractTts.RELATIVE_RATE = 'relativeRate';
/** TTS relative pitch property. @type {string} */
cvox.AbstractTts.RELATIVE_PITCH = 'relativePitch';
/** TTS relative volume property. @type {string} */
cvox.AbstractTts.RELATIVE_VOLUME = 'relativeVolume';
/** TTS color property (for the lens display). @type {string} */
cvox.AbstractTts.COLOR = 'color';
/** TTS CSS font-weight property (for the lens display). @type {string} */
cvox.AbstractTts.FONT_WEIGHT = 'fontWeight';
/** TTS punctuation-echo property. @type {string} */
cvox.AbstractTts.PUNCTUATION_ECHO = 'punctuationEcho';
/** TTS pause property. @type {string} */
cvox.AbstractTts.PAUSE = 'pause';
/**
* TTS personality for annotations - text spoken by ChromeVox that
* elaborates on a user interface element but isn't displayed on-screen.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_ANNOTATION = {
'relativePitch': -0.25,
// TODO:(rshearer) Added this color change for I/O presentation.
'color': 'yellow',
'punctuationEcho': 'none'
};
/**
* TTS personality for announcements - text spoken by ChromeVox that
* isn't tied to any user interface elements.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT = {
'punctuationEcho': 'none'
};
/**
* TTS personality for alerts from the system, such as battery level
* warnings.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_SYSTEM_ALERT = {
'punctuationEcho': 'none',
'doNotInterrupt': true
};
/**
* TTS personality for an aside - text in parentheses.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_ASIDE = {
'relativePitch': -0.1,
'color': '#669'
};
/**
* TTS personality for capital letters.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_CAPITAL = {
'relativePitch': 0.6
};
/**
* TTS personality for deleted text.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_DELETED = {
'punctuationEcho': 'none',
'relativePitch': -0.6
};
/**
* TTS personality for quoted text.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_QUOTE = {
'relativePitch': 0.1,
'color': '#b6b',
'fontWeight': 'bold'
};
/**
* TTS personality for strong or bold text.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_STRONG = {
'relativePitch': 0.1,
'color': '#b66',
'fontWeight': 'bold'
};
/**
* TTS personality for emphasis or italicized text.
* @type {Object}
*/
cvox.AbstractTts.PERSONALITY_EMPHASIS = {
'relativeVolume': 0.1,
'relativeRate': -0.1,
'color': '#6bb',
'fontWeight': 'bold'
};
/**
* Flag indicating if the TTS is being debugged.
* @type {boolean}
*/
cvox.AbstractTts.DEBUG = true;
/**
* Speech queue mode that interrupts the current utterance.
* @type {number}
*/
cvox.AbstractTts.QUEUE_MODE_FLUSH = 0;
/**
* Speech queue mode that does not interrupt the current utterance.
* @type {number}
*/
cvox.AbstractTts.QUEUE_MODE_QUEUE = 1;
/**
* Character dictionary. These symbols are replaced with their human readable
* equivalents. This replacement only occurs for single character utterances.
* @type {Object.<string, string>}
*/
cvox.AbstractTts.CHARACTER_DICTIONARY = {
' ': 'space',
'`': 'backtick',
'~': 'tilde',
'!': 'exclamation',
'@': 'at',
'#': 'pound',
'$': 'dollar',
'%': 'percent',
'^': 'caret',
'&': 'ampersand',
'*': 'asterisk',
'(': 'open_paren',
')': 'close_paren',
'-': 'dash',
'_': 'underscore',
'=': 'equals',
'+': 'plus',
'[': 'left_bracket',
']': 'right_bracket',
'{': 'left_brace',
'}': 'right_brace',
'|': 'pipe',
';': 'semicolon',
':': 'colon',
',': 'comma',
'.': 'dot',
'<': 'less_than',
'>': 'greater_than',
'/': 'slash',
'?': 'question_mark',
'"': 'quote',
'\'': 'apostrophe',
'\t': 'tab',
'\r': 'return',
'\n': 'new_line',
'\\': 'backslash'
};
/**
* Pronunciation dictionary. Each key must be lowercase, its replacement
* should be spelled out the way most TTS engines will pronounce it
* correctly. This particular dictionary only handles letters and numbers,
* no symbols.
* @type {Object.<string, string>}
*/
cvox.AbstractTts.PRONUNCIATION_DICTIONARY = {
'admob': 'ad-mob',
'adsense': 'ad-sense',
'adwords': 'ad-words',
'angularjs': 'angular j s',
'bcc': 'B C C',
'cc': 'C C',
'chromevox': 'chrome vox',
'cr48': 'C R 48',
'ctrl': 'control',
'doubleclick': 'double-click',
'gmail': 'gee mail',
'gtalk': 'gee talk',
'http': 'H T T P',
'https' : 'H T T P S',
'igoogle': 'eye google',
'pagerank': 'page-rank',
'username': 'user-name',
'www': 'W W W',
'youtube': 'you tube'
};
/**
* Pronunciation dictionary regexp.
* @type {RegExp};
* @private
*/
cvox.AbstractTts.pronunciationDictionaryRegexp_;
/**
* Substitution dictionary. These symbols or patterns are ALWAYS substituted
* whenever they occur, so this should be reserved only for unicode characters
* and characters that never have any different meaning in context.
*
* For example, do not include '$' here because $2 should be read as
* "two dollars".
* @type {Object.<string, string>}
*/
cvox.AbstractTts.SUBSTITUTION_DICTIONARY = {
'://': 'colon slash slash',
'\u00bc': 'one fourth',
'\u00bd': 'one half',
'\u2190': 'left arrow',
'\u2191': 'up arrow',
'\u2192': 'right arrow',
'\u2193': 'down arrow',
'\u21d0': 'left double arrow',
'\u21d1': 'up double arrow',
'\u21d2': 'right double arrow',
'\u21d3': 'down double arrow',
'\u21e6': 'left arrow',
'\u21e7': 'up arrow',
'\u21e8': 'right arrow',
'\u21e9': 'down arrow',
'\u2303': 'control',
'\u2318': 'command',
'\u2325': 'option',
'\u25b2': 'up triangle',
'\u25b3': 'up triangle',
'\u25b4': 'up triangle',
'\u25b5': 'up triangle',
'\u25b6': 'right triangle',
'\u25b7': 'right triangle',
'\u25b8': 'right triangle',
'\u25b9': 'right triangle',
'\u25ba': 'right pointer',
'\u25bb': 'right pointer',
'\u25bc': 'down triangle',
'\u25bd': 'down triangle',
'\u25be': 'down triangle',
'\u25bf': 'down triangle',
'\u25c0': 'left triangle',
'\u25c1': 'left triangle',
'\u25c2': 'left triangle',
'\u25c3': 'left triangle',
'\u25c4': 'left pointer',
'\u25c5': 'left pointer',
'\uf8ff': 'apple'
};
/**
* Substitution dictionary regexp.
* @type {RegExp};
* @private
*/
cvox.AbstractTts.substitutionDictionaryRegexp_;
/**
* repetition filter regexp.
* @type {RegExp}
* @private
*/
cvox.AbstractTts.repetitionRegexp_ =
/([-\/\\|!@#$%^&*\(\)=_+\[\]\{\}.?;'":<>])\1{2,}/g;
/**
* Constructs a description of a repeated character. Use as a param to
* string.replace.
* @param {string} match The matching string.
* @return {string} The description.
* @private
*/
cvox.AbstractTts.repetitionReplace_ = function(match) {
var count = match.length;
return ' ' + (new goog.i18n.MessageFormat(cvox.ChromeVox.msgs.getMsg(
cvox.AbstractTts.CHARACTER_DICTIONARY[match[0]])))
.format({'COUNT': count}) + ' ';
};
/**
* @override
*/
cvox.AbstractTts.prototype.getDefaultProperty = function(property) {
return this.propertyDefault[property];
};