
Вступление
Однажды я подумал, насколько трудно и дорого в наши дни сделать голосового помощника, который будет впопад отвечать на вопросы.
А если конкретнее, то веб-приложение, которое записывает аудио с вопросом, расшифровывает аудио в текст, находит подходящий ответ, тоже текстовый, и возвращает аудио версию ответа — вот функциональные требования, который я себе набросал.
Клиентская часть
Я создал простой React проект с помощью create-react-app и добавил компонент “RecorderAndTranscriber”, который и содержит весь функционал клиентской части. Стоит отметить использование метода getUserMedia из MediaDevices API чтобы получить доступ к микрофону. Дальше этот доступ достаётся MediaRecorder, через который уже и записывается аудио. Для таймера я использую setInterval.

Пустой массив необязательным параметром в React hook — useEffect, чтобы он вызывался только раз, при создании компонента.
useEffect(() => { const fetchStream = async function() { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); setRecorderState((prevState) => { return { ...prevState, stream, }; }); } fetchStream(); }, []);
Сохранённый поток используем для создания экземпляра MediaRecorder, который я тоже сохраняю.
useEffect(() => { if (recorderState.stream) { setRecorderState((prevState) => { return { ...prevState, recorder: new MediaRecorder(recorderState.stream), }; }); } }, [recorderState.stream]);
Дальше я добавил блок для запуска счётчика секунд, прошедших с начала записи.
useEffect(() => { const tick = function() { setRecorderState((prevState) => { if (0 <= prevState.seconds && 59 > prevState.seconds) { return { ...prevState, seconds: 1 + prevState.seconds, }; } else { handleStop(); return prevState; } }); } if (recorderState.initTimer) { let intervalId = setInterval(tick, 1000); return () => clearInterval(intervalId); } }, [recorderState.initTimer]);
Hook срабатывает только при изменении значения initTimer, а callback для setInterval обновляет значение счётчика и останавливает запись если длина записи 60 секунд. Дело в том, что 60 секунд и/или 10Mb это ограничение Speech-to-Text API на аудио файлы, которые можно расшифровать, отправляя файлы напрямую. Большие файлы нужно сначала загружать в файловое хранилище Google Cloud Storage. Подробнее про ограничения можно прочитать тут.
Следующий момент, который стоит упомянуть, это как происходит запись.
const handleStart = function() { if (recorderState.recorder && 'inactive' === recorderState.recorder.state) { const chunks = []; setRecorderState((prevState) => { return { ...prevState, initTimer: true, }; }); recorderState.recorder.ondataavailable = (e) => { chunks.push(e.data); }; recorderState.recorder.onstop = () => { const blob = new Blob(chunks, { type: audioType }); setRecords((prevState) => { return [...prevState, { key: uuid(), audio: window.URL.createObjectURL(blob), blob: blob } ]; }); setRecorderState((prevState) => { return { ...prevState, initTimer: false, seconds: 0, }; }); }; recorderState.recorder.start(); } }
Для начала я проверяю, что экземпляр класса MediaRecorder существует и его статус inactive, один из трёх возможных. Дальше обновляется переменная initTimer, чтобы создать и запустить interval. Чтобы контролировать запись я подписался на обработку двух событий ondataavailable и onstop. В обработчике для ondataavailable сохраняется новый кусочек аудио в заранее созданный массив. А по срабатыванию onstop, из кусочков создаётся blod файл и добавляется к списку готовых записей. В объекте записи я сохраняю url на аудио файл, чтобы использовать в DOM элементе audio, как значение для src, а поле blob, чтобы отправлять на серверную часть.
Серверная часть
Для поддержания работы клиентской части я выбрал связку Node.js и Express. Создал файл index.js, в котором и собрал API с методами:
а) getTranscription(audio_blob_file)
б) getWordErrorRate(text_from_google, text_from_human)
с) getAnswer(text_from_google)
Чтобы вычислить Word Error Rate я взял скрипт на python из проекта tensorflow/lingvo и переписал его в js. По сути это просто решение задачи Edit Distance, плюс расчёт ошибки по каждому из трёх типов: удаление, добавление, замена. Я получил не самый интеллектуальный метод сравнения текстов, но достаточный, чтобы в дальнейшем можно было добавлять дополнительные параметры к запросам к Speech-to-Text.
Для getTranscription я взял готовый код из документации к Speech-to-Text, а для перевода текста ответа в аудио файл — из документации к Text-to-Speech. Немного запутанным оказалось создание ключа доступа к google cloud с серверной части. Для начала нужно было создать проект, потом включить Speech-to-Text API и Text-to-Speech API, создать ключ доступа и, наконец, добавить путь к ключу в переменную GOOGLE_APPLICATION_CREDENTIALS.
Чтобы json файл с ключом, нужно создать Service account для проекта.



После нажатия Create and Continue и Done во вкладке Credentials, в таблице Service Accounts появиться новый аккаунт. Если перейти в этот аккаунт, на вкладке Keys можно нажать на Add Key, и получить json-файл с ключом. Этот ключ необходим, чтобы серверная часть могла получить доступ к Google Cloud сервисам, активированным в проекте.
На этом предлагаю закончить первую часть. Описание базы данных и моих экспериментов с ненормативной лексикой будет во второй части статьи.
ссылка на оригинал статьи https://habr.com/ru/post/655653/
Добавить комментарий