2019-02-15 12:14:12 chen_pan_pan 阅读数 6385

小程序实现语音识别功能,通过语音的方式代替手动输入查询。经过查询微信小程序api,发现微信并没有对外提供语音识别的api,所以要另外想办法。经过多发查找资料发现了思路。

解决思路:

微信小程序提供了录音的功能,通过录音的方式,然后把录音文件传到服务器,后台服务器将语音转码,然后再调用第三方语音识别api,我这里使用的是百度的api,最后在将识别的文字返回给微信小程序。

直接上代码:

小程序端代码:

startRecord: function() {

if (this.recorderManager == null) {

this.recorderManager = wx.getRecorderManager();

this.options = {

duration: 10000,

sampleRate: 16000,

numberOfChannels: 1,

encodeBitRate: 64000,

format: 'mp3',

frameSize: 50

}

}

this.recorderManager.start(this.options);

this.recorderManager.onStop((res) => {

console.log(res)

wx.uploadFile({

url: 'https://xxxx',//将录音文件传到后台服务器

filePath: res.tempFilePath,

method:'POST',

name: 'file',

header: {

'content-type': 'multipart/form-data'

},

success: function(res) {

console.log(res);

},

fail: function() {

console.log("语音识别失败");

}

})

});

},

stopRecord: function() {

this.recorderManager.stop()

}

 

服务端代码:

注意:需要使用mp3plugin.jar包,网上可以下载到。

    // 百度语音识别
    public static final String APP_ID = "xxx";
    public static final String API_KEY = "xxx";
    public static final String SECRET_KEY = "xxx";

    /**
     * @Description TODO
     * @return
     */
    @RequestMapping(value = "speechRecognition", method = RequestMethod.POST)
    @ResponseBody
    public Object speechReco(HttpServletRequest request) {
        MultipartFile file = ((MultipartHttpServletRequest) request).getFile("file");
        try {
            byte[] pcmBytes = mp3Convertpcm(file.getInputStream());
            org.json.JSONObject resultJson = speechBdApi(pcmBytes);
            System.out.println(resultJson.toString());
            if (null != resultJson && resultJson.getInt("err_no") == 0) {
                return resultJson.getJSONArray("result").get(0).toString().split(",")[0];
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return "";
    }

    /**
     * @Description MP3转换pcm
     * @param mp3Stream
     *            原始文件流
     * @return 转换后的二进制
     * @throws Exception
     */
    public byte[] mp3Convertpcm(InputStream mp3Stream) throws Exception {
        // 原MP3文件转AudioInputStream
        BufferedInputStream zipTest=new BufferedInputStream(mp3Stream);
        //重新包装一层,不然会报错。
        AudioInputStream mp3audioStream = AudioSystem.getAudioInputStream(zipTest);
        // 将AudioInputStream MP3文件 转换为PCM AudioInputStream
        AudioInputStream pcmaudioStream = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED,
                mp3audioStream);
        byte[] pcmBytes = IOUtils.toByteArray(pcmaudioStream);
        pcmaudioStream.close();
        mp3audioStream.close();
        return pcmBytes;
    }

    /**
     * @Description 调用百度语音识别API
     * @param pcmBytes
     * @return
     */
    public static org.json.JSONObject speechBdApi(byte[] pcmBytes) {
        // 初始化一个AipSpeech
        AipSpeech client = new AipSpeech(APP_ID, API_KEY, SECRET_KEY);
        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);
        // 调用接口
        org.json.JSONObject res = client.asr(pcmBytes, "pcm", 16000, null);
        return res;
    }

 

如果我的文章帮助到了大家,减少大家的弯路,愿意打赏的请扫下面的二维码。也可留言。

 

2018-11-01 20:45:26 TXX_c 阅读数 7263

微信小程序目前录音仅能生成 mp3和acc两种格式,而百度语音识别,讯飞语音识别都不支持mp3和acc的;

