34

I'm using the Speech Synthesis API on Google Chrome v34.0.1847.131. The API is implemented in Chrome starting in v33.

The text-to-speech works for the most part, except when assigning a callback to onend. For instance, the following code:

var message = window.SpeechSynthesisUtterance("Hello world!");
message.onend = function(event) {
    console.log('Finished in ' + event.elapsedTime + ' seconds.');
};
window.speechSynthesis.speak(message);

will sometimes call onend and sometimes not call it. The timing appears to be completely off. When it does get called, the printed elapsedTime is always some epoch time like 1399237888.

huu
  • 7,032
  • 2
  • 34
  • 49
  • just because specs are written at w3...doesn't mean they work or work exactly like that in all browsers especially if they are new and experimental like speechsynthesis..not that this capability is some sort of technological breakthrough but it's first time to be brought to browsers. – Muhammad Umer May 12 '14 at 01:40
  • I checked what you were doing and yes the problem exists...for me on end event hasn't fired even once...and onstart misses sometimes too. – Muhammad Umer May 12 '14 at 01:51
  • 1
    Thanks for checking. It's encouraging to see someone else with this issue. I guess this might just be a half-baked implementation, so this question may not have an answer. – huu May 12 '14 at 01:54
  • 1
    check this out...for some weird reason if you log out the utterance object `message` then it works fine. :D http://jsfiddle.net/QYw6b/ – Muhammad Umer May 12 '14 at 02:06
  • Wow, that's interesting. The `event.elapsedTime` is still bunk, but `onend` appears to be firing off every single time. – huu May 12 '14 at 02:08
  • 3
    in fact i think problem is calling the speak function right away after declaring message object is the problem..if you just do this ` setTimeout(function(){speechSynthesis.speak(u);},100);` it works...or attack speak function to click event it works fine too. – Muhammad Umer May 12 '14 at 02:09
  • `setTimeout(function(){speechSynthesis.speak(u);},1);` works as well and is indistinguishable from just calling it straight. Seems that the API only likes to live inside of a callback. Would love some more insight into why this is the behavior we're observing, but probably only Googlers know for sure. If you'd like, you can write up an answer to this question with the information you've provided and I'll give it its due credit :) – huu May 12 '14 at 02:14
  • i am also able to get time – Muhammad Umer May 12 '14 at 02:29
  • 1
    I still see this on chrome v46 for Windows. I don't recall it happening on Chrome for android. – Frank Schwieterman Nov 04 '15 at 00:31

9 Answers9

25

According to this comment on the bug mentioned in the answer from Kevin Hakanson, it might be a problem with garbage collection. Storing the utterance in a variable before calling speak seems to do the trick:

window.utterances = [];
var utterance = new SpeechSynthesisUtterance( 'hello' );
utterances.push( utterance );
speechSynthesis.speak( utterance );
Community
  • 1
  • 1
techpeace
  • 2,606
  • 1
  • 22
  • 21
  • 2
    Thanks for this! Once I changed the scope my utterance variable I didn't have the issue anymore. I agree that there might be a bug related to the browser's internal GC. – vbguyny Jan 01 '17 at 05:51
  • Worked! To make it clear for beginners like me, making the scope of the `SpeechSynthesisUtterance` variable broader, e.g. global, does the trick. – M3RS Feb 08 '18 at 11:30
  • What is the push doing? and what is utterances? – Kanerva Peter Aug 02 '18 at 23:41
  • `utterances` is an array created in the first line. `push` adds an item to the end of an array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push – techpeace Aug 06 '18 at 15:57
  • Works like a charm! – Norly Canarias Feb 07 '19 at 04:04
  • This solution didn't work for me. My scenario is that i am a calling my `speak_please()` function from within the `onend` callback (so calling `speak_please()` from within `speak_please()`), and it's not waiting for the speaking to end before calling `speak_please()` - it just calls it immediately so it is iterating through all divs until the end at lightening speed. Chrome Version 75.0.3770.100 (Official Build) (64-bit) – user1063287 Jun 28 '19 at 06:50
  • Actually, I think my issue is that `onend` is not just fired when an utterance "naturally" comes to an end, but also after `synth.cancel()` so every time i cancel an utterance, I trigger `onend` to occur - thus i rapidly create and cancel multiple utterances until there are no divs left. – user1063287 Jun 28 '19 at 07:07
  • 1
    nice one man! storing just the current utterance on global `window` and removing it once all speaking is done — works perfect for me! – chestozo Nov 28 '20 at 13:55
