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

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

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

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

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

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

### Демо

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

![](https://pp.userapi.com/c831409/v831409509/1c0226/S_tm-BfW-U8.jpg)

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

![](https://pp.userapi.com/c831409/v831409509/1c022e/uWRjlOvjopk.jpg)

### 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`.


```js
speechRecognitionList.addFromString(grammar, 1);
```


Затем мы добавляем `SpeechGrammarList` (en-US) к уже созданному объекту распознавания речи, присваивая его значение свойству `SpeechRecognition.grammars` (en-US). Также зададим ещё несколько свойств объекту, прежде чем двигаться дальше:

* `SpeechRecognition.lang` (en-US): устанавливает язык распознавания. Его установка - это хорошая практика, поэтому рекомендуется не пропускать.
* `SpeechRecognition.interimResults` (en-US): определяет, должна ли система распознавания речи возвращать промежуточные результаты или только конечные результаты. Только конечные результаты подойдут для этой нашего простого приложения.
* `SpeechRecognition.maxAlternatives` (en-US): устанавливает количество альтернативных потенциальных совпадений, которые должны быть возвращены на каждый результат. Иногда это может быть полезно, скажем, если результат распознан не точно, и вы хотите отобразить пользователю список вариантов. Но это для простого примера это не нужно, поэтому мы просто указываем один (который по сути является вариантом по умолчанию).


```js
recognition.grammars = speechRecognitionList;
//recognition.continuous = false;
recognition.lang = "ru-RU";
recognition.interimResults = false;
recognition.maxAlternatives = 1;
```


**Примечание:** `SpeechRecognition.continuous` (en-US) задаёт, отслеживаются ли продолжающиеся результаты или только 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 (en-US), который запускается сразу после получения успешного результата. Значение цвета получаем вызовом функции `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` (en-US) возвращает объект `SpeechRecognitionResultList` (en-US), содержащий в себе другие объекты типа `SpeechRecognitionResult` (en-US). У него есть геттер, поэтому он может быть доступен как массив, поэтому переменная `last` определяет ссылку на `SpeechRecognitionResult` из списка. Каждый объект `SpeechRecognitionResult` содержит объекты `SpeechRecognitionAlternative` (en-US), которые содержат отдельные распознанные слова. Они также имеют геттеры, поэтому к ним можно получить доступ как к массивам, поэтому логично, что [0] возвращает значение `SpeechRecognitionAlternative` по индексу 0. Затем мы возвращаем строку, содержащую индивидуально распознанный результат, используя который и можем установить цвет фона.

Мы также используем свойство `SpeechRecognition.speechend` (en-US), чтобы задать обработчик на завершение работы распознавателя речи (вызов `SpeechRecognition.stop()` (en-US) ), как только одно слово было распознано, и входящий речевой поток был остановлен.
```

recognition.onspeechend = function() {
  recognition.stop();
  microphoneWrapper.style.visibility = 'visible';
  audioRecordAnimation.style.visibility = 'hidden';
};
```


#### Обработка ошибок

Последние два обработчика используются для отлова ошибок: когда речь была признана не в соответствии с определённой грамматикой или произошла ошибка. По логике, `SpeechRecognition.onnomatch` (en-US), должен обрабатывать первый случай, но обратите внимание, что на данный момент он не срабатывает правильно в Firefox или Chrome, он просто возвращает все, что было распознано в любом случае:
```

recognition.onnomatch = function(event) {
  alert("I didn't recognise that color.");
};
```


`SpeechRecognition.onerror` (en-US) обрабатывает случаи, когда имела место быть фактическая ошибка при распознавании. Свойство `SpeechRecognitionError.error` (en-US) содержит возвращаемую фактическую ошибку:
```

recognition.onerror = function(event) {
  alert(`Error occurred in recognition: ${event.error}`);
};
```

Синтез речи
-----------

Синтез речи (text-to-speech или tts) подразумевает получение синтезированного текста приложения и его речевое воспроизведение.

 Для этой цели Web Speech API предоставляет интерфейс - `SpeechSynthesis` (en-US) - плюс ряд близких интерфейсов для нужного нам воспроизведения текста (utterances - "дикция"), набор голосов, которыми приложение будет "говорить", и т. д.
 Опять же, большинство ОС имеют некоторые встроенные системы синтеза речи, которые будут задействованы нашим API для этой цели.

### Демо

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

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

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

![](https://pp.userapi.com/c847220/v847220505/1103b9/Jlnq5hDThyQ.jpg)

### 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` (en-US), возвращает экземпляр `SpeechSynthesis` (en-US), интерфейс контроллера для синтеза речи в вебе.
```

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()` (en-US). Сначала мы вызываем `SpeechSynthesis.getVoices()` (en-US), который возвращает список всех доступных вариантов голосов, представленных объектами `SpeechSynthesisVoice` (en-US). Затем мы проходимся по списку, создавая элемент `<option>` для каждого отдельного случая, задаём его текстовое содержимое, соответствующее названию голоса (взято из `SpeechSynthesisVoice.name` (en-US)), языка голоса (из `SpeechSynthesisVoice.lang` (en-US)), и "по умолчанию", если голос является голосом по умолчанию для механизма синтеза (проверяется, если функция `SpeechSynthesisVoice.default` (en-US) возвращает значение `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` (en-US) и будет только возвращать список голосов при запуске `SpeechSynthesis.getVoices()` (en-US). Однако, в Chrome вам нужно дождаться триггера события перед заполнением списка, следовательно, нужно условие, описанное в блоке с `if` ниже.
```

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


#### Озвучка введённого текста

Затем мы создаём обработчик событий, чтобы начать "произносить" текст, введённый в текстовом поле, при нажатии на кнопку `Enter/Return` или на `Play`. Для этого используем обработчик `onsubmit` в html-формы. В функции-обработчике `speak()` мы создаём новый экземпляр `SpeechSynthesisUtterance()` (en-US), передавая значение текстового поля в конструктор.

Затем нам нужно выяснить, какой голос использовать. Мы используем свойство `HTMLSelectElement` (en-US) `selectedOptions` для получения выбранного элемента `<option>`, у которого берём атрибут data-name, и находим объект `SpeechSynthesisVoice` (en-US), имя которого соответствует значению имеющегося атрибута. После этого устанавливаем соответствующий "голосовой" объект как значение свойства `SpeechSynthesisUtterance.voice` (en-US).

Наконец, мы устанавливаем `SpeechSynthesisUtterance.pitch` (en-US) (высота тона) и `SpeechSynthesisUtterance.rate` (en-US) (скорость) в соответствии со значениями соответствующих элементов формы. Затем, после всего проделанного, мы запускаем произношение речи, вызывая `SpeechSynthesis.speak()` (en-US), и передавая ему экземпляр `SpeechSynthesisUtterance` в качестве аргумента.

 Внутри функции `speak()` мы выполняем проверку на то, воспроизводится ли речь в данный момент, с помощью свойства `SpeechSynthesis.speaking` (en-US)
 Если да, то останавливаем процесс функцией `SpeechSynthesis.cancel()` (en-US) и запускаем рекурсивно заново.

В последней части функции мы включаем обработчик `SpeechSynthesisUtterance.onpause` (en-US), чтобы показать пример применения `SpeechSynthesisEvent` (en-US) в различных ситуациях. Вызов `SpeechSynthesis.pause()` (en-US)возвращает сообщение с информацией о номере символа и слове, на котором была вызвана пауза.

Наконец, мы назовём `blur()` у текстового поля. Это, прежде всего, для того, чтобы скрыть клавиатуру в ОС Firefox.


```js
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();
};
```


#### Обновление отображаемых значений высоты тона и скорости

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


```js
pitch.onchange = function () {
  pitchValue.textContent = pitch.value;
};

rate.onchange = function () {
  rateValue.textContent = rate.value;
};
```