天天看点

树莓派开发自己的智能语音系统

作者:IT智能化专栏

目录:

一、开篇

二、开发环境搭建

三、百度语音识别API

四、语音唤醒

五、交互音乐播放器

六、实现播报新闻

正文

一、开篇

自从各大云平台逐步开放智能云服务之后,我就有了结合目前流行的开源硬件开发属于自己的智能语音系统的想法。当时还没有接触树莓派这个开源硬件,后来之所以选用它,主要还是因为树莓派提供了合适的性能支持和开发环境,正好也在学习python,而树莓派提供了很好的python开发环境,另外选择python作为开发语言也是因为python的易用性,有着众多的粉丝支持。

当然,在我们选择软硬件开发环境时,还得考虑自己的项目需求,接下来谈谈这个项目。最初的想法是做一个家用机器人,当然是可以自主移动的机器人,结合语音和视觉,实现语音控制,视觉识别交互导航等功能,因为个人觉得不能自主移动的机器人都不能叫机器人,另外,希望机器人能实现一般的NLP,识别情绪和语境,增强交互体验。想法总是远高于能力,随着项目的进展,原本期待的功能被一点点放弃或者降低要求。项目最终还是实现了大部分的功能,只是效果没有想象的理想,甚至相去甚远,这就是现实。后来我总结了一下原因,树莓派目前的性能不足于支持需要大数据和深度学习支持的算法,比如视觉导航和NLP等,树莓派提供的硬件支持远远不够,期待将来整合了AI芯片的开源硬件出来再试试。不过,我还是想把项目开发的过程整理出来,供大家参考。

好了,既然选择了树莓派,就先了解一下它是什么,能干嘛。

树莓派开发自己的智能语音系统

树莓派虽小,但五脏俱全和普通电脑无异,电脑能做的大部分事情,在树莓派上都能做,而树莓派以其低能耗、移动便携性、足够多的GPIO扩展性等特性,很多在普通电脑上难以做好的事情,用树莓派却可以轻松实现,只要你动起来,没有什么实现不了的。

看上图,小小的板子整合了四个标准USB接口,HDMI,AUDIO,40P,CSI视频接口,WIFI/有线网络接口,蓝牙接口等。非常丰富的接口提供了无限可能。配合一张TF卡,烧录上respi-debian系统就可以开始你的DIY之路了。

为了方便使用,我把原来的旧的DELL电脑改造了一下,塞进我的树莓派,改造显示屏,连上树莓派,就成了树莓派笔记本电脑了,只是原本的笔记本键盘改造起来比较麻烦,没找到合适的接口,只好外接一个键盘凑合使用。当然,你完全不需要像我这样折腾,树莓派可以通过VNC连接远程登录操控使用,也可以连接台式机的显示屏再外接键盘鼠标一样很方便。下图就是我改造好的树莓派笔记本。

树莓派开发自己的智能语音系统

介绍完主角,回到项目本身的需求,既然需要实现智能语音和视觉,就少不了语音输入输出设备和摄像头。为了省事,我直接使用了JABRA的speak710会议音箱,另外买了一个逻技的网络摄像头,这两个设备最好都使用有线连接,确保数据传输不容易出现问题,尽管树莓派提供了蓝牙接口,但不推荐。当然,你可以选择其它合适的音箱,最好是带阵列麦克风的,这样语音识别效果会好很多。摄像头当然高清的比较好了。

关于树莓派,就简单介绍这些,考虑到不是为了介绍树莓派而介绍,不会深入讲解,感兴趣的可以到网上搜索关于树莓派的资料深入了解。基于项目简单了解一下就行了,后面遇到问题再去找解决方案即可,不必担心。

接下来我将正式开始项目的搭建和开发。

二、开发环境搭建

开篇已经介绍了项目的大概需求,接下来讨论一下如何搭建项目的开发环境。树莓派系统raspbian烧录好之后已经内建了python2.7,不过建议安装python3版本,毕竟现在很多模块已经升级为3以上版本了。另外很重要的模块,语音模块pyaudion/mpg123/alsa(一般内建好了),和计算机视觉模块openCV,当然还有pyGPIO模块等,不过,没关系,还有很多模块需要安装,等具体到编程时可以随时添加,不必一次性搞定。

这里着重提一下关于openCV的安装,网络上很多关于这个模块的安装方法,但是很少有一次性成功的,我这里推荐使用以下链接的指引来一步步安装,如果你有足够耐心,基本上不会有问题。但如果你讨厌等待,你也可以直接使用openCV的简易安装,直接用pip安装简易版的就行,python-openCV。对于简单的视频获取和操作是可以胜任的,对于复杂应用还是推荐使用完整openCV模块。参考这个链接:

https://www.pyimagesearch.com/2017/09/04/raspbian-stretch-install-opencv-3-python-on-your-raspberry-pi/。作者Adrian Rosebrock很资深的计算机视觉专家,他还写了基本书专门介绍openCV的使用和应用开发,如果你有兴趣可以到网上搜出来学习学习。