12

While this is how I found it to make it work, I am not sure if this is the right behavior....

First don't call the speak function it right away, use callback.

2nd, to get time use timeStamp instead of elapsedTime. You could have just used performance.now() as well.

var btn = document.getElementById('btn');
speechSynthesis.cancel()
var u = new SpeechSynthesisUtterance();
u.text = "This text was changed from the original demo.";

var t;
u.onstart = function (event) {
    t = event.timeStamp;
    console.log(t);
};

u.onend = function (event) {
    t = event.timeStamp - t;
    console.log(event.timeStamp);
    console.log((t / 1000) + " seconds");
};

btn.onclick = function () {speechSynthesis.speak(u);};

Demo: http://jsfiddle.net/QYw6b/2/

you get time passed and both events fired for sure.

icedwater
  • 4,701
  • 3
  • 35
  • 50
Muhammad Umer
  • 17,263
  • 19
  • 97
  • 168
  • although it does works this example, as soon as you use a non-english voice, it stops triggering the onend event. – Zero Dragon Dec 30 '20 at 23:17
6

You could use the EventListener for start and end like I did for Speakerbot (http://www.speakerbot.de/).

Here my face changes while words are spoken.

newUtt = new SpeechSynthesisUtterance();

newUtt.addEventListener('start', function () {
     console.log('started');
})

newUtt.addEventListener('end', function () {
     console.log('stopped');
})
fimbim
  • 113
  • 2
  • 6
4

I found both solutions suggested here not working in an app I just wrote. The only solution I could come up with is a (sort of) busy waiting:

function speak( text, onend ) {
  window.speechSynthesis.cancel();
  var ssu = new SpeechSynthesisUtterance( text );
  window.speechSynthesis.speak( ssu );
  function _wait() {
    if ( ! window.speechSynthesis.speaking ) {
      onend();
      return;
    }
    window.setTimeout( _wait, 200 );
  }
  _wait();
}

you can find a complete example in this codepen

  • I like how you start the function by calling `cancel()` and that solves my problem. Though for me `speechSynthesis.speaking` is true far longer than it should be, so the busy waiting isn't useful for me. – ubershmekel Mar 29 '21 at 05:10
3

This looks similar to a Chromium bug reported on Jul 12, 2015.

Issue 509488: Web Speech API: 'end' event of SpeechSynthesisUtterance object is not dispatched sometimes

Kevin Hakanson
  • 41,386
  • 23
  • 126
  • 155
1

print the utterance before speak seems working... If I remove the console, this issue will happen, don't know why

console.log("utterance", utterThis);
synth.speak(utterThis);
yun
  • 125
  • 2
  • 11
0

I also found the only way to make this work reliably is to use .cance. I use a 17 second timeout. All of my recordings are under 20 seconds so this works for me.

utterance.onstart = function (event) {
setTimeout(function(){window.speechSynthesis.cancel();},17000);
};

Before I would run into this problem once every 8-10 messages it attempted. Once I added .cancel it seems to always work. I also call set timeout when invoking.

setTimeout(function(){window.speechSynthesis.speak(utterance);},100);
0

The only thing that worked for me was to avoid voices with localService being true. Those voices never fired onend while the other voices (localService being false) did fire onend.

ubershmekel
  • 11,864
  • 10
  • 72
  • 89
0

Also, on Chrome (but not Safari), the callback is never called if you try to speak an empty string.

Guillaume
  • 21,685
  • 6
  • 63
  • 95