Skip to content

Instantly share code, notes, and snippets.

@bkardell
Created August 24, 2017 12:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bkardell/abd9edf8ae80ec06eef13ee2d63b3b61 to your computer and use it in GitHub Desktop.
Save bkardell/abd9edf8ae80ec06eef13ee2d63b3b61 to your computer and use it in GitHub Desktop.
/* 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
}
}
@Terrycoco
Copy link

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:

let female = new BasicVoiceSpeaker({
    filter: [
     {name: /Samantha/i},  //mac iphone
      {name: /Victoria/i},
      {name: /Google US English/i}, //chrome
      {name: /English United States/i},
      {name: /Zira/i},
     {lang: /en-US/},
    ]
   });

   let male =  new BasicVoiceSpeaker({
     filter: [
        {name: /tom/i}, //iphone (not working)
        {name: /daniel/i}, //mac
       {name: /david/i},
       {name: /us english male/i}, 
        {name: /english male/i}, //chrome (uk)
       {name: /united states/i},
        {lang: /en-GB/},
       {lang:/en/}
      ]
   });

   let italian = new BasicVoiceSpeaker({
    filter: [
       {lang: /it/}, //chrome
       {name: /victoria/i},
       {name: /vicki/i},
    ]
   })

Then, I call these voices:

   Promise.all([
     male.say('I am supposed to be a male US Speaker.'),
     italian.say('And I am supposed to be a female italian speaker.'),
     female.say('I am supposed to be a female US Speaker.'),
    ]).then(() => {
      document.querySelector('#out').innerText = "Conversation over."
    });

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment