Получение списка голосов в speechSynthesis of Chrome (Web Speech API)

Следующий 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 все еще не работает должным образом. Он возвращается пустым при загрузке страницы, но работает позже.

Вопросы:

Поскольку это может быть ошибка в бета-версии, я отказался от вопросов "Почему".

Теперь вопрос, если вы хотите получить доступ 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>

Мой вопрос был: почему window.speechSynthesis.getVoices() вернуть пустой массив после загрузки страницы и onready функция срабатывает? Как вы можете видеть, если вы нажмете на ссылку, та же функция возвращает массив доступных голосов Chrome по onclick Тригер?

Похоже хром загружает window.speechSynthesis после загрузки страницы!

Проблема не в ready событие. Если я уберу строку var voice=... от ready функция, для первого клика показывает пустой список в консоли. Но второй клик работает отлично.

Похоже на то 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);

14 ответов

Решение

Согласно сообщению об ошибках Web Speech API (E11 2013-10-17), голосовой список загружается асинхронно на страницу. onvoiceschanged событие запускается, когда они загружены.

voiceschanged: запускается, когда содержимое SpeechSynthesisVoiceList, возвращаемое методом getVoices, изменилось. Примеры включают: синтез на стороне сервера, где список определяется асинхронно, или когда голоса на стороне клиента установлены / удалены.

Итак, хитрость заключается в том, чтобы установить свой голос из обратного вызова для этого прослушивателя событий:

// wait on voices to be loaded before fetching list
window.speechSynthesis.onvoiceschanged = function() {
    window.speechSynthesis.getVoices();
    ...
};

Изучив поведение в Google Chrome и Firefox, все голоса можно получить вот так:

Поскольку это связано с чем-то асинхронным, лучше всего сделать это с помощью обещания:

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. Использование обещания делает код более чистым. Все, что связано с получением голосов, находится в этом коде обещания. Если вы не используете обещание, а вместо этого вставляете этот код в свою речь, это довольно беспорядочно.
  4. Вы можете написать voiceObtained пообещайте разрешиться голосом, который вам нужен, а затем ваша функция сказать что-то может просто сделать: voiceObtained.then(voice => { }) и внутри этого обработчика вызовите window.speechSynthesis.speak()что-то говорить. Или вы даже можете написать обещаниеspeechReady("hello world").then(speech => { window.speechSynthesis.speak(speech) }) что-то сказать.

Вы можете использовать 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);

Вот ответ

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);
  });
}

Сначала я использовал onvoiceschange, но он продолжал работать даже после того, как голоса были загружены, поэтому моей целью было избежать изменения onvoices любой ценой.

Это то, что я придумал. Вроде пока работает, обновлю, если сломается.

loadVoicesWhenAvailable();

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

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

Другой способ убедиться, что голоса загружаются до того, как они понадобятся, - связать их состояние загрузки с обещанием, а затем отправить свои речевые команды из then:

const awaitVoices = new Promise(done => speechSynthesis.onvoiceschanged = done);

function listVoices() {
    awaitVoices.then(()=> {
        let voices = speechSynthesis.getVoices();
        console.log(voices);
    });
}

Когда вы звоните listVoices, он либо будет ждать загрузки голосов в первую очередь, либо отправит вашу операцию на следующий тик.

Во-первых, большое спасибо за этот ответ. Во-вторых, вот полезный JSBin, если кто-то снова сталкивается с этим вопросом / ответом: http://jsbin.com/gosaqihi/9/edit?js,console

Решение 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 для MAC, Linux(Ubuntu), Windows и Android

Android имеет нестандартный en_GB, в то время как другие используют en-GB в качестве кода языка. Также вы увидите, что один и тот же язык (lang) имеет несколько имен

На Mac Chrome вы получаете en-GB Daniel, помимо en-GB Google UK Английский женский и n-GB Google UK Английский мужской

en-GB Daniel (Mac и iOS) en-GB Google Великобритания английский женский en-GB Google UK английский мужской en_GB английский Великобритания hi-IN Google हिन्दी hi-IN Lekha (Mac и iOS) hi_IN хинди Индия

Я использовал этот код для успешной загрузки голосов:

<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;
    }
  }

Я нашел код "onvoiceschange" из этой статьи: https://hacks.mozilla.org/2016/01/firefox-and-the-web-speech-api/

Работает в Firefox/Safari и Chrome (и в Google Apps Script тоже - но только в HTML).

Хотя принятый ответ отлично работает, но если вы используете 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);
      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();
    }
  });
}
          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 — предложения не повторяются.

Android Chrome - отключить сохранение данных. Это было полезно для меня.(Chrome 71.0.3578.99)

// wait until the voices load
   window.speechSynthesis.onvoiceschanged = function() {
    window.speechSynthesis.getVoices();

};

Мне нужно было провести собственное исследование, чтобы убедиться, что я правильно понял, поэтому просто делюсь (не стесняйтесь редактировать).

Моя цель состоит в том, чтобы:

  • Получить список голосов, доступных на моем устройстве
  • Заполните элемент select этими голосами (после загрузки определенной страницы)
  • Используйте простой для понимания код

Основные функциональные возможности продемонстрированы в официальном живом демо MDN:

https://github.com/mdn/web-speech-api/tree/master/speak-easy-synthesis

но я хотел понять это лучше.

Чтобы сломать тему вниз...

SpeechSynthesis

SpeechSynthesis интерфейс API Web Speech является интерфейсом контроллера для речевого сервиса; это может быть использовано для извлечения информации о доступных на устройстве голосах синтеза, речи начала и приостановки, а также других команд.

Источник

onvoiceschanged

onvoiceschanged собственность SpeechSynthesis Интерфейс представляет обработчик событий, который будет запущен, когда список SpeechSynthesisVoice объекты, которые будут возвращены SpeechSynthesis.getVoices() метод изменился (когда voiceschanged событие пожаров.)

Источник

Пример А

Если мое заявление просто имеет:

var synth = window.speechSynthesis;
console.log(synth);
console.log(synth.onvoiceschanged);

Консоль инструментов разработчика Chrome покажет:

введите описание изображения здесь

Пример Б

Если я изменю код на:

var synth = window.speechSynthesis;

console.log("BEFORE");
console.log(synth);
console.log(synth.onvoiceschanged);

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

console.log(voices);
console.log(synth);
console.log(synth.onvoiceschanged);

Состояния до и после одинаковы, и voices это пустой массив.

введите описание изображения здесь

Решение

Хотя я не уверен, что выполняю Обещания, у меня сработало следующее:

Определение функции

var synth = window.speechSynthesis;
// declare so that values are accessible globally
var voices = [];


function set_up_speech() {

    return new Promise(function(resolve, reject) {

        // get the voices
        var voices = synth.getVoices();

        // get reference to select element
        var $select_topic_speaking_voice = $("#select_topic_speaking_voice");

        // for each voice, generate select option html and append to select
        for (var i = 0; i < voices.length; i++) {

            var option = $("<option></option>");

            var suffix = "";

            // if it is the default voice, add suffix text  
            if (voices[i].default) {
                suffix = " -- DEFAULT";
            }

            // create the option text
            var option_text = voices[i].name + " (" + voices[i].lang + suffix + ")";

            // add the option text
            option.text(option_text);

            // add option attributes
            option.attr("data-lang", voices[i].lang);
            option.attr("data-name", voices[i].name);

            // append option to select element
            $select_topic_speaking_voice.append(option);
        }

        // resolve the voices value
        resolve(voices)

    });

}

Вызов функции

// in your handler, populate the select element    
if (page_title === "something") {
set_up_speech()
}
Другие вопросы по тегам