Получение списка голосов в 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));
Примечание:
- Когда событие
voiceschanged
пожары, нам нужно позвонить.getVoices()
еще раз. Исходный массив не будет заполнен содержимым. - В Google Chrome нам не нужно звонить
getVoices()
первоначально. Нам нужно только слушать событие, и оно произойдет. В Firefox прослушивания недостаточно, нужно позвонитьgetVoices()
а затем послушайте событиеvoiceschanged
, и установите массив, используяgetVoices()
как только вы получите уведомление. - Использование обещания делает код более чистым. Все, что связано с получением голосов, находится в этом коде обещания. Если вы не используете обещание, а вместо этого вставляете этот код в свою речь, это довольно беспорядочно.
- Вы можете написать
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()
}