最后发现腾讯云阿里云支持mp3,反正用的是腾讯的小程序,将就着用腾讯的接口咯

不想解说了,直接说流程+代码+注释吧

准备:

1.小程序账号(appid)

2.腾讯云控制后台申请语音识别服务(获取SecretId 和SecretKey),对应代码中的马赛克,自行替换

 

不建议直接搬代码就用,所以多少看下接口文档吧

小程序录音接口腾讯云语音识别接口

主要流程:

一、小程序录音及临时路径获取

二、根据临时读取录音文件并转换成base64编码

三、调用语音识别接口(submit())

  1. 参数获取

  2. 加密并获取签名(需要用到hmac_sha1加密,找了挺久才找到的,在最下面)

  3. 发送请求

const app = getApp();
const recorder = wx.getRecorderManager();
const player = wx.createInnerAudioContext();
const file = wx.getFileSystemManager();
var that;
Page({
  /**
   * 页面的初始数据
   */
  data: {
    apikey: '马赛克',
    secret_id:'马赛克',
    token:"",
    recording: false,
    cancel_record:false,
    start_y: '',

    fileBase64: '', //base64的文件
    rate: 8000,
    filePath: '',//录音文件
    fileLen:0,//录音长度
  },
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    that = this;
    //先定好停止录音后要干嘛
    recorder.onStop(function suc(e) {
      //保存录音文件的临时路径
      that.setData({
        filePath: e.tempFilePath,
      })
      wx.setStorageSync('filePath', e.tempFilePath);
      //友好的菊花加载
      wx.showLoading({
        title: '文件读取中...'
      })
      //读取录音文件,并转为base64编码
      file.readFile({
        filePath: e.tempFilePath,
        encoding: 'base64',
        success: function (e) {
          that.setData({
            fileBase64: e.data
          })
          console.log(e);
        },
        complete() {
          wx.hideLoading();
        },
      })
    })
  },
  //语音识别
  submit(){    
    let apikey = that.data.apikey;
    let param = {
      Action: 'SentenceRecognition',
      Version: '2018-05-22',
      ProjectId: '0',//腾讯云项目ID
      SubServiceType: 2, //一句话识别,没得选
      EngSerViceType: '8k',//引擎类型 8k(电话) 或 16k,没说明白
      SourceType: 1,//0 url  1数据
      VoiceFormat: 'mp3',//格式 MP3 或 wav
      UsrAudioKey: new Date().getTime(),//唯一识别?直接用时间戳吧珂珂
      Data: that.data.fileBase64,
      DataLen: app.base64_decode(that.data.fileBase64).length,//未进行base64编码时的数据长度
      // Url :'',你懂的
      Timestamp: parseInt(new Date().getTime()/1000),
      Nonce: parseInt(new Date().getTime() / 1000),
      SecretId: that.data.secret_id,      
    };
    
    //把参数按键值大小排序并拼接成字符串
    let data = ksort(param);
    let arr = [];
    for (var x in data) {
      data[x] = encodeURI(data[x]);
      arr.push(x + '=' + data[x]);
    }
    let str = arr.join('&');
    //签名生成
    let sign = 'POSTaai.tencentcloudapi.com/?' + str;
    sign = b64_hmac_sha1(apikey,sign);
    data['Signature'] = sign;
    //友好的菊花提示
    wx.showLoading({
      title: '发送中...',
    })
    wx.request({
      url: 'https://aai.tencentcloudapi.com/',
      data:data,
      header:{
        'content-type':'application/x-www-form-urlencoded'
      },
      method:'post',
      success:function(e){console.log(e.data.Response)},
      complete(){wx.hideLoading();}
    })
  },
  //手指按下
  clickDown(e){
    console.log('start');
    that.setData({
      recording: true,
      start_y: e.touches[0].clientY,
      cancel_record: false,
    })
    
    //开始录音
    recorder.start({
      duration: 60000,//最大时长
      sampleRate: that.data.rate,//采样率
      numberOfChannels: 1,//录音通道数
      encodeBitRate: 24000,//编码码率,有效值见下表格
      format: 'mp3',//音频格式
      // frameSize: 2000,//指定大小 kb
    })
  },
  //手指移动
  clickMove(e){
    if (e.touches[0].clientY - that.data.start_y <= -50) {
      that.setData({
        cancel_record: true,
      })
    } else {
      that.setData({
        cancel_record: false,
      })
    }
    return false;
  },
  //手指松开
  clickUp(e){
    console.log('end');
    if (that.data.cancel_record) {
      wx.showModal({
        title: '提示',
        content: '您选择了取消发送,确定吗?',
        confirmText: '继续发送',
        cancelText: '取消重录',
        success: res => {
          if (res.confirm) {
            wx.showToast({
              title: '发送成功',
            })
          } else {
            wx.showToast({
              title: '您选择了取消',
            })
          }
          that.setData({
            recording: false
          })
        }
      })
    } else {
      wx.showToast({
        title: '发送成功',
      })
      that.setData({
        recording: false
      })
    }
    recorder.stop();
    return false;
    
  },
  //播放
  play(){
    player.src = that.data.filePath;
    player.play();
  },

})