树莓派开发自己的智能语音系统

接下来配置语音输入输出设备,树莓派默认的音频设备是HDMI或者3.5audio,我们需要作一些设置修改为USB音频输入输出设备,以便让我们的speaker正常工作。请按照以下步骤进行:

建议通过 .asoundrc 文件来配置麦克风和音响。

首先确保已接好麦克风和音响。

获得声卡编号和设备编号

之后查看当前已接入的所有录音设备:

arecord -l

得到的结果类似这样:

pi@pi:~ $ arecord -l

**** List of CAPTURE Hardware Devices ****

card 1: J710 [Jabra Speak 710], device 0: USB Audio [USB Audio]

Subdevices: 1/1

Subdevice #0: subdevice #0

上面的结果说明当前接入了一个录音设备,就是speak 710,选择你要使用的录音设备,并记下声卡编号(或名字)和设备编号。例如,我希望使用 Jabra Speak 710 这个设备,则声卡编号为 1 (声卡名为 J710),设备编号为 0 。

类似的方法获取音响的声卡编号和设备编号:

aplay -l

结果类似这样:

pi@pi:~ $ aplay -l

**** List of PLAYBACK Hardware Devices ****

card 0: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]

Subdevices: 7/7

Subdevice #0: subdevice #0

Subdevice #1: subdevice #1

Subdevice #2: subdevice #2

Subdevice #3: subdevice #3

Subdevice #4: subdevice #4

Subdevice #5: subdevice #5

Subdevice #6: subdevice #6

card 0: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]

Subdevices: 1/1

Subdevice #0: subdevice #0

card 1: J710 [Jabra Speak 710], device 0: USB Audio [USB Audio]

Subdevices: 1/1

Subdevice #0: subdevice #0

上面的结果说明当前接入了两个播放设备,其中 card 0 是树莓派自带的声卡,如果您是使用 AUX 3.5 口外接的音响/或耳机,那么应该使用 card 0;card 1则是其他的设备。记下您要使用的声卡编号和设备编号。这里我们使用card 1。

配置 .asoundrc

首先创建 /home/pi/.asoundrc :

touch /home/pi/.asoundrc

之后添加您选择的声卡编号和设备。这里举两种常见的配置。

第一种:您使用的是一个自带音响和录音的组合设备(例如会议麦克风喇叭,或者一块连接了麦克风和音响的独立USB声卡),那么只需设置 pcm 为该组合设备的编号即可。示例:

pcm.!default {

type plug slave {

pcm "hw:1,0"

}

}

ctl.!default {

type hw

card 1

}

上面的 hw:1,0 表示使用 card 1,设备 0。即 C-Media USB Headphone Set 。如果配成 hw:Set,0,效果相同(个人更推荐使用声卡名字)。

第二种:您使用的是一个单独的 USB 麦克风,并直接通过树莓派的 AUX 3.5 口外接一个音响。那么可以参考如下配置:

pcm.!default {

type asym

playback.pcm {

type plug

slave.pcm "hw:0,0"

}

capture.pcm {

type plug

slave.pcm "hw:2,0"

}

}

ctl.!default {

type hw

card 2

}

由于播放设备(playback)和录音设备(capture)是独立的,所以需要各自配置。

完成后可以测试下命令行录音和播放,看看是否能正常工作。

录音:

arecord -d 3 temp.wav

回放录音:

aplay temp.wav

音频设备配置这一步比较容易出问题,一定要认真一步步来,不要着急。测试成功之后,你的语音输入输出设备就可以正常工作了。

小小的建议,当主要的配置完成后,最好备份你的系统,树莓派系统在开发时容易出现一些问题,当你无法解决的时候,你需要重装系统,如果完全重头到尾来一遍,相信你会崩溃的。所以,当大的框架搭好后,养成备份的习惯是有必要的。

三、百度语音识别API

上一篇完成了软硬件环境的准备,这个章节将介绍如何借助免费的百度智能云平台API实现语音识别,会涉及一些代码,但是不多,主要是学习如何通过python和百度的API建立连接并返回识别结果,识别的过程不需要你操心,百度云帮你搞定就是了。类似的云平台还有很多,国内主要是百度,科大讯飞,阿里云,国外主要用google和亚马逊的alexa,中文识别的话当然推荐用国内的,如果你想弄一个英语聊天机器人,你最好用国外的。另外如果你想搭建聊天的机器人,推荐使用图灵机器人云平台,免费版的也提供了很多功能。闲话少说,进入正题。

树莓派开发自己的智能语音系统

第一步当然是先到云平台上注册,注册完之后你需要创建一个应用,系统会自动生成一个APIKey和SecretKey,如下图:

树莓派开发自己的智能语音系统

百度智能云API

