Применение Web Speech API
Web Speech API предоставляет 2 основных типа функциональности —распознавание речи пользователя иречевое воспроизведение текста. Это предоставляет новые возможности для взаимодействия с интерфейсом и открывает перед нами новые горизонты создания уникального пользовательского опыта. Эта статья даёт краткое описание обоих направлений с примерами кода и ссылкой на работающее приложение онлайн.
Распознавание речи
Механизм распознавания речи способен принимать речевой поток через микрофон устройства, а затем проверять его, используя свои внутренние алгоритмы. Для более точной работы рекомендуется использовать интерфейсSpeechGrammar, предоставляющий контейнер для определённого набора грамматики, которое ваше приложение должно использовать. Грамматика определяется с помощьюJSpeech Grammar Format(JSGF.).
После того, как пользовательская речь была распознана, алгоритм возвращает результат (список результатов) в качестве текстовой строки, с которой мы можем продолжить работу.
Примечание:В Chrome распознавание речи на веб-странице завязано на взаимодействие с сервером. Ваш звук отправляется на веб-службу для обработки распознавания, поэтому приложение не будет работать в офлайн-режиме.
Демо
Демонстрацию использования распознавания речи можно увидеть в приложенииSpeech color changer. При клике по экрану можно произнести название цвета, и фон приложения изменится на этот цвет.
Для запуска демонстрации откройтестраницу приложения в браузере мобильного устройства с поддержкой распознавания речи (например, в Chrome).
HTML и CSS
Разметка и стили предельно просты. У нас есть значок микрофона, на который мы можем кликнуть для начала записи, анимация звукозаписи, которая включается после клика, и фоновый контейнер, который будет изменять свой цвет, в зависимости от того, что озвучит пользователь.
CSS задаёт простые отзывчивые стили, для корректного отображения и работы на всех устройствах
JavaScript
А вот на реализацию логики давайте обратим более пристальное внимание.
Поддержка Chrome
Как уже упоминалось ранее, в настоящее время Chrome поддерживает интерфейс распознавания речи с указанными префиксами, поэтому в начале нашего кода мы включаем строки префиксов для использования нужных объектов в Chrome и ссылки на объекты без префиксов для Firefox.
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;const SpeechGrammarList = window.SpeechGrammarList || window.webkitSpeechGrammarList;const SpeechRecognitionEvent = window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;
Грамматика
Следующая часть нашего кода определяет грамматику, которую мы хотим, применять для поиска соответствий.
Определяем следующие переменные:
const colors = { красный: 'red', оранжевый: 'orange', жёлтый: 'yellow', зелёный: 'green', голубой: 'blue', синий: 'darkblue', фиолетовый: 'violet'};const colorsList = Object.keys(colors);const grammar = '#JSGF V1.0; grammar colors; public <color> = ' + colorsList.join(' | ') + ' ;';Формат “грамматики“ используемой нами - это JSpeech Grammar Format (JSGF) - по ссылке можете почитать про это больше.
Быстро пробежимся по основным принципам:
- Линии разделены точкой с запятой, как и в JavaScript.
- Первая строка -
#JSGF V1.0;
- указывает формат и версию. Это всегда необходимо включать в первую очередь. - Вторая строка указывает значение, которое мы хотим распознать. public объявляет, что это общедоступное правило, строка в угловых скобках определяет распознанное имя для этого значения (цвет), а список элементов, следующих за знаком равенства, - это альтернативные варианты, которые будут распознаны и могут быть приняты в качестве возможного значения. Обратите внимание, как каждый из них разделяется вертикальной линией ("|" - "pipe character").
- У вас может быть множество значений, определённых отдельно, как указано выше, и содержащих довольно сложные определения грамматики. Для нашего демонстрационного примера мы делаем все просто.
Подключение грамматики к нашему распознаванию речи
Следующее, что нужно сделать, это определить экземпляр объекта распознавания речи для управления записью нашего приложения.
Это делается с помощью конструктораSpeechRecognition()
. Мы также создаём новый речевой грамматический список, чтобы содержать нашу грамматику, используя конструкторSpeechGrammarList()
.
const recognition = new SpeechRecognition();const speechRecognitionList = new SpeechGrammarList();
Добавляем нашу "грамматику" в список, используя методSpeechGrammarList.addFromString()
. Он принимает в качестве параметров строку, плюс необязательное значение веса, которое указывает важность этой грамматики по отношению к другим грамматикам, доступным в списке (может быть от 0 до 1 включительно). Добавленная грамматика доступна в списке как экземпляр объектаSpeechGrammar
.
speechRecognitionList.addFromString(grammar, 1);
Затем мы добавляемSpeechGrammarList
к уже созданному объекту распознавания речи, присваивая его значение свойствуSpeechRecognition.grammars
. Также зададим ещё несколько свойств объекту, прежде чем двигаться дальше:
SpeechRecognition.lang
: устанавливает язык распознавания. Его установка - это хорошая практика, поэтому рекомендуется не пропускать.SpeechRecognition.interimResults
: определяет, должна ли система распознавания речи возвращать промежуточные результаты или только конечные результаты. Только конечные результаты подойдут для этой нашего простого приложения.SpeechRecognition.maxAlternatives
: устанавливает количество альтернативных потенциальных совпадений, которые должны быть возвращены на каждый результат. Иногда это может быть полезно, скажем, если результат распознан не точно, и вы хотите отобразить пользователю список вариантов. Но это для простого примера это не нужно, поэтому мы просто указываем один (который по сути является вариантом по умолчанию).
recognition.grammars = speechRecognitionList;//recognition.continuous = false;recognition.lang = "ru-RU";recognition.interimResults = false;recognition.maxAlternatives = 1;
Примечание:SpeechRecognition.continuous
задаёт, отслеживаются ли продолжающиеся результаты или только 1 результат, каждый раз, когда запись начата. Это закомментировано, поскольку данное свойство в ещё не реализовано в Gecko.
Вы можете получить аналогичный результат, просто прекратив распознавание после получения первого результата.
Запуск распознавания речи
После получения ссылок на DOM-элементы, необходимые нам для обработки пользовательских событий и обновления цвета фона приложения, мы реализуем обработчикonclick
, чтобы при нажатии на значок микрофона сервис распознавания речи начинал работу. Запуск происходит путём вызова функцииSpeechRecognition.start()
.
microphoneIcon.onclick = function() { recognition.start(); console.log('Ready to receive a color command.');};recognition.onaudiostart = function() { microphoneWrapper.style.visibility = 'hidden'; audioRecordAnimation.style.visibility = 'visible';};
Получение и обработка результата
После того, как процесс распознавания речи был запущен, есть много обработчиков событий, которые могут быть использованы для работы с результатом и другой сопутствующей информацией (см.Список обработчиков событий SpeechRecognition.) Наиболее распространённый, который вы, вероятно, и будете использовать, этоSpeechRecognition.onresult, который запускается сразу после получения успешного результата. Значение цвета получаем вызовом функцииgetColor()
function getColor(speechResult) { for (let index = 0; index < colorsList.length; index += 1) { if (speechResult.indexOf(colorsList[index]) !== -1) { const colorKey = colorsList[index]; return [colorKey, colors[colorKey]]; } } return null;}recognition.onresult = function(event) { const last = event.results.length - 1; const colors = getColor(event.results[last][0].transcript); recognitionTextResult.textContent = 'Результат: ' + colors[0]; speechRecognitionSection.style.backgroundColor = colors[1]; console.log('Confidence: ' + event.results[0][0].confidence);};
Третья строка здесь выглядит немного усложнённой, поэтому давайте разберёмся с ней подробнее. СвойствоSpeechRecognitionEvent.results
возвращает объектSpeechRecognitionResultList
, содержащий в себе другие объекты типаSpeechRecognitionResult
. У него есть геттер, поэтому он может быть доступен как массив, поэтому переменнаяlast
определяет ссылку наSpeechRecognitionResult
из списка. Каждый объектSpeechRecognitionResult
содержит объектыSpeechRecognitionAlternative
, которые содержат отдельные распознанные слова. Они также имеют геттеры, поэтому к ним можно получить доступ как к массивам, поэтому логично, что [0] возвращает значениеSpeechRecognitionAlternative
по индексу 0. Затем мы возвращаем строку, содержащую индивидуально распознанный результат, используя который и можем установить цвет фона.
Мы также используем свойствоSpeechRecognition.speechend
, чтобы задать обработчик на завершение работы распознавателя речи (вызовSpeechRecognition.stop()
), как только одно слово было распознано, и входящий речевой поток был остановлен.
recognition.onspeechend = function() { recognition.stop(); microphoneWrapper.style.visibility = 'visible'; audioRecordAnimation.style.visibility = 'hidden';};
Обработка ошибок
Последние два обработчика используются для отлова ошибок: когда речь была признана не в соответствии с определённой грамматикой или произошла ошибка. По логике,SpeechRecognition.onnomatch
, должен обрабатывать первый случай, но обратите внимание, что на данный момент он не срабатывает правильно в Firefox или Chrome, он просто возвращает все, что было распознано в любом случае:
recognition.onnomatch = function(event) { alert("I didn't recognise that color.");};
SpeechRecognition.onerror
обрабатывает случаи, когда имела место быть фактическая ошибка при распознавании. СвойствоSpeechRecognitionError.error
содержит возвращаемую фактическую ошибку:
recognition.onerror = function(event) { alert(`Error occurred in recognition: ${event.error}`);};
Синтез речи
Синтез речи (text-to-speech или tts) подразумевает получение синтезированного текста приложения и его речевое воспроизведение.
Для этой цели Web Speech API предоставляет интерфейс -SpeechSynthesis
- плюс ряд близких интерфейсов для нужного нам воспроизведения текста (utterances - "дикция"), набор голосов, которыми приложение будет "говорить", и т. д.Опять же, большинство ОС имеют некоторые встроенные системы синтеза речи, которые будут задействованы нашим API для этой цели.
Демо
Демонстрацию использования синтеза речи можно увидеть в приложенииSpeak easy synthesis. Оно содержит набор элементов управления формой для ввода текста, который будет синтезирован, и настройки высоты тона, скорости и голоса, которые будут использоваться при произнесении текста. После ввода текста можно нажатьEnter/Return, чтобы услышать его.
Для запуска демонстрации откройтестраницу приложения в браузере мобильного устройства с поддержкой синтеза речи.
HTML и CSS
HTML и CSS снова достаточно тривиальны.Заголовок и форму с некоторыми простыми элементами управления.Элемент<select>
изначально пуст, но заполняется с помощью<option>
через JavaScript (см. ниже).
CSS задаёт простые отзывчивые стили, для корректного отображения и работы на всех устройствах
<section> <h1>Синтез речи</h1> <p>Введите текст в поле ниже и нажмите кнопку "Play", чтобы прослушать запись. Выбирайте возможные голоса из списка ниже</p> <form> <input type="text"> <div> <div> <div> <div>Темп (Rate)</div> <div>1</div> </div> <div> <div>Диапазон (Pitch)</div> <div>1</div> </div> </div> <div> <input type="range" min="0.5" max="2" value="1" step="0.1"> <input type="range" min="0" max="2" value="1" step="0.1"> </div> </div> <select> </select> <button type="submit">Play</button> </form>
JavaScript
Давайте более детально рассмотрим скрипт, задающий логику нашему приложения.
Задание переменных
Прежде всего, создаём ссылки на все нужные нам DOM-элементы.
Входная точка API -window.speechSynthesis
, возвращает экземплярSpeechSynthesis
, интерфейс контроллера для синтеза речи в вебе.
const synth = window.speechSynthesis;const inputForm = document.querySelector('form');const inputTxt = document.querySelector('.text');const voicesList = document.querySelector('select');const pitch = document.querySelector('#pitch');const pitchValue = document.querySelector('.value--pitch-value');const rate = document.querySelector('#rate');const rateValue = document.querySelector('.value--rate-value');let voices = [];
Заполнение выпадающего списка
Чтобы заполнить элемент<select>
различными вариантами голоса, доступных на устройстве, напишем функциюpopulateVoiceList()
. Сначала мы вызываемSpeechSynthesis.getVoices()
, который возвращает список всех доступных вариантов голосов, представленных объектамиSpeechSynthesisVoice
. Затем мы проходимся по списку, создавая элемент<option>
для каждого отдельного случая, задаём его текстовое содержимое, соответствующее названию голоса (взято изSpeechSynthesisVoice.name
), языка голоса (изSpeechSynthesisVoice.lang
), и "по умолчанию", если голос является голосом по умолчанию для механизма синтеза (проверяется, если функцияSpeechSynthesisVoice.default
возвращает значениеtrue
.)
Мы также задаёмdata-
атрибуты для каждого варианта, содержащие имя и язык связанного голоса, благодаря чему мы можем легко их собрать их позже, а затем вложить все варианты в качестве дочерних элементов нашего списка (<select>
).
function populateVoiceList() { voices = synth.getVoices(); const selectedIndex = voicesList.selectedIndex < 0 ? 0 : voicesList.selectedIndex; voicesList.innerHTML = ''; for(i = 0; i < voices.length ; i++) { const option = document.createElement('option'); option.textContent = voices[i].name + ' (' + voices[i].lang + ')'; if(voices[i].default) { option.textContent += ' -- DEFAULT'; } option.setAttribute('data-lang', voices[i].lang); option.setAttribute('data-name', voices[i].name); voiceSelect.appendChild(option); } voicesList.selectedIndex = selectedIndex;}
Когда мы собираемся запустить функцию, мы делаем следующее. Это связано с тем, что Firefox не поддерживает свойствоSpeechSynthesis.onvoiceschanged
и будет только возвращать список голосов при запускеSpeechSynthesis.getVoices()
. Однако, в Chrome вам нужно дождаться триггера события перед заполнением списка, следовательно, нужно условие, описанное в блоке сif
ниже.
populateVoiceList(); if (speechSynthesis.onvoiceschanged !== undefined) { speechSynthesis.onvoiceschanged = populateVoiceList;}
Озвучка введённого текста
Затем мы создаём обработчик событий, чтобы начать "произносить" текст, введённый в текстовом поле, при нажатии на кнопкуEnter/Return
или наPlay
. Для этого используем обработчикonsubmit
в html-формы. В функции-обработчикеspeak()
мы создаём новый экземплярSpeechSynthesisUtterance()
, передавая значение текстового поля в конструктор.
Затем нам нужно выяснить, какой голос использовать. Мы используем свойствоHTMLSelectElement
selectedOptions
для получения выбранного элемента<option>
, у которого берём атрибут data-name, и находим объектSpeechSynthesisVoice
, имя которого соответствует значению имеющегося атрибута. После этого устанавливаем соответствующий "голосовой" объект как значение свойстваSpeechSynthesisUtterance.voice
.
Наконец, мы устанавливаемSpeechSynthesisUtterance.pitch
(высота тона) иSpeechSynthesisUtterance.rate
(скорость) в соответствии со значениями соответствующих элементов формы. Затем, после всего проделанного, мы запускаем произношение речи, вызываяSpeechSynthesis.speak()
, и передавая ему экземплярSpeechSynthesisUtterance
в качестве аргумента.
Внутри функцииspeak()
мы выполняем проверку на то, воспроизводится ли речь в данный момент, с помощью свойстваSpeechSynthesis.speaking
Если да, то останавливаем процесс функциейSpeechSynthesis.cancel()
и запускаем рекурсивно заново.
В последней части функции мы включаем обработчикSpeechSynthesisUtterance.onpause
, чтобы показать пример примененияSpeechSynthesisEvent
в различных ситуациях. ВызовSpeechSynthesis.pause()
возвращает сообщение с информацией о номере символа и слове, на котором была вызвана пауза.
Наконец, мы назовёмblur()
у текстового поля. Это, прежде всего, для того, чтобы скрыть клавиатуру в ОС Firefox.
function speak() { if (synth.speaking) { console.error("speechSynthesis.speaking"); synth.cancel(); setTimeout(speak, 300); } else if (inputTxt.value !== "") { const utterThis = new SpeechSynthesisUtterance(inputTxt.value); utterThis.onend = function (event) { console.log("SpeechSynthesisUtterance.onend"); }; utterThis.onerror = function (event) { console.error("SpeechSynthesisUtterance.onerror"); }; const selectedOption = voicesList.selectedOptions[0].getAttribute("data-name"); for (i = 0; i < voices.length; i++) { if (voices[i].name === selectedOption) { utterThis.voice = voices[i]; } } utterThis.onpause = function (event) { const char = event.utterance.text.charAt(event.charIndex); console.log( "Speech paused at character " + event.charIndex + ' of "' + event.utterance.text + '", which is "' + char + '".', ); }; utterThis.pitch = pitch.value; utterThis.rate = rate.value; synth.speak(utterThis); }}inputForm.onsubmit = function (event) { event.preventDefault(); speak(); inputTxt.blur();};
Обновление отображаемых значений высоты тона и скорости
Последний пример кода просто обновляет значения высоты тона/скорости, отображаемые в пользовательском интерфейсе, каждый раз, когда позиции ползунка перемещаются.
pitch.onchange = function () { pitchValue.textContent = pitch.value;};rate.onchange = function () { rateValue.textContent = rate.value;};