叮当:一个开源的树莓派中文智能音箱项目

这个项目其实来源于我生活中的一个需求:我每天晚上都会去厨房做一个面包当明天的早餐,当我把用料按顺序准备好放进面包机时,我需要准确预约到明天早上我吃早餐的时间。然而,几乎每次在这个时候我都没有带手机在身边,而是都放在客厅里充电,这时只能跑去客厅看时间。虽然厨房到客厅只有几步之遥,但自己又是懒癌患者,每天都要这么来回奔波就觉得很不方便。要解决这个问题当然有很多种方法,比如直接买个小时钟放在厨房。不过我更希望“连看都不用看”,直接有人告诉我时间。所以,我需要一个像 Amazon Echo 那样的智能音箱。

然而,不论是 Amazon Echo 、Google Home 还是微软 Cortana 音箱,在国内的使用都是个问题。虽然国内也有类似的智能音箱产品,但我没有用过这些产品,不知道可定制性如何。比如,如果我需要开发个功能让它告诉我某种面包的配方是什么,这些产品就不一定能做到了。考虑再三,我决定自己动手写一个。整个项目用了差不多三个星期的业余零碎时间。

先放上项目主页:http://dingdang.hahack.com

下面分享一下我在开发这个项目过程中的心得。

如果您在使用微信或者其他客户端程序阅读本文时发现视频无法播放,请改为在浏览器中打开。

硬件

首先要解决的是硬件问题。我选择在 Raspberry Pi 上开发。于是我买了块 Raspberry Pi 三代主板。麦克风和音响方面,出于美观的目的,买了个自带音响的 USB 全向会议麦克风。整套设备看起来就像这样:

后面觉得这个麦克风自带的音响音质太一般了,所以我又外接了一个小音箱。然后再插了一个摄像头,用来实现拍照功能。最终的完全体进化成了这样:

硬件有了,接下来就得开始写软件了。主要的框架借鉴了 Jasper 项目,并加入了我自己的定制和想法。这里说说一些有意思的部分。

指令接收

智能音箱要解决的一个最重要的问题就是如何接收指令。这里头主要涉及两个问题:

被动唤醒(Passive Listening),即“什么时候开始听”。这个阶段只监听唤醒词。当听到唤醒词时,进入主动聆听。 主动聆听(Active Listening),即“什么时候结束听”。这个阶段主动聆听用户的任何语音指令,然后对听到的内容进行分析处理。

被动唤醒阶段的基本策略是:每次以 16000 的采样率录制 1024 个采样作为一个采样集,然后对采样集进行信号强度估计,当某个采样集信号强度大于一个阈值时,就认为可能接受到了指令。然后持续录制多 1 秒时间,再转交给语音识别模块。当语音识别模块认为是唤醒词时,进入主动聆听阶段。

主动聆听的策略与被动唤醒基本相似,每次以 16000 的采样率录制 1024 个采样作为一个采样集,然后对采样集进行信号强度估计,当某个采样集信号强度低于一个阈值约 1 秒的时间时,就认为用户已说完了指令。当然还要考虑环境吵杂,一直处于聆听的可能。因此可以再加一个超时保护,超过 12 秒就结束聆听。

语音处理

说说STT(语音识别)引擎和TTS(文本转文本)引擎的选择。由于被动唤醒会试图识别所有听到的内容,出于隐私保护的目的,应该使用离线的语音识别引擎,因此我选择的是 PocketSphinx 。而对于主动聆听,由于是在唤醒阶段才会进行转换,进入主动聆听前会有蜂鸣提示,用户也会清楚此时叮当正在听他们说话,相对来说隐私泄露的可能性就比较低,因此我选择的是在线的百度 STT 语音识别服务,也省下了扩展语音识别模型的工夫,有利于更好地实现插件可扩展。TTS 引擎方面同样也先支持了百度的语音合成。

在实际测试中,PocketSphinx 的识别出乎意料的好。由于我的离线指令集只有几个候选唤醒词,PocketSphinx 对这些唤醒词的识别非常灵敏,甚至有时候其他声音也可能被误当成唤醒词而唤醒叮当。但即使被意外唤醒了,不去理会叮当就可以了。

相比之下,百度的语音识别就比较迟钝了。有时候明明我发音很清晰了,还是会识别成另外的含义。通过在百度的语音识别平台上传自定义的语音识别词库 可以提高识别的准确率。另外,由于我用的是 Restful API,网速比较差的时候响应也比较慢。我在家用的是 10M 带宽的网络,反应速度还算可以接受。我准备后面尝试接入更多的语音识别平台,看看识别速度和准确度方面能否有所提升。

下面这个视频是我与叮当对话的演示。我把唤醒词设置成了“小梅”:

一个问题是当回答内容比较长(比如问叮当当天的新闻)时,合成语音的耗时会变得很长,给人的感受是叮当的响应很慢。所以我加了个 read_long_content 的选项。当内容过长时,改成发送到用户的邮箱或者微信。下面这段音频是一个例子:

长内容发微信

插件

叮当最好玩的部分当然就是玩插件了,通过写插件可以让叮当接入各种各样的服务,完成各种各样的事情。我在叮当里也内置了几个插件(见注脚1)。为了方便用户扩展,我把 ~/.dingdang/contrib 设定为第三方插件目录,允许让用户在里头编写插件并提交到 dingdang-contrib 项目共享。

