Как преобразовать речь в эмоции с помощью Web Speech API и Node.js

Доброго времени суток, друзья!

Представляю Вашему вниманию перевод статьи Diogo Spínola «How to Build a Speech to Emotion Converter with the Web Speech API and Node.js».

Вы когда-нибудь задумывались о том, может ли Node.js определять позитивный или негативный оттенок речи?

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

Я спросил себя, возможно ли реализовать что-то подобное в браузере с помощью Node.js.

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

Вот как мне удалось этого добиться.

Проект


Определение речи -> перевод звука в текст -> оценка текста -> результат

Когда вы приступаете к работе над проектом, необходимо хотя бы в общих чертах понимать цель и то, как ее можно достичь. Сначала я определился с тем, что мне нужно:

  • запись голоса
  • способ перевести запись в текст
  • способ оценить текст
  • способ отобразить результат

Некоторое время спустя я обнаружил, что с задачей записи голоса и его преобразования в текст прекрасно справляется Web Speech API, доступный в Google Chrome. В его интерфейсе распознавания речи есть все, что нужно.

Что касается оценки текста, то я нашел AFINN, представляющий список слов с рейтингом. Он содержит всего 2477 слов, но этого более чем достаточно для нашего проекта.

Поскольку мы работаем в браузере, для отображения результата мы будем использовать HTML, CSS и JavaScript.

Теперь мы можем конкретизировать задачи:

  • браузер «слушает» пользователя и возвращает некоторый текст, используя WSAPI
  • браузер отправляет текст на Node.js-сервер
  • сервер оценивает текст, используя AFINN, и возвращает результат
  • браузер показывает эмодзи в зависимости от результата

Если вы умеете настраивать проект, можете пропустить следующий раздел.

Файлы и настройка проекта

Директория нашего проекта (файловая структура) будет выглядеть следующим образом:

src/ |-public // папка для браузера   |-style // папка для CSS и эмодзи     |-css // опционально       |-emojis.css     |-images // папка для эмодзи   |-index.html   |-recognition.js package.json server.js // Node.js-сервер 

Разметка выглядит так:

<html> <head>     <title>         Речь в эмоции     </title>     <link rel="stylesheet" href="style/css/emojis.css"> </head> <body>     здесь пока ничего нет     <script src="recognition.js"></script> </body> </html> 

Оборачиваем recognition.js в DOMContentLoaded, чтобы скрипт не выполнился до загрузки страницы:

