你有没有构思过这样的项目啊 -- 用 Node.js 来检查我们发的语音内容是积极的还是消极的?
我之前收到 Grammarly 的一封讨论语气检测的推广邮件,说是可以用程序检查我们写的内容是什么,然后告诉我们这个内容给人的感觉是积极进取的,自信的还是其他什么感觉。
我就想 -- 有没有可能使用浏览器和 Node.js 来构建一个简化版本呢?
所以我就开发了这么一个小项目,这里把方法分享给大家。
步骤
在你开始构建一个项目时,你应该(至少模糊地)勾勒出你的目标以及构思如何实现。在开始搜索之前,我罗列出需要做的事情:
- 记录语音
- 将语音转换成为文字
- 文字内容测评
- 将结果反馈给说话人
搜索一番之后,我发现 Web Speech API 就可以录音以及将语音转换成文字,在 Google Chrome 就可以使用这个功能,语音识别部分正是我们需要的。
而文字内容测评呢,我发现在 AFINN 上面有测评词汇列表。虽然只有 2477 个词汇,但是对我们的项目来说也够用了。
结果反馈这一步,我们用 HTML,JavaScript 和 CSS 编写程序来处理结果,将 emoji 展示在网页上。
现在我们知道要用到些什么了,总结一下:
- 浏览器接收用户的语音,通过 Web Speech API 返回一些文字
- 向 Node.js 服务器发起处理文字的请求
- 依据 AFINN 的列表测评文字返回分数
- 浏览器根据分数显示不同的 emoji
注意: 如果你熟悉项目设置,那么你可以跳过以下“项目文件和设置”部分。
项目文件和设置
项目文件夹和文件结构如下:
|-public // folder with the content that we will feed to the browser
|-style // folder for our css and emojis
|-css // optional folder, we have only one obvious file
|-emojis.css
|-images // folder for the emojis
|-index.html
|-recognition.js
package.json
server.js // our Node.js server
前端 index.html 包括 JS 和 CSS 代码:
<html>
<head>
<title>
Speech to emotion
</title>
<link rel="stylesheet" href="style/css/emojis.css">
</head>
<body>
nothing for now
<script src="recognition.js"></script>
</body>
</html>
recognition.js 在 DOMContentLoaded 事件中,确保在执行 JS 代码前加载页面。
document.addEventListener('DOMContentLoaded', speechToEmotion, false);
function speechToEmotion() {
// Web Speech API section code will be added here
}
emojis.css 暂时为空。
在文件夹中运行 npm run init,创建 package.json。
接下来,我们需要用 npm install 安装两个包,让事情变得简单点:
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) {
// Valence of emotion section code will be here for not it returns nothing
res.send({})
})
app.listen(port, function () {
console.log(`Listening on port ${port}!`)
})
接着,在终端运行 npm run server-debug,在 localhost:3000 打开浏览器,会看到 HTML 文件中的 “nothing for now” 信息。
Web Speech API
在 Chrome 上使用这个 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 的属性,像给 lang 设置属性值一样把它的值设置为 true,这样就可以不间断地捕捉音频了。
刷新页面,首先会弹出对话框问我们是否启用麦克风,选择 yes,然后我们就可以讲话,并且在 Chrome DevTools console 查看语音信息转换成文字的结果。
注意:截至本文发布时,Chrome 和安卓系统支持使用这个 API,可能之后 Edge 也会支持。或许可以通过一些脚本或者工具解决浏览器兼容性问题,不过我自己没有尝试。你可以在 Can I use 搜索兼容性问题。
发起请求
通过简单的 fetch 方法发起请求。
情绪效价
效价用于评估情绪是正面还是负面的,以及是高唤醒情绪还是低唤醒情绪。
在这个项目中,我们使用两种情绪:开心,正面情绪,分值为正;沮丧,负面情绪,分值为负。分值为零则表示中立的情绪,表达的意思是“额,什么情况”。
AFINN 列表的计分从 -5 分到 5 分,以下是列表的词汇示例:
hope 2
hopeful 2
hopefully 2
hopeless -2
hopelessness -2
hopes 2
hoping 2
horrendous -3
horrible -3
horrific -3
举个例子,“我希望这个不危险”,其中“希望”是 2 分,“危险”是 -3 分,那么这句话的总分就是 -1 分。这个列表里没有包含的词汇我们可以忽略不计分。
将文件渲染为 JSON:
{
<word>: <score>,
<word1>: <score1>,
..
}
然后我们就可以检测文字里的每个词语,计算总分。不过,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 // this returns our request query "text"
const score = sentiment.analyze(text);
res.send(score)
})
sentiment.analyze(<our_text_variable>) 会执行上面说的步骤: 根据 AFINN 列表检查文字中每个词语,最后给我们一个总分。
变量 score 的对象:
返回分值之后,我们需要让它在浏览器中显示。
注意: AFINN 是英文的,要在 Web Speech API 使用其他语言的话,可以搜索类似 AFINN 的分值列表,以匹配相应的语言。
精彩部分
最后一步,更新 index.html,以展示 emoji:
<html>
<head>
<title>
Speech to emotion
</title>
<link rel="stylesheet" href="style/css/emojis.css">
</head>
<body>
<!-- We replace the "nothing for now" -->
<div class="emoji">
<img class="idle">
</div>
<!-- And leave the rest alone -->
<script src="recognition.js"></script>
</body>
</html>
本项目中使用的 emoji 允许商用,免费,可以在 这里 获得。谢谢创作 emoji 的艺术家!
我们下载一些喜欢的 icon,把它们添加到 images 文件夹,稍后我们会使用这些 emoji:
- error 错误 - 出现错误时
- idle 懒洋洋 - 麦克风未启用时
- listening 倾听 - 麦克风已连接等待输入时
- negative 负面 - 分值为正时
- neutral 中性 - 分值为零时
- positive 正面 - 分值为负时
- searching 搜索 - 发起服务器请求时
在 emojis.css 中添加:
做完以上修改之后,我们重新加载页面,会显示懒洋洋 emoji,这是因为我们还没有根据不同场景替换元素的 idle 类。
我们还需要对 recognition.js 进行一步操作,添加一个函数以更改 emoji。
/**
* @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
}
在收到服务器请求的反馈之后,我们开始检测分值为正负或是零,相应地调用 setEmoji 函数。
最后,添加 onerror 和 onaudiostart 事件,修改 onend 事件,这样就设置为正确的 emoji 了。
recognition.onerror = function(event) {
console.error('Recognition error -> ', event.error)
setEmoji('error')
}
recognition.onaudiostart = function() {
setEmoji('listening')
}
recognition.onend = function() {
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 = function() {
setEmoji('listening')
}
recognition.onend = function() {
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
}
}
测试项目,显示结果:
注意:我们可以在 html 中添加一个元素,替换 console.log.,来检测识别出的内容,这样可以持续检测。
思考
其实我们可以对这个项目的某些部分可以做很多优化:
- 检测讽刺的内容
- 由于语音转文字 API 的敏感词限制,目前没办法检测出愤怒的情绪
- 也许可以省掉语音转文字这一步,直接根据语音显示出相应的 emoji
我在做这个项目的过程中发现,已经有一些类似的应用了,比如电话销售人员根据客户的语调和情绪判断是否可以成单。还有我在文章开头说的那封邮件,Grammarly 他们用这个方法来检测用户发邮件中用词的语调!这些应用还挺有意思的。
希望这篇文章可以给你一些启发。如果你用我介绍的方法做了什么项目,请告诉我啊。
你可以在我的 GitHub 仓库查看代码。
原文链接:How to Build a Speech to Emotion Converter with the Web Speech API and Node.js,作者:Diogo Spínola