Применение Web Speech API

Web Speech API предоставляет 2 основных типа функционала — распознавание речи пользователя и речевое воспроизведение текста. Это предоставляет новые возможности для взаимодействия с интерфейсом и открывает перед нами новые горизонты создания уникального пользовательского опыта. Эта статья дает краткое описание обоих направлений с примерами кода и ссылкой на работающее приложение онлайн.

Распознавание речи

Механизм распознавания речи способен принимать речевой поток через микрофон устройства, а затем проверять его, используя свои внутренние алгоритмы. Для более точной работы рекомендуется использовать интерфейс SpeechGrammar, предоставляющий контейнер для определенного набора грамматики, которое ваше приложение должно использовать. Грамматика определяется с помощью JSpeech Grammar Format(JSGF.).

После того, как пользовательская речь была распознана, алгоритм возвращает результат (список результатов) в качестве текстовой строки, с которой мы можем продолжить работу.

Внимание: В Chrome распознавание речи на веб-странице завязано на взаимодействие с сервером. Ваш звук отправляется на веб-службу для обработки распознавания, поэтому приложение не будет работать в оффлайн-режиме.

Демо

Для запуска демо достаточно перейти по ссылке на приложение или скачать репозиторий, установить зависимости (npm install) и запустить приложение (npm run start), после чего открыть localhost:4001 в браузере.

после озвучки команды

Браузерная поддержка

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

  • Мобильный и десктопный Firefox поддерживает его в Gecko 44+ без префиксов, и его можно включить, установив значение флага media.webspeech.recognition.enable на true в about:config

  • Chrome для настольных компьютеров и версия для Android поддерживали его с версии 33, но с прописанными префиксами, поэтому вам нужно использовать префиксную версию, например webkitSpeechRecognition

Традиционно, самая актуальная информация по поддержке чего-либо в браузерах на caniuse.

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 для этой цели.

Демо

То же самое приложение из предыдущего примера.
Ccылка на приложение или репозиторий (клонируем, затем npm install && npm run start в терминале, после чего открыть localhost:4001 в браузере).

Пользовательский интерфейс включает в себя набор элементов для ввода текста, задания высоты тона, скорости воспроизведения и непосредственного выбора голоса, которым будет текст произнесен.

После ввода текста вы можете нажать Play для запуска.

Браузерная поддержка

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

  • Мобильный и десктопный Firefox поддерживает его в Gecko 44+ без префиксов, и его можно включить, установив значение флага media.webspeech.synth.enabled на true в about:config

  • Chrome для настольных компьютеров и версия для Android поддерживали его с версии 33 без префиксов

  • Традиционно, самая актуальная информация по поддержке чего-либо в браузерах на caniuse.

HTML и CSS

HTML и CSS снова достаточно тривиальны.
Заголовок и форму с некоторыми простыми элементами управления.
Элемент <select> изначально пуст, но заполняется с помощью <option> через JavaScript (см. ниже).

CSS задает простые отзывчивые стили, для корректного отображения и работы на всех устройствах

<section>
  <h1>Синтез речи</h1>
  <p>Введите текст в поле ниже и нажмите кнопку "Play", чтобы прослушать запись. Выбирайте возможные голоса из списка ниже</p>

  <form>
    <input type="text" class="text">
    <div class="row">
      <div class="values-box">
        <div class="value-box">
          <div>Темп (Rate)</div>
          <div class="value value--rate-value">1</div>
        </div>

        <div class="value-box">
          <div>Диапазон (Pitch)</div>
          <div class="value value--pitch-value">1</div>
        </div>
      </div>

      <div class="ranges-box">
        <input type="range" min="0.5" max="2" value="1" step="0.1" id="rate">
        <input type="range" min="0" max="2" value="1" step="0.1" id="pitch">
      </div>
    </div>

    <select>
    </select>

    <button id="play" 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;
};