Node + 訊飛語音 定時(shí)播放天氣預(yù)報(bào)音頻
前言
最近看了幾篇文章,總覺得自己沒發(fā)揮樹莓派的作用,于是就琢磨著,哎,靈光一閃,整一個(gè)早晨叫醒服務(wù),于是便有了本篇水文。
功能
每天早上八點(diǎn)鐘,定時(shí)播放音頻(音頻內(nèi)容為當(dāng)天天氣預(yù)報(bào)和空氣質(zhì)量),播放完成之后繼續(xù)等待到明天的八點(diǎn)鐘播放。
效果如下
技術(shù)
開始本來是想加個(gè)客戶端的,但是一想先先直接跑個(gè)服務(wù)就用的node試試,所以本文只需要你會(huì)用js就行
node(服務(wù)) 訊飛語音(轉(zhuǎn)音頻) play(播放語言) 聚合api(天氣預(yù)報(bào)接口) scheduleJob(定時(shí)任務(wù))
準(zhǔn)備工作
我們需要文字識(shí)別轉(zhuǎn)成音頻,訊飛是可以白嫖的
訊飛語音
請(qǐng)先在訊飛注冊(cè)賬號(hào)及 > 創(chuàng)建應(yīng)用 > 實(shí)名認(rèn)證
https://passport.xfyun.cn/
然后去控制臺(tái)找到服務(wù)接口認(rèn)證信息
https://console.xfyun.cn/services/tts
找到需要的 key

聚合數(shù)據(jù)
天氣預(yù)報(bào)api,如果有你其他的當(dāng)然也可以用其他的,這個(gè)也是白嫖
就不做重點(diǎn)講了
官網(wǎng) https://www.juhe.cn/
天氣預(yù)報(bào) https://www.juhe.cn/docs/api/id/73
快速上手
初始化項(xiàng)目
npm init -y
然后安裝我們需要的依賴, 下面是依賴的版本
{
"name": "node-jiaoxing",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"dev": "node src/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"node-schedule": "^2.0.0",
"request": "^2.88.2",
"websocket": "^1.0.31"
}
}
安裝依賴
npm insatll
創(chuàng)建lib文件夾
創(chuàng)建一個(gè)
lib文件夾,放入別人封裝好的訊飛的資源,其主要作用就是將文本轉(zhuǎn)為音頻
index.js
const fs = require("fs");
const WebSocketClient = require('websocket').client;
const { getWssInfo, textToJson } = require('./util');
const xunfeiTTS = (auth, business, text, fileName, cb) => {
let audioData = [];
const client = new WebSocketClient();
client.on('connect', (con) => {
con.on('error', error => {
throw (error);
});
con.on('close', () => {
const buffer = Buffer.concat(audioData);
fs.writeFile(fileName, buffer, (err) => {
if (err) {
throw (err);
cb(err);
} else {
cb(null, 'OK');
}
});
})
con.on('message', (message) => {
if (message.type == 'utf8') {
const ret = JSON.parse(message.utf8Data);
audioData.push(Buffer.from(ret.data.audio, 'base64'));
if (ret.data.status == 2) {
con.close();
}
}
});
if (con.connected) {
const thejson = textToJson(auth, business, text);
con.sendUTF(thejson);
}
});
client.on('connectFailed', error => {
throw (error);
});
const info = getWssInfo(auth);
client.connect(info.url);
}
module.exports = xunfeiTTS;
util.js
function btoa(text) {
return Buffer.from(text, "utf8").toString("base64");
}
function getWssInfo(auth, path = "/v2/tts", host = "tts-api.xfyun.cn") {
const { app_skey, app_akey } = auth;
const date = new Date(Date.now()).toUTCString();
const request_line = `GET ${path} HTTP/1.1`;
const signature_origin = `host: ${host}\ndate: ${date}\n${request_line}`;
let crypto = require("crypto");
const signature = crypto
.createHmac("SHA256", app_skey)
.update(signature_origin)
.digest("base64");
const authorization_origin = `api_key="${app_akey}",algorithm="hmac-sha256",headers="host date request-line",signature="${signature}"`;
const authorization = btoa(authorization_origin);
const thepath = `${path}?authorization=${encodeURIComponent(
authorization
)}&host=${encodeURIComponent(host)}&date=${encodeURIComponent(date)}`;
const final_url = `wss://${host}${thepath}`;
return { url: final_url, host: host, path: thepath };
}
function textToJson(auth, businessInfo, text) {
const common = { app_id: auth.app_id };
const business = {};
business.aue = "raw";
business.sfl = 1;
business.auf = "audio/L16;rate=16000";
business.vcn = "xiaoyan";
business.tte = "UTF8";
business.speed = 50;
Object.assign(business, businessInfo);
const data = { text: btoa(text), status: 2 };
return JSON.stringify({ common, business, data });
}
module.exports = {
btoa,
getWssInfo,
textToJson
};
創(chuàng)建src文件夾
主入口文件
index.js
// 引入路徑模塊
const path = require("path");
const { promisify } = require("util");
// 訊飛TTS
const xunfeiTTS = require("../lib/index");
const tts = promisify(xunfeiTTS);
// 轉(zhuǎn)換音頻
const openGreetings = async (app_id, app_skey, app_akey, text) => {
const auth = { app_id, app_skey, app_akey };
// 訊飛 api 參數(shù)配置
// 接口文檔 https://www.xfyun.cn/doc/tts/online_tts/API.html
const business = {
aue: "lame", // 音頻編碼
sfl: 1, // 開啟流式返回
speed: 50,// 語速
pitch: 50, // 高音
volume: 100,// 音量
bgs: 0 // 背景音樂
};
// 存儲(chǔ)文件的路徑
const file = path.resolve('./src/good-morning.wav');
try {
// 執(zhí)行請(qǐng)求
await tts(auth, business, text, file).then(res => {});
} catch (e) {
console.log("test exception", e);
}
};
openGreetings('訊飛的APPID', '訊飛的APISecret', '訊飛的APIKey', '早上好,帥氣的嚴(yán)老濕')
測試一波
執(zhí)行 npm run dev 可以看到,執(zhí)行完成之后,已經(jīng)在src目錄下面創(chuàng)建了 good-morning.wav 音頻

