30分鐘快速實(shí)現(xiàn)小程序語音識別功能
前言
為了參加某個作秀活動,研究了一波如何結(jié)合小程序、科大訊飛實(shí)現(xiàn)語音錄入、識別的實(shí)現(xiàn)??拼笥嶏w開發(fā)文檔中只給出 Python 的 demo,并沒有給出 node.js 的 sdk,但問題不大。本文將從小程序相關(guān)代碼到最后對接科大訊飛 api 過程,一步步介紹,半個小時,搭建完成小程序語音識別功能!不能再多了!
當(dāng)然,前提是最好掌握有一點(diǎn)點(diǎn)小程序、node.js 甚至是音頻相關(guān)的知識。下面話不多說了,來一起看看詳細(xì)的介紹吧
架構(gòu)先行
架構(gòu)比較簡單,大伙兒可以先看下圖。除了小程序,需要提供 3 個服務(wù),文件上傳、音頻編碼及對接科大訊飛的服務(wù)。
node.js 對接科大訊飛的 api,npm 上已經(jīng)有同學(xué)提供了 sdk,有興趣的同學(xué)可以去搜索了解一下,筆者這里是直接調(diào)用了科大訊飛的 api 接口。
擼起袖子加油干
1、創(chuàng)建小程序
鵝廠的小程序文檔非常詳細(xì),在這里筆者就不對如何創(chuàng)建一個小程序的步驟進(jìn)行詳細(xì)闡述了。有需要的同學(xué)可以查看鵝廠的小程序開發(fā)文檔。
1.1 相關(guān)代碼
我們摘取小程序里面,語音錄入和語音上傳部分的代碼。
// 根據(jù)wx提供的api創(chuàng)建錄音管理對象
const recorderManager = wx.getRecorderManager();
// 監(jiān)聽語音識別結(jié)束后的行為
recorderManager.onStop(recorderResponse => {
// tempFilePath 是錄制的音頻文件
const { tempFilePath } = recorderResponse;
// 上傳音頻文件,完成語音識別翻譯
wx.uploadFile({
url: 'http://127.0.0.1:7001/voice', // 該服務(wù)在后面搭建。另外,小程序發(fā)布時要求后臺服務(wù)提供https服務(wù)!這里的地址僅為開發(fā)環(huán)境配置。
filePath: tempFilePath,
name: 'file',
complete: res => {
console.log(res); // 我們期待res,就是翻譯后的內(nèi)容
}
});
});
// 開始錄音,觸發(fā)條件可以是按鈕或其他,由你自己決定
recorderManager.start({
duration: 5000 // 最長錄制時間
// 其他參數(shù)可以默認(rèn),更多參數(shù)可以查看https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.start.html
});
2、搭建文件服務(wù)器
步驟 1 代碼中提到了一個 url 地址大家應(yīng)該都還記得。
http://127.0.0.1:7001/voice
小程序本身還并沒有提供語音識別的功能,所以在這里我們需要借助于“后端”服務(wù)的能力,完成我們語音識別翻譯的功能。
2.1 egg.js 服務(wù)初始化
我們使用 egg.js 的 cli 快速初始化一個工程,當(dāng)然你也可以使用 express、koa、kraken 等等框架,框架的選型在此不是重點(diǎn)我們就不做展開闡述了。對 egg.js 不熟悉的同學(xué)可以查看egg.js 的官網(wǎng)。
npm i egg-init -g egg-init voice-server --type=simple cd voice-server npm i
安裝完成后,執(zhí)行以下代碼
npm run dev
隨后訪問瀏覽器http://127.0.0.1:7001應(yīng)該可以看到一個Hi, egg 的頁面。至此我們的服務(wù)初始化完成。
2.2 文件上傳接口
a) 修改 egg.js 的文件上傳配置
打開 config/config.default.js,添加以下兩項(xiàng)配置
module.exports = appInfo => {
...
config.multipart = {
fileSize: '2gb', // 限制文件大小
whitelist: [ '.aac', '.m4a', '.mp3' ], // 支持上傳的文件后綴名
};
config.security = {
csrf: {
enable: false // 關(guān)閉csrf
}
};
...
}
b) 添加 VoiceController
打開 app/controller 文件夾,新建文件 voice.js。編寫 VoiceController 使其繼承于 egg.js 的 Controller。具體代碼如下:
const Controller = require('egg').Controller;
const fs = require('fs');
const path = require('path');
const pump = require('mz-modules/pump');
const uuidv1 = require('uuid/v1'); // 依賴于uuid庫,用于生成唯一文件名,使用npm i uuid安裝即可
// 音頻文件上傳后存儲的路徑
const targetPath = path.resolve(__dirname, '..', '..', 'uploads');
class VoiceController extends Controller {
constructor(params) {
super(params);
if (!fs.existsSync(targetPath)) {
fs.mkdirSync(targetPath);
}
}
async translate() {
const parts = this.ctx.multipart({ autoFields: true });
let stream;
const voicePath = path.join(targetPath, uuidv1());
while (!isEmpty((stream = await parts()))) {
await pump(stream, fs.createWriteStream(voicePath));
}
// 到這里就完成了文件上傳。如果你不需要文件落地,也可以在后續(xù)的操作中,直接使用stream操作文件流
...
// 音頻編碼
// 科大訊飛語音識別
...
}
}
c) 最后一步,新增路由規(guī)則
寫完 controller 之后,我們依據(jù) egg.js 的規(guī)則,在 router.js 里面新增一個路由。
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/voice', controller.voice.translate);
};
OK,至此你可以測試一下從小程序錄音,錄音完成后上傳到后臺文件服務(wù)器的完整流程。如果沒問題,那恭喜你你已經(jīng)完成了 80%的工作了!
3、音頻編碼服務(wù)
在上文中,小程序錄音的方法 recorderManager.start 的時候我們提及到了“更多參數(shù)”。其中有一個參數(shù)是 format,支持 aac 和 mp3 兩種(默認(rèn)是 aac)。然后我們查閱了科大訊飛的 api 文檔,音頻編碼支持“未壓縮的 pcm 或 wav 格式”。
什么 aac、pcm、wav?emmm.. OK,我們只是前端,既然格式不對等,那只需要完成 aac -> pcm 轉(zhuǎn)化即可,ffmpeg 立即浮現(xiàn)在筆者的腦海里。一番搜索,命令大概是這樣子的:
ffmpeg -i uploads/a3f588d0-edf8-11e8-b6f5-2929aef1b7f8.aac -f s16le -ar 8000 -ac 2 -y decoded.pcm
# -i 后面帶的是源文件
# -f s16le 指的是編碼格式
# -ar 8000 編碼碼率
# -ac 2 通道
接下來我們使用 node.js 來實(shí)現(xiàn)上述命令。
3.1 引入相關(guān)依賴包
npm i ffmpeg-static npm i fluent-ffmpeg
3.2 創(chuàng)建一個編碼服務(wù)
在 app/service 文件夾中,創(chuàng)建 ffmpeg.js 文件。新建 FFmpegService 繼承于 egg.js 的 Service
const { Service } = require('egg');
const ffmpeg = require('fluent-ffmpeg');
const ffmpegStatic = require('ffmpeg-static');
const path = require('path');
const fs = require('fs');
ffmpeg.setFfmpegPath(ffmpegStatic.path);
class FFmpegService extends Service {
async aac2pcm(voicePath) {
const command = ffmpeg(voicePath);
// 方便測試,我們將轉(zhuǎn)碼后文件落地到磁盤
const targetDir = path.join(path.dirname(voicePath), 'pcm');
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir);
}
const target = path.join(targetDir, path.basename(voicePath)) + '.pcm';
return new Promise((resolve, reject) => {
command
.audioCodec('pcm_s16le')
.audioChannels(2)
.audioBitrate(8000)
.output(target)
.on('error', error => {
reject(error);
})
.on('end', () => {
resolve(target);
})
.run();
});
}
}
module.exports = FFmpegService;
3.3 調(diào)用 ffmpegService,獲得 pcm 文件
回到 app/controller/voice.js 文件中,我們在文件上傳完成后,調(diào)用 ffmpegService 提供的 aac2pcm 方法,獲取到 pcm 文件的路徑。
// app/controller/voice.js
...
async translate() {
...
...
const pcmPath = await this.ctx.service.ffmpeg.aac2pcm(voicePath);
...
}
...
4、對接科大訊飛 API
首先,需要到科大訊飛開放平臺注冊并新增應(yīng)用、開通應(yīng)用的語音聽寫服務(wù)。
我們再寫一個服務(wù),在 app/service 文件夾下創(chuàng)建 xfyun.js 文件,實(shí)現(xiàn) XFYunService 繼承于 egg.js 的 Service。
4.1 引入相關(guān)依賴
npm i axios // 網(wǎng)絡(luò)請求庫 npm i md5 // 科大訊飛接口中需要md5計(jì)算 npm i form-urlencoded // 接口中需要對部分內(nèi)容進(jìn)行urlencoded
4.2 XFYunService 實(shí)現(xiàn)
const { Service } = require('egg');
const fs = require('fs');
const formUrlencoded = require('form-urlencoded').default;
const axios = require('axios');
const md5 = require('md5');
const API_KEY = 'xxxx'; // 在科大訊飛控制臺上可以查到服務(wù)的APIKey
const API_ID = 'xxxxx'; // 同樣可以在控制臺查到
class XFYunService extends Service {
async voiceTranslate(voicePath) {
// 繼上文,暴力的讀取文件
let data = fs.readFileSync(voicePath);
// 將內(nèi)容進(jìn)行base64編碼
data = new Buffer(data).toString('base64');
// 進(jìn)行url encode
data = formUrlencoded({ audio: data });
const params = {
engine_type: 'sms16k',
aue: 'raw'
};
const x_CurTime = Math.floor(new Date().getTime() / 1000) + '',
x_Param = new Buffer(JSON.stringify(params)).toString('base64');
return axios({
url: 'http://api.xfyun.cn/v1/service/v1/iat',
method: 'POST',
data,
headers: {
'X-Appid': API_ID,
'X-CurTime': x_CurTime,
'X-Param': x_Param,
'X-CheckSum': md5(API_KEY + x_CurTime + x_Param)
}
}).then(res => {
// 查詢成功后,返回response的data
return res.data || {};
});
}
}
module.exports = XFYunService;
4.3 調(diào)用 XFYunService,完成語音識別
再次回到 app/controller/voice.js 文件中,我們在 ffmpeg 轉(zhuǎn)碼完成后,調(diào)用 XFYunService 提供的 voiceTranslate 方法,完成語音識別。
// app/controller/voice.js
...
async translate() {
...
...
const result = await this.ctx.service.xfyun.voiceTranslate(pcmPath);
this.ctx.body = result;
if (+result.code !== 0) {
this.ctx.status = 500;
}
}
...
至此我們完成語音識別的代碼編寫。主要流程其實(shí)很簡單,通過小程序錄入語音文件,上傳到文件服務(wù)器之后,通過 ffmpeg 獲取到 pcm 文件, 最后再轉(zhuǎn)發(fā)到科大訊飛的 api 接口進(jìn)行識別。
以上,如有錯漏,歡迎指正!
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
JavaScript Canvas實(shí)現(xiàn)井字棋游戲
這篇文章主要為大家詳細(xì)介紹了JavaScript Canvas實(shí)現(xiàn)井字棋游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08
刪除javascript所創(chuàng)建子節(jié)點(diǎn)的方法
這篇文章主要介紹了刪除javascript所創(chuàng)建子節(jié)點(diǎn)的方法,涉及javascript針對頁面節(jié)點(diǎn)元素的操作技巧,需要的朋友可以參考下2015-05-05
js獲取html參數(shù)及向swf傳遞參數(shù)應(yīng)用介紹
HTML頁面是在客戶端執(zhí)行的,這樣要獲取參數(shù)必須使用客戶端腳本如JavaScript,在這點(diǎn)上與服務(wù)器端腳本獲取參數(shù)方式有所不同接下來將詳細(xì)介紹下感興趣的你可不要錯過了哈2013-02-02
JS判斷輸入的字符串是否是數(shù)字的方法(正則表達(dá)式)
下面小編就為大家?guī)硪黄狫S判斷輸入的字符串是否是數(shù)字的方法(正則表達(dá)式)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11
微信小程序?qū)崿F(xiàn)隨機(jī)驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)隨機(jī)驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05
JavaScript對象拷貝與Object.assign用法實(shí)例分析
這篇文章主要介紹了JavaScript對象拷貝與Object.assign用法,結(jié)合實(shí)例形式分析了javascript深拷貝與淺拷貝以及Object.assign的功能與相關(guān)使用技巧,需要的朋友可以參考下2018-06-06

