talkr-tts
Version:
Wraps speechSynthesis calls for applications that want to animate to the voice
403 lines (365 loc) • 10.1 kB
JavaScript
const hasWindow = typeof window === 'object' && window !== null && window.self === window && window.navigator !== null;
const bIsiOS = hasWindow && /iPad|iPhone|iPod/.test(window.navigator.userAgent) && !window.MSStream;
const hasSpeechSynthesis = hasWindow && window.speechSynthesis !== null;
// const iOSEnglishVoices = ['Karen', 'Serena', 'Moira', 'Tessa', 'Samantha'];
// const bIsAndroid = /android/i.test(navigator.userAgent);
const iOSVoiceNames = [
'Maged',
'Zuzana',
'Sara',
'Anna',
'Melina',
'Karen',
'Daniel',
'Moira',
'Tessa',
'Alex',
'Monica',
'Paulina',
'Satu',
'Amelie',
'Thomas',
'Carmit',
'Lekha',
'Mariska',
'Damayanti',
'Alice',
'Kyoko',
'Yuna',
'Ellen',
'Xander',
'Nora',
'Zosia',
'Luciana',
'Joana',
'Ioana',
'Milena',
'Laura',
'Alva',
'Kanya',
'Yelda',
'Ting-Ting',
'Sin-Ji',
'Mei-Jia'
];
const minimumPriority = 10;
const voiceProperties = {
'Microsoft David Desktop - English (United States)': { male: true, priority: 13 },
'Microsoft Hazel Desktop - English (Great Britain)': { male: false, priority: 14 },
'Microsoft Zira Desktop - English (United States)': { male: false, priority: 14 },
'Google US English': { male: false, priority: 24 },
'Google UK English Female': { male: false, priority: 24 },
'Google UK English Male': { male: true, priority: 23 },
'Lee': { male: true, priority: 33 },
'Fred': { male: true, priority: 8 },
'Alex': { male: true, priority: 34 },
'Daniel': { male: true, priority: 33 },
'Daniel (Enhanced)': { male: true, priority: 43 },
'Oliver': { male: true, priority: 33 },
'Oliver (Enhanced)': { male: true, priority: 43 },
'Samantha': { male: false, priority: 38 },
'Victoria': { male: false, priority: 9 },
'Veena': { male: false, priority: 32 },
'Tessa': { male: false, priority: 37 },
'Fiona': { male: false, priority: 37 },
'Moira': { male: false, priority: 37 },
'Karen': { male: false, priority: 37 }
};
const languageProperties = {
'en': 'English',
'zh': 'Chinese',
'ru': 'Russian',
'de': 'German',
'it': 'Italian',
'pt': 'Portuguese',
'vi': 'Vietnamese',
'tr': 'Turkish',
'th': 'Thai',
'es': 'Spanish',
'fr': 'French',
'sk': 'Slovak',
'ro': 'Romanian',
'pl': 'Polish',
'nl': 'Dutch',
'ko': 'Korean',
'ja': 'Japanese',
'he': 'Hebrew',
'el': 'Greek',
'fi': 'Finish',
'no': 'Norwegian',
'da': 'Danish',
'cs': 'Czech',
'ar': 'Arabic'
};
const languagePropertiesInverted = {
'English': 'en',
'Chinese': 'zh',
'Russian': 'ru',
'German': 'de',
'Italian': 'it',
'Portuguese': 'pt',
'Vietnamese': 'vi',
'Turkish': 'tr',
'Thai': 'th',
'Spanish': 'es',
'French': 'fr',
'Slovak': 'sk',
'Romanian': 'ro',
'Polish': 'pl',
'Dutch': 'nl',
'Korean': 'ko',
'Japanese': 'ja',
'Hebrew': 'he',
'Greek': 'el',
'Finish': 'fi',
'Norwegian': 'no',
'Danish': 'da',
'Czech': 'cs',
'Arabic': 'ar'
};
/*
var langs =
[['Afrikaans', ['af-ZA']],
['Bahasa Indonesia',['id-ID']],
['Bahasa Melayu', ['ms-MY']],
['Català', ['ca-ES']],
['Čeština', ['cs-CZ']],
['Deutsch', ['de-DE']],
['English', ['en-AU', 'Australia'],
['Español', ['es-AR', 'Argentina'],
['Euskara', ['eu-ES']],
['Français', ['fr-FR']],
['Galego', ['gl-ES']],
['Hrvatski', ['hr_HR']],
['IsiZulu', ['zu-ZA']],
['Íslenska', ['is-IS']],
['Italiano', ['it-IT', 'Italia'],
['Magyar', ['hu-HU']],
['Nederlands', ['nl-NL']],
['Norsk bokmål', ['nb-NO']],
['Polski', ['pl-PL']],
['Português', ['pt-BR', 'Brasil'],
['Română', ['ro-RO']],
['Slovenčina', ['sk-SK']],
['Suomi', ['fi-FI']],
['Svenska', ['sv-SE']],
['Türkçe', ['tr-TR']],
['български', ['bg-BG']],
['Pусский', ['ru-RU']],
['Српски', ['sr-RS']],
['한국어', ['ko-KR']],
['中文', ['cmn-Hans-CN', '普通话 (中国大陆)'],
['日本語', ['ja-JP']],
['Lingua latīna', ['la']]];
*/
export default class Voice {
static getLangauges() {
var codes = [];
var languages = [];
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
if (hasSpeechSynthesis) {
speechSynthesis.getVoices().forEach(function (v) {
codes.push(v.lang.substring(0, 2));
});
}
let unique = codes.filter(onlyUnique);
unique.forEach(function (l) {
if (l in languageProperties) {
languages.push(languageProperties[l]);
} else {
languages.push(l);
}
});
return languages;
}
static getVoicesForLangauge(language) {
var code = languagePropertiesInverted[language];
var names = [];
var voices = [];
if (code) {
voices = Voice.getVoicesWithLangSubstring(code);
voices.forEach(function (v) {
names.push(v.name);
});
return names;
}
return [];
}
static getVoicesWithLangSubstring(langSubstr) {
let filteredvoices = [];
if (hasSpeechSynthesis) {
filteredvoices = speechSynthesis.getVoices().filter(function (v) {
if (voiceProperties[v.name]) {
if (voiceProperties[v.name].priority < minimumPriority) {
return false;
}
}
return v.lang.substring(0, langSubstr.length) === langSubstr;
});
}
// iOS returns more than they let you select.
if (bIsiOS) {
let newVoices = [];
let last = filteredvoices.length - 1;
for (let i = last; i >= 0; --i) {
for (let j = 0; j < iOSVoiceNames.length; ++j) {
if (filteredvoices[i].name === iOSVoiceNames[j]) {
newVoices.push(filteredvoices[i]);
}
}
}
filteredvoices = newVoices;
}
// So does Android...all versions?
function sortCompare(a, b) {
let aPriority = 0;
let bPriority = 0;
if (voiceProperties[a.name]) {
aPriority = voiceProperties[a.name].priority;
}
if (voiceProperties[b.name]) {
bPriority = voiceProperties[b.name].priority;
}
if (a.name.includes('Enhanced')) {
aPriority++;
}
if (b.name.includes('Enhanced')) {
bPriority++;
}
return bPriority - aPriority;
}
return filteredvoices.sort(sortCompare);
}
static getTTSVoice(voiceObj) {
return Voice._getBestVoice(voiceObj.name, voiceObj.locale, voiceObj.isMale, voiceObj.index);
}
static _getBestVoice(name, locale, bIsMale, index) {
if (name) {
let namedVoice = Voice._getVoiceWithName(name);
if (namedVoice) {
return namedVoice;
}
}
let matchingVoices = Voice._getBestVoices(locale, bIsMale);
if (matchingVoices.length > 0) {
if (index != null) {
index = Math.abs(parseInt(index, 10));
index = index % matchingVoices.length;
} else {
index = 0;
}
return matchingVoices[index];
}
// Try english voices if we didn't get any for the provided locale.
if (locale !== 'en') {
console.warn('No voices for locale: ' + locale);
return Voice._getBestVoice(null, 'en', bIsMale, index);
}
let lastResorts = [];
if (hasSpeechSynthesis) {
lastResorts = speechSynthesis.getVoices();
}
if (lastResorts[0]) {
return lastResorts[0];
}
return null;
}
static _getBestVoices(locale, bIsMale) {
let voices = [];
let genderVoices = [];
if (locale && locale.length > 1) {
voices = Voice.getVoicesWithLangSubstring(locale);
}
voices.forEach(function (v) {
if (v.name in voiceProperties) {
if (bIsMale === voiceProperties[v.name].male) {
genderVoices.push(v);
}
} else {
// We don't know the gender of this voice. Let's cross our fingers.
genderVoices.push(v);
}
});
// Try with only the first two letters of the locale.
if (genderVoices.length === 0 && locale.length > 2) {
return this._getBestVoices(locale.substring(0, 2), bIsMale);
}
// Any gender will do at this point.
if (genderVoices.length === 0) {
voices.forEach(function (v) {
genderVoices.push(v);
});
}
return genderVoices;
}
static _getVoicesWithName(name) {
if (hasSpeechSynthesis) {
return speechSynthesis.getVoices().filter(function (v) {
return v.name === name;
});
}
return [];
}
static _getVoiceWithName(name) {
if (hasSpeechSynthesis) {
let voices = speechSynthesis.getVoices().filter(function (v) {
return v.name === name;
});
if (voices.length > 0) {
return voices[0];
}
}
return null;
}
static doesSupportPitchAndRate(name) {
let namedVoice = Voice._getVoiceWithName(name);
if (namedVoice) {
return namedVoice.localService === true;
}
return false;
}
constructor(locale, bIsMale, preference) {
this._locale = locale;
this._bIsMale = bIsMale;
this._name = preference;
this._index = null;
if (this._locale === undefined) {
this._locale = Voice.defaultlocale;
}
if (this._bIsMale === undefined) {
this._bIsMale = Voice.defaultbIsMale;
}
if (this._name !== undefined) {
if (this._name in voiceProperties) {
this._bIsMale = voiceProperties[this._name].male;
}
}
// A number for the name indicates we should choose a different
// voice than the first.
if (this._name && !isNaN(this._name)) {
this._index = this._name;
this._name = null;
}
Voice.defaultlocale = this._locale;
Voice.defaulbIsMale = this._bIsMale;
}
get index() {
return this._index;
}
get name() {
return this._name;
}
get locale() {
return this._locale;
}
get isMale() {
return this._bIsMale;
}
get language() {
return languageProperties[this._locale.substring(0, 2)];
}
}
Voice.defaultlocale = 'en-US';
Voice.defaultbIsMale = false;