天天看點

樹莓派開發自己的智能語音系統

作者: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())
           

關于新聞類的播報實作,就介紹到這裡,回去自己動手比什麼都有成就感。智能音箱的喚醒除了用語音喚醒之外,其實你可以通過傳感器來喚醒,比如加一個定時器,到點自動播放你喜歡的節目,或者加上一個紅外傳感器,當人接近時,啟動對話,等等,在樹莓派上,你可以定制你自己喜歡的方式來互動,隻有想不到,沒有什麼不可能的。

繼續閱讀