Created
August 24, 2017 12:59
-
-
Save bkardell/abd9edf8ae80ec06eef13ee2d63b3b61 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Why? see http://bkardell.com/blog/Greetings-Professor-Falken.html */ | |
class BasicVoiceSpeaker { | |
/* | |
The constructor takes an optional regexp for determining the voice | |
this at least lets us simplify the process of searching for a decent voice | |
that sort of matches something we might expect | |
*/ | |
constructor(config={}) { | |
const synth = window.speechSynthesis, | |
options = { | |
filter: () => {}, | |
pitch: 1, | |
rate: 1, | |
volume: 1 | |
} | |
Object.assign(options, config) | |
BasicVoiceSpeaker.__last = BasicVoiceSpeaker.__last || Promise.resolve() | |
if (Array.isArray(config.filter) && config.filter.length > 0) { | |
options.filter = function (voices) { | |
let voice | |
for (let i=0; i<config.filter.length;i++){ | |
let nameTest = config.filter[i].name, | |
langTest = config.filter[i].lang, | |
voice = voices.find((v) => { | |
let nameResult = (nameTest) ? nameTest.test(v.name) : true, | |
langResult = (langTest) ? langTest.test(v.lang) : true | |
return nameResult && langResult | |
}) | |
if (voice) { | |
return voice | |
} | |
} | |
} | |
} | |
// We only ever need get the list of voices once, so | |
// let's just expose a promise for that | |
BasicVoiceSpeaker.voicesReady = BasicVoiceSpeaker.voicesReady || new Promise((resolve) => { | |
let voices = synth.getVoices() | |
if (voices.length ===0 ) { | |
synth.onvoiceschanged = () => { | |
resolve(synth.getVoices()) | |
} | |
} else { | |
resolve(voices) | |
} | |
}) | |
this.ready = new Promise((resolve, reject) => { | |
return BasicVoiceSpeaker.voicesReady.then((voices) => { | |
resolve() | |
}) | |
}) | |
// There's also a problem with log utterances and managing this is a | |
// serious pita with events to this end, let's create an internal, | |
// promise based 'speech transaction' | |
this.__sayThis = (shortText) => { | |
return this.ready.then(() => { | |
return new Promise((resolve, reject) => { | |
let utterThis = new SpeechSynthesisUtterance(shortText), | |
voices = speechSynthesis.getVoices(), | |
//choose voice at the moment of queing, unfortunately | |
// we can't currently do better than this | |
voice = options.filter(voices) || voices.find((v) => { | |
let docLang = document.documentElement.lang || 'en' | |
return docLang == (v.lang.split(/-|_/)[0]) | |
}) || voices[0] | |
utterThis.pitch = options.pitch | |
utterThis.rate = options.rate | |
utterThis.volume = options.volume | |
utterThis.voice = voice | |
if (voice.voiceURI) { | |
utterThis.voiceURI = voice.voiceURI | |
utterThis.lang = voice.lang | |
} | |
//console.log(`promising `, shortText) | |
utterThis.onend = () => { | |
//console.log(`done speaking, resolving..`, shortText) | |
resolve() | |
} | |
utterThis.onerror = () => { | |
resolve() | |
} | |
setTimeout(() => { | |
window.__utterance = utterThis | |
synth.speak(utterThis) | |
},0) | |
}) | |
}) | |
} | |
} | |
// oy, even queing is buggy... a whole bunch of things | |
// get spoken and not resolved if I do Promise.all(queue) | |
__sayNext (queue) { | |
return this.__sayThis(queue.shift()).then(() => { | |
return (queue.length > 0) ? this.__sayNext(queue) : null | |
}) | |
} | |
// the method we expose will do some simple auto-queuing for us | |
// to avoid the long text problems and keep things simple.. | |
// periods make for a natural place to pause, so this isn't | |
// 'bullet proof' but in practice it seems to work pretty well. | |
say(text) { | |
let queue = [], ret | |
if (text.length > 64) { | |
text.split(/[.,&"\n]/).forEach((shortText) => { | |
queue.push(shortText) | |
}) | |
} else { | |
queue.push(text) | |
} | |
ret = BasicVoiceSpeaker.__last.then(() => { | |
return this.__sayNext(queue).then(() => { | |
console.log('all my speaking should be done now') | |
}) | |
}) | |
BasicVoiceSpeaker.__last = ret | |
return ret | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Brian--
I'm writing you because I've tried everything. I've been struggling with the Web Speech API for awhile now, and I came across your blog on the topic and your basic speaker code, both of which I found extremely helpful. I'm using your basic-speaker code, but I have found a problem on ios mobile devices, and I'm wondering if you have tested this on them, and have found a workaround?
I'm TRYING to accomplish a simple web app where I can change from male to female programmatically, like a conversation. I wrote a little test which, using your code, sets up 3 speakers. A male (us with fallback to uk), a female us, and an italian:
Then, I call these voices:
This works just fine on desktops I've tested -- chrome (Linux), where the US male comes out british, and MacOS Sierra. However, on both ipad and iphone, I can only get out the default English speaker. In other words, what has been set in settings as the default voice is the only thing that plays, regardless of what voice has been selected. What's weird is that the Italian speaker DOES come out fine.
Have you had any luck doing this on ios devices? Have I done something wrong here?
Appreciate any help of guidance you could give! Thanks in advance, Terry