这两个key是你连接百度云API的钥匙,所以一定得有。

第二步去图灵机器人网站注册一个账户,和百度云类似,注册后你一样会得到类似以下的东西:

树莓派开发自己的智能语音系统

好了,到目前为止,API注册完成,接下来就是如何去调用啦,是不是有点小激动^_^.

因为不是科班出身,在编程上没有那么正规,基本上想到什么写什么,没有认真规划程序的逻辑和架构,虽然没有系统的搭建,既然要写出来给大家看,好歹得整理一下实现的思路才行,不然很容易把大家搞晕,自己也会写晕。好吧,我大概整理了一下。

树莓派开发自己的智能语音系统

大概的程序架构,有点乱

大概的框架就是这样,因为这个章节是实现语音识别和对话,所以先看这个模块的程序构成,其中voice.py,voiceAPI.py属于百度提供的连接API的标准程序,只需要该一下上面的Key就行了,当然是改成你自己的。

为了便于后续调用,我把函数封装成Voice类,详细请看voice.py

# -*- coding: utf-8 -*-
"""
__autor__ : JERRY
"""
import sys
import os
#reload(sys)
#sys.setdefaultencoding("utf-8")
class Voice:
 def __init__(self):
 self.RECORD_PATH = r"./record_voice.wav"#定义录音的路径
 def recordVoice(self):
 print ("开始录音...")
 os.system('sudo arecord -D "plughw:1" -f S16_LE -r 16000 -d 4 %s'%self.RECORD_PATH)
 print ("录音结束...")
 
 def recordCMD(self):
 print ("开始录音...")
 os.system('sudo arecord -D "plughw:1" -f S16_LE -r 16000 -d 3 %s'%self.RECORD_PATH)
 print ("录音结束...")
 def playVoice(self,url): #播放声音
 #print url
 os.system('mpg123 "%s"'%url)#使用mplayer也可以确保系统安装了mpg123.
           

关于python类函数的调用,请读者自己度娘详情,相信很快了解怎么弄。这里不会详细介绍如何使用python。下面这个程序baiduAPI.py封装了BaiDuAPI类和TuLingAPI类:

# -*- coding: utf-8 -*-
"""
__autor__ : JERRY
"""
import sys
import requests
import json
import urllib2
import base64
import urllib
reload(sys)
sys.setdefaultencoding("utf-8")
class BaiDuAPI:
 def __init__(self):
 self.GRANT_TYPE = "client_credentials"
 self.CLIENT_ID = "你自己的key" #百度应用的 API Key
 self.CLIENT_SECRET = "你自己的secret" #百度应用的 API Secret
 self.TOKEN_URL = "https://openapi.baidu.com/oauth/2.0/token"
 self.RECOGNITION_URL = "http://vop.baidu.com/server_api"
 self.CUID = "b8-27-eb-be-eb-08"
 self.RECOGNITION_PATH = r"./record_voice.wav"
 self.SYNTHESIS_PATH = r"./play_voice.mp3"
 def getToken(self): #获取access_token
 body = {
 "grant_type":self.GRANT_TYPE,
 "client_id":self.CLIENT_ID,
 "client_secret":self.CLIENT_SECRET
 }
 r = requests.post(self.TOKEN_URL,data=body,verify=True)
 self.access_token = json.loads(r.text)["access_token"]
 return self.access_token
 def voiceRecognition(self): #语音识别
 erro_dict = {
 3300:"输入参数不正确",
 3301:"音频质量过差",
 3302:"鉴权失败",
 3303:"语音服务器后端问题",
 3304:"用户的请求QPS超限",
 3305:"用户的日pv(日请求量)超限",
 3307:"语音服务器后端识别出错问题",
 3308:"音频过长",
 3309:"音频数据问题",
 3310:"输入的音频文件过大",
 3311:"采样率rate参数不在选项里",
 3312:"音频格式format参数不在选项里"
 }
 f = open(self.RECOGNITION_PATH,"rb")
 voice_data = f.read()
 f.close()
 speech_data = base64.b64encode(voice_data).decode("utf-8")
 speech_length = len(voice_data)
 post_data = {
 "format": "wav",
 "rate": 16000,
 "channel": 1,
 "cuid": self.CUID,
 "token": self.access_token,
 "speech": speech_data,
 "len": speech_length
 }
 json_data = json.dumps(post_data).encode("utf-8")
 json_length = len(json_data)
 req = urllib2.Request(self.RECOGNITION_URL, data=json_data)
 req.add_header("Content-Type", "application/json")
 req.add_header("Content-Length", json_length)
 resp = urllib2.urlopen(req)
 resp = resp.read()
 resp_data = json.loads(resp.decode("utf-8"))
 try:
 recognition_result = resp_data["result"][0]
 print (recognition_result)
 return recognition_result
 except:
 print (erro_dict[resp_data["err_no"]])
 return False
 def voiceSynthesis(self,word): #语音合成
 token = self.access_token
 cuid = self.CUID
 word = urllib.quote(word.encode("utf8"))
 url = "http://tsn.baidu.com/text2audio?tex="+word+"&lan=zh&cuid="+cuid+"&ctp=1&tok="+token+"&per=4"
 #urllib.urlretrieve(url,self.SYNTHESIS_PATH)
 '''
 voice_data = urllib2.urlopen(url).read()
 voice_fp=open(filename,'wb+')
 voice_fp.write(voice_data)
 voice_fp.close()
 '''
 return url
