[Разработка веб-сайтов, JavaScript, API] Наиболее полное руководство по практическому использованию Web Speech API
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этой статье я хочу поделиться с вами результатами изучения основных возможностей Web Speech API (далее — WSA).
Введение
WSA — это экспериментальная технология, состоящая из двух интерфейсов: SpeechSynthesis (интерфейс для перевода текста в речь) и SpeechRecognition (интерфейс для распознавания речи).
О том, что из себя представляют названные интерфейсы и что в себя включают можно почитать на MDN или в рабочем черновике (данный черновик, в отличие от большинства спецификаций, написан более-менее человеческим языком).
Что касается поддержки, то вот что об этом говорит Can I use:
Как мы видим, SpeechSynthesis поддерживается большинством современных браузеров. К сожалению, поддержка SpeechRecognition оставляет желать лучшего. Я говорю "к сожалению", поскольку SpeechRecognition предоставляет более интересные возможности с точки зрения практического применения в веб-приложениях, в чем вы сами убедитесь после прочтения статьи. Полагаю, такая ситуация обусловлена сложностью технической реализации распознавания речи.
Мы с вами реализуем 4 простых приложения, по 2 на каждый интерфейс:
- Плеер для озвучивания текста со всеми возможностями, которые предоставляет SpeechSynthesis, включая выбор языка (голоса)
- Страницу с возможностью озвучивания ее текстового содержимого
- "Диктофон" для распознавания речи и ее перевода в текст с автоматическим и кастомным форматированием
- Одностраничное приложение с голосовым управлением, в том числе, переключение страниц, прокрутка и изменение цветовой темы (или схемы), используемой на сайте
Если вам это интересно, то прошу следовать за мной.
Плеер для озвучивания текста
Наша разметка будет выглядеть следующим образом:
<div id="wrapper">
<h1>Speech Synthesis - Player</h1>
<label
>Text:
<textarea id="textarea">Привет! Как дела?</textarea>
</label>
<label
>Voice:
<select id="select"></select>
</label>
<label
>Volume:
<input id="volume" type="range" min="0" max="1" step="0.1" value="1" />
<span>1</span>
</label>
<label
>Rate:
<input id="rate" type="range" min="0" max="3" step="0.5" value="1" />
<span>1</span>
</label>
<label
>Pitch:
<input id="pitch" type="range" min="0" max="2" step="0.5" value="1" />
<span>1</span>
</label>
<div id="buttons">
<button class="speak">Speak</button>
<button class="cancel">Cancel</button>
<button class="pause">Pause</button>
<button class="resume">Resume</button>
</div>
</div>
У нас имеется поле для ввода текста (textarea), который мы будем озвучивать; выпадающий список (select) для выбора голоса, которым будет озвучиваться текст; инпуты-диапазоны для установки громкости (volume), скорости (rate) воспроизведения и высоты голоса (pitch), а также кнопки для запуска (speak), отмены (cancel), приостановления (pause) и продолжения (resume) воспроизведения. В общем, ничего особенного.
Обратите внимание на атрибуты инпутов min, max, step и value. Значения данных атрибутов взяты из черновика спецификации, но некоторые из них отданы на откуп производителям браузеров, т.е. зависят от конкретной реализации. Также обратите внимание на наличие почти у всех элементов идентификаторов. Мы будем использовать эти id для прямого доступа к элементам в скрипте в целях сокращения кода, однако в реальных приложениях так лучше не делать во избежание загрязнения глобального пространства имен.
Переходим к JavaScript. Создаем экземпляр SpeechSynthesisUtterance ("utterance" можно перевести как "выражение текста словами"):
const U = new SpeechSynthesisUtterance()
Пытаемся получить доступные голоса (именно "пытаемся", поскольку в первый раз, по крайней мере, в Chrome возвращается пустой массив):
let voices = speechSynthesis.getVoices()
При вызове метода getVoices() возникает событие voiceschanged. Обрабатываем это событие для "настоящего" получения голосов и формирования выпадающего списка:
speechSynthesis.onvoiceschanged = () => {
voices = speechSynthesis.getVoices()
populateVoices(voices)
}
Объект голоса (назовем его так) выглядит следующим образом:
0: SpeechSynthesisVoice
default: true
lang: "de-DE"
localService: false
name: "Google Deutsch"
voiceURI: "Google Deutsch"
Реализуем функцию формирования выпадающего списка:
function populateVoices(voices) {
// Перебираем голоса и создаем элемент `option` для каждого
// Текстовым содержимым `option` является название голоса, а значением - индекс голоса в массиве голосов
voices.forEach((voice, index) => {
select.options[index] = new Option(voice.name, index)
})
// Делаем голосом по умолчанию `Google русский`
// Он нравится мне больше, чем голос от `Microsoft`
const defaultVoiceIndex = voices.findIndex(
(voice) => voice.name === 'Google русский'
)
select.selectedIndex = defaultVoiceIndex
// После этого инициализируем обработчики событий
initializeHandlers()
}
Функция инициализации обработчиков событий выглядит так:
function initializeHandlers() {
// Ниже перечислены почти все события, которые возникают при работе с рассматриваемым интерфейсом
U.onstart = () => console.log('Started')
U.onend = () => console.log('Finished')
U.onerror = (err) => console.error(err)
// Мне не удалось добиться возникновения этих событий
U.onpause = () => console.log('Paused')
U.onresume = () => console.log('Resumed')
// Обработка изменения настроек
wrapper.onchange = ({ target }) => {
if (target.type !== 'range') return
handleChange(target)
}
// Обработка нажатия кнопок
buttons.addEventListener('click', ({ target: { className } }) => {
// SpeechSynthesis предоставляет таким методы как `speak()`, `cancel()`, `pause()` и `resume()`
// Вызов метода `speak()` требует предварительной подготовки
// Кроме того, мы проверяем наличие текста в очереди на озвучивание с помощью атрибута `speaking`
// Есть еще два атрибута: `pending` и `paused`, но они не всегда возвращают корректные значения
switch (className) {
case 'speak':
if (!speechSynthesis.speaking) {
convertTextToSpeech()
}
break
case 'cancel':
return speechSynthesis.cancel()
case 'pause':
return speechSynthesis.pause()
case 'resume':
return speechSynthesis.resume()
default:
return
}
})
}
Обработка изменения настроек:
function handleChange(el) {
el.nextElementSibling.textContent = el.value
}
Функция преобразования текста в речь:
function convertTextToSpeech() {
// Получаем текст
const trimmed = textarea.value.trim()
if (!trimmed) return
// Передаем его экземпляру `SpeechSynthesisUtterance`
U.text = trimmed
// Получаем выбранный голос
const voice = voices[select.value]
// Передаем голос и другие настройки экземпляру
U.voice = voice
// язык
U.lang = voice.lang
// громкость
U.volume = volume.value
// скорость
U.rate = rate.value
// высота голоса
U.pitch = pitch.value
// Запускаем озвучивание!
speechSynthesis.speak(U)
}
После установки всех настроек наш экземпляр SpeechSynthesisUtterance выглядит следующим образом:
SpeechSynthesisUtterance
lang: "ru-RU"
onboundary: null
onend: () => console.log('Finished')
onerror: (err) => console.error(err)
onmark: null
onpause: () => console.log('Paused')
onresume: () => console.log('Resumed')
onstart: () => console.log('Started')
pitch: 1
rate: 1
text: "Привет! Как дела?"
voice: SpeechSynthesisVoice { voiceURI: "Google русский", name: "Google русский", lang: "ru-RU", localService: false, default: false }
volume: 1
Уже в процессе написания статьи я решил добавить управление с помощью клавиатуры:
window.onkeydown = ({ key }) => {
switch (key.toLowerCase()) {
case 's':
if (!speechSynthesis.speaking) {
convertTextToSpeech()
}
break
case 'c':
return speechSynthesis.cancel()
case 'p':
return speechSynthesis.pause()
case 'r':
return speechSynthesis.resume()
default:
return
}
}
Поиграть с кодом можно здесь:
Извините, данный ресурс не поддреживается. :(
Страница с возможность озвучивания текстового содержимого
Что касается SpeechSynthesis, то с точки зрения практического использования данного интерфейса, сложно что-то придумать, кроме создания страниц с возможностью озвучивания размещенного на них текста. Этим мы и займемся.
Преимущество использования SpeechSynthesis заключается в реальном улучшении доступности контента приложения для людей с ограниченными возможностями здоровью. Основная проблема, на мой взгляд, состоит в том, что пользователи не привыкли к подобным приложениям, следовательно, им надо каким-то образом намекнуть о существовании такой возможности. Причем, сделать это нужно в очень ясной, но при этом ненавязчивой форме.
Наша разметка будет выглядеть так:
<div id="wrapper">
<h1>Speech Synthesis - Page Reader</h1>
<div>
<button class="play" tabindex="1"></button>
<p>
JavaScript — мультипарадигменный язык программирования. Поддерживает
объектно-ориентированный, императивный и функциональный стили.
Является реализацией спецификации ECMAScript (стандарт ECMA-262).
</p>
</div>
<div>
<button class="play" tabindex="2"></button>
<p>
JavaScript обычно используется как встраиваемый язык для программного
доступа к объектам приложений. Наиболее широкое применение находит в
браузерах как язык сценариев для придания интерактивности
веб-страницам.
</p>
</div>
<div>
<button class="play" tabindex="3"></button>
<p>
Основные архитектурные черты: динамическая типизация, слабая
типизация, автоматическое управление памятью, прототипное
программирование, функции как объекты первого класса.
</p>
</div>
</div>
У нас имеется три блока (div) с кнопками для озвучивания текста (play) и, собственно, текстом, который будет озвучиваться (первые три абзаца статьи по JavaScript из Википедии). Обратите внимание, что я добавил кнопкам атрибут tabindex, чтобы переключаться между ними с помощью tab и нажимать с помощью space. Однако, имейте ввиду, что использовать атрибут tabindex не рекомендуется по причине того, что браузер использует переключение фокуса с помощью tab для повышения доступности.
С вашего позволения, я приведу код скрипта целиком:
// Это часть должна быть вам знакома по предыдущему примеру
let voices = speechSynthesis.getVoices()
let defaultVoice
speechSynthesis.onvoiceschanged = () => {
voices = speechSynthesis.getVoices()
defaultVoice = voices.find((voice) => voice.name === 'Google русский')
wrapper.addEventListener('click', handleClick)
window.addEventListener('keydown', handleKeydown)
}
const PLAY = 'play'
const PAUSE = 'pause'
const RESUME = 'resume'
function handleClick({ target }) {
switch (target.className) {
case PLAY:
// При нажатии кнопки `play` в момент озвучивания другого текста,
// нам нужно прекратить текущее озвучивание перед началом нового
speechSynthesis.cancel()
const { textContent } = target.nextElementSibling
// Об этой части см. ниже
textContent.split('.').forEach((text) => {
const trimmed = text.trim()
if (trimmed) {
const U = getUtterance(target, text)
speechSynthesis.speak(U)
}
})
break
case PAUSE:
// CSS-класс кнопки отвечает за отображаемую рядом с ней иконку
// ``- ожидание/стоп, `` - озвучивание/воспроизведение, `` - пауза
// иконка `` используется в качестве индикатора кнопки, находящейся в фокусе
target.className = RESUME
speechSynthesis.pause()
break
case RESUME:
target.className = PAUSE
speechSynthesis.resume()
break
default:
break
}
}
// При нажатии `escape` прекращаем озвучивание текста
// Можете добавить свои контролы
function handleKeydown({ code }) {
switch (code) {
case 'Escape':
return speechSynthesis.cancel()
default:
break
}
}
function getUtterance(target, text) {
const U = new SpeechSynthesisUtterance(text)
U.voice = defaultVoice
U.lang = defaultVoice.lang
U.volume = 1
U.rate = 1
U.pitch = 1
// Я специально не стал скрывать смену иконок при начале/окончании воспроизведения
U.onstart = () => {
console.log('Started')
target.className = PAUSE
}
U.onend = () => {
console.log('Finished')
target.className = PLAY
}
U.onerror = (err) => console.error(err)
return U
}
Тонкий момент в приведенном коде — это преобразование озвучиваемого текста в массив предложений (разделителем является точка (.)), перебор массива, и воспроизведение каждого предложения по отдельности (точнее, помещение всех предложений одного за другим в очередь на озвучивание) — textContent.split('.').forEach(...). Причина такого решения заключается в том, что озвучивание длинного текста тихо обрывается примерно на 220 символе (в Chrome). Черновик спецификации для такого случае предусматривает специальную ошибку text-to-long (текст слишком длинный), но данной ошибки не возникает, озвучивание просто резко прекращается, причем, для восстановления работы SpeechSynthesis зачастую приходится перезагружать вкладку (при запущенном сервере для разработки даже это не всегда срабатывает). Возможно, вам удастся найти другое решение.
Поиграть с кодом можно здесь:
Извините, данный ресурс не поддреживается. :(
"Диктофон" для распознавания речи
После того, как мы обстоятельно рассмотрели SpeechSynthesis, можно переходить ко второму, немного более сложному, но, вместе с тем, и более интересному интерфейсу, входящему в состав WSA — SpeechRecoginition.
Вот наша начальная разметка:
<div id="wrapper">
<h1>Speech Recognition - Dictaphone</h1>
<textarea id="final_text" cols="30" rows="10"></textarea>
<input type="text" id="interim_text" />
<div id="buttons">
<button class="start">Старт</button>
<button class="stop">Стоп</button>
<button class="abort">Сброс</button>
<button class="copy">Копия</button>
<button class="clear">Очистить</button>
</div>
</div>
У нас имеется поле для вставки финального (распознанного) текста (final_text) и поле для вставки промежуточного (находящегося в процессе распознавания) текста (interim_text), а также панель управления (buttons). Выбор элементов textarea и input для хранения текста обусловлен как вариативностью промежуточных результатов, которые меняются на лету, так и необходимостью внесения небольших правок в распознанный текст, что связано с естественным несовершенством перевода устной речи в текст. Стоит отметить, что, в целом, Chrome очень неплохо справляется с задачей распознавания речи и автоматическим форматированием распознанного текста.
Кроме кнопок для управления распознаванием речи (start, stop и abort), мы реализуем возможность копирования распознанного текста в буфер обмена с помощью Clipboard API и очистки соответствующего поля.
Начнем с определения переменных для финального текста и индикатора распознавания:
let final_transcript = ''
let recognizing = false
Далее, создаем и настраиваем экземпляр SpeechRecognition:
// Напоминаю, что `WSA`, в целом, и, особенно, `SpeechRecognition` являются экпериментальными
const speechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition
// Создаем экземпляр `SpeechRecognition`
const recognition = new speechRecognition()
// Свойство `continuous` определяет, продолжается ли распознавание речи после получения первого финального результата
recognition.continuous = true
// обработка промежуточных результатов
recognition.interimResults = true
// максимальное количество альтернатив распознанного слова
recognition.maxAlternatives = 3
// язык
recognition.lang = 'ru-RU'
Добавляем обработчики событий запуска, ошибки и окончания распознавания речи:
recognition.onstart = () => {
console.log('Распознавание голоса запущено')
}
recognition.onerror = ({ error }) => {
console.error(error)
}
recognition.onend = () => {
console.log('Распознавание голоса закончено')
// Снова запускаем распознавание, если индикатор имеет значение `true`
if (!recognizing) return
recognition.start()
}
SpeechRecognition хорошо справляется с распознаванием слов, фраз и целых предложений, он даже выполняет вполне уместную "капитализацию" строки. Но он не "понимает" знаков препинания. Для того, чтобы помочь ему в этом определим словарь в виде объекта:
const DICTIONARY = {
точка: '.',
запятая: ',',
вопрос: '?',
восклицание: '!',
двоеточие: ':',
тире: '-',
абзац: '\n',
отступ: '\t'
}
Предполагается, что для решение задач, связанных с форматированием, будут использоваться специальные объекты SpeechGrammar и SpeechGrammarList, а также специальный синтаксис JSpeech Grammar Format, однако, в данной статье мы ограничимся обычным объектом.
Вы можете добавить в словарь любые пары ключ/значение, которые посчитаете нужными. Обратите внимание на то, что все ключи в нашем словаре представлены одним словом. Дело в том, что рассматриваемый интерфейс плохо справляется с ключами, которые состоят более чем из одного слова, например, "вопросительный знак", "восклицательный знак" и т.п. Я понимаю, что пара вопрос: '?' не лучшее решение, но для примера сойдет.
Также нам потребуются две функции для редактирования промежуточного и финального результатов:
function editInterim(s) {
return s
.split(' ')
.map((word) => {
word = word.trim()
return DICTIONARY[word.toLowerCase()] ? DICTIONARY[word.toLowerCase()] : word
})
.join(' ')
}
function editFinal(s) {
return s.replace(/\s{1,}([\.+,?!:-])/g, '$1')
}
Функция editInterim() разбивает фразу на массив слов, перебирает слова, удаляет пробелы в начале и конце слова и заменяет символом из словаря при совпадении. Обратите внимание, что мы приводим слово в нижний регистр только при поиске совпадения. Если мы сделаем это в строке word = word.trim(), то нивелируем автоматическую "капитализацию" строки, выполняемую браузером.
Функция editFinal() удаляет пробел перед каждым из указанных символов — мы разделяем слова пробелами при объединении в функции editInterim(). Следует отметить, что по умолчанию каждое распознанное слово с двух сторон обрамляется пробелами.
Итак, событие, которое интересует нас больше всего — это result. Реализуем его обработку:
recognition.onresult = (e) => {
// Промежуточные результаты обновляются на каждом цикле распознавания
let interim_transcript = ''
// Перебираем результаты с того места, на котором остановились в прошлый раз
for (let i = e.resultIndex; i < e.results.length; i++) {
// Атрибут `isFinal` является индикатором того, что речь закончилась
if (e.results[i].isFinal) {
// Редактируем промежуточные результаты
const result = editInterim(e.results[i][0].transcript)
// и добавляем их к финальному результату
final_transcript += result
} else {
// В противном случае, записываем распознанные слова в промежуточный результат
interim_transcript += e.results[i][0].transcript
}
}
// Записываем промежуточные результаты в `input`
interim_text.value = interim_transcript
// Редактируем финальный результат
final_transcript = editFinal(final_transcript)
// и записываем его в `textarea`
final_text.value = final_transcript
}
Событие распознавания выглядит следующим образом:
SpeechRecognitionEvent
bubbles: false
cancelBubble: false
cancelable: false
composed: false
currentTarget: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …}
defaultPrevented: false
emma: null
eventPhase: 0
interpretation: null
isTrusted: true
path: []
resultIndex: 1
// здесь нас интересуют только результаты
results: SpeechRecognitionResultList {0: SpeechRecognitionResult, 1: SpeechRecognitionResult, length: 2}
returnValue: true
srcElement: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …}
target: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …}
timeStamp: 59862.61999979615
type: "result"
Результаты (SpeechRecognitionResultList) выглядят так:
results: SpeechRecognitionResultList
0: SpeechRecognitionResult
0: SpeechRecognitionAlternative
confidence: 0.7990190982818604
transcript: "привет"
isFinal: true
length: 1
length: 1
Вот почему для получения результата мы обращаемся к e.results[0].transcript. На самом деле, поскольку мы указали maxAlternatives = 3, во всех результатах будет представлено по три SpeechRecognitionAlternative. Первым (с индексом 0) всегда будет наиболее подходящий результат с точки зрения браузера.
Все, что нам осталось сделать, это реализовать обработку нажатия кнопок:
buttons.onclick = ({ target }) => {
switch (target.className) {
case 'start':
// Обнуляем переменную для финального результата
final_transcript = ''
// Запускаем распознавание
recognition.start()
// Устанавливаем индикатор распознавания в значение `true`
recognizing = true
// Очищаем `textarea`
final_text.value = ''
// Очищаем `input`
interim_text.value = ''
break
case 'stop':
// Останавливаем распознавание
recognition.stop()
// Устанавливаем значение индикатора распознавания в значение `false`
recognizing = false
break
case 'abort':
// Прекращаем распознавание
recognition.abort()
recognizing = false
break
case 'copy':
// Копируем текст из `textarea` в буфер обмена
navigator.clipboard.writeText(final_text.value)
// Сообщаем об этом пользователю
target.textContent = 'Готово'
const timerId = setTimeout(() => {
target.textContent = 'Копия'
clearTimeout(timerId)
}, 3000)
break
case 'clear':
// Обнуляем переменную для финального результата
final_transcript = ''
// Очищаем `textarea`
final_text.value = ''
break
default:
break
}
}
Тестируем. Нажимаем на кнопку "Старт", дожидаемся, когда красная точка рядом с фавиконкой перестанет мигать (о готовности к распознаванию можно сообщать пользователю через обработку события speechstart), произносим фразу (произношение должно быть максимально четким и ясным), например, "привет точка как дела вопрос". В input на какое-то время появляется "Привет точка Как дела вопрос", затем этот промежуточный результат редактируется и переносится в textarea в виде "Привет. Как дела?". Отлично, наша машина нас понимает.
Поиграть с кодом можно здесь:
Извините, данный ресурс не поддреживается. :(
Подумал о том, что было бы неплохо реализовать функцию для удаления последнего распознанного слова на тот случай, если результат распознавания получился некорректным. Для этого потребуется пара удалить: () => removeLastWord() (если добавить эту пару в словарь, то потребуется дополнительная проверка typeof DICTIONARY[word] === 'function') и примерно такая операция:
function removeLastWord() {
const oldStr = final_text.value
const newStr = oldStr.substring(0, oldStr.lastIndexOf(' '))
final_text.value = newStr
}
Однако, добавлять операцию в словарь — плохая идея, для этого лучше использовать отдельный объект, чем мы и займемся в следующем разделе.
Одностраничное приложение с голосовым управлением
Такой вариант использования SpeechRecognition, на мой взгляд, представляет наибольший интерес с точки зрения возможности применения данной технологии в реальных приложениях.
Мы будем генерировать страницы программным способом, поэтому разметка нам не потребуется.
Создаем директорию pages с тремя файлами:
// home.js
export default /*html*/ `
<div id="wrapper">
<div>Section 1</div>
<div>Section 2</div>
<div>Section 3</div>
<div>Section 4</div>
<div>Section 5</div>
<div>Section 6</div>
<div>Section 7</div>
<div>Section 8</div>
<div>Section 9</div>
</div>
`
// product.js
export default /*html*/ `
<h1>This is the Product Page</h1>
`
// about.js
export default /*html*/ `
<h1>This is the About Page</h1>
`
Основной скрипт начинается с импорта страниц и генерации домашней страницы:
import HomePage from './pages/home.js'
import ProductPage from './pages/product.js'
import AboutPage from './pages/about.js'
const { body } = document
body.innerHTML = HomePage
Нам потребуются константы для прокрутки страницы и объект с операциями:
// Константы для прокрутки
const DOWN = 'down'
const UP = 'up'
const RIGHT = 'right'
const LEFT = 'left'
// Операции
const ACTIONS = {
// операции переключения страниц
home: () => (body.innerHTML = HomePage),
product: () => (body.innerHTML = ProductPage),
about: () => (body.innerHTML = AboutPage),
// операции прокрутки
down: () => scroll(DOWN),
up: () => scroll(UP),
left: () => scroll(LEFT),
right: () => scroll(RIGHT),
// операции переключения цветовой темы
light: () => body.removeAttribute('class'),
dark: () => (body.className = 'dark')
}
Далее следуют настройки SpeechRecognition и обработчики событий начала, ошибки и окончания распознавания, аналогичные рассмотренным в предыдущем разделе (кроме настройки языка — для управления страницей мы будем использовать американский английский: recognition.lang = 'en-US').
Обработка события result:
recognition.onresult = (e) => {
// Перебираем результаты
for (let i = e.resultIndex; i < e.results.length; i++) {
if (e.results[i].isFinal) {
const result = e.results[i][0].transcript.toLowerCase()
// Выводим слова в консоль, чтобы убедиться в корректности распознавания (как выяснилось, слово `product` очень плохо распознается, возможно, у меня проблемы с его правильным произношением)
console.log(result)
// Преобразуем фразу в массив, перебираем слова и выполняем соответствующие операции
result
.split(' ')
.forEach((word) => {
word = word.trim().toLowerCase()
// ACTION[word] - это функция
return ACTIONS[word] ? ACTIONS[word]() : ''
})
}
}
}
Функция для выполнения прокрутки выглядит следующим образом:
function scroll(direction) {
let newPosition
switch (direction) {
case DOWN:
newPosition = scrollY + innerHeight
break
case UP:
newPosition = scrollY - innerHeight
break
case RIGHT:
newPosition = scrollX + innerWidth
break
case LEFT:
newPosition = scrollX - innerWidth
break
default:
break
}
if (direction === DOWN || direction === UP) {
scrollTo({
top: newPosition,
behavior: 'smooth'
})
} else {
scrollTo({
left: newPosition,
behavior: 'smooth'
})
}
}
Наконец, добавляем обработку нажатия клавиш клавиатуры:
window.addEventListener('keydown', (e) => {
e.preventDefault()
switch (e.code) {
// Нажатие пробела запускает распознавание
case 'Space':
recognition.start()
recognizing = true
break
// Нажатие `escape` останавливает распознавание
case 'Escape':
recognition.stop()
recognizing = false
default:
break
}
})
Проверяем работоспособность приложения. Нажимаем пробел, дожидаемся готовности браузера к распознаванию речи, произносим следующие фразы:
- home, product, about — переключение страниц
- dark, light — переключение цветовой темы
- down, up, left, right — выполнение прокрутки к соответствующему разделу страницы (имеет видимый эффект только на домашней странице, иногда приходится добавлять слово scroll, например, scroll down)
Поиграть с кодом можно здесь:
Извините, данный ресурс не поддреживается. :(
Заключение
Как мы видим, WSA существенно расширяет арсенал JavaScript, связанный с добавлением динамики, "оживлением" веб-страниц. Сложно сказать, насколько широкое распространение получит данная технология и насколько активно она будет использоваться. Несмотря на то, что WSA — это технология общего назначения, очевидно, что она внесет серьезный вклад, в первую очередь, в развитие так называемого доступного Интернета.
Мы с вами рассмотрели все основные возможности, предоставляемые WSA. Единственное, что мы не сделали, это не реализовали совместное использование SpeechSynthesis и SpeechRecognition. Первое, что приходит на ум — это голосовой помощник, который реагирует на ключевые слова, встречающиеся во фразе, произнесенной пользователем, отвечая определенными шаблонами и задавая уточняющие вопросы, при необходимости. Более простой вариант: волшебный шар — задаешь любой вопрос, предполагающий однозначный ответ, программа какое-то время "думает" и отвечает да, нет или, например, не знаю, спросите позже. Пусть это будет вашим домашним заданием, все необходимые знания для этого у вас имеются. Обязательно поделитесь результатом в комментариях.
Облачные серверы от Маклауд быстрые и безопасные.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
оригинал
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Машинное обучение] SberCloud + Intel oneAPI = бесплатное облако для ML-разработчиков
- [Веб-дизайн, Habr, JavaScript, Программирование, HTML] Косяк колхозной простоты в коде веб-сайта Хабра
- [Python, Программирование, Google App Engine] Как использовать GraphQL Federation для инкрементальной миграции с монолита (Python) на микросервисы (Go) (перевод)
- [Высокая производительность, Разработка веб-сайтов, Программирование] Как создать архитектуру для работы с высокой нагрузкой вашего веб-проекта? (перевод)
- [Информационная безопасность, История IT, Научно-популярное] Как ИТ перестали быть скучными (перевод)
- [Читальный зал] Языки «Властелина колец»: как язык создал целую вселенную
- [JavaScript, Программирование, Node.JS] RESTful backend приложение. Базовый шаблон
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] RxJS Challenge: Неделя 1
- [ВКонтакте API, GitHub, Тестирование веб-сервисов] Бенчмарки VKUI и других ребят из UI-библиотек
- [Анализ и проектирование систем, IT-инфраструктура, IPTV] Программные зонды Elecard Boro – Мониторинг IPTV
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_api, #_webspeechapi, #_javascript, #_sajty (сайты), #_api, #_vds, #_vps, #_bystrye_servery (быстрые серверы), #_blog_kompanii_maklaud (
Блог компании Маклауд
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_api
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:59
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этой статье я хочу поделиться с вами результатами изучения основных возможностей Web Speech API (далее — WSA). Введение WSA — это экспериментальная технология, состоящая из двух интерфейсов: SpeechSynthesis (интерфейс для перевода текста в речь) и SpeechRecognition (интерфейс для распознавания речи). О том, что из себя представляют названные интерфейсы и что в себя включают можно почитать на MDN или в рабочем черновике (данный черновик, в отличие от большинства спецификаций, написан более-менее человеческим языком). Что касается поддержки, то вот что об этом говорит Can I use: Как мы видим, SpeechSynthesis поддерживается большинством современных браузеров. К сожалению, поддержка SpeechRecognition оставляет желать лучшего. Я говорю "к сожалению", поскольку SpeechRecognition предоставляет более интересные возможности с точки зрения практического применения в веб-приложениях, в чем вы сами убедитесь после прочтения статьи. Полагаю, такая ситуация обусловлена сложностью технической реализации распознавания речи. Мы с вами реализуем 4 простых приложения, по 2 на каждый интерфейс:
Если вам это интересно, то прошу следовать за мной. Плеер для озвучивания текста Наша разметка будет выглядеть следующим образом: <div id="wrapper">
<h1>Speech Synthesis - Player</h1> <label >Text: <textarea id="textarea">Привет! Как дела?</textarea> </label> <label >Voice: <select id="select"></select> </label> <label >Volume: <input id="volume" type="range" min="0" max="1" step="0.1" value="1" /> <span>1</span> </label> <label >Rate: <input id="rate" type="range" min="0" max="3" step="0.5" value="1" /> <span>1</span> </label> <label >Pitch: <input id="pitch" type="range" min="0" max="2" step="0.5" value="1" /> <span>1</span> </label> <div id="buttons"> <button class="speak">Speak</button> <button class="cancel">Cancel</button> <button class="pause">Pause</button> <button class="resume">Resume</button> </div> </div> У нас имеется поле для ввода текста (textarea), который мы будем озвучивать; выпадающий список (select) для выбора голоса, которым будет озвучиваться текст; инпуты-диапазоны для установки громкости (volume), скорости (rate) воспроизведения и высоты голоса (pitch), а также кнопки для запуска (speak), отмены (cancel), приостановления (pause) и продолжения (resume) воспроизведения. В общем, ничего особенного. Обратите внимание на атрибуты инпутов min, max, step и value. Значения данных атрибутов взяты из черновика спецификации, но некоторые из них отданы на откуп производителям браузеров, т.е. зависят от конкретной реализации. Также обратите внимание на наличие почти у всех элементов идентификаторов. Мы будем использовать эти id для прямого доступа к элементам в скрипте в целях сокращения кода, однако в реальных приложениях так лучше не делать во избежание загрязнения глобального пространства имен. Переходим к JavaScript. Создаем экземпляр SpeechSynthesisUtterance ("utterance" можно перевести как "выражение текста словами"): const U = new SpeechSynthesisUtterance()
Пытаемся получить доступные голоса (именно "пытаемся", поскольку в первый раз, по крайней мере, в Chrome возвращается пустой массив): let voices = speechSynthesis.getVoices()
При вызове метода getVoices() возникает событие voiceschanged. Обрабатываем это событие для "настоящего" получения голосов и формирования выпадающего списка: speechSynthesis.onvoiceschanged = () => {
voices = speechSynthesis.getVoices() populateVoices(voices) } Объект голоса (назовем его так) выглядит следующим образом: 0: SpeechSynthesisVoice
default: true lang: "de-DE" localService: false name: "Google Deutsch" voiceURI: "Google Deutsch" Реализуем функцию формирования выпадающего списка: function populateVoices(voices) {
// Перебираем голоса и создаем элемент `option` для каждого // Текстовым содержимым `option` является название голоса, а значением - индекс голоса в массиве голосов voices.forEach((voice, index) => { select.options[index] = new Option(voice.name, index) }) // Делаем голосом по умолчанию `Google русский` // Он нравится мне больше, чем голос от `Microsoft` const defaultVoiceIndex = voices.findIndex( (voice) => voice.name === 'Google русский' ) select.selectedIndex = defaultVoiceIndex // После этого инициализируем обработчики событий initializeHandlers() } Функция инициализации обработчиков событий выглядит так: function initializeHandlers() {
// Ниже перечислены почти все события, которые возникают при работе с рассматриваемым интерфейсом U.onstart = () => console.log('Started') U.onend = () => console.log('Finished') U.onerror = (err) => console.error(err) // Мне не удалось добиться возникновения этих событий U.onpause = () => console.log('Paused') U.onresume = () => console.log('Resumed') // Обработка изменения настроек wrapper.onchange = ({ target }) => { if (target.type !== 'range') return handleChange(target) } // Обработка нажатия кнопок buttons.addEventListener('click', ({ target: { className } }) => { // SpeechSynthesis предоставляет таким методы как `speak()`, `cancel()`, `pause()` и `resume()` // Вызов метода `speak()` требует предварительной подготовки // Кроме того, мы проверяем наличие текста в очереди на озвучивание с помощью атрибута `speaking` // Есть еще два атрибута: `pending` и `paused`, но они не всегда возвращают корректные значения switch (className) { case 'speak': if (!speechSynthesis.speaking) { convertTextToSpeech() } break case 'cancel': return speechSynthesis.cancel() case 'pause': return speechSynthesis.pause() case 'resume': return speechSynthesis.resume() default: return } }) } Обработка изменения настроек: function handleChange(el) {
el.nextElementSibling.textContent = el.value } Функция преобразования текста в речь: function convertTextToSpeech() {
// Получаем текст const trimmed = textarea.value.trim() if (!trimmed) return // Передаем его экземпляру `SpeechSynthesisUtterance` U.text = trimmed // Получаем выбранный голос const voice = voices[select.value] // Передаем голос и другие настройки экземпляру U.voice = voice // язык U.lang = voice.lang // громкость U.volume = volume.value // скорость U.rate = rate.value // высота голоса U.pitch = pitch.value // Запускаем озвучивание! speechSynthesis.speak(U) } После установки всех настроек наш экземпляр SpeechSynthesisUtterance выглядит следующим образом: SpeechSynthesisUtterance
lang: "ru-RU" onboundary: null onend: () => console.log('Finished') onerror: (err) => console.error(err) onmark: null onpause: () => console.log('Paused') onresume: () => console.log('Resumed') onstart: () => console.log('Started') pitch: 1 rate: 1 text: "Привет! Как дела?" voice: SpeechSynthesisVoice { voiceURI: "Google русский", name: "Google русский", lang: "ru-RU", localService: false, default: false } volume: 1 Уже в процессе написания статьи я решил добавить управление с помощью клавиатуры: window.onkeydown = ({ key }) => {
switch (key.toLowerCase()) { case 's': if (!speechSynthesis.speaking) { convertTextToSpeech() } break case 'c': return speechSynthesis.cancel() case 'p': return speechSynthesis.pause() case 'r': return speechSynthesis.resume() default: return } } Поиграть с кодом можно здесь: Извините, данный ресурс не поддреживается. :( Страница с возможность озвучивания текстового содержимого Что касается SpeechSynthesis, то с точки зрения практического использования данного интерфейса, сложно что-то придумать, кроме создания страниц с возможностью озвучивания размещенного на них текста. Этим мы и займемся. Преимущество использования SpeechSynthesis заключается в реальном улучшении доступности контента приложения для людей с ограниченными возможностями здоровью. Основная проблема, на мой взгляд, состоит в том, что пользователи не привыкли к подобным приложениям, следовательно, им надо каким-то образом намекнуть о существовании такой возможности. Причем, сделать это нужно в очень ясной, но при этом ненавязчивой форме. Наша разметка будет выглядеть так: <div id="wrapper">
<h1>Speech Synthesis - Page Reader</h1> <div> <button class="play" tabindex="1"></button> <p> JavaScript — мультипарадигменный язык программирования. Поддерживает объектно-ориентированный, императивный и функциональный стили. Является реализацией спецификации ECMAScript (стандарт ECMA-262). </p> </div> <div> <button class="play" tabindex="2"></button> <p> JavaScript обычно используется как встраиваемый язык для программного доступа к объектам приложений. Наиболее широкое применение находит в браузерах как язык сценариев для придания интерактивности веб-страницам. </p> </div> <div> <button class="play" tabindex="3"></button> <p> Основные архитектурные черты: динамическая типизация, слабая типизация, автоматическое управление памятью, прототипное программирование, функции как объекты первого класса. </p> </div> </div> У нас имеется три блока (div) с кнопками для озвучивания текста (play) и, собственно, текстом, который будет озвучиваться (первые три абзаца статьи по JavaScript из Википедии). Обратите внимание, что я добавил кнопкам атрибут tabindex, чтобы переключаться между ними с помощью tab и нажимать с помощью space. Однако, имейте ввиду, что использовать атрибут tabindex не рекомендуется по причине того, что браузер использует переключение фокуса с помощью tab для повышения доступности. С вашего позволения, я приведу код скрипта целиком: // Это часть должна быть вам знакома по предыдущему примеру
let voices = speechSynthesis.getVoices() let defaultVoice speechSynthesis.onvoiceschanged = () => { voices = speechSynthesis.getVoices() defaultVoice = voices.find((voice) => voice.name === 'Google русский') wrapper.addEventListener('click', handleClick) window.addEventListener('keydown', handleKeydown) } const PLAY = 'play' const PAUSE = 'pause' const RESUME = 'resume' function handleClick({ target }) { switch (target.className) { case PLAY: // При нажатии кнопки `play` в момент озвучивания другого текста, // нам нужно прекратить текущее озвучивание перед началом нового speechSynthesis.cancel() const { textContent } = target.nextElementSibling // Об этой части см. ниже textContent.split('.').forEach((text) => { const trimmed = text.trim() if (trimmed) { const U = getUtterance(target, text) speechSynthesis.speak(U) } }) break case PAUSE: // CSS-класс кнопки отвечает за отображаемую рядом с ней иконку // ``- ожидание/стоп, `` - озвучивание/воспроизведение, `` - пауза // иконка `` используется в качестве индикатора кнопки, находящейся в фокусе target.className = RESUME speechSynthesis.pause() break case RESUME: target.className = PAUSE speechSynthesis.resume() break default: break } } // При нажатии `escape` прекращаем озвучивание текста // Можете добавить свои контролы function handleKeydown({ code }) { switch (code) { case 'Escape': return speechSynthesis.cancel() default: break } } function getUtterance(target, text) { const U = new SpeechSynthesisUtterance(text) U.voice = defaultVoice U.lang = defaultVoice.lang U.volume = 1 U.rate = 1 U.pitch = 1 // Я специально не стал скрывать смену иконок при начале/окончании воспроизведения U.onstart = () => { console.log('Started') target.className = PAUSE } U.onend = () => { console.log('Finished') target.className = PLAY } U.onerror = (err) => console.error(err) return U } Тонкий момент в приведенном коде — это преобразование озвучиваемого текста в массив предложений (разделителем является точка (.)), перебор массива, и воспроизведение каждого предложения по отдельности (точнее, помещение всех предложений одного за другим в очередь на озвучивание) — textContent.split('.').forEach(...). Причина такого решения заключается в том, что озвучивание длинного текста тихо обрывается примерно на 220 символе (в Chrome). Черновик спецификации для такого случае предусматривает специальную ошибку text-to-long (текст слишком длинный), но данной ошибки не возникает, озвучивание просто резко прекращается, причем, для восстановления работы SpeechSynthesis зачастую приходится перезагружать вкладку (при запущенном сервере для разработки даже это не всегда срабатывает). Возможно, вам удастся найти другое решение. Поиграть с кодом можно здесь: Извините, данный ресурс не поддреживается. :( "Диктофон" для распознавания речи После того, как мы обстоятельно рассмотрели SpeechSynthesis, можно переходить ко второму, немного более сложному, но, вместе с тем, и более интересному интерфейсу, входящему в состав WSA — SpeechRecoginition. Вот наша начальная разметка: <div id="wrapper">
<h1>Speech Recognition - Dictaphone</h1> <textarea id="final_text" cols="30" rows="10"></textarea> <input type="text" id="interim_text" /> <div id="buttons"> <button class="start">Старт</button> <button class="stop">Стоп</button> <button class="abort">Сброс</button> <button class="copy">Копия</button> <button class="clear">Очистить</button> </div> </div> У нас имеется поле для вставки финального (распознанного) текста (final_text) и поле для вставки промежуточного (находящегося в процессе распознавания) текста (interim_text), а также панель управления (buttons). Выбор элементов textarea и input для хранения текста обусловлен как вариативностью промежуточных результатов, которые меняются на лету, так и необходимостью внесения небольших правок в распознанный текст, что связано с естественным несовершенством перевода устной речи в текст. Стоит отметить, что, в целом, Chrome очень неплохо справляется с задачей распознавания речи и автоматическим форматированием распознанного текста. Кроме кнопок для управления распознаванием речи (start, stop и abort), мы реализуем возможность копирования распознанного текста в буфер обмена с помощью Clipboard API и очистки соответствующего поля. Начнем с определения переменных для финального текста и индикатора распознавания: let final_transcript = ''
let recognizing = false Далее, создаем и настраиваем экземпляр SpeechRecognition: // Напоминаю, что `WSA`, в целом, и, особенно, `SpeechRecognition` являются экпериментальными
const speechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition // Создаем экземпляр `SpeechRecognition` const recognition = new speechRecognition() // Свойство `continuous` определяет, продолжается ли распознавание речи после получения первого финального результата recognition.continuous = true // обработка промежуточных результатов recognition.interimResults = true // максимальное количество альтернатив распознанного слова recognition.maxAlternatives = 3 // язык recognition.lang = 'ru-RU' Добавляем обработчики событий запуска, ошибки и окончания распознавания речи: recognition.onstart = () => {
console.log('Распознавание голоса запущено') } recognition.onerror = ({ error }) => { console.error(error) } recognition.onend = () => { console.log('Распознавание голоса закончено') // Снова запускаем распознавание, если индикатор имеет значение `true` if (!recognizing) return recognition.start() } SpeechRecognition хорошо справляется с распознаванием слов, фраз и целых предложений, он даже выполняет вполне уместную "капитализацию" строки. Но он не "понимает" знаков препинания. Для того, чтобы помочь ему в этом определим словарь в виде объекта: const DICTIONARY = {
точка: '.', запятая: ',', вопрос: '?', восклицание: '!', двоеточие: ':', тире: '-', абзац: '\n', отступ: '\t' } Предполагается, что для решение задач, связанных с форматированием, будут использоваться специальные объекты SpeechGrammar и SpeechGrammarList, а также специальный синтаксис JSpeech Grammar Format, однако, в данной статье мы ограничимся обычным объектом. Вы можете добавить в словарь любые пары ключ/значение, которые посчитаете нужными. Обратите внимание на то, что все ключи в нашем словаре представлены одним словом. Дело в том, что рассматриваемый интерфейс плохо справляется с ключами, которые состоят более чем из одного слова, например, "вопросительный знак", "восклицательный знак" и т.п. Я понимаю, что пара вопрос: '?' не лучшее решение, но для примера сойдет. Также нам потребуются две функции для редактирования промежуточного и финального результатов: function editInterim(s) {
return s .split(' ') .map((word) => { word = word.trim() return DICTIONARY[word.toLowerCase()] ? DICTIONARY[word.toLowerCase()] : word }) .join(' ') } function editFinal(s) { return s.replace(/\s{1,}([\.+,?!:-])/g, '$1') } Функция editInterim() разбивает фразу на массив слов, перебирает слова, удаляет пробелы в начале и конце слова и заменяет символом из словаря при совпадении. Обратите внимание, что мы приводим слово в нижний регистр только при поиске совпадения. Если мы сделаем это в строке word = word.trim(), то нивелируем автоматическую "капитализацию" строки, выполняемую браузером. Функция editFinal() удаляет пробел перед каждым из указанных символов — мы разделяем слова пробелами при объединении в функции editInterim(). Следует отметить, что по умолчанию каждое распознанное слово с двух сторон обрамляется пробелами. Итак, событие, которое интересует нас больше всего — это result. Реализуем его обработку: recognition.onresult = (e) => {
// Промежуточные результаты обновляются на каждом цикле распознавания let interim_transcript = '' // Перебираем результаты с того места, на котором остановились в прошлый раз for (let i = e.resultIndex; i < e.results.length; i++) { // Атрибут `isFinal` является индикатором того, что речь закончилась if (e.results[i].isFinal) { // Редактируем промежуточные результаты const result = editInterim(e.results[i][0].transcript) // и добавляем их к финальному результату final_transcript += result } else { // В противном случае, записываем распознанные слова в промежуточный результат interim_transcript += e.results[i][0].transcript } } // Записываем промежуточные результаты в `input` interim_text.value = interim_transcript // Редактируем финальный результат final_transcript = editFinal(final_transcript) // и записываем его в `textarea` final_text.value = final_transcript } Событие распознавания выглядит следующим образом: SpeechRecognitionEvent
bubbles: false cancelBubble: false cancelable: false composed: false currentTarget: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …} defaultPrevented: false emma: null eventPhase: 0 interpretation: null isTrusted: true path: [] resultIndex: 1 // здесь нас интересуют только результаты results: SpeechRecognitionResultList {0: SpeechRecognitionResult, 1: SpeechRecognitionResult, length: 2} returnValue: true srcElement: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …} target: SpeechRecognition {grammars: SpeechGrammarList, lang: "ru-RU", continuous: true, interimResults: true, maxAlternatives: 3, …} timeStamp: 59862.61999979615 type: "result" Результаты (SpeechRecognitionResultList) выглядят так: results: SpeechRecognitionResultList
0: SpeechRecognitionResult 0: SpeechRecognitionAlternative confidence: 0.7990190982818604 transcript: "привет" isFinal: true length: 1 length: 1 Вот почему для получения результата мы обращаемся к e.results[0].transcript. На самом деле, поскольку мы указали maxAlternatives = 3, во всех результатах будет представлено по три SpeechRecognitionAlternative. Первым (с индексом 0) всегда будет наиболее подходящий результат с точки зрения браузера. Все, что нам осталось сделать, это реализовать обработку нажатия кнопок: buttons.onclick = ({ target }) => {
switch (target.className) { case 'start': // Обнуляем переменную для финального результата final_transcript = '' // Запускаем распознавание recognition.start() // Устанавливаем индикатор распознавания в значение `true` recognizing = true // Очищаем `textarea` final_text.value = '' // Очищаем `input` interim_text.value = '' break case 'stop': // Останавливаем распознавание recognition.stop() // Устанавливаем значение индикатора распознавания в значение `false` recognizing = false break case 'abort': // Прекращаем распознавание recognition.abort() recognizing = false break case 'copy': // Копируем текст из `textarea` в буфер обмена navigator.clipboard.writeText(final_text.value) // Сообщаем об этом пользователю target.textContent = 'Готово' const timerId = setTimeout(() => { target.textContent = 'Копия' clearTimeout(timerId) }, 3000) break case 'clear': // Обнуляем переменную для финального результата final_transcript = '' // Очищаем `textarea` final_text.value = '' break default: break } } Тестируем. Нажимаем на кнопку "Старт", дожидаемся, когда красная точка рядом с фавиконкой перестанет мигать (о готовности к распознаванию можно сообщать пользователю через обработку события speechstart), произносим фразу (произношение должно быть максимально четким и ясным), например, "привет точка как дела вопрос". В input на какое-то время появляется "Привет точка Как дела вопрос", затем этот промежуточный результат редактируется и переносится в textarea в виде "Привет. Как дела?". Отлично, наша машина нас понимает. Поиграть с кодом можно здесь: Извините, данный ресурс не поддреживается. :( Подумал о том, что было бы неплохо реализовать функцию для удаления последнего распознанного слова на тот случай, если результат распознавания получился некорректным. Для этого потребуется пара удалить: () => removeLastWord() (если добавить эту пару в словарь, то потребуется дополнительная проверка typeof DICTIONARY[word] === 'function') и примерно такая операция: function removeLastWord() {
const oldStr = final_text.value const newStr = oldStr.substring(0, oldStr.lastIndexOf(' ')) final_text.value = newStr } Однако, добавлять операцию в словарь — плохая идея, для этого лучше использовать отдельный объект, чем мы и займемся в следующем разделе. Одностраничное приложение с голосовым управлением Такой вариант использования SpeechRecognition, на мой взгляд, представляет наибольший интерес с точки зрения возможности применения данной технологии в реальных приложениях. Мы будем генерировать страницы программным способом, поэтому разметка нам не потребуется. Создаем директорию pages с тремя файлами: // home.js
export default /*html*/ ` <div id="wrapper"> <div>Section 1</div> <div>Section 2</div> <div>Section 3</div> <div>Section 4</div> <div>Section 5</div> <div>Section 6</div> <div>Section 7</div> <div>Section 8</div> <div>Section 9</div> </div> ` // product.js export default /*html*/ ` <h1>This is the Product Page</h1> ` // about.js export default /*html*/ ` <h1>This is the About Page</h1> ` Основной скрипт начинается с импорта страниц и генерации домашней страницы: import HomePage from './pages/home.js'
import ProductPage from './pages/product.js' import AboutPage from './pages/about.js' const { body } = document body.innerHTML = HomePage Нам потребуются константы для прокрутки страницы и объект с операциями: // Константы для прокрутки
const DOWN = 'down' const UP = 'up' const RIGHT = 'right' const LEFT = 'left' // Операции const ACTIONS = { // операции переключения страниц home: () => (body.innerHTML = HomePage), product: () => (body.innerHTML = ProductPage), about: () => (body.innerHTML = AboutPage), // операции прокрутки down: () => scroll(DOWN), up: () => scroll(UP), left: () => scroll(LEFT), right: () => scroll(RIGHT), // операции переключения цветовой темы light: () => body.removeAttribute('class'), dark: () => (body.className = 'dark') } Далее следуют настройки SpeechRecognition и обработчики событий начала, ошибки и окончания распознавания, аналогичные рассмотренным в предыдущем разделе (кроме настройки языка — для управления страницей мы будем использовать американский английский: recognition.lang = 'en-US'). Обработка события result: recognition.onresult = (e) => {
// Перебираем результаты for (let i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) { const result = e.results[i][0].transcript.toLowerCase() // Выводим слова в консоль, чтобы убедиться в корректности распознавания (как выяснилось, слово `product` очень плохо распознается, возможно, у меня проблемы с его правильным произношением) console.log(result) // Преобразуем фразу в массив, перебираем слова и выполняем соответствующие операции result .split(' ') .forEach((word) => { word = word.trim().toLowerCase() // ACTION[word] - это функция return ACTIONS[word] ? ACTIONS[word]() : '' }) } } } Функция для выполнения прокрутки выглядит следующим образом: function scroll(direction) {
let newPosition switch (direction) { case DOWN: newPosition = scrollY + innerHeight break case UP: newPosition = scrollY - innerHeight break case RIGHT: newPosition = scrollX + innerWidth break case LEFT: newPosition = scrollX - innerWidth break default: break } if (direction === DOWN || direction === UP) { scrollTo({ top: newPosition, behavior: 'smooth' }) } else { scrollTo({ left: newPosition, behavior: 'smooth' }) } } Наконец, добавляем обработку нажатия клавиш клавиатуры: window.addEventListener('keydown', (e) => {
e.preventDefault() switch (e.code) { // Нажатие пробела запускает распознавание case 'Space': recognition.start() recognizing = true break // Нажатие `escape` останавливает распознавание case 'Escape': recognition.stop() recognizing = false default: break } }) Проверяем работоспособность приложения. Нажимаем пробел, дожидаемся готовности браузера к распознаванию речи, произносим следующие фразы:
Поиграть с кодом можно здесь: Извините, данный ресурс не поддреживается. :( Заключение Как мы видим, WSA существенно расширяет арсенал JavaScript, связанный с добавлением динамики, "оживлением" веб-страниц. Сложно сказать, насколько широкое распространение получит данная технология и насколько активно она будет использоваться. Несмотря на то, что WSA — это технология общего назначения, очевидно, что она внесет серьезный вклад, в первую очередь, в развитие так называемого доступного Интернета. Мы с вами рассмотрели все основные возможности, предоставляемые WSA. Единственное, что мы не сделали, это не реализовали совместное использование SpeechSynthesis и SpeechRecognition. Первое, что приходит на ум — это голосовой помощник, который реагирует на ключевые слова, встречающиеся во фразе, произнесенной пользователем, отвечая определенными шаблонами и задавая уточняющие вопросы, при необходимости. Более простой вариант: волшебный шар — задаешь любой вопрос, предполагающий однозначный ответ, программа какое-то время "думает" и отвечает да, нет или, например, не знаю, спросите позже. Пусть это будет вашим домашним заданием, все необходимые знания для этого у вас имеются. Обязательно поделитесь результатом в комментариях. Облачные серверы от Маклауд быстрые и безопасные. Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации! оригинал =========== Источник: habr.com =========== Похожие новости:
Блог компании Маклауд ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ), #_javascript, #_api |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:59
Часовой пояс: UTC + 5