NOTE
由于訊飛API升級(jí),舊的語(yǔ)音合成接口已不可用包吝,新的實(shí)現(xiàn)方式直接扔github
了譬圣。
前言
為了能讓剛買(mǎi)的樹(shù)莓派有效的利用起來(lái)(避免吃灰)躺苦,今天分享一下如何用樹(shù)莓派做天氣鬧鐘。
環(huán)境及工具
樹(shù)莓派3B+产还、IDE匹厘、XShell、FileZilla(FTP文件上傳)脐区、小音箱愈诚。
查詢(xún)天氣
準(zhǔn)備
既然要做天氣鬧鐘,那肯定先要知道今天的天氣是什么坡椒,查詢(xún)天氣服務(wù)還是很大眾的一種服務(wù)扰路,很多網(wǎng)站都可以提供了查詢(xún)天氣的API接口,搜索一下倔叼。
做數(shù)據(jù)服務(wù)的網(wǎng)站就那么幾家汗唱,簡(jiǎn)單瀏覽之后,選擇了阿里云市場(chǎng)里的墨跡天氣API(免費(fèi)是重點(diǎn)丈攒,免費(fèi)的可用1000次哩罪,最近有0元/10000次的活動(dòng))。云市場(chǎng)-免費(fèi)版氣象服務(wù)(cityid)-墨跡天氣
這個(gè)API提供一個(gè)根據(jù)城市Id查詢(xún)?nèi)炀?jiǎn)天氣預(yù)報(bào)的接口巡验,深得我心际插,買(mǎi)。
購(gòu)買(mǎi)成功后显设,需要從
阿里云控制臺(tái)-產(chǎn)品與服務(wù)-API網(wǎng)關(guān)-調(diào)用API-已購(gòu)API
中查到請(qǐng)求Token框弛。詳細(xì)查詢(xún)過(guò)程
點(diǎn)擊操作中的詳情
可以看到Token, 之后需要點(diǎn)擊授權(quán)
按鈕,為你要調(diào)取的接口生成一個(gè)授權(quán)碼(阿里云API網(wǎng)關(guān)需要這個(gè))捕捂。
接口詳情
url: http://freecityid.market.alicloudapi.com/whapi/json/alicityweather/briefforecast3days
method: POST
body: {
"cityid": "城市id"瑟枫,
“token”: "API詳情頁(yè)查詢(xún)到的"
}
resp: {
"code": 0,
"data": {
"city": {
"cityId": 284609,
"counname": "中國(guó)",
"name": "東城區(qū)",
"pname": "北京市"
},
"forecast": [
{
"conditionDay": "多云",
"conditionIdDay": "1",
"conditionIdNight": "31",
"conditionNight": "多云",
"predictDate": "2016-09-01",
"tempDay": "27",
"tempNight": "18",
"updatetime": "2016-09-01 09:07:08",
"windDirDay": "西北風(fēng)",
"windDirNight": "西北風(fēng)",
"windLevelDay": "3",
"windLevelNight": "2"
},
...省略?xún)蓚€(gè)...
]
},
"msg": "success",
"rc": {
"c": 0,
"p": "success"
}
}
拿到接口,接下來(lái)肯定就是寫(xiě)代碼調(diào)用接口了指攒,調(diào)用過(guò)程中涉及的部分問(wèn)題慷妙,都寫(xiě)在代碼注釋里了(這里及后面的所有請(qǐng)求都是用的python的requests庫(kù),本身娛樂(lè)項(xiàng)目允悦,也就沒(méi)有生成requirement文件)膝擂。
Num2Word.py (將數(shù)字轉(zhuǎn)為中文字符串)
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import math
class Num2Word:
words = {
0: '零',
1: '一',
2: '二',
3: '三',
4: '四',
5: '五',
6: '六',
7: '七',
8: '八',
9: '九',
10: '十',
100: '百',
1000: '千',
10000: '萬(wàn)',
}
# TODO 1024 -> 一千二十四 ==> 1024 -> 一千零二十四
# TODO 1004 -> 一千四 ==> 1024 -> 一千零四
# TODO 1024.2 小數(shù)點(diǎn)
@staticmethod
def to_word(num):
if isinstance(num, int):
pass
elif isinstance(num, str):
num = int(num)
else:
raise TypeError('num must be int or str')
if num < 0:
return '負(fù)' + Num2Word.to_word(-num)
else:
quotient = num
remainder = 0
s = ""
ten_num = 0
while quotient > 0:
quotient = int(num / 10)
remainder = num % 10
if remainder > 0:
if ten_num > 0:
s = Num2Word.words[remainder] + Num2Word.words[int(math.pow(10, ten_num))] + s
else:
s = Num2Word.words[remainder] + s
num = int(num / 10)
ten_num += 1
return s
MoJiWeather.py
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import requests
import json
import logging
import sys
import os
from sys import path
from Num2Word import Num2Word
from VoicePlayer import VoicePlayer
from XunFeiTTS import XunFeiTTS
logging.basicConfig(
level=logging.DEBUG,
handlers=[logging.StreamHandler()],
format='%(levelname)s:%(asctime)s:%(message)s'
)
class RespBody():
# data = "{\"code\":0,\"data\":{\"city\":{\"cityId\":50,\"counname\":\"中國(guó)\",\"name\":\"閔行區(qū)\",\"pname\":\"上海市\(zhòng)",\"timezone\":\"8\"},\"forecast\":[{\"conditionDay\":\"多云\",\"conditionIdDay\":\"1\",\"conditionIdNight\":\"31\",\"conditionNight\":\"多云\",\"predictDate\":\"2018-10-17\",\"tempDay\":\"23\",\"tempNight\":\"14\",\"updatetime\":\"2018-10-17 22:09:00\",\"windDirDay\":\"北風(fēng)\",\"windDirNight\":\"北風(fēng)\",\"windLevelDay\":\"3-4\",\"windLevelNight\":\"3-4\"},{\"conditionDay\":\"多云\",\"conditionIdDay\":\"1\",\"conditionIdNight\":\"31\",\"conditionNight\":\"多云\",\"predictDate\":\"2018-10-18\",\"tempDay\":\"21\",\"tempNight\":\"12\",\"updatetime\":\"2018-10-17 22:09:00\",\"windDirDay\":\"北風(fēng)\",\"windDirNight\":\"北風(fēng)\",\"windLevelDay\":\"5-6\",\"windLevelNight\":\"3-4\"},{\"conditionDay\":\"多云\",\"conditionIdDay\":\"1\",\"conditionIdNight\":\"31\",\"conditionNight\":\"多云\",\"predictDate\":\"2018-10-19\",\"tempDay\":\"22\",\"tempNight\":\"13\",\"updatetime\":\"2018-10-17 22:09:00\",\"windDirDay\":\"東北風(fēng)\",\"windDirNight\":\"東北風(fēng)\",\"windLevelDay\":\"3-4\",\"windLevelNight\":\"3\"}]},\"msg\":\"success\",\"rc\":{\"c\":0,\"p\":\"success\"}}"
def __init__(self, d) -> None:
self.__dict__ = d
class Forecast():
def __init__(self, d) -> None:
self.prdict_date = d.predictDate # yyyy-MM-dd
self.update_time = d.updatetime # yyyy-MM-dd HH:mm:ss
self.condition_day = d.conditionDay # 多云
self.condition_night = d.conditionNight # 多云
self.temp_day = d.tempDay # 23
self.temp_night = d.tempNight # 14
self.wind_dir_day = d.windDirDay # 北風(fēng)
self.wind_dir_night = d.windDirNight # 北風(fēng)
self.wind_level_day = d.windLevelDay # 3-4
self.wind_level_night = d.windLevelNight # 4
def wind_level_to_word(self, wind_level):
wind_level = str(wind_level)
if not wind_level.__contains__('-'):
return Num2Word.to_word(wind_level)
return Num2Word.to_word(wind_level.split('-')[0]) + '至' + Num2Word.to_word(wind_level.split('-')[1])
def to_chinese(self):
# date轉(zhuǎn)為文字
month = self.prdict_date.split('-')[1]
day = self.prdict_date.split('-')[2]
date_word = Num2Word.to_word(month) + '月' + Num2Word.to_word(day) + '日'
return "%s, 白天天氣%s, 溫度%s度, %s%s級(jí), 夜間天氣%s, 溫度%s度, %s%s級(jí)" % \
(date_word, self.condition_day, Num2Word.to_word(self.temp_day), self.wind_dir_day, self.wind_level_to_word(self.wind_level_day),
self.condition_night, Num2Word.to_word(self.temp_night), self.wind_dir_night, self.wind_level_to_word(self.wind_level_night))
class MoJiWeather():
def __init__(self) -> None:
self.config = {
"baseURL": "http://freecityid.market.alicloudapi.com",
"forecastURL": "/whapi/json/alicityweather/briefforecast3days",
"AppCode": "阿里云的授權(quán)碼",
"headers": {
"Host":"freecityid.market.alicloudapi.com",
"gateway_channel":"http",
"Content-Type":"application/x-www-form-urlencoded; charset=utf-8",
"Authorization":"APPCODE 阿里云的授權(quán)碼"
},
"token": "墨跡天氣token"
}
self.city_codes = {
"BeiJing": "2",
"ShangHaiMinHang": "50" # 國(guó)內(nèi)城市地區(qū)id見(jiàn)末尾附錄
}
def fetch_forecast(self, cityId):
req_body = {
"cityId": str(cityId),
"token": self.config["token"]
}
json_str = json.dumps(req_body)
url = self.config["baseURL"] + self.config["forecastURL"]
# print(url)
# print(self.config["headers"])
resp = requests.post(url=url, data=req_body, headers=self.config["headers"])
resp_json = resp.content.decode('utf8')
# print(resp_json)
# print(resp.headers)
logging.debug("[MoJiWeather.fetch_forecast] - status = %s" % resp.status_code)
logging.debug("[MoJiWeather.fetch_forecast] - resp json = %s" % resp_json)
resp_body = json.loads(resp_json, object_hook=RespBody)
code = resp_body.code
if code == 0:
data = resp_body.data
city = data.city
province_name = city.pname
city_name = city.name
logging.info("[MoJiWeather.fetch_forecast] - %s, %s" % (province_name, city_name))
three_days_forecast_list = data.forecast
return three_days_forecast_list
else:
logging.info("[MoJiWeather.fetch_forecast] - Resp Not Success")
return []
現(xiàn)在可以測(cè)試一下代碼運(yùn)行效果了,MojiWeather.fetch_forecast
方法返回的是天氣預(yù)報(bào)數(shù)組(字典數(shù)組)隙弛。為了方便測(cè)試架馋,就直接在MoJiWeather
中創(chuàng)建一個(gè)main方法來(lái)獲取數(shù)據(jù)。
if __name__ == '__main__':
mo_ji_weather = MoJiWeather()
# 天氣預(yù)報(bào)的數(shù)組
forecast_list = mo_ji_weather.fetch_forecast(mo_ji_weather.city_codes["ShangHaiMinHang"])
print(forecast_list)
forecast_words = []
for forecast in forecast_list:
# 將dict轉(zhuǎn)為Forecast對(duì)象
f = Forecast(forecast)
# 將天氣預(yù)報(bào)轉(zhuǎn)為中文文字
forecast_words.append(f.to_chinese())
print(f.to_chinese())
# 將三個(gè)預(yù)報(bào)文本拼接成一個(gè)字符串
s = ",".join(forecast_words)
運(yùn)行結(jié)果
語(yǔ)音合成
到目前為止全闷,我們已經(jīng)能夠拿到最近三天內(nèi)的天氣預(yù)報(bào)了绩蜻,既然是做天氣鬧鐘,那就要讓程序會(huì)“說(shuō)話”室埋,也就是把文字轉(zhuǎn)為語(yǔ)音(語(yǔ)音合成)办绝。國(guó)內(nèi)做語(yǔ)音合成伊约,第一個(gè)想到的就是訊飛了,而且訊飛語(yǔ)音合成也有免費(fèi)版的(每日500次限額孕蝉,只有一個(gè)發(fā)音人可選)屡律,訊飛TTS介紹頁(yè)也可以體驗(yàn)語(yǔ)音合成,經(jīng)測(cè)試降淮,訊飛的TTS還是挺清晰的超埋。
然后按照下面的步驟去獲取訊飛的API-KEY,完成準(zhǔn)備工作佳鳖。
剩下的工作就是按文檔下代碼
XunFeiTTS.py
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import hashlib
import base64
import time
import json
import requests
import os
import logging
logging.basicConfig(
level=logging.DEBUG,
handlers=[logging.StreamHandler()],
format='%(levelname)s:%(asctime)s:%(message)s'
)
class XunFeiTTS:
def __init__(self) -> None:
self.app_id = "訊飛App id" # 訊飛的應(yīng)用id
self.app_key = "訊飛TOKEN" # 訊飛的token
self.tts_url = "http://api.xfyun.cn/v1/service/v1/tts"
def __gen_sig(self, req_params_base64, time_now):
"""
授權(quán)認(rèn)證霍殴,生成認(rèn)證信息
:param req_params_base64: 請(qǐng)求參數(shù)的base64串
:param time_now: 當(dāng)前時(shí)間
:return:
"""
s = self.app_key + time_now + req_params_base64
hl = hashlib.md5()
hl.update(s.encode(encoding='utf8'))
return hl.hexdigest()
def __gen_req_header(self, time_now, req_params_base64, sig):
"""
生成請(qǐng)求頭
:param time_now: 當(dāng)前時(shí)間
:param req_params_base64: 請(qǐng)求參數(shù)的base64串
:param sig:
:return:
"""
header = {
"X-Appid": self.app_id,
"X-CurTime": time_now,
"X-Param": req_params_base64,
"X-CheckSum": sig,
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
return header
def fetch_voice(self, text):
"""
根據(jù)傳入text生成語(yǔ)音
:param text:
:return:
"""
req_params = {
"auf": "audio/L16;rate=16000",
"aue": "raw", # 返回的語(yǔ)音格式 raw為wav格式語(yǔ)音, lame為MP3格式語(yǔ)音
"voice_name": "xiaoyan",
"speed": "50",
"volume": "50",
"pitch": "50",
"engine_type": "intp65",
"text_type": "text",
"text": text + " 噻"
}
time_now = str(time.time()).split('.')[0]
req_params_json = json.dumps(req_params)
req_params_base64 = str(base64.b64encode(req_params_json.encode('ascii')).decode('ascii'))
header = self.__gen_req_header(time_now, req_params_base64, self.__gen_sig(req_params_base64, time_now))
resp = requests.post(url=self.tts_url, data=req_params, headers=header)
content_type = resp.headers['Content-type']
# 請(qǐng)求成功時(shí), contentType為audio.mpeg, 失敗時(shí)系吩,contentType為text/plain, 返回異常信息
if content_type == 'audio/mpeg':
# 將語(yǔ)音寫(xiě)入文件voice.wav
f = open('voice.wav', 'wb')
f.write(resp.content)
f.close()
logging.info("[XunFeiTTS.fetch_voice] - Fetch Voice Success! Save As %s" % f.name)
else:
resp_json = resp.content.decode('utf-8')
logging.info("[XunFeiTTs.fetch_voice] - %s" % resp_json)
resp_dict = json.loads(resp_json)
logging.error("[XunFeiTTS.fetch_voice] - ErrCode = %s, Desc = %s" % (resp_dict['code'], resp_dict['desc']))
現(xiàn)在我們需要重新修改MoJiWeather中的main方法来庭,調(diào)用訊飛TTS將天氣預(yù)報(bào)的字符串轉(zhuǎn)變?yōu)檎Z(yǔ)音。
接下來(lái)就是使用pyaudio庫(kù)來(lái)播放天氣預(yù)報(bào)語(yǔ)音(python上有很多庫(kù)可以播放音頻穿挨,試了幾個(gè)之后月弛,感覺(jué)還是pyaudio更適合這個(gè)例子)。
如果在樹(shù)莓派上使用pip安裝pyaudio時(shí)出現(xiàn)
Pyaudio installation error - 'command 'gcc' failed with exit status 1'
錯(cuò)誤科盛,請(qǐng)?jiān)跇?shù)莓派上執(zhí)行下面的命令
sudo apt-get install python-dev
sudo apt-get install portaudio19-dev
sudo apt-get install libportaudio0 libportaudio2 libportaudiocpp0 portaudio19-dev
pip3 install pyaudio
播放wav語(yǔ)音的工具類(lèi)
VoicePlayer.py
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import pyaudio
import wave
import os
import logging
logging.basicConfig(
level=logging.DEBUG,
handlers=[logging.StreamHandler()],
format='%(levelname)s:%(asctime)s:%(message)s'
)
class VoicePlayer:
def __init__(self) -> None:
self.chunk = 1024
def play(self, filename):
logging.debug("[VoicePlayer.play] - load file %s" % filename)
chunk = 1024
wf = wave.open('voice.wav', 'rb')
p = pyaudio.PyAudio()
stream = p.open(
format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(chunk)
while data != '':
stream.write(data)
data = wf.readframes(chunk)
if data == b'':
break
stream.close()
p.terminate()
logging.debug("[VoicePlayer.play] - Voice Play Finish")
整合
到這里帽衙,就可以把上面的代碼整合到一起了,這里要注意一下Python的包引入問(wèn)題贞绵,在MoJiWeather.py
中會(huì)引入Num2Word.py
,VoicePlayer.py
,XunFeiTTS.py
厉萝,在運(yùn)行程序時(shí)請(qǐng)保證這幾個(gè)文件都在同一目錄下
MoJiWeather.py
if __name__ == '__main__':
mo_ji_weather = MoJiWeather()
forecast_list = mo_ji_weather.fetch_forecast(mo_ji_weather.city_codes["ShangHaiMinHang"])
print(forecast_list)
xun_fei_tts = XunFeiTTS()
s = ""
forecast_words = []
for forecast in forecast_list:
f = Forecast(forecast)
forecast_words.append(f.to_chinese())
print(f.to_chinese())
s = ",".join(forecast_words)
logging.debug("[MojiWeather.main] - %s" % s)
xun_fei_tts.fetch_voice(s)
voice_player = VoicePlayer()
voice_player.play('voice.wav')
部署
程序編寫(xiě)完成后,將代碼通過(guò)FileZilla上傳工具把代碼上傳到樹(shù)莓派≌ケ溃現(xiàn)在距離天氣鬧鐘只差最后一步(鬧鐘)冀泻。借助linux的crond定時(shí)任務(wù)就可以很容易的實(shí)現(xiàn)鬧鐘這一功能。在樹(shù)莓派上執(zhí)行crontab -e
就可以編輯crond任務(wù)了蜡饵,第一次打開(kāi)時(shí)會(huì)提示讓你選擇一個(gè)編輯器,按個(gè)人喜好選擇即可胳施。
crond 配置說(shuō)明使用
crontab -e
命令時(shí)要注意不要手滑按成crontab -r
(這兩個(gè)鍵挨著很近)溯祸,后者是【清空corntab配置】。
實(shí)際效果
左邊的就是淘寶上幾十塊買(mǎi)的小音響舞肆,分藍(lán)牙和有線兩種鏈接模式焦辅,顏值還是很高的。文章里的代碼貼的比較多(娛樂(lè)項(xiàng)目椿胯,代碼寫(xiě)的比較糟筷登,想吐槽就吐吧orz),請(qǐng)各位看客海涵哩盲。