class TurLingAPI:
 def __init__(self):
 self.Tuling_API_KEY = "你自己的key"
 self.URL = "http://www.tuling123.com/openapi/api"
 def turlingReply(self,word): #图灵获取回复
 body = {"key": self.Tuling_API_KEY,
 "info": word.encode("utf-8")}
 res = requests.post(self.URL, data=body, verify=True)
 if res:
 date = json.loads(res.text)
 print (date["text"])
 return date["text"]
 else:
 print ("对不起,未获取到回复信息")
 return False
           

以上程序除了key之外,不需要改动,直接调用类函数即可。你可以在树莓派上测试一下是否可以实现,以下是测试代码:

# -*- coding: utf-8 -*-
"""
__autor__ : Jerry zhong
"""
from voice import Voice
import voiceAPI
import sys,os
import random
#调用类
voice = Voice()
baiduAPI = voiceAPI.BaiDuAPI()
turlingAPI = voiceAPI.TurLingAPI()
baiduAPI.getToken()
#定义对话函数
def dialogue(text):
 url = baiduAPI.voiceSynthesis(text)
 voice.playVoice(url)
#定义一个聊天函数
def talk():
 while True:
 voice.recordVoice()#开始录音5秒
 try:
 recognition_result = baiduAPI.voiceRecognition()#返回识别结果
 except:
 print("connection issue happened.")
 continue
 #return recognition_result #返回识别结果 don't use it otherwise exit the while the following code will not execute.
 if recognition_result:#返回识别结果为真
 try:
 reply_result = turlingAPI.turlingReply(recognition_result)#调用图灵聊天机器人
 except:#处理网络异常
 print("connection issue happened.")
 continue
 if reply_result:#返回聊天结果为真时合成声音并播放
 url = baiduAPI.voiceSynthesis(reply_result)
 voice.playVoice(url)
 else:
 dialogue(random.choice(["嗯,不知道该怎么回答呢","超出我的认知范围了","我的大脑短路了,哎","你得耐心点,网络不好"]))
 else: #返回识别结果为假
 talk_num +=1 
 if talk_num >2: #超过3次无法获取应答,退出聊天
 break
 else:
 continue
 dialogue(random.choice(["有事再叫我!","嗯,我先闪了","嗯,下次再聊","再见咯"]))#如果超时没有回应退出
#直接调用运行主程序结束按ctr+c
talk()
           

如果你的程序不能运行,可能缺少笔必要的库,或者语法问题,自己仔细检查,编程就是如此,反复测试直到通过为止。当你发现识别和合成都没有问题时,恭喜你已经成功一半了。接下来我们要让她实现跟多的功能,不然只是和机器人尬聊感觉很傻。

四、语音唤醒

上一篇实现了语音交互功能,接下来给你的智能音箱取个名字,以便和你交流,就比如大家都有名字,一样你要让智能音箱知道是在和你交流,得有一个唤醒词,这个唤醒词很重要,智能音箱正常情况下是待机状态的,只要听到对应的语音唤醒之后才开启聊天功能,不然就静默待机以节约资源。

怎么选择合适的唤醒词-也就是名字,这里有些讲究,参考以下建议:1)音节覆盖广,最好覆盖不同音节(比如,“小爱同学”就是个好词),尽量避免只有元音音节的字,比如:阿;2)相似音节要规避,发音能清晰明亮;3)使用不常说的词,避免误唤醒。

网上有开源的唤醒词生成工具,这里推荐使用snowboy的,https://snowboy.kitt.ai。你可以使用github账户登录,登录后进入唤醒词创建页面,这里不详细介绍,按照上述原则,一步步往下走就行,

树莓派开发自己的智能语音系统

最后导出生成好的唤醒词,一般是 .pmdl结尾的文档,这个文档在后续程序中会用到。接下来到 Github网站下载python文件,

https://github.com/Kitt-AI/snowboy/tree/master/examples/Python3,其中snowboy开头的几个文件是必须的,确保这些文件和唤醒词文件在同一目录下。

为了便于调用,我这里对snowboydecoder_arecord.py做了些修改,最后面添加了两句用于跳出循环:

if callback is not None:
 callback()
 self.recording=False
 break
           

基本上源文件不需要做太多修改,接下来编写一个唤醒函数:

