获取speechSynthesis中声音列表(Web Speech API)

62

以下HTML在第一次点击时在控制台中显示空数组:

<!DOCTYPE html>
<html>
    <head>
        <script>
            function test(){
                console.log(window.speechSynthesis.getVoices())
            }
        </script>
    </head>
    <body>
        <a href="#" onclick="test()">Test</a>
    </body>
</html>

第二次点击您将获得预期的列表。

如果您添加 onload 事件来调用此函数 (<body onload="test()">),那么您可以在第一次单击时获取正确的结果。请注意,onload 上的第一次调用仍然无法正常工作。它返回空值,但之后可以正常工作。

问题:

由于这可能是 beta 版本中的一个错误,我放弃了“为什么”的问题。

现在,问题是如果您想在页面加载时访问 window.speechSynthesis:

  • 这个问题的最佳解决方法是什么?
  • 如何确保它会在页面加载时加载 speechSynthesis

背景和测试:

我正在测试 Web Speech API 中的新功能,然后我在我的代码中遇到了这个问题:

<script type="text/javascript">
$(document).ready(function(){
    // Browser support messages. (You might need Chrome 33.0 Beta)
    if (!('speechSynthesis' in window)) {
      alert("You don't have speechSynthesis");
    }

    var voices = window.speechSynthesis.getVoices();
    console.log(voices) // []

    $("#test").on('click', function(){
        var voices = window.speechSynthesis.getVoices();
        console.log(voices); // [SpeechSynthesisVoice, ...]
    });
});
</script>
<a id="test" href="#">click here if 'ready()' didn't work</a>

我的问题是:为什么在页面加载完成并触发“onready”函数后,window.speechSynthesis.getVoices()返回空数组?如果您点击链接,可以看到相同的函数通过“onclick”触发返回Chrome可用语音的数组?
似乎Chrome在页面加载后加载window.speechSynthesis
问题不在于“ready”事件。如果我从“ready”函数中删除var voice=...这一行,第一次单击时它会在控制台中显示空列表。但第二次单击正常工作。
似乎window.speechSynthesis需要更多时间才能在第一次调用后加载。您需要调用两次!但是,您还需要等待并让其在对window.speechSynthesis进行第二次调用之前加载。例如,如果您首次运行以下代码,则会在控制台中显示两个空数组:
// First speechSynthesis call
var voices = window.speechSynthesis.getVoices();
console.log(voices);

// Second speechSynthesis call
voices = window.speechSynthesis.getVoices();
console.log(voices);

嗯,我猜测一下,你的页面默认是否有与语音合成相关的HTML属性标签?如果没有,Chrome可能需要一些时间来自行解决这个问题。 - Rooster
我有一个关于我的问题的更新。也许问题不在于 ready - Mehdi
在Dart中,我使用Timer在5秒延迟后调用了getVoices(),并获得了一个可用语音列表。 - Nawaf Alsulami
@Mehdi。你应该在上面进行一些编辑并写出答案。调用getVoices()两次,间隔一秒,可以解决这个问题。 - Nawaf Alsulami
13个回答

115
根据Web Speech API Errata(E11 2013-10-17),语音列表是异步加载到页面中的。当它们被加载时,会触发onvoiceschanged事件。

voiceschanged:当SpeechSynthesisVoiceList的内容发生变化时(getVoices方法将返回该内容),例如:在服务器端合成时异步确定列表,或者客户端语音被安装/卸载时,会触发此事件。

因此,关键是从该事件侦听器的回调中设置您的语音。
// wait on voices to be loaded before fetching list
window.speechSynthesis.onvoiceschanged = function() {
    window.speechSynthesis.getVoices();
    ...
};

5
那是一个糟糕的决定。直到加载完成(法语),声音才会正常。 - jgmjgm
8
这些声音是异步加载的,但它们是否同时加载还是onvoiceschanged事件会被触发多次? - Douglas De Rizzo Meneghetti
5
您是否需要调用 speechSynthesis.getVoices() 并等待 onvoiceschanged 事件被触发,或者该事件是在页面加载期间或之后的某个时候每次都会被触发? - Douglas De Rizzo Meneghetti
2
@DouglasDeRizzoMeneghetti - 在Chrome上进行测试时,我发现在执行一些语音工作(例如首先调用getVoices())之前,不会调用onvoiceschanged回调函数。 - Kolban
2
不完整的答案。 - zaph
显示剩余4条评论