//对象按键值排序方法
function ksort(obj){
    let temp = 'Action';
    let k_arr = [];
    for(var x in obj){
      k_arr.push(x);
    }
    k_arr.sort();
    let res = {};
    for(let i = 0;i<k_arr.length;i++){
      let k = k_arr[i];
      res[k] = obj[k];
    }
    return res;
}

//这个是找了很久的加密函数
function b64_hmac_sha1(k, d, _p, _z) {
  // heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
  // _p = b64pad, _z = character size; not used here but I left them available just in case
  if (!_p) { _p = '='; } if (!_z) { _z = 8; } function _f(t, b, c, d) { if (t < 20) { return (b & c) | ((~b) & d); } if (t < 40) { return b ^ c ^ d; } if (t < 60) { return (b & c) | (b & d) | (c & d); } return b ^ c ^ d; } function _k(t) { return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : (t < 60) ? -1894007588 : -899497514; } function _s(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF), m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); } function _r(n, c) { return (n << c) | (n >>> (32 - c)); } function _c(x, l) { x[l >> 5] |= 0x80 << (24 - l % 32); x[((l + 64 >> 9) << 4) + 15] = l; var w = [80], a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, e = -1009589776; for (var i = 0; i < x.length; i += 16) { var o = a, p = b, q = c, r = d, s = e; for (var j = 0; j < 80; j++) { if (j < 16) { w[j] = x[i + j]; } else { w[j] = _r(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1); } var t = _s(_s(_r(a, 5), _f(j, b, c, d)), _s(_s(e, w[j]), _k(j))); e = d; d = c; c = _r(b, 30); b = a; a = t; } a = _s(a, o); b = _s(b, p); c = _s(c, q); d = _s(d, r); e = _s(e, s); } return [a, b, c, d, e]; } function _b(s) { var b = [], m = (1 << _z) - 1; for (var i = 0; i < s.length * _z; i += _z) { b[i >> 5] |= (s.charCodeAt(i / 8) & m) << (32 - _z - i % 32); } return b; } function _h(k, d) { var b = _b(k); if (b.length > 16) { b = _c(b, k.length * _z); } var p = [16], o = [16]; for (var i = 0; i < 16; i++) { p[i] = b[i] ^ 0x36363636; o[i] = b[i] ^ 0x5C5C5C5C; } var h = _c(p.concat(_b(d)), 512 + d.length * _z); return _c(o.concat(h), 512 + 160); } function _n(b) { var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", s = ''; for (var i = 0; i < b.length * 4; i += 3) { var r = (((b[i >> 2] >> 8 * (3 - i % 4)) & 0xFF) << 16) | (((b[i + 1 >> 2] >> 8 * (3 - (i + 1) % 4)) & 0xFF) << 8) | ((b[i + 2 >> 2] >> 8 * (3 - (i + 2) % 4)) & 0xFF); for (var j = 0; j < 4; j++) { if (i * 8 + j * 6 > b.length * 32) { s += _p; } else { s += t.charAt((r >> 6 * (3 - j)) & 0x3F); } } } return s; } function _x(k, d) { return _n(_h(k, d)); } return _x(k, d);
}