import snowboydecoder_arecord as sd
import sys
import signal
import os
interrupted = False
wakeup_status = False
def signal_handler(signal, frame):
 global interrupted
 interrupted = True
def interrupt_callback():
 global interrupted
 return interrupted
def Wakeup():
 model = "./小雅同学.pmdl" #你的唤醒词文件
 # capture SIGINT signal, e.g., Ctrl+C
 signal.signal(signal.SIGINT, signal_handler)
 detector = sd.HotwordDetector(model, sensitivity=0.5)
 
 print('Listening... Press Ctrl+C to exit')
 #os.close(sys.stderr.fileno())#ignore the error messages detector.start(detected_callback=sd.play_audio_file,interrupt_check=interrupt_callback,sleep_time=0.03)
 wakeup_status = True
 return wakeup_status #执行完返回唤醒状态
 detector.terminate()
           

基本上只需在demo程序上做简单修改就可以,所以没有详细注释代码。这个函数调用之后返回设备的唤醒状态,之后我会把wakeup()函数放在主程序的开始位置,开机后设备进入待唤醒状态,默认wakeup_status=False,当麦克风接收到唤醒词时,wakeup_statu返回True,智能音箱将执行后续程序代码。

好了,关于设备唤醒的介绍就这样,基本上直接借用snowboy的代码,你要的就是在demo例子中修改几行代码就行了,非常简单。

五、交互音乐播放器

继续折腾树莓派,上篇讲到语音唤醒,这就为下一步开发新的功能打下基础了。今天开始开发一个通过语音交互方式控制的音乐播放器。大概梳理一下这个播放器的实现过程。

树莓派开发自己的智能语音系统

首先这个播放器是一个网络播放器,利用百度音乐的API来实现的,目前这个接口是免费的,所以实现起来比较容易,当然你也可以尝试其它的音乐API接口,或者用一些爬虫技术来实现,但是可能不稳定,不过练练手还是可以的。百度音乐提供了详细的音乐分类列表,可以通过这个链接访问,

http://fm.baidu.com/dev/api/?tn=channellist 打开这个网页你会发现这是一个字典,提供了hash code, channellist,音乐频道列表,提取一个元素来分析一下,每个元素都是一个字典,比如:

{"channel_id":"public_tuijian_spring", #频道名称
"channel_name":"\u6f2b\u6b65\u6625\u5929", #utf-8编码格式的频道名称
 "channel_order":0, #频道序号
 "cate_id":"tuijian", #类别-推荐
 "cate":"\u63a8\u8350\u9891\u9053", #类别-utf-8格式
 "cate_order":1, #类别序号
 "source_type":1, #源类型
"source_id":0, #源识别号
"pv_order":11} #歌曲类别序号
           

好的,解读完这个channel_list,接下来就要通过python来解读,并实现歌曲的获取下载和播放了。直接上代码,代码已经做了详细的注释,阅读起来应该比较简单。

#-*-coding:utf-8-*-
import json
import threading
import re
import os
import urllib
import requests
import urllib2
import random
from voice import Voice
import voiceAPI
voice = Voice()
baiduAPI = voiceAPI.BaiDuAPI()
#turlingAPI = voiceAPI.TurLingAPI()
baiduAPI.getToken()
import socket
socket.setdefaulttimeout(10)
def CNNError():
 url = "audio/CNNerror.mp3" #语音播放连接错误提示音,自己录制一段现成的音频
 os.sys('mpg123 "%s"'%url) #调用mpg123程序播放音频文件
 return
def dialogue(text): #合成并播放一段话
 try:
 url = baiduAPI.voiceSynthesis(text)
 except:
 url1 = "audio/cnnerror.mp3"
 os.system('mpg123 "%s"'%url1)
 return
 voice.playVoice(url)
 
#http://fm.baidu.com/dev/api/?tn=channellist
def get_channel_list(page_url): #获取频道列表函数
 try:
 htmlDoc = urllib2.urlopen(page_url).read().decode('utf8')
 except:
 return {}
 with open("./channle.json", mode='w') as file:
 file.write(htmlDoc)
 
 file = open('channle.json')
 content = json.load(file)
 channel_list = content['channel_list']
 '''
 #output the channel_list:
 for channel in channel_list:
 print(channel['channel_name'])
 '''
 return channel_list
 
def get_song_list(channel_url): #获取对应频道的歌曲列表函数
 try:
 htmlDoc = urllib2.urlopen(channel_url).read().decode('utf8')
 except:
 return{}
 
 #with open("./songs.json", mode = 'w', encoding = 'utf-8') as file:# python3
 with open("./songs.json",mode = 'w') as file: #python2
 file.write(htmlDoc)
 
 file = open('songs.json')
 content = json.load(file)
 song_id_list = content['list']
 
 #for song in song_id_list:
 #print(song['id'])
 return song_id_list
 