9

您可以使用setInterval来等待语音加载完成,然后清除setInterval,并按需使用它们:

var timer = setInterval(function() {
    var voices = speechSynthesis.getVoices();
    console.log(voices);
    if (voices.length !== 0) {
      var msg = new SpeechSynthesisUtterance(/*some string here*/);
      msg.voice = voices[/*some number here to choose from array*/];
      speechSynthesis.speak(msg);
      clearInterval(timer);
    }
}, 200);

$("#test").on('click', timer);

1
计时器对我来说是更好的选择,因为在Safari上似乎不支持onvoiceschanged。 - Thibs
监听事件更好...我们不想使用setTimeoutsetInterval来不断地轮询它...除非没有其他方法。 - nonopolarity
@Thibs voiceschanged事件现在在Safari中可以使用了,我不知道它是什么时候被添加的。 - Samathingamajig

9

在研究了Google Chrome和Firefox的行为后,以下是能够获得所有音频的方法:

由于涉及到异步操作,最好使用Promise来实现:

const allVoicesObtained = new Promise(function(resolve, reject) {
  let voices = window.speechSynthesis.getVoices();
  if (voices.length !== 0) {
    resolve(voices);
  } else {
    window.speechSynthesis.addEventListener("voiceschanged", function() {
      voices = window.speechSynthesis.getVoices();
      resolve(voices);
    });
  }
});

allVoicesObtained.then(voices => console.log("All voices:", voices));

注意:

  1. 当事件 voiceschanged 触发时,我们需要再次调用 .getVoices()。原始数组不会被填充内容。
  2. 在 Google Chrome 上,我们最初不必调用 getVoices()。我们只需要监听事件,然后它就会发生。在 Firefox 上,仅监听是不够的,您必须调用 getVoices(),然后监听事件 voiceschanged,并使用 getVoices() 设置数组一旦您得到通知。
  3. 使用 promise 使代码更加简洁。与获取语音相关的所有内容都在此 promise 代码中。如果您不使用 promise 而是将此代码放入语音例程中,则会非常混乱。
  4. 您可以编写一个名为 voiceObtained 的 promise 来解析您想要的语音,然后您的函数可以说出某些内容: voiceObtained.then(voice => { }) 在该处理程序内部,调用 window.speechSynthesis.speak() 来说出某些内容。或者,您甚至可以编写一个 promise speechReady("hello world").then(speech => { window.speechSynthesis.speak(speech) }) 来说某些内容。

5

这是答案

function synthVoice(text) {

  const awaitVoices = new Promise(resolve=> 
    window.speechSynthesis.onvoiceschanged = resolve)  
  .then(()=> {
    const synth = window.speechSynthesis;

    var voices = synth.getVoices();
    console.log(voices)

    const utterance = new SpeechSynthesisUtterance();
    utterance.voice = voices[3];        
    utterance.text = text;

    synth.speak(utterance);
  });
}

在一个 Chrome 标签页中,已经成功地完成了合成。现在尝试在另一个 Chrome 标签页中进行。是的,我添加了 synthVoice("Hello world!"),但是没有听到任何声音。 - jlettvin

3

起初我使用了onvoiceschanged,但它即使在声音加载完成后仍然不断触发,所以我的目标是尽可能避免使用onvoiceschanged。

这是我想出的方法。到目前为止,它似乎可以工作,如果出问题会进行更新。

loadVoicesWhenAvailable();

function loadVoicesWhenAvailable() {
         voices = synth.getVoices();

         if (voices.length !== 0) {
                console.log("start loading voices");
                LoadVoices();
            }
            else {
                setTimeout(function () { loadVoicesWhenAvailable(); }, 10)
            }
    }

4
LoadVoices() 是指什么? - user1063287

2

Salman Oskooi提供的setInterval解决方案完美无缺

请查看https://jsfiddle.net/exrx8e1y/