页面

<view class="main">
  <button bindtap='play'>播放录音</button>
  <button bindtap='submit'>上传</button>
  
  <button bindtouchstart="clickDown" bind:touchend="clickUp"  touchcancel="clickUp" bindtouchmove="clickMove" class="record_btn">
  <span wx:if="{{recording}}">{{cancel_record?'松开取消':'松开发送'}}</span>
  <span wx:else>按下录音</span>
  </button>
</view>

样式

.record_btn{
  position:absolute;
  bottom: 0;
  width: 100%;
  height:110rpx;
}

简洁的页面

录音=>上传=>然后去看控制台日志哈

2017-12-18 11:12:20 u011072139 阅读数 2811

现在人工智能非常火爆,很多朋友都想学,但是一般的教程都是为博硕生准备的,太难看懂了。最近发现了一个非常适合小白入门的教程,不仅通俗易懂而且还很风趣幽默。所以忍不住分享一下给大家。点这里可以跳转到教程。

由于最近项目中需要实现小程序端的语音识别,所以写了一个例子,供需要的同学参考。下面贴下代码

小程序端代码:

 

<!--index.wxml-->
<view class="container">
  <view class="userinfo">
    <button wx:if="{{!hasUserInfo && canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" background-size="cover"></image>
      <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    </block>
  </view>
  <!-- <view class="usermotto"> -->
      <button type="default" bindtap='startLy' style='margin-top:10rpx;color:green;' disabled='{{disabled1}}' loading='{{loading1}}'>录音</button>
      
      <button type="default" bindtap='endLy' style='margin-top:10rpx;color:red;'>结束</button>

      <button type="default" bindtap='upload' style='margin-top:10rpx;color:blue;' disabled='{{disabled}}' loading='{{loading}}'>识别</button>
      <view>识别结果:{{res}}</view>
  <!-- </view> -->
</view>
//index.js
//获取应用实例
const app = getApp()

const recorderManager = wx.getRecorderManager()

recorderManager.onStart(() => {
  console.log('recorder start')
})
recorderManager.onResume(() => {
  console.log('recorder resume')
})
recorderManager.onPause(() => {
  console.log('recorder pause')
})
recorderManager.onStop((res) => {
  console.log('recorder stop', res)
  const { tempFilePath } = res
})
recorderManager.onFrameRecorded((res) => {
  const { frameBuffer } = res
  console.log('frameBuffer.byteLength', frameBuffer.byteLength)
})

const options = {
  //duration: 10000,
  sampleRate: 16000,//采样率
  numberOfChannels: 1,
  encodeBitRate: 96000,//编码码率
  format: 'mp3',
  frameSize: 50
}

const innerAudioContext = wx.createInnerAudioContext()
var tempPath = "";

