本文记录我基于小程序云开发模式,进行小程序“口算卡”的全部历程,篇幅较长,分为以下篇章
- 需求概述及简单上手
- 小程序云函数使用(写入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)
- 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
- 语音识别,从小程序云函数到自建服务器转码识别
打赏
微信扫一扫,打赏作者吧~