def get_song_real_url(song_url): #获取特定歌曲的链接函数
 try:
 htmlDoc = urllib2.urlopen(song_url).read().decode('utf8')
 #print(htmlDoc)
 except:
 return(None, None, 0)
 
 with open("./song.json", mode = 'w') as file:
 file.write(htmlDoc)
 
 file = open('song.json')
 content = json.load(file)
 #print(content['data']['songList'])
 try:
 song_link = content['data']['songList'][0]['songLink']
 song_name = content['data']['songList'][0]['songName']
 song_size = int(content['data']['songList'][0]['size'])
 except:
 print('get real link failed')
 return(None, None, 0)
 
 #print(song_name + ':' + song_link)
 return song_name, song_link, song_size
 
def donwn_mp3_by_link(song_link, song_name, song_size): #通过歌曲链接下载歌曲
 #file_name = song_name + ".mp3"
 file_name = "song_temp.mp3" #save the song as temp
 base_dir = os.path.dirname(__file__)
 
 file_full_path = os.path.join(base_dir, file_name)
 if os.path.exists(file_full_path):
 return
 
 #print("begin DownLoad %s, size = %d" % (song_name, song_size))
 mp3 = urllib2.urlopen(song_link) 
 
 block_size = 8192
 down_loaded_size = 0
 #download_success = True
 
 file = open(file_full_path, "wb")
 while True:
 try:
 buffer = mp3.read(block_size)
 down_loaded_size += len(buffer)
 
 if(len(buffer) == 0):
 if down_loaded_size < song_size:
 if os.path.exists(file_full_path):
 os.remove(file_full_path)
 print('download time out, file deleted')
 with open('log.txt', 'a') as log_file:
 log_file.write("time out rm %s\n" % file_name)
 break
 
 #print('%s %d of %d' % (song_name, down_loaded_size, song_size))#show the progress of downloading
 file.write(buffer)
 
 if down_loaded_size >= song_size:
 print('%s download finshed' % file_full_path)
 #download_success = True
 break
 
 except:
 if os.path.getsize(file_full_path) < song_size:
 if os.path.exists(file_full_path):
 os.remove(file_full_path)
 print('download time out, file deleted')
 with open('log.txt', 'a') as log_file:
 log_file.write("time out rm %s\n" % file_name)
 #download_success = False
 break
 
 file.close()
 #return download_success
 
def downViaMutiThread(song_info_list): #通过多线程下载歌曲本例中未采用
 
 task_threads = [] #存储线程
 for song_name, song_link, song_size in song_info_list:
 t = threading.Thread(target = donwn_mp3_by_link, args = (song_link, song_name, song_size))
 task_threads.append(t)
 
 for task in task_threads:
 task.start()
 for task in task_threads:
 task.join()