Page({
  data: {
    motto: 'Hello World',
    userInfo: {},
    hasUserInfo: false,
    canIUse: wx.canIUse('button.open-type.getUserInfo'),
    res:'',
    loading: false,
    disabled: false,
    loading1: false,
    disabled1: false
  },
  //事件处理函数
  bindViewTap: function() {
    wx.navigateTo({
      url: '../logs/logs'
    })
  },
  onLoad: function () {
    if (app.globalData.userInfo) {
      this.setData({
        userInfo: app.globalData.userInfo,
        hasUserInfo: true
      })
    } else if (this.data.canIUse){
      // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
      // 所以此处加入 callback 以防止这种情况
      app.userInfoReadyCallback = res => {
        this.setData({
          userInfo: res.userInfo,
          hasUserInfo: true
        })
      }
    } else {
      // 在没有 open-type=getUserInfo 版本的兼容处理
      wx.getUserInfo({
        success: res => {
          app.globalData.userInfo = res.userInfo
          this.setData({
            userInfo: res.userInfo,
            hasUserInfo: true
          })
        }
      })
    }
  },
  getUserInfo: function(e) {
    console.log(e)
    app.globalData.userInfo = e.detail.userInfo
    this.setData({
      userInfo: e.detail.userInfo,
      hasUserInfo: true
    })
  },
  startLy: function(e){
    var that = this
    that.setData({ loading1: true });
    that.setData({ disabled1: true });
    recorderManager.start(options)
  },
  endLy: function(e){
    var that = this
    that.setData({ loading1: false });
    that.setData({ disabled1: false });
    recorderManager.stop();
    recorderManager.onStop((res) => {
      console.log('recorder stop', res)
      tempPath = res.tempFilePath;
      console.log('录音地址' + tempPath)
    })

  },
  upload: function(){
    var that = this
    that.setData({ loading: true});
    that.setData({ disabled: true});
    wx.uploadFile({
      //url: 'http://192.168.30.3:8080/gt_store_checking_in/voice/upload.action', //仅为示例,非真实的接口地址
      url: 'http://192.168.30.25:8080/gt_store_checking_in/voice/upload.action', //仅为示例,非真实的接口地址
      filePath: tempPath,
      name: 'file',
      formData: {
        'user': 'test'
      },
      success: function (res) {
        var data = res.data
        that.setData({res: data})
        //do something
        console.log('succ',data)
        that.setData({ loading: false });
        that.setData({ disabled: false });
      },
      fail: function(res){
        that.setData({ loading: false });
        that.setData({ disabled: false });
        console.log('fail',res.errMsg)
      }
    })
  }

  

})



后端使用java+百度语音识别+ffmpeg格式转换,后端带面,见小程序 语音识别(二)

 

 

 

 

 

2019-10-11 20:42:34 Change_xiao 阅读数 245

微信前台录入语音PHP后台调用百度AI进行语音识别

先去百度AI下载 PHP sdk
目录如下
在这里插入图片描述
**

由于微信小程序暂且支持真机调试才会出现效果
所以真机调试的设备必须与js中发起请求的url在同一网络下

**

微信前台代码
.html

<button type='primary' bindtouchstart='startrecorderHandel' bindtouchend="sendrecorderHandel">点击录音</button>
<view>
  你说的话是:
  <view>
    {{msg}}
  </view>
</view>

.js

// 录音对象
const recorderManager = wx.getRecorderManager();

function sendRecord(src) {
  var obj = {
    url: "http://49.122.47.146/php-yuyin/yuyin.php",
    filePath: src,
    name: "recordFile",
    header: {
      'Content-Type': 'application/json'
    },
    success: function (result) {
      var data = JSON.parse(result.data);
      // msg 为最终语音识别的字符串
      var msg = data.result;
      // 获取当前页面对象
      var page = getCurrentPages()[0];
      page.setData({ msg: msg });
    },
    fail: function (err) {
      console.log(err);
    }
  };
  wx.uploadFile(obj)
}

// 结束录音的时候触发 
recorderManager.onStop((res) => {
  // 获取文件路径-提交到后台-后台发送到百度
  sendRecord(res.tempFilePath);
})

recorderManager.onError ((res) => {
  console.log("error", res);
});

