Преобразование текста в речь c помощью JavaScript

Давайте поговорим об использовании JavaScript для синтеза речи, то есть о преобразовании текста в речь. Это довольно удобно и можно использовать, например для того, чтобы браузер читал текст вслух. Всё, что будет рассмотрено далее, делается с помощью чистого JavaScript и его SpeechSynthesis API. Увидите, что начать будет на удивление легко, однако когда погрузитесь в этот процесс, начнётся борьба с особенностями реализации для разных браузеров и платформ.

использование JavaScript для синтеза речи

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

Быстрый старт

Вот базовый пример кода для преобразования текста в речь.

const utterance = new SpeechSynthesisUtterance('Добрый день!');
window.speechSynthesis.speak(utterance);

Серьезно, это — всё, что необходимо для начала преобразования текста в речь! Вот пример, который можно послушать.

See this code Simple Text to Speech on x.xhtml.ru.

В этом примере есть строка кода, которая требует пояснений.

window.speechSynthesis.cancel();

Если window.speechSynthesis уже что-то говорит, и ему передаётся новый текст, то он будет добавлен в очередь. Поэтому в примере сперва отменяется всё, что уже воспроизводится и чтение текста начинается с начала. Это всё, что делает метод cancel().

Настройка голоса

Вы можете изменять голос, используемый при чтении текста. Набор доступных голосов будет отличаться в зависимости от браузера и ОС. Чтобы получить массив доступных голосов (SpeechSynthesisVoice) можно использовать метод getVoices():

const voices = window.speechSynthesis.getVoices();

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

window.speechSynthesis.onvoiceschanged = function() {
  const updatedVoices = window.speechSynthesis.getVoices();
};

Чтобы использовать голос, надо выбрать его из массива и установить для объекта SpeechSynthesisUtterance перед чтением текста. Вот пример, в котором используется последний голос из массива:

const voices = window.speechSynthesis.getVoices();
const lastVoice = voices[voices.length - 1];

const utterance = new SpeechSynthesisUtterance('Добрый день!');
utterance.voice = lastVoice; // заменить голос
window.speechSynthesis.speak(utterance);

Ещё можно настроить параметры высоты тона (pitch), скорости (rate) и громкости (volume). Все они, если не указано иное, по умолчанию равны 1.

const utterance = new SpeechSynthesisUtterance('Добрый день!');
utterance.pitch = 0.7;  // пониже
utterance.rate = 1.4;   // побыстрее
utterance.volume = 0.8; // потише
window.speechSynthesis.speak(utterance);

Вот здесь начинается беспорядок. Разные голоса могут иметь разные диапазоны значений высоты тона pitch и скорости rate. Вдобавок к этому, у разных браузеров есть свои особенности при настройке этих свойств. Эти причуды будут рассмотрены в конце этой статьи.

Эксперименты показали, что безопаснее всего использовать значения pitch и rate от 0.1 до 2 включительно. К счастью, volume (громкость) настраивается без сюрпризов: от 0 до 1.

В-общем, дальше надо попробовать все это.

See this code Text to Speech With Voice Customization on x.xhtml.ru.

Пауза и продолжение воспроизведения

Воспроизведение речи можно приостановить и возобновить. Это довольно просто.

window.speechSynthesis.pause();
window.speechSynthesis.resume();

К сожалению, пауза/продолжение не работают на Android. Об этих особенностях подробнее будет рассказано далее в этой статье.

Чтобы узнать, приостановлено ​​ли воспроизведение речи, надо проверить свойство window.speechSynthesis.paused. Чтобы узнать, идет ли воспроизведение речи, надо проверить свойство window.speechSynthesis.speaking. Стоит обратить внимание, что они не исключают друг друга. Свойство speaking может быть установлено, даже если воспроизведение приостановлено. Кроме того, можно находиться в состоянии paused, даже когда нечего воспроизводить.

События

Можно прослушивать события объекта SpeechSynthesisUtterance и реагировать на них.

  • start возникает в начале воспроизведения.
  • pause возникает, когда воспроизведение приостановлено.
  • resume возникает, когда воспроизведение продолжено.
  • end возникает, когда закончился текст для воспроизведения. Кроме Safari, в остальных браузерах это событие срабатывает когда воспроизведение ещё и отменено.
  • boundary возникает, когда речь достигает нового слова или предложения. Это событие не работает на Android, но об этом чуть позже.