# talk to robot to get the song what you like to listen.
def conversation(): #通过对话控制播放歌曲
 dialogue("需要我帮你推荐吗?")
 voice.recordVoice()
 try:
 recognition_result = baiduAPI.voiceRecognition()
 except:
 CNNError()
 #print(recognition_result)
 while recognition_result:
 if "推荐" in recognition_result:
 channel_selected = "public_tuijian_"+random.choice(["spring","autumn","winter","rege","ktv","billboard","chengmingqu","wangluo","kaiche","yingshi","suibiantingting"]) 
 return channel_selected
 else:
 dialogue("请告诉我你喜欢的类型,比如:风格,时光,心情,语种")
 voice.recordVoice()
 try:
 recognition_result = baiduAPI.voiceRecognition()
 except:
 CNNError()
 continue
 while recognition_result:
 if "经典" in recognition_result or "老歌" in recognition_result or "老哥" in recognition_result:
 channel_selected = "public_shiguang_"+"jingdianlaoge"
 return channel_selected
 elif "70后" in recognition_result or "七十" in recognition_result or "其实" in recognition_result or "七零" in recognition_result:
 channel_selected = "public_shiguang_"+"70hou"
 return channel_selected
 elif "80后" in recognition_result or "八十" in recognition_result or "八零" in recognition_result:
 channel_selected = "public_shiguang_"+"80hou"
 return channel_selected
 elif "90后" in recognition_result or "九十" in recognition_result or "九零" in recognition_result:
 channel_selected = "public_shiguang_"+"90hou"
 return channel_selected
 elif "儿歌" in recognition_result:
 channel_selected = "public_shiguang_"+"erge"
 return channel_selected
 elif "旅行" in recognition_result or "履行" in recognition_result:
 channel_selected = "public_shiguang_"+"lvxing"
 return channel_selected
 elif "夜店" in recognition_result:
 channel_selected = "public_shiguang_"+"yedian"
 return channel_selected
 elif "流行" in recognition_result:
 channel_selected = "public_fengge_"+"liuxing"
 return channel_selected
 elif "摇滚" in recognition_result:
 channel_selected = "public_fengge_"+"yaogun"
 return channel_selected
 elif "民谣" in recognition_result:
 channel_selected = "public_fengge_"+"minyao"
 return channel_selected
 elif "音乐" in recognition_result:
 channel_selected = "public_fengge_"+"qingyinyue"
 return channel_selected
 elif "小清新" in recognition_result:
 channel_selected = "public_fengge_"+"xiaoqingxin"
 return channel_selected
 elif "中国风" in recognition_result or "中国" in recognition_result:
 channel_selected = "public_fengge_"+"zhongguofeng"
 return channel_selected
 elif "无趣" in recognition_result or "舞曲" in recognition_result or "误区" in recognition_result:
 channel_selected = "public_fengge_"+"dj"
 return channel_selected
 elif "电影原声" in recognition_result or "电影" in recognition_result:
 channel_selected = "public_fengge_"+"dianyingyuansheng"
 return channel_selected
 elif "轻松假日" in recognition_result or "轻松" in recognition_result or "假日" in recognition_result:
 channel_selected = "public_xinqing_"+"qingsongjiari"
 return channel_selected
 elif "欢快" in recognition_result:
 channel_selected = "public_xinqing_"+"huankuai"
 return channel_selected
 elif "甜蜜" in recognition_result:
 channel_selected = "public_xinqing_"+"tianmi"
 return channel_selected
 elif "寂寞" in recognition_result:
 channel_selected = "public_xinqing_"+"jimo"
 return channel_selected
 elif "情歌" in recognition_result:
 channel_selected = "public_xinqing_"+"qingge"
 return channel_selected
 elif "舒缓" in recognition_result:
 channel_selected = "public_xinqing_"+"shuhuan"
 return channel_selected
 elif "慵懒午后" in recognition_result or "慵懒" in recognition_result or "午后" in recognition_result:
 channel_selected = "public_xinqing_"+"yonglanwuhou"
 return channel_selected
 elif "伤感" in recognition_result or "伤" in recognition_result:
 channel_selected = "public_xinqing_"+"shanggan"
 return channel_selected
 elif "华语" in recognition_result or "话语" in recognition_result or "花语" in recognition_result:
 channel_selected = "public_yuzhong_"+"huayu"
 return channel_selected
 elif "欧美" in recognition_result or "英语" in recognition_result:
 channel_selected = "public_yuzhong_"+"oumei"
 return channel_selected
 elif "日语" in recognition_result:
 channel_selected = "public_yuzhong_"+"riyu"
 return channel_selected
 elif "韩语" in recognition_result:
 channel_selected = "public_yuzhong_"+"hanyu"
 return channel_selected
 elif "粤语" in recognition_result or "业余" in recognition_result or "揶揄" in recognition_result:
 channel_selected = "public_yuzhong_"+"yueyu"
 return channel_selected
 else:
 channel_selected = "public_tuijian_"+random.choice(["spring","autumn","winter","rege","ktv","billboard","chengmingqu","wangluo","kaiche","yingshi","suibiantingting"]) 
 return channel_selected
 else:
 channel_selected = "public_tuijian_"+random.choice(["spring","autumn","winter","rege","ktv","billboard","chengmingqu","wangluo","kaiche","yingshi","suibiantingting"]) 
 return channel_selected
 
# to play music 主函数调用上面的函数实现
def Music(): 
 
 # 第一步,获取频道列表channel
 page_url = 'http://fm.baidu.com/dev/api/?tn=channellist'
 channel_list = get_channel_list(page_url)
	
 #channel_name_select = input("Please input the channel name: ")
 channel_name_select = conversation()
 print(channel_name_select)
 # 获取歌曲列表
 #channel_url = 'http://fm.baidu.com/dev/api/?tn=playlist&format=json&id=%s' % 'public_yuzhong_yueyu'
 channel_url = 'http://fm.baidu.com/dev/api/?tn=playlist&format=json&id=%s' % channel_name_select
 #print(channel_url)
 song_id_list = get_song_list(channel_url)
 print(song_id_list)
 song_id_sum = []
 for song_id in song_id_list:
 song_id_sum.append(song_id['id'])
 while True:
 song_choose = random.choice(song_id_sum)#randonly choose a song to download
 song_url = "http://music.baidu.com/data/music/fmlink?type=mp3&rate=320&songIds=%s" % song_choose
 song_name, song_link, song_size = get_song_real_url(song_url)
		
 if song_size != 0: 
 #single thread way
 #最后下载歌曲
 try:
 donwn_mp3_by_link(song_link, song_name, song_size)
 if os.path.exists('song_temp.mp3'):
 dialogue("这首歌的名字叫:"+song_name+",你想听吗?")
 voice.recordVoice()
 try:
 recognition_result = baiduAPI.voiceRecognition()
 except:
 CNNError()
 continue
 while recognition_result:
 if "不" in recognition_result or "换" in recognition_result:
 break
 elif "退出" in recognition_result:
 return
 else:
 os.system('mpg123 song_temp.mp3')#play the song.
 os.system('rm -f song_temp.mp3')#song completed then delete it.
 break
 else: 
 os.system('mpg123 song_temp.mp3')#play the song.
 os.system('rm -f song_temp.mp3')#song completed then delete it.
 #break # exit the function
 else:
 CNNError()
 break
 except: 
 CNNError()
 break
 else:
 continueyu
           