Time:顾名思义就是询问时间的,先满足我的个人需求。

Echo:简单的回声/传话功能。当接入微信时,可以利用这个功能实现远程给家里发语音消息。

Email:询问邮箱中有多少未读邮件。在这之前用户需要先配置好邮箱账户。另外,叮当还有个定期消息检查机制,每 30 秒会执行一次邮件检查,如果有未读邮件,将会在当次运行会话中给用户语音播报一次。

Camera:用于调起摄像头拍照(如果安装了摄像头的话)。如果接入了邮箱或微信,照片将发送到用户的邮箱或微信中(选择哪种取决于配置中 prefers_email 的值)。

Unclear:用于处理未知的问题。如果接入了对话机器人(例如图灵机器人),将转交给对话机器人应答;否则将给予类似 “我没听清楚” 这样的回复。

下面这个视频是 Camera 插件的演示(见注脚2):

另外,如果接入了微信,还可以让叮当安静地拍一张家里的照片,而不发出任何声音。下面这个视频演示了如何使用微信与家里的机器人交互,包括远程控制拍照。

这对于需要远程监控家里的情况的用户而言就非常方便了,比如家里有小孩的情况。

音乐播放

既然是智能音箱,当然少不了播放音乐的功能。所以我额外写了个播放网易云音乐的插件 NetEaseMusic 。出于版权考虑,并不集成进官方插件中,而是放进 dingdang-contrib 里头。

这个插件的实现比较复杂。普通的插件接受到指令,响应完就退出了。而为了能支持各种指令控制音乐播放,这个插件在接收到播放控制指令后并不退出插件,而是进入一个播放器模式,这个模式主动聆听得到的指令只会在播放控制指令集中匹配,其他的插件指令都不起作用。只有当用户要求退出播放时才回到普通模式。NetEaseMusic 的播放控制指令如下:

指令 相同指令 用途 播放音乐 – 进入音乐播放模式。在音乐播放模式下,其他的插件功能将不可用。 下一首 切歌, 下一首歌, 下首歌 切换到下一首歌。如果没有下一首歌,就回到列表中第一首歌 上一首 上一首歌,上首歌 切换到上一首歌。如果没有上一首歌,就跳到列表中最后一首歌 大声点 大点声,大声 调高播放音量 小声点 小点声,小声 降低播放音量 随机播放 – 随机播放列表中的音乐 顺序播放 – 顺序播放列表中的音乐 暂停播放 – 暂停音乐的播放 播放 继续 继续音乐的播放 榜单 – 播放推荐榜单 歌单 – 播放用户的歌单(如果有多张,将只播放第一张) 结束播放 退出播放,停止播放 退出音乐播放模式。 搜索 查找 搜索歌曲/歌手。将自动播放搜索结果。 什么歌 – 正在播放的是什么歌

实现这个插件的过程中还参考了 Vellow 的 MusicBox 项目(见注脚3)以及 yaphone 的 RasWxNeteaseMusic 。为了方便重用,我把 MusicBox 的核心 API 抽离了出来封成了一个 MusicBoxApi 库 。比较坑爹的是就在我准备发布叮当的前几天,老的获取音乐地址的方式彻底不能用了,而新的接口批量获取的地址不知道为什么是乱序的,于是我只能在播放每首歌前都调用一下新版的获取地址的 POST 接口,又增加了一点响应时间(见注脚4)。

下面这段音频是使用叮当控制音乐播放的演示:

播放音乐

完成了音乐播放功能后,叮当的好玩程度提高了很多。以前要听歌,至少得把电脑或者手机打开。现在只需要喊一声叫叮当播放歌曲就可以了。想换歌、搜索歌曲、调节音量都是说句话就搞定的事情,生活幸福指数大幅提升 ^_^ 。

总结和后续

对于有 Coding 能力的 Hacker 而言,自己动手做一个智能音箱,不仅可以当做业余练手项目,还可以自由地定制硬件模块,并实现自己需要的各种功能,这远比直接购买一个 Amazon Echo 有趣得多。

后面我计划做的事情有:

尝试接入更多的 STT / TTS 服务,优化叮当的响应时间;

结合 NLP 技术实现更复杂的指令识别,比如提醒功能;

加入人体感应模块等传感器,把它变得更加智能。

更重要的,我更希望能有其他有兴趣的朋友参与进来,一同开发完善这个智能音箱项目。我相信,这种个性化服务的产品本身就应该是完全可定制的。而您的加入可以使叮当变得更智能!

完整的插件信息可以在这里找到。 能支持拍照的智能音箱估计没多少吧?这就是自己开发智能音箱的好处,想接什么模块就接什么模块。想实现什么功能就实现什么功能。 Github 上搜索关键词“musicbox” 可以找到好几个类似的仓库,其实都是基于 Vellow 最初的那个版本的修改。我实际上参考的是 darknessomi 的版本 ,但我认为最大的功劳还是该给 Vellow 。 如果您知道如何批量获取播放地址且保持原来的列表顺序,还请告诉我方法。

感谢 hahack42 的投稿。