Все эти события SpeechSynthesisEvent возвращают несколько дополнительных свойств, которые могут пригодиться. Вот пример обращения с одним из них.

const utterance = new SpeechSynthesisUtterance('Это всё слова!');
utterance.addEventListener('pause', function(event) {
  console.log('Приостановлено после ' + event.elapsedTime + 'ms.');
});
window.speechSynthesis.speak(utterance);

События boundary возвращают свойство name, которое будет установлено, как word или sentence, в зависимости от ситуации. Ещё события boundary во всех браузерах возвращают значение charIndex и charLength, но charLength не возвращается в Safari. Собрав их вместе, можно определить, какое слово в тексте произносится в момент срабатывания события.

В этом примере есть кнопки для воспроизведения, паузы и остановки разговора. Здесь реализовано прослушивание событий start, pause, resume и end для доступности кнопок, когда это необходимо. Для выделения текущего слова используются события boundary.

See this code Text to Speech With Event Handling on x.xhtml.ru.

Особенности, зависимые от браузера/платформы

Синтез речи на JavaScript имеет неплохую поддержку браузерами, но всё ещё считается экспериментальным. Его легко использовать на базовом уровне, но если понадобится более глубокое погружение, придётся столкнуться с некоторыми проблемами и особенностями поведения. Далее будут перечислены некоторые ошибки и подводные камни, чтобы было легче с ними разобраться.

Использование различных SpeechSynthesisVoice для голоса:

  • [Chrome/Firefox для Android] на устройстве невозможно изменить голос по умолчанию.
  • [Safari] в window.speechSynthesis не определен addEventListener, поэтому его не получится использовать для отслеживания события voiceschanged. Вместо него надо использовать onvoiceschanged, как описано здесь.
  • [Safari] надо помнить, что SpeechSynthesisVoice.name не всегда уникален. Если нужен уникальный идентификатор для голосов, следует использовать SpeechSynthesisVoice.voiceURI.

Настройка свойств SpeechSynthesisUtterance:

  • [Safari] значения pitch (тональности) от 0.5 и ниже звучат одинаково.
  • [Safari] если значение rate (скорость речи) установлено 0.5 или выше, а затем оно меняется на менее 0.5, то при продолжении воспроизведения сохраняется предыдущая более высокая скорость.
  • [Edge] при использовании нелокального голоса, любое значение, установленное для pitch, игнорируется и голос всегда будет звучать, как будто установлено 1.
  • [Chrome] при использовании нелокального голоса, установка pitch в 0 вернётся к 1.
  • [Chrome] при использовании нелокального голоса, воспроизведение не произойдёт, если rate окажется более 2.
  • [все браузеры] некоторые голоса могут вводить дополнительные ограничения для диапазона значений pitch и rate.

Обработка событий SpeechSynthesisEvent:

  • [Chrome/Firefox на Android] событие boundary не возникает.
  • [Safari] событие end не возникает, если синтез речи остановлен с помощью метода cancel().
  • [Safari] событие boundary не возвращает charLength.
  • [macOS] у события boundary значение name не бывает sentence. Вместо этого будет дополнительное событие со значением word.
  • [Chrome/Edge в Windows] события boundary, которые происходят для предложений, возвращают значение charLength, равное 0, а не ожидаемую длину предложения.
  • [Chrome/Edge/Safari] для всех событий, кроме boundary, значение charIndex всегда равно 0.

Остальное

  • [Chrome/Firefox на Android] пауза и продолжение воспроизведения не работают.
  • [iOS] при включенном переключателе «soft mute» речи не слышно.
  • [Android/iOS] есть ещё пара проблем с мобильными устройствами, о которых здесь не упоминалось, но про них можно прочитать в этой полезной статье.

Заключение

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

Преобразование текста в речь (синтез речи) в JavaScript на базовом уровне действительно имеет хорошую кроссбраузерную совместимость — просто надо соблюдать осторожность, если потребуется что-то, выходящее за рамки базового функционала.