X

3,小程序云函数(语音识别,从思路到实现到放弃使用云函数)

本文记录我基于小程序云开发模式,进行小程序“口算卡”的全部历程,篇幅较长,分为以下篇章

  1. 需求概述及简单上手
  2. 小程序云函数使用(写入DB及读取DB)
  3. 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
  4. 语音识别,从小程序云函数到自建服务器转码识别

怎么才能用的更舒服,那肯定是小孩报出答案后,自动识别语音,然后比较一下就可以了;

为了这个舒服开始悲催的开发之旅

使用语音插件,出名的就是腾讯官方的语音同译,遗憾的是,它不能识别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,
    })
  },
  1. 分段后返回了长度和 farmeBuffer ,不理解 farmeBuffer ,小程序端将 farmeBuffer 值 base64 后传给云函数 talk
  2. 云函数 talk 将base64 还原,保存成 /tmp/_openid.mp3 文件
  3. 云函数 talk 将/tmp/_openid.mp3 发送给 api.rest7.com 进行转码为wav 并保存到 /tmp/_openid.wav
  4. 云函数 talk 调用百度api node sdk 对 /tmp/_openid.wav 进行识别
  5. 通过以上步骤,虽然实现了语音识别,但是识别效率和识别率非常低,根本达不到快速响应的目的;

期间遇见的问题:

  1. nodejs 的天然异步,之前写服务端都是同步执行的代码,首次用nodejs写后台,确实有些搓手不及,幸好有promise,await,async 大法,可以从容面对;
  2. 百度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 之旅


  1. 需求概述及简单上手
  2. 小程序云函数使用(写入DB及读取DB)
  3. 小程序云函数(语音识别,从思路到实现到放弃使用云函数)
  4. 语音识别,从小程序云函数到自建服务器转码识别
打赏
微信扫一扫,打赏作者吧~
admin :