戴好耳機(jī),迫不及待的打開音頻,傳來早上好,帥氣的嚴(yán)老濕
我們到這里就已經(jīng)完成了訊飛語音的接入
問候語修改
我們不可能一直是早上好吧
所以我們需要根據(jù)系統(tǒng)的時(shí)間來
const greetings = {
"7, 10": ["早上好", "上午"],
"11,13": ["中午好", "中午"],
"14,17": ["下午好", "下午"],
"18,23": ["晚上好", "晚上"],
}
const getTimeInfo = () => {
const TIME = new Date()
// 年月日
let year = TIME.getFullYear()
let month = TIME.getMonth() + 1
let date = TIME.getDate()
// 時(shí)分
let hours = TIME.getHours()
let minutes = TIME.getMinutes()
// 生成的問候文本
let greetingsStr = ""
// 遍歷定義的問候數(shù)據(jù)
for (const key in greetings) {
if(hours >= key.split(",")[0] && hours <= key.split(",")[1]) {
let greetingsKey = greetings[key]
greetingsStr = `${greetingsKey[0]},現(xiàn)在是${greetingsKey[1]},${hours}點(diǎn),${minutes}分`
}
}
// 中午好,現(xiàn)在是中午12點(diǎn),12分,今天是2021年,8月,28日
return `${greetingsStr},今天是${year}年,${month}月,${date}日`
}
現(xiàn)在我們拿到的數(shù)據(jù)就是中午好,現(xiàn)在是中午12點(diǎn),12分,今天是2021年,8月,28日
在執(zhí)行轉(zhuǎn)換音頻的時(shí)候,我們可以動(dòng)態(tài)調(diào)用 getTimeInfo 拿到當(dāng)前的文本傳遞過去轉(zhuǎn)成音頻
openGreetings('訊飛的APPID', '訊飛的APISecret', '訊飛的APIKey', getTimeInfo())
自動(dòng)播放
當(dāng)我創(chuàng)建好音頻后,我希望立刻播放
引入play資源
所以我們需要使用一個(gè)庫 play , 老嚴(yán)也是直接拿資源,然后放入lib文件夾中,叫play.js
if(typeof exports === 'undefined'){
var play = {
sound: function ( wav ) {
debug.log(wav);
var e = $('#' + wav);
debug.log(e);
$('#alarm').remove();
$(e).attr('autostart',true);
$('body').append(e);
return wav;
}
};
}
else{
var colors = require('colors'),
child_p = require('child_process'),
exec = child_p.exec,
spawn = child_p.spawn,
ee = require('events'),
util = require('util');
var Play = exports.Play = function Play() {
var self = this;
if (!(this instanceof Play)) {
return new Play();
}
ee.EventEmitter.call(this);
this.playerList = [
'afplay',
'mplayer',
'mpg123',
'mpg321',
'play',
];
this.playerName = false;
this.checked = 0;
var i = 0, child;
for (i = 0, l = this.playerList.length; i < l; i++) {
if (!this.playerName) {
(function inner (name) {
child = exec(name, function (error, stdout, stderr) {
self.checked++;
if (!self.playerName && (error === null || error.code !== 127 )) {
self.playerName = name;
self.emit('checked');
return;
}
if (name === self.playerList[self.playerList.length-1]) {
self.emit('checked');
}
});
})(this.playerList[i]);
}
else {
break;
}
}
};
util.inherits(Play, ee.EventEmitter);
Play.prototype.usePlayer = function usePlayer (name) {
this.playerName = name;
}
Play.prototype.sound = function sound (file, callback) {
var callback = callback || function () {};
var self = this;
if (!this.playerName && this.checked !== this.playerList.length) {
this.on('checked', function () {
self.sound.call(self, file, callback);
});
return false;
}
if (!this.playerName && this.checked === this.playerList.length) {
console.log('No suitable audio player could be found - exiting.'.red);
console.log('If you know other cmd line music player than these:'.red, this.playerList);
console.log('You can tell us, and will add them (or you can add them yourself)'.red);
this.emit('error', new Error('No Suitable Player Exists'.red, this.playerList));
return false;
}
var command = [file],
child = this.player = spawn(this.playerName, command);
console.log('playing'.magenta + '=>'.yellow + file.cyan);
child.on('exit', function (code, signal) {
if(code == null || signal != null || code === 1) {
console.log('couldnt play, had an error ' + '[code: '+ code + '] ' + '[signal: ' + signal + '] :' + this.playerName.cyan);
this.emit('error', code, signal);
}
else if ( code == 127 ) {
console.log( self.playerName.cyan + ' doesn\'t exist!'.red );
this.emit('error', code, signal);
}
else if (code == 2) {
console.log(file.cyan + '=>'.yellow + 'could not be read by your player:'.red + self.playerName.cyan)
this.emit('error', code, signal);
}
else if (code == 0) {
console.log( 'completed'.green + '=>'.yellow + file.magenta);
callback();
}
else {
console.log( self.playerName.cyan + ' has an odd error with '.yellow + file.cyan);
console.log(arguments);
emit('error');
}
});
this.emit('play', true);
return true;
}
}
使用play
在 src/ index.js 文件中引入
// 播放器
const play = require('../lib/play').Play();
在 openGreetings 中執(zhí)行轉(zhuǎn)換音頻完成之后播放文件 play.sound(file);
const openGreetings = async (app_id, app_skey, app_akey, text) => {
const auth = { app_id, app_skey, app_akey };
const business = {
aue: "lame",
sfl: 1,
speed: 50,
pitch: 50,
volume: 100,
bgs: 0
};
const file = path.resolve("./src/good-morning.wav");
try {
await tts(auth, business, text, file).then(res => {
// 執(zhí)行播放
play.sound(file);
});
} catch (e) {
console.log("test exception", e);
}
};
測試一下
執(zhí)行 npm run dev
執(zhí)行開始,先去轉(zhuǎn)換音頻,然后自動(dòng)開始播放

