| // 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); |
| } |
| |
| for (p in properties) { |
| if (!mergedProperties.hasOwnProperty(p)) { |
| mergedProperties[p] = properties[p]; |
| } |
| } |
| |
| 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; |
| |
| |
| /** |
| * 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]; |
| }; |