本文记录我基于小程序云开发模式,进行小程序“口算卡”的全部历程,篇幅较长,分为以下篇章
- 需求概述及简单上手
- 小程序云函数使用(写入DB及读取DB)
- 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
- 语音识别,从小程序云函数到自建服务器转码识别
怎么才能用的更舒服,那肯定是小孩报出答案后,自动识别语音,然后比较一下就可以了;
为了这个舒服开始悲催的开发之旅
使用语音插件,出名的就是腾讯官方的语音同译,遗憾的是,它不能识别1个字的语音,可能他们认为一个字没啥意义吧;只能放弃了插件;
在语音识别API中,选中了百度的API,原因:免费、不限流量、不限频次
看看语音识别思路图
思路1,使用小程序云函数方式,与自己的转码api(在自己的服务器,安装 ffmpeg,通过 node 服务进行处理音频文件)交互实现转码,再与百度api交互获得语音内容
思路2,使用小程序云函数方式,与转码 api 交互实现转码,再与百度api交互获得语音内容
思路3,自己的服务器,使用node 搭建 socket 服务器,进行高效数据交互,安装 ffmpeg,转码,再与百度api交互获得语音内容
思路3 是实现了思路2之后做出的决定,思路1 是思路2的延展(自建转码服务api),所以躺坑思路2;
下来说说思路2的具体实现到最后的实现与放弃
小程序开启录音,MP3格式,单声道(numberOfChannels),采样率(sampleRate) 16000,分段录制大小(frameSize) 7kb~50kb
startRM(){ console.log('startRM'); recorderManager.start({ duration:1000*600, format:'mp3',//acc/mp3 sampleRate:16000, //encodeBitRate: 64000, //sampleRate: 8000, numberOfChannels: 1, frameSize:7, }) },
- 分段后返回了长度和 farmeBuffer ,不理解 farmeBuffer ,小程序端将 farmeBuffer 值 base64 后传给云函数 talk
- 云函数 talk 将base64 还原,保存成 /tmp/_openid.mp3 文件
- 云函数 talk 将/tmp/_openid.mp3 发送给 api.rest7.com 进行转码为wav 并保存到 /tmp/_openid.wav
- 云函数 talk 调用百度api node sdk 对 /tmp/_openid.wav 进行识别
- 通过以上步骤,虽然实现了语音识别,但是识别效率和识别率非常低,根本达不到快速响应的目的;
期间遇见的问题:
- nodejs 的天然异步,之前写服务端都是同步执行的代码,首次用nodejs写后台,确实有些搓手不及,幸好有promise,await,async 大法,可以从容面对;
- 百度api无法语音的情况,小程序启动录音 sampleRate 采样率 可以尝试 8000 和 16000 切换
talk/index.js 代码
var http = require('http'); var fs = require('fs'); var queryString = require('querystring'); function doUpload(url,filename,filesize=1024*1024){ var boundaryKey = 'A' + new Date().getTime(); //随便加个前缀A 避免全数字作为分界符 var parse_u=require('url').parse(url,true); var isHttp=parse_u.protocol=='http:'; var options={ host:parse_u.hostname, port:parse_u.port||(isHttp?80:443), path:parse_u.path, method:'POST', headers:{ 'Content-Type':'multipart/form-data; boundary='+boundaryKey, //'Content-Length':content.length } }; //console.log('doUpload.options',options); return new Promise(function (success,fail) { var req = require(isHttp?'http':'https').request(options,function(res){ var _data=''; res.on('data', function(chunk){ _data += chunk; //console.log('POST.data',_data); }); res.on('end', function(endRes){ //console.log('POST.end',endRes); //fn!=undefined && fn(_data); success(_data) }); }).on('error', function(err){ fail(err) }); //ClientRequest writableStream 注意:文件字段的分界符 req.write('--'+boundaryKey+'\r\nContent-Disposition:form-data; name="file"; filename="'+filename+'"\r\nContent-Type:audio/mp3'); // 1M缓冲 var fileStream = fs.createReadStream(filename, {bufferSize:filesize}); fileStream.pipe(req, {end: false}); fileStream.on('end', function(){ // ::注意::文件字段内容和其他字段之间空2行,字段名和字段值之间空2行 req.write('\r\n\r\n--'+boundaryKey+'\r\n'+'Content-Disposition: form-data; name="format"\r\n\r\n'+'wav'); req.end('\r\n--'+ boundaryKey + '--'); //注意:结束时的分界符 末尾'--' }); }) } function download(url,filename){ filename=filename.replace('.mp3','.wav') var parse_u=require('url').parse(url,true); var isHttp=parse_u.protocol=='http:'; var options={ host:parse_u.hostname, port:parse_u.port||(isHttp?80:443), path:parse_u.path, }; console.log('download.options',options); return new Promise(function (success,fail) { var file = fs.createWriteStream(filename); require(isHttp?'http':'https').get(options, function(res) { res.on('data', function(data) { file.write(data); }).on('end', function() { file.end(); success(filename) }); }).on('error', function(err){ fail(err) }); }) } function baiduApi(wavFile,cuid){ var AipSpeechClient = require("baidu-aip-sdk").speech; // 设置APPID/AK/SK var APP_ID = "你的APP_ID"; var API_KEY = "你的API_KEY "; var SECRET_KEY = "你的SECRET_KEY"; // 新建一个对象,建议只保存一个对象调用服务接口 var client = new AipSpeechClient(APP_ID, API_KEY, SECRET_KEY); var HttpClient = require("baidu-aip-sdk").HttpClient; HttpClient.setRequestInterceptor(function(requestOptions) { // 查看参数 //console.log(requestOptions) // 修改参数 requestOptions.timeout = 5000; // 返回参数 return requestOptions; }); let voice = fs.readFileSync(wavFile); let voiceBuffer = new Buffer(voice); // 识别本地文件,附带参数 return client.recognize(voiceBuffer, 'wav', 16000, {dev_pid: '1536', cuid: cuid}) } function writeFile(fileName,dataBuffer){ return new Promise(function(success,fail){ fs.writeFile(fileName, dataBuffer, function(err) { //console.log(fileName,err) success(fileName); }); }) } //talk 运行主函数 exports.main = async (event, context) => { //update base64 mp3 //将小程序传递来的base64转为buffer let dataBuffer = new Buffer(event.base64, 'base64'); let fileName="/tmp/"+event.userInfo.openId+Math.random()+".mp3"; //将buffer写入MP3文件 fileName=await writeFile(fileName,dataBuffer); //let postRes=await post('http://api.rest7.com/v1/sound_convert.php',{format:'wav'}) //将MP3文件转码得到wav下载地址 let postRes=await doUpload('http://api.rest7.com/v1/sound_convert.php',fileName) console.log('POST.res',postRes); if(postRes){ postRes=JSON.parse(postRes); } let wavFile='' //下载wav文件 if(postRes.file){ wavFile=await download(postRes.file,fileName) console.log('wavFile',wavFile); } let res={}; //使用wav文件调用百度sdk解析语音 if(wavFile){ res=await baiduApi(wavFile,event.userInfo.openId); } //将百度sdk解析后的结果返回给前端 return {wavFile:wavFile,...res} }
经过了思路二的折腾,证明网络请求果真是消耗资源的大头,所以有了思路3,socket方式,下章我们开始 socket 之旅
- 需求概述及简单上手
- 小程序云函数使用(写入DB及读取DB)
- 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
- 语音识别,从小程序云函数到自建服务器转码识别
打赏
微信扫一扫,打赏作者吧~