加需求
播放完成之后,我還需要播放一段我喜歡的音樂
這讓我很為難啊,得加錢!“加個(gè)毛,play.js 有播放完成的 callback”
play.sound(file, function(){
// 上一個(gè)播放完成之后,我們開始播放一首《小雞小雞》-王蓉
play.sound('./src/xiaojixiaoji.m4a')
});
這個(gè)音樂資源應(yīng)該不用教學(xué)了吧,隨便去扒拉兩首自己喜歡的歌,常見的格式都可以 m4a,mp3,wav等等
有條件的同學(xué) 可以找聲音甜美的妹子,錄一個(gè)喊你起床的音頻,放在播放問候語之前,效果更佳
天氣預(yù)報(bào)
天氣預(yù)報(bào),我這里是用的聚合數(shù)據(jù)的,當(dāng)然如果你有其他的天氣預(yù)報(bào) api 也可以,老嚴(yán)這里只是做個(gè)簡單的示例
// 請(qǐng)求
const request = require('request');
然后調(diào)用
// 獲取天氣的城市
const city = "長沙"
let text
request(`http://apis.juhe.cn/simpleWeather/query?city=${encodeURI(city)}&key=聚合數(shù)據(jù)key`,
(err, response, body) => {
if (!err && response.statusCode == 200){
let res = JSON.parse(body).result.realtime
text = `
${getTimeInfo()},
接下來為您播報(bào)${city}實(shí)時(shí)天氣預(yù)報(bào),
今天,長沙天氣為${res.info}天,
室外溫度為${res.temperature}度,
室外濕度為百分之${res.humidity},
${res.direct},
${res.power},
天氣預(yù)報(bào)播放完畢,
接下來播放您喜歡的音樂
`
openGreetings('訊飛的APPID', '訊飛的APISecret', '訊飛的APIKey', text)
}
}
)
拿到的文本就是這樣的
中午好,現(xiàn)在是中午,12點(diǎn),27分,今天是2021年,8月,28日,
接下來為您播報(bào)長沙實(shí)時(shí)天氣預(yù)報(bào),
今天,長沙天氣為晴天,
室外溫度為27度,
室外濕度為百分之76,
南風(fēng),
3級(jí),
天氣預(yù)報(bào)播放完畢,
接下來播放您喜歡的音樂
定時(shí)任務(wù)
為什么要定時(shí)任務(wù),因?yàn)槲覀兊男枨笫?,每天早上八點(diǎn)鐘播放,所以我們用到了 schedule
schedule 之前也有講過,在幾個(gè)月前的《Node.js之自動(dòng)發(fā)送郵件 | 僅二十行代碼即可》郵件中也提到過
因?yàn)槲覀冊(cè)谇懊嬉呀?jīng)下載了,我們只需要引入就好了
引入
const schedule = require('node-schedule');
使用
// 定時(shí)每天8點(diǎn)0分0秒執(zhí)行
schedule.scheduleJob('0 0 8 * * *', ()=>{
request(`http://apis.juhe.cn/simpleWeather/query?city=${encodeURI(city)}&key=聚合數(shù)據(jù)key`,
(err, response, body) => {
if (!err && response.statusCode == 200){
let res = JSON.parse(body).result.realtime
text = `
${getTimeInfo()},
接下來為您播報(bào)${city}實(shí)時(shí)天氣預(yù)報(bào),
今天,長沙天氣為${res.info}天,
室外溫度為${res.temperature}度,
室外濕度為百分之${res.humidity},
${res.direct},
${res.power},
天氣預(yù)報(bào)播放完畢,
接下來播放您喜歡的音樂
`
openGreetings('訊飛的APPID', '訊飛的APISecret', '訊飛的APIKey', text)
}
}
)
});
貼上index.js 所有代碼
// 引入路徑模塊
const path = require("path");
const { promisify } = require("util");
// 訊飛TTS
const xunfeiTTS = require("../lib/index");
const tts = promisify(xunfeiTTS);
// 播放器
const play = require("../lib/play").Play();
// 請(qǐng)求
const request = require("request");
// 定時(shí)任務(wù)
const schedule = require('node-schedule');
// 問候語及時(shí)間
const greetings = {
"7, 10": ["早上好", "上午"],
"11,13": ["中午好", "中午"],
"14,17": ["下午好", "下午"],
"18,23": ["晚上好", "晚上"]
};
const getTimeInfo = () => {
const TIME = new Date();
// 年月日
let year = TIME.getFullYear();
let month = TIME.getMonth() + 1;
let date = TIME.getDate();
// 時(shí)分
let hours = TIME.getHours();
let minutes = TIME.getMinutes();
// 生成的問候文本
let greetingsStr = "";
// 遍歷定義的問候數(shù)據(jù)
for (const key in greetings) {
if (hours >= key.split(",")[0] && hours <= key.split(",")[1]) {
let greetingsKey = greetings[key];
greetingsStr = `${greetingsKey[0]},現(xiàn)在是${greetingsKey[1]},${hours}點(diǎn),${minutes}分`;
}
}
// 中午好,現(xiàn)在是中午12點(diǎn),12分,今天是2021年,8月,28日
return `${greetingsStr},今天是${year}年,${month}月,${date}日`;
};
const openGreetings = async (app_id, app_skey, app_akey, text) => {
const auth = { app_id, app_skey, app_akey };
// 訊飛 api 參數(shù)配置
const business = {
aue: "lame",
sfl: 1,
speed: 50,
pitch: 50,
volume: 100,
bgs: 0
};
// 存儲(chǔ)文件的路徑
const file = path.resolve("./src/good-morning.wav");
try {
// 執(zhí)行請(qǐng)求
await tts(auth, business, text, file).then(res => {
play.sound(file, function() {
play.sound("./src/xiaojixiaoji.m4a");
});
});
} catch (e) {
console.log("test exception", e);
}
};
const city = "長沙";
let text;
schedule.scheduleJob("0 0 8 * * *", () => {
request(
`http://apis.juhe.cn/simpleWeather/query?city=${encodeURI(city)}&key=聚合key`,
(err, response, body) => {
if (!err && response.statusCode == 200) {
let res = JSON.parse(body).result.realtime;
text = `
${getTimeInfo()},
接下來為您播報(bào)${city}實(shí)時(shí)天氣預(yù)報(bào),
今天,長沙天氣為${res.info}天,
室外溫度為${res.temperature}度,
室外濕度為百分之${res.humidity},
${res.direct},
${res.power},
天氣預(yù)報(bào)播放完畢,
接下來播放您喜歡的音樂
`;
openGreetings('訊飛的APPID', '訊飛的APISecret', '訊飛的APIKey', text)
}
}
);
});
完結(jié)撒花
到了這里,執(zhí)行 npm run dev ,就相當(dāng)于你開了一個(gè)鬧鐘,但是前提是你得保證服務(wù)不會(huì)停
今天早上我就是被這玩意叫醒的,但是推薦大家不要把聲音開太大了
本來是想裝到樹莓派上去的,但是樹莓派的音頻接口出了點(diǎn)問題,所以沒裝上
然后我就裝在了自己的電腦上
全部代碼鏈接: https://pan.baidu.com/s/1aQXQSCB-83P-xlkD35ZndQ 密碼: dao7
參考文檔
https://www.xfyun.cn/doc/tts/online_tts/API.html
https://github.com/Marak/play.js
https://www.npmjs.com/package/xf-tts-socket
