本文记录我基于小程序云开发模式,进行小程序“口算卡”的全部历程,篇幅较长,分为以下篇章
- 需求概述及简单上手
- 小程序云函数使用(写入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
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
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)
- 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
- 语音识别,从小程序云函数到自建服务器转码识别