document.addEventListener('DOMContentLoaded', speechToEmotion, false)  function speechToEmotion() {     // Web Speech API } 

emojis.css оставляем пустой.

Выполняем npm run init для создания package.json в нашей директории.

Также нам необходимо установить два пакета: expressjs — для быстрого запуска HTTP-сервера и nodemon — для слежения за server.js.

package.json выглядит так:

{     "name": "speech-to-emotion",     "version": "1.0.0",     "description": "We speak and it feels us :o",     "main": "index.js",     "scripts": {         "server": "node server.js",         "server-debug": "nodemon --inspect server.js"     },     "author": "daspinola",     "license": "MIT",     "dependencies": {         "express": "^4.17.1"     },     "devDependencies": {         "nodemon": "^2.0.2"     } } 

server.js начинается так:

const express = require('express') const path = require('path')  const port = 3000 const app = express()  app.use(express.static(path.join(__dirname, 'public')))  app.get('/', function(req, res) {     res.sendFile(path.join(__dirname, 'index.html')) })  app.get('/emotion', function(req, res) {     // здесь пока ничего нет     res.send() })  app.listen(port, function() {     console.log(`Listening on port ${port}!`) }) 

Теперь мы можем запустить сервер с помощью команды npm run server-debug и отрыть браузер на localhost:3000. Мы увидим «здесь пока ничего нет» из html.

Web Speech API

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

Он работает с событиями, которые могут определять, например, начало и конец захвата аудио.

Пока нам нужны только события onresult и onend для определения содержимого записи и окончания работы микрофона.

Приступаем к написанию recognition.js:

const recognition = new webkitSpeechRecognition() recognition.lang = 'en-US'  recognition.onresult = function(event) {     const results = event.results     const transcript = results[0][0].transcript      console.log('text ->', transcript) }  recognition.onend = function() {     console.log('disconnected') }  recognition.start() 

Данный код на несколько секунд включает микрофон для прослушивания звука. Если ничего не найдено, происходит отключение.

Список доступных языков можно посмотреть здесь.

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

const recognition = new webkitSpeechRecognition() recognition.lang = 'en-US' recognition.continuous = true  recognition.onresult = function(event) {     const results = event.results;     const transcript = results[results.length - 1][0].transcript      console.log('text ->', transcript) }  recognition.onend = function() {     console.log('disconnected') }  recognition.start() 

Мы добавили свойство continuous и изменили transcript, чтобы получать только последний результат.

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

Ненормативная лексика подвергается цензуре и, похоже, на сегодняшний день нет способа ее убрать. Это означает, что мы не можем оценивать ненормативную лексику, даже если она присутствует в AFINN.

Примечание. В настоящее время WSAPI поддерживается только в Chrome и Edge. Возможно, существуют полифилы.

Запрос на сервер

Для отправки запроса нам достаточно простого fetch. Мы отправляем transcript как параметр запроса, который мы назовем text.

Допишем onresult:

recognition.onresult = function(event){     const results = event.results     const transcript = results[results.length - 1][0].transcript      // отправляем запрос к /emotion (конечной точке, которую мы определили в начале проекта и настройках)     fetch(`/emotion?text=${transcript}`)     .then(response => response.json())     .then(result => console.log('result -> ', result)) // должно быть undefined     .catch(e => console.error('Request error -> ', e)) } 

Для передачи длинных сообщений лучше использовать POST-запросы. В нашем случае GET-запроса вполне достаточно.

Эмоции

Наши эмоции могут быть позитивными или негативными, т.е. иметь большую или меньшую степень возбуждения.

В нашем проекте мы используем два вида эмоций: радость для позитивной стороны (больше 0) и огорчение для негативной стороны (ниже 0). Рейтинг 0 будет соответствовать нейтральному оттенку. 0 мы будем интерпретировать как «что?!».

Рейтинг слов из списка AFINN варьируется от -5 до 5 следующим образом:
hope 2
hopeful 2
hopefully 2
hopeless -2
hopelessness -2
hopes 2
hoping 2
horrendous -3
horrible -3
horrific -3
слово<пробел>рейтинг

Допустим, я произношу в микрофон фразу «Надеюсь, это не ужасно» (I hope this is not horrendous; horrendous — устрашающий, вселяющий ужас). У «надеюсь» (hope) будет 2 очка, у «устрашающий» (horrendous) -3, в целом наша фраза будет негативной с рейтингом -1. Слова, отсутствующие в списке, не оцениваются.

Мы можем конвертировать файл в формат JSON:

{     <слово>: <рейтинг>,     <слово1>: <рейтинг1>     ... } 

Затем мы можем проверить все слова и суммировать значения их рейтинга. Andrew Sliwinski уже сделал это в «sentiment» (настроение). Так что мы не будем писать код с нуля, а используем его наработку.

Устанавливаем пакет с помощью npm install sentiment и добавляем в server.js импорт библиотеки:

const Sentiment = require('sentiment') 

Далее немного изменим маршрут ‘/emotion’:

app.get('/emotion', function(req, res){     const sentiment = new Sentiment()     const text = req.query.text     const score = sentiment.analyze(text)      res.send(score) }) 

sentiment.analyze() проверяет каждое слово по списку AFINN и возвращает результат.

Переменная score — это объект, который выглядит так:

{     score: 7,     comparative: 2.3333333333333335,     calculation: [ { awesome: 4 }, { good: 3 } ],     tokens: [ 'good', 'awesome', 'film' ],     words: [ 'awesome', 'good' ],     positive: [ 'awesome', 'good' ],     negative: [] } 

Нам осталось отобразить результат в браузере.

Создаем улыбку

Обновим index.html, добавив область, куда мы будем выводить эмодзи:

<html>   <head>     <title>       Speech to emotion     </title>     <link rel="stylesheet" href="style/css/emojis.css">   </head>   <body>     <div class="emoji">       <img class="idle">     </div>     <script src="recognition.js"></script>   </body> </html> 

Эмодзи позаимствованы отсюда (они свободны для некоммерческого использования). Респект художнику.

Скачиваем понравившиеся эмодзи и помещаем их в папку. Нам нужны следующие эмодзи:
error — ошибка
idle — микрофон отключен
listening — микрофон включен
negative — негативный результат
neutral — 0
positive — позитивный результат
seaching — выполнение запроса на сервер

emojis.css выглядит так:

.emoji img {   width: 100px;   width: 100px; }  .emoji .error {   content:url("../images/error.png"); }  .emoji .idle {   content:url("../images/idle.png"); }  .emoji .listening {   content:url("../images/listening.png"); }  .emoji .negative {   content:url("../images/negative.png"); }  .emoji .neutral {   content:url("../images/neutral.png"); }  .emoji .positive {   content:url("../images/positive.png"); }  .emoji .searching {   content:url("../images/searching.png"); } 

Перезагрузив страницу, мы увидим эмодзи idle. Эмодзи никогда не изменится, поскольку мы не добавили возможность манипулирования классами тега «img».

Для того, чтобы исправить ситуацию, нам необходимо немного поправить recognition.js, добавив функцию смены эмодзи:

/** * @param {string} тип - может быть любым из: * error|idle|listening|negative|positive|searching */ function setEmoji(type){     const emojiElem = document.querySelector('.emoji img')     emojiElem.classList = type } 

После определения рейтинга на стороне сервера мы вызываем функцию setEmoji с соответствующим параметром:

console.log(transcript)  setEmoji('searching')  fetch(`/emotion?text=${transcript}`) .then(response => response.json()) .then(result => {     if(result.score > 0){         setEmoji('positive')     } else if(result.score < 0){         setEmoji('negative')     } else {         setEmoji('listening')     } }) .catch(e => {     console.error('Request error ->', e)     recognition.abort() }) 

Наконец, мы добавляем события onerror и onaudiostart и меняем событие onend, устанавливая соответствующие эмодзи:

recognition.onerror = function(event) {     console.error('Recognition error ->', event.error)     setEmoji('error') }  recognition.onaudiostart = () => setEmoji('listening')  recognition.onend = () => setEmoji('idle') 

«Финальный» recognition.js выглядит так:

document.addEventListener('DOMContentLoaded', speechToEmotion, false);  function speechToEmotion() {   const recognition = new webkitSpeechRecognition()   recognition.lang = 'en-US'   recognition.continuous = true    recognition.onresult = function(event) {     const results = event.results;     const transcript = results[results.length-1][0].transcript      console.log(transcript)      setEmoji('searching')      fetch(`/emotion?text=${transcript}`)       .then((response) => response.json())       .then((result) => {         if (result.score > 0) {           setEmoji('positive')         } else if (result.score < 0) {           setEmoji('negative')         } else {           setEmoji('listening')         }       })       .catch((e) => {         console.error('Request error -> ', e)         recognition.abort()       })   }    recognition.onerror = function(event) {     console.error('Recognition error -> ', event.error)     setEmoji('error')   }    recognition.onaudiostart = () => setEmoji('listening')    recognition.onend = () => setEmoji('idle')    recognition.start();    /**    * @param {string} type - could be any of the following:    *   error|idle|listening|negative|positive|searching    */   function setEmoji(type) {     const emojiElem = document.querySelector('.emoji img')     emojiElem.classList = type   } } 

Вместо console.log для вывода результатов распознавания мы можем добавить соответствующий элемент в html.

Заключение

Вот какие недостатки имеет проект:

  • он не умеет распознавать сарказм
  • он не умеет распознавать гнев (в том числе, из-за цензуры API на ненормативную лексику)
  • вероятно, можно обойтись без преобразования речи в текст

Ссылка на видео.

Ссылка на репозиторий.

Благодарю за внимание. До новых встреч.

ссылка на оригинал статьи https://habr.com/ru/post/488132/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *