| // Copyright 2014 The Chromium Authors |
| // 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. |
| */ |
| |
| import {LocalStorage} from '../../common/local_storage.js'; |
| import {Msgs} from '../common/msgs.js'; |
| import {TtsInterface} from '../common/tts_interface.js'; |
| import * as ttsTypes from '../common/tts_types.js'; |
| |
| /** |
| * @typedef {{ |
| * pitch: number, |
| * rate: number, |
| * volume: number, |
| * }} |
| */ |
| let PropertyValues; |
| |
| /** |
| * Creates a new instance. |
| * @implements {TtsInterface} |
| */ |
| export class AbstractTts { |
| constructor() { |
| 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.). |
| * @protected {PropertyValues} |
| */ |
| this.propertyDefault; |
| |
| /** |
| * Min value for TTS properties. |
| * @protected {PropertyValues} |
| */ |
| this.propertyMin; |
| |
| /** |
| * Max value for TTS properties. |
| * @protected {PropertyValues} |
| */ |
| this.propertyMax; |
| |
| /** |
| * Step value for TTS properties. |
| * @protected {PropertyValues} |
| */ |
| this.propertyStep; |
| |
| this.init_(); |
| } |
| |
| /** @private */ |
| init_() { |
| const pitchDefault = 1; |
| const pitchMin = 0.2; |
| const pitchMax = 2.0; |
| const pitchStep = 0.1; |
| |
| const rateDefault = 1; |
| const rateMin = 0.2; |
| const rateMax = 5.0; |
| const rateStep = 0.1; |
| |
| const volumeDefault = 1; |
| const volumeMin = 0.2; |
| const volumeMax = 1.0; |
| const volumeStep = 0.1; |
| |
| this.propertyDefault = { |
| pitch: pitchDefault, |
| rate: rateDefault, |
| volume: volumeDefault, |
| }; |
| |
| this.propertyMin = { |
| pitch: pitchMin, |
| rate: rateMin, |
| volume: volumeMin, |
| }; |
| |
| this.propertyMax = { |
| pitch: pitchMax, |
| rate: rateMax, |
| volume: volumeMax, |
| }; |
| |
| this.propertyStep = {rate: rateStep, pitch: pitchStep, volume: volumeStep}; |
| |
| if (AbstractTts.substitutionDictionaryRegexp_ === undefined) { |
| // Create an expression that matches all words in the substitution |
| // dictionary. |
| const symbols = []; |
| for (const symbol in ttsTypes.SubstitutionDictionary) { |
| symbols.push(symbol); |
| } |
| const expr = '(' + symbols.join('|') + ')'; |
| AbstractTts.substitutionDictionaryRegexp_ = new RegExp(expr, 'ig'); |
| } |
| } |
| |
| /** |
| * @param {string} textString |
| * @param {ttsTypes.QueueMode} queueMode |
| * @param {ttsTypes.TtsSpeechProperties=} properties |
| * @override |
| */ |
| speak(textString, queueMode, properties) { |
| return this; |
| } |
| |
| /** @override */ |
| isSpeaking() { |
| return false; |
| } |
| |
| /** @override */ |
| stop() {} |
| |
| /** @override */ |
| addCapturingEventListener(listener) {} |
| |
| /** @override */ |
| removeCapturingEventListener(listener) {} |
| |
| /** @override */ |
| increaseOrDecreaseProperty(propertyName, increase) { |
| const min = this.propertyMin[propertyName]; |
| const max = this.propertyMax[propertyName]; |
| const step = this.propertyStep[propertyName]; |
| let current = this.ttsProperties[propertyName]; |
| current = increase ? current + step : current - step; |
| this.ttsProperties[propertyName] = Math.max(Math.min(current, max), min); |
| } |
| |
| /** |
| * Converts an engine property value to a percentage from 0.00 to 1.00. |
| * @param {string} property The property to convert. |
| * @return {?number} The percentage of the property. |
| */ |
| propertyToPercentage(property) { |
| return (this.ttsProperties[property] - this.propertyMin[property]) / |
| Math.abs(this.propertyMax[property] - this.propertyMin[property]); |
| } |
| |
| /** |
| * 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 |
| */ |
| mergeProperties(properties) { |
| const mergedProperties = new Object(); |
| let p; |
| if (this.ttsProperties) { |
| for (p in this.ttsProperties) { |
| mergedProperties[p] = this.ttsProperties[p]; |
| } |
| } |
| if (properties) { |
| const tts = ttsTypes.TtsSettings; |
| 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]; |
| } |
| |
| const context = this; |
| const mergeRelativeProperty = function(abs, rel) { |
| if (typeof (properties[rel]) === 'number' && |
| typeof (mergedProperties[abs]) === 'number') { |
| mergedProperties[abs] += properties[rel]; |
| const min = context.propertyMin[abs]; |
| const 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 |
| */ |
| preprocess(text, properties) { |
| if (text.length === 1 && text.toLowerCase() !== text) { |
| // Describe capital letters according to user's setting. |
| if (LocalStorage.get('capitalStrategy') === 'increasePitch') { |
| // Closure doesn't allow the use of for..in or [] with structs, so |
| // convert to a pure JSON object. |
| const CAPITAL = ttsTypes.Personality.CAPITAL.toJSON(); |
| for (const prop in CAPITAL) { |
| if (properties[prop] === undefined) { |
| properties[prop] = CAPITAL[prop]; |
| } |
| } |
| } else if (LocalStorage.get('capitalStrategy') === 'announceCapitals') { |
| text = Msgs.getMsg('announce_capital_letter', [text]); |
| } |
| } |
| |
| if (LocalStorage.get('usePitchChanges') === 'false') { |
| delete properties.relativePitch; |
| } |
| |
| // Since dollar and sterling pound signs will be replaced with text, move |
| // them to after the number if they stay between a negative sign and a |
| // number. |
| text = text.replace(AbstractTts.negativeCurrencyAmountRegexp_, match => { |
| const minus = match[0]; |
| const number = match.substring(2); |
| const currency = match[1]; |
| |
| return minus + number + currency; |
| }); |
| |
| // 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( |
| AbstractTts.substitutionDictionaryRegexp_, function(symbol) { |
| return ' ' + ttsTypes.SubstitutionDictionary[symbol] + ' '; |
| }); |
| |
| // Handle single characters that we want to make sure we pronounce. |
| if (text.length === 1) { |
| return ttsTypes.CharacterDictionary[text] ? |
| (new goog.i18n.MessageFormat( |
| Msgs.getMsg(ttsTypes.CharacterDictionary[text]))) |
| .format({'COUNT': 1}) : |
| text.toUpperCase(); |
| } |
| |
| // Expand all repeated characters. |
| text = text.replace( |
| AbstractTts.repetitionRegexp_, AbstractTts.repetitionReplace_); |
| |
| return text; |
| } |
| |
| /** |
| * 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 |
| */ |
| static repetitionReplace_(match) { |
| const count = match.length; |
| return ' ' + |
| (new goog.i18n.MessageFormat( |
| Msgs.getMsg(ttsTypes.CharacterDictionary[match[0]]))) |
| .format({'COUNT': count}) + |
| ' '; |
| } |
| |
| /** |
| * @override |
| */ |
| getDefaultProperty(property) { |
| return this.propertyDefault[property]; |
| } |
| |
| /** @override */ |
| toggleSpeechOnOrOff() { |
| return true; |
| } |
| |
| /** @override */ |
| resetTextToSpeechSettings() { |
| for (const [key, value] of Object.entries(this.propertyDefault)) { |
| this.ttsProperties[key] = value; |
| } |
| } |
| } |
| |
| /** |
| * Default TTS properties for this TTS engine. |
| * @type {Object} |
| * @protected |
| */ |
| AbstractTts.prototype.ttsProperties; |
| |
| /** |
| * Pronunciation dictionary regexp. |
| * @private {RegExp} |
| */ |
| AbstractTts.pronunciationDictionaryRegexp_; |
| |
| /** |
| * Substitution dictionary regexp. |
| * @private {RegExp} |
| */ |
| AbstractTts.substitutionDictionaryRegexp_; |
| |
| /** |
| * repetition filter regexp. |
| * @private {RegExp} |
| */ |
| AbstractTts.repetitionRegexp_ = |
| /([-\/\\|!@#$%^&*\(\)=_+\[\]\{\}.?;'":<>\u2022\u25e6\u25a0])\1{2,}/g; |
| |
| /** |
| * Regexp filter for negative dollar and pound amounts. |
| * @private {RegExp} |
| */ |
| AbstractTts.negativeCurrencyAmountRegexp_ = |
| /-[£\$](\d{1,3})(\d+|(,\d{3})*)(\.\d{1,})?/g; |