function myFunction() {

  dtlarea=document.getElementById("details");
  //dtlarea.style.display="none";
  dtltxt="";

  var mytimer = setInterval(function() {

      var voices = speechSynthesis.getVoices();
      //console.log(voices);
      if (voices.length !== 0) {

        var msg = new SpeechSynthesisUtterance();

        msg.rate = document.getElementById("rate").value; // 0.1 to 10
        msg.pitch = document.getElementById("pitch").value; //0 to 2
        msg.volume = document.getElementById("volume").value; // 0 to 1

        msg.text = document.getElementById("sampletext").value; 
        msg.lang =  document.getElementById("lang").value; //'hi-IN';

        for(var i=0;i<voices.length;i++){

            dtltxt+=voices[i].lang+' '+voices[i].name+'\n';

            if(voices[i].lang==msg.lang) {
              msg.voice = voices[i]; // Note: some voices don't support altering params
              msg.voiceURI = voices[i].voiceURI;
              // break;
            }
        }

        msg.onend = function(e) {
          console.log('Finished in ' + event.elapsedTime + ' seconds.');
          dtlarea.value=dtltxt; 
        };

        speechSynthesis.speak(msg);

        clearInterval(mytimer);

      }
  }, 1000);

} 

这在Chrome for MAC、Linux(Ubuntu)、Windows和Android上都可以正常工作。

Android有非标准的en_GB语言代码,而其他语言代码为en-GB。此外,您会发现相同的语言(lang)有多个名称。

在Mac Chrome上,您会看到en-GB Daniel以及en-GB Google UK English Female和n-GB Google UK English Male。

en-GB Daniel (Mac和iOS) en-GB Google UK English Female en-GB Google UK English Male en_GB English United Kingdom hi-IN Google हिन्दी hi-IN Lekha (Mac和iOS) hi_IN Hindi India


1

我使用这段代码成功地加载了语音:

<select id="voices"></select>

...

  function loadVoices() {
    populateVoiceList();
    if (speechSynthesis.onvoiceschanged !== undefined) {
      speechSynthesis.onvoiceschanged = populateVoiceList;
    }
  }

  function populateVoiceList() {
    var allVoices = speechSynthesis.getVoices();
    allVoices.forEach(function(voice, index) {
      var option = $('<option>').val(index).html(voice.name).prop("selected", voice.default);
      $('#voices').append(option);
    });
    if (allVoices.length > 0 && speechSynthesis.onvoiceschanged !== undefined) {
      // unregister event listener (it is fired multiple times)
      speechSynthesis.onvoiceschanged = null;
    }
  }

我从这篇文章https://hacks.mozilla.org/2016/01/firefox-and-the-web-speech-api/中找到了“onvoiceschanged”代码。
注意:需要JQuery。
适用于Firefox / Safari和Chrome(以及Google应用脚本 - 但仅限于HTML)。

1
async function speak(txt) {
    await initVoices();
    const u = new SpeechSynthesisUtterance(txt);
    u.voice = speechSynthesis.getVoices()[3];
    speechSynthesis.speak(u);
}

function initVoices() {
  return new Promise(function (res, rej){
    speechSynthesis.getVoices();
    if (window.speechSynthesis.onvoiceschanged) {
       res();
    } else {
      window.speechSynthesis.onvoiceschanged = () => res();
    }
  });
}

1

尽管被接受的答案非常好,但如果您正在使用SPA并且不是加载完整页面,则在链接之间导航时,声音将不可用。

这将在完整页面加载时运行

window.speechSynthesis.onvoiceschanged

对于单页应用(SPA),它不会运行。

您可以检查它是否未定义,运行它,否则从窗口对象中获取它。

以下是一个可行的示例:

let voices = [];
if(window.speechSynthesis.onvoiceschanged == undefined){
     window.speechSynthesis.onvoiceschanged = () => {
     voices = window.speechSynthesis.getVoices();
   }
 }else{
    voices = window.speechSynthesis.getVoices();
 }
 // console.log("voices", voices);

1
    let voices = speechSynthesis.getVoices();
    let gotVoices = false;
    if (voices.length) {
        resolve(voices, message);
    } else {
        speechSynthesis.onvoiceschanged = () => {
            if (!gotVoices) {
                voices = speechSynthesis.getVoices();
                gotVoices = true;
                if (voices.length) resolve(voices, message);
            }
        };
    }

function resolve(voices, message) {
    var synth = window.speechSynthesis;
    let utter = new SpeechSynthesisUtterance();
    utter.lang = 'en-US';
    utter.voice = voices[65];
    utter.text = message;
    utter.volume = 100.0;
    synth.speak(utter);
}

适用于Edge、Chrome和Safari - 不重复句子。


适用于我使用的React JS。Edge - Quimbo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接