语音识别使用了一些容错处理,但是使用容错要小心,可能会出现误识别的情况。不过这种情况很难避免,确保80%的识别率就很好了。

代码的确长了一点,不过没有特别难理解的地方。暂时还没有想到好的办法实现中断,就是当歌曲播放过程中,如果你不想听了,想换首歌,这个时候用到多线程,目前程序中没有定义主线程和子线程的切换,所以这个功能暂时不能用,只能等播放的歌曲播完,才可以接收语音命令,播放过程中不接受指令。希望你能解决这个问题。

好了,简单的互动网络播放器基本搭建好了,只需要在主程序中调用这个主函数即可。下一篇将介绍新闻播报功能。

六、实现播报新闻

上一篇介绍如何利用百度音乐的API构建互动的音乐播放器,接下来我们继续增加播报新闻的功能,这个实现起来也不难,如果你对网络爬虫有所研究,应该还可以实现很多你想要实现的功能,你可以定制化你需要的信息,让智能音箱帮你播报给你听。

树莓派开发自己的智能语音系统

让资讯随时念给你听

基本的逻辑就是,编写一个爬虫函数,抓取百度新闻页面的新闻内容,返回文本文件给百度语音合成音频,然后调用播放器播放就是了。直接上代码:

import urllib2, urllib
#from urllib import request
import re
from voice import Voice
import voiceAPI
import random
voice = Voice()
baiduAPI = voiceAPI.BaiDuAPI()
baiduAPI.getToken()
def dialogue(text):
 try:
 url = baiduAPI.voiceSynthesis(text)
 except:
 url1 = "audio/cnnerror.mp3"
 os.system('mpg123 "%s"'%url1)
 return
 voice.playVoice(url)
def getNews_guonei(): #获取国内新闻
 url = "http://news.baidu.com/guonei"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news) #播放新闻
 #return news
def getNews_guoji(): #获取国际新闻
 url = "http://news.baidu.com/guoji"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
def getNews_mil(): #获取军事新闻
 url = "http://news.baidu.com/mil"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
 
def getNews_caijing(): #播报财经新闻
 url = "http://news.baidu.com/finance"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
def getNews_yule(): #播报娱乐新闻
 url = "http://news.baidu.com/ent"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
 
def getNews_tech(): #播报科技新闻
 url = "http://news.baidu.com/tech"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="instant-news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
 
def getNews_net(): #播报互联网新闻
 url = "http://news.baidu.com/internet"
 response = urllib2.urlopen(url)
 html = response.read().decode('utf-8')
 pattern_of_instant_news = re.compile('<div id="internet_news.*?</div>',re.S)
 instant_news_html = re.findall(pattern_of_instant_news,html)[0]
 pattern_of_news = re.compile('<li><a.*?>(.*?)</a></li>',re.S)
 news_list = re.findall(pattern_of_news,instant_news_html)
 for news in news_list:
 #print(news)
 dialogue(news)
           

只需要把播报新闻的函数放到主程序中调用即可。顺便也整一个笑话的爬虫,至于怎么让智能音箱播报给你听,自己开动脑筋,按照上面播报新闻的思路应该也很容易完成。

糗事爬虫代码清单:

# -*- coding:utf-8 -*-
import requests
from bs4 import BeautifulSoup
from itertools import *
import time

#伪装成浏览器
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = {'User-Agent': user_agent}

def get_url(start, end):
 #根据网址结构构造爬取多页的函数
 for num in (start, end):
 url = 'http://www.qiushibaike.com/text/page/'+str(num)+'/'
 get(url)
 time.sleep(3)

def get(url):
 #删选出搞笑数大于1000并且评论在十条以上的段子
 wb = requests.get(url,headers = headers)
 soup = BeautifulSoup(wb.text,'lxml')
 contents = soup.select('a > div[class="content"] > span')
 marks = soup.select('div > span > i')
 comments = soup.select('div > span > a > i')
 for content, mark, comment in zip(contents, marks, comments):
 if int(mark.get_text()) > 1000 and int(comment.get_text()) > 10:
 print (content.get_text())
           

关于新闻类的播报实现,就介绍到这里,回去自己动手比什么都有成就感。智能音箱的唤醒除了用语音唤醒之外,其实你可以通过传感器来唤醒,比如加一个定时器,到点自动播放你喜欢的节目,或者加上一个红外传感器,当人接近时,启动对话,等等,在树莓派上,你可以定制你自己喜欢的方式来交互,只有想不到,没有什么不可能的。

继续阅读