Page({

  /**
   * 页面的初始数据
   */
  data: {
    msg: ""
  },
  // 按下按钮的时候触发
  startrecorderHandel() {
    // 开始录音
    recorderManager.start({
    });
  },
  // 松开按钮的时候触发-发送录音
  sendrecorderHandel() {
    // 结束录音
    recorderManager.stop();
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    wx.authorize({
      scope: 'record'
    })
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {

  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {

  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {

  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {

  }
})

PHP后台代码

<?php
/**
 * 调用百度AI识别语音
 */
require_once "aip-speech-php-sdk-1.6.0/AipSpeech.php";

$name = "./upload/".$_FILES['recordFile']['name'];
if(move_uploaded_file($_FILES['recordFile']['tmp_name'],$name)){

    $APP_ID = '你的$APP_ID';//你的$APP_ID
    $API_KEY = '你的$API_KEY';//你的$API_KEY
    $SECRET_KEY = '你的$SECRET_KEY';//你的$SECRET_KEY

    $client = new AipSpeech($APP_ID, $API_KEY, $SECRET_KEY);
    $res = $client->asr(file_get_contents($name), 'm4a', 16000, array(
        'dev_pid' => 1536,
    ));
    echo json_encode($res);
    die();
}else{
    echo "上传失败";
}

die();
2018-11-24 09:38:23 weixin_34026484 阅读数 490

前言

为了参加某个作秀活动,研究了一波如何结合小程序、科大讯飞实现语音录入、识别的实现。科大讯飞开发文档中只给出 Python 的 demo,并没有给出 node.js 的 sdk,但问题不大。本文将从小程序相关代码到最后对接科大讯飞 api 过程,一步步介绍,半个小时,搭建完成小程序语音识别功能!不能再多了!当然,前提是最好掌握有一点点小程序、node.js 甚至是音频相关的知识。

架构先行

架构比较简单,大伙儿可以先看下图。除了小程序,需要提供 3 个服务,文件上传、音频编码及对接科大讯飞的服务。

node.js 对接科大讯飞的 api,npm 上已经有同学提供了 sdk,有兴趣的同学可以去搜索了解一下,笔者这里是直接调用了科大讯飞的 api 接口。

撸起袖子加油干

1、创建小程序

鹅厂的小程序文档非常详细,在这里笔者就不对如何创建一个小程序的步骤进行详细阐述了。有需要的同学可以查看鹅厂的小程序开发文档

1.1 相关代码

我们摘取小程序里面,语音录入和语音上传部分的代码。

// 根据wx提供的api创建录音管理对象
const recorderManager = wx.getRecorderManager();

// 监听语音识别结束后的行为
recorderManager.onStop(recorderResponse => {
    // tempFilePath 是录制的音频文件
    const { tempFilePath } = recorderResponse;

    // 上传音频文件,完成语音识别翻译
    wx.uploadFile({
        url: 'http://127.0.0.1:7001/voice', // 该服务在后面搭建。另外,小程序发布时要求后台服务提供https服务!这里的地址仅为开发环境配置。
        filePath: tempFilePath,
        name: 'file',
        complete: res => {
            console.log(res); // 我们期待res,就是翻译后的内容
        }
    });
});

// 开始录音,触发条件可以是按钮或其他,由你自己决定
recorderManager.start({
    duration: 5000 // 最长录制时间
    // 其他参数可以默认,更多参数可以查看https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.start.html
});
复制代码

2、搭建文件服务器

步骤 1 代码中提到了一个 url 地址大家应该都还记得。

http://127.0.0.1:7001/voice
复制代码

小程序本身还并没有提供语音识别的功能,所以在这里我们需要借助于“后端”服务的能力,完成我们语音识别翻译的功能。

2.1 egg.js 服务初始化

我们使用 egg.js 的 cli 快速初始化一个工程,当然你也可以使用 express、koa、kraken 等等框架,框架的选型在此不是重点我们就不做展开阐述了。对 egg.js 不熟悉的同学可以查看egg.js 的官网

npm i egg-init -g
egg-init voice-server --type=simple
cd voice-server
npm i
复制代码

安装完成后,执行以下代码

npm run dev
复制代码

随后访问浏览器http://127.0.0.1:7001应该可以看到一个Hi, egg 的页面。至此我们的服务初始化完成。

2.2 文件上传接口

a) 修改 egg.js 的文件上传配置

打开 config/config.default.js,添加以下两项配置

module.exports = appInfo => {
    ...
    config.multipart = {
        fileSize: '2gb', // 限制文件大小
        whitelist: [ '.aac', '.m4a', '.mp3' ], // 支持上传的文件后缀名
    };

    config.security = {
        csrf: {
            enable: false // 关闭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));
        }
        // 到这里就完成了文件上传。如果你不需要文件落地,也可以在后续的操作中,直接使用stream操作文件流

        ...
        // 音频编码
        // 科大讯飞语音识别
        ...
    }
}
复制代码

c) 最后一步,新增路由规则

写完 controller 之后,我们依据 egg.js 的规则,在 router.js 里面新增一个路由。

module.exports = app => {
    const { router, controller } = app;
    router.get('/', controller.home.index);
    router.get('/voice', controller.voice.translate);
};
复制代码

OK,至此你可以测试一下从小程序录音,录音完成后上传到后台文件服务器的完整流程。如果没问题,那恭喜你你已经完成了 80%的工作了!

3、音频编码服务

在上文中,小程序录音的方法 recorderManager.start 的时候我们提及到了“更多参数”。其中有一个参数是 format,支持 aac 和 mp3 两种(默认是 aac)。然后我们查阅了科大讯飞的 api 文档,音频编码支持“未压缩的 pcm 或 wav 格式”。

什么 aac、pcm、wav?emmm.. OK,我们只是前端,既然格式不对等,那只需要完成 aac -> pcm 转化即可,ffmpeg 立即浮现在笔者的脑海里。一番搜索,命令大概是这样子的:

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 来实现上述命令。

3.1 引入相关依赖包

npm i ffmpeg-static
npm i fluent-ffmpeg
复制代码

3.2 创建一个编码服务

在 app/service 文件夹中,创建 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);

        // 方便测试,我们将转码后文件落地到磁盘
        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 调用 ffmpegService,获得 pcm 文件

回到 app/controller/voice.js 文件中,我们在文件上传完成后,调用 ffmpegService 提供的 aac2pcm 方法,获取到 pcm 文件的路径。

// app/controller/voice.js
...
async translate() {
    ...
    ...
    const pcmPath = await this.ctx.service.ffmpeg.aac2pcm(voicePath);
    ...
}
...
复制代码

4、对接科大讯飞 API

首先,需要到科大讯飞开放平台注册并新增应用、开通应用的语音听写服务。

我们再写一个服务,在 app/service 文件夹下创建 xfyun.js 文件,实现 XFYunService 继承于 egg.js 的 Service。

4.1 引入相关依赖

npm i axios // 网络请求库
npm i md5 // 科大讯飞接口中需要md5计算
npm i form-urlencoded // 接口中需要对部分内容进行urlencoded
复制代码

4.2 XFYunService 实现

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'; // 在科大讯飞控制台上可以查到服务的APIKey
const API_ID = 'xxxxx'; // 同样可以在控制台查到

class XFYunService extends Service {
    async voiceTranslate(voicePath) {
        // 继上文,暴力的读取文件
        let data = fs.readFileSync(voicePath);
        // 将内容进行base64编码
        data = new Buffer(data).toString('base64');
        // 进行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 调用 XFYunService,完成语音识别

再次回到 app/controller/voice.js 文件中,我们在 ffmpeg 转码完成后,调用 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;
    }
}
...
复制代码

至此我们完成语音识别的代码编写。主要流程其实很简单,通过小程序录入语音文件,上传到文件服务器之后,通过 ffmpeg 获取到 pcm 文件, 最后再转发到科大讯飞的 api 接口进行识别。

附上项目代码:speech-recognizer

以上,如有错漏,欢迎指正!

@Author: _Jay

微信小程序 语音识别开发

博文 来自: ckzhouzhe
没有更多推荐了,返回首页