专栏首页zhang微信小程序实时语音识别实践
原创

微信小程序实时语音识别实践

1.项目需求

将微信小程序移动端录音器采集到的音频流实时地翻译成文本

2.项目准备

前往注册

  • 微信开发者工具

前往下载

  • 腾讯云语音识别-实时语音识别API说明文档

参考文档

  • 腾讯云语音识别-实时语音识别 Node.js SDK

参考文档

3.项目演示

  • 搭建nodejs服务端

任意安装一款Linux发行版系统(安装过程略)

[root@zhang .nvm]# cat /etc/redhat-release 
CentOS release 6.9 (Final)

安装2.0版本以上的git客户端,如果你的系统是Centos发行版的,可以参考下面的安装演示;如果是其他发行版,可以参考git官网指引,通过简单的命令即可安装

非Centos发行版系统安装方式参考Git官方文档下载指引

Centos发行版系统(这里是Centos6.9)安装流程如下:

安装Git依赖包:

检查是否安装"Development Tools"软件组,若未安装则执行安装命令

[root@zhang tmp]# yum grouplist | grep "Development Tools"
[root@zhang tmp]# 
[root@zhang yum.repos.d]# yum  groupinstall "Development Tools" -y
[root@zhang yum.repos.d]# yum grouplist | grep "Development tools"
   Development tools

安装其他软件包(如果已安装了会提示已安装)

yum install zlib-devel -y
yum install perl-ExtUtils-MakeMaker -y
yum install asciidoc -y 
yum install xmlto -y
yum install openssl-devel -y 
yum install gcc -y 
yum install curl-devel -y 
yum install expat-devel -y 
yum install gettext-devel -y

卸载现有Git

[root@zhang git-2.0.5]# yum remove git -y

下载2.0版本的Git客户端,如果下载慢,可以用网速较好的机器下载后再上传到服务器中,下载后解压

[root@zhang tmp]# wget https://www.kernel.org/pub/software/scm/git/git-2.0.5.tar.gz
[root@zhang tmp]# ls -lh | grep git
drwxrwxr-x 19 root  root   12K Dec 19  2014 git-2.0.5
-rw-r--r--  1 root  root  4.7M Dec 19  2014 git-2.0.5.tar.gz

进入解压目录,三步编译安装法安装

软件配置与检查

[root@zhang git-2.0.5]# ./configure --prefix=/usr/local/git

编译成二进制文件

[root@zhang git-2.0.5]# make

安装编译后的文件到指定目录

[root@zhang git-2.0.5]# make install

将Git的运行程序路径配置到全局环境变量中(路径为"/usr/local/git/bin")

[root@zhang git-2.0.5]# vi /etc/profile
[root@zhang git-2.0.5]# cat /etc/profile | grep "export PATH="
export PATH=/usr/local/nginx/sbin:/usr/local/php/bin:/usr/local/mysql/bin:$PATH:/usr/local/git/bin

使得修改生效

[root@zhang git-2.0.5]# source /etc/profile
[root@zhang git-2.0.5]# 

查看git版本号

[root@zhang git-2.0.5]# git --version
git version 2.0.5

安装nvm

参考官方文档:https://github.com/nvm-sh/nvm/blob/master/README.md

[root@zhang git-2.0.5]# curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 13527  100 13527    0     0   5385      0  0:00:02  0:00:02 --:--:-- 12131
=> Downloading nvm from git to '/root/.nvm'
=> Cloning into '/root/.nvm'...
remote: Enumerating objects: 290, done.
remote: Counting objects: 100% (290/290), done.
remote: Compressing objects: 100% (257/257), done.
remote: Total 290 (delta 35), reused 97 (delta 20), pack-reused 0
Receiving objects: 100% (290/290), 163.27 KiB | 8.00 KiB/s, done.
Resolving deltas: 100% (35/35), done.
Checking connectivity... done.
=> Compressing and cleaning up git repository

=> Appending nvm source string to /root/.bashrc
=> Appending bash_completion source string to /root/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

在当前用户的环境变量配置文件"~/.bash_profile"或者全局环境变量配置文件"/etc/profile"中加入如下内容

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

[root@zhang ~]# vi ~/.bash_profile
[root@zhang ~]# tail -2f ~/.bash_profile 
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

重载环境变量

source ~/.bash_profile

测试nvm是否安装成功

[root@zhang ~]# nvm --version
0.35.3
[root@zhang ~]# 

安装Node.js 7.10.1 版本及以上

[root@zhang iai]# nvm install v10.6.0
Downloading and installing node v10.6.0...
Downloading https://nodejs.org/dist/v10.6.0/node-v10.6.0-linux-x64.tar.xz...
######################################################################## 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v10.6.0 (npm v6.1.0)
[root@zhang iai]# node -v
v10.6.0
  • 安装实时语音识别Node.js SDK

检测node版本,需要在Node.js 7.10.1 及以上

node -v

创建项目目录rvoice

mkdir rvoice

获取sdk下载链接

进入项目目录下载sdk

cd rvoice
wget https://sdk-1300466766.cos.ap-shanghai.myqcloud.com/realtime/nodejs_realtime_asr_sdk_v1.zip?_ga=1.82907414.1051517272.1595213062

解压

ls
nodejs_realtime_asr_sdk_v1.zip?_ga=1.82907414.1051517272.1595213062
unzip nodejs_realtime_asr_sdk_v1.zip\?_ga\=1.82907414.1051517272.1595213062 > /dev/null
ls -lh
total 2.1M
drwxr-xr-x 3 root root 4.0K Jul 31 10:18 __MACOSX
drwxr-xr-x 5 root root 4.0K Jul 10 17:27 nodejs_realtime_asr_sdk_v1
-rw-r--r-- 1 root root 2.1M Jul 16 02:50 nodejs_realtime_asr_sdk_v1.zip?_ga=1.82907414.1051517272.1595213062

安装依赖

cd nodejs_realtime_asr_sdk_v1
npm install 

 ls -lh
total 48K
drwxr-xr-x  2 root root 4.0K Jul 10 17:35 demo
-rwxr-xr-x  1 root root   78 Jul 10 17:27 index.js
-rwxr-xr-x  1 root root  12K Jul 10 17:27 LICENSE
drwxr-xr-x 50 root root 4.0K Jul 10 17:27 node_modules
-rwxr-xr-x  1 root root  471 Jul 10 17:27 package.json
-rwxr-xr-x  1 root root  13K Jul 10 17:27 package-lock.json
drwxr-xr-x  2 root root 4.0K Jul 10 17:27 tencentcloud
  • 配置服务端SSL证书

在实现Web功能之前,我们需要知道小程序的服务端只允许HTTPS协议的地址,所以我们应该通过nodejs的HTTPS模块来实现一个加密的Web服务,具体流程如下:

1)通过一个已经实名认证的腾讯云账号在控制台进入“SSL证书”控制台,点击【申请免费证书】为你的小程序服务端域名免费申请一个SSL加密证书

2)申请成功后下载证书文件压缩包

3)解压缩后进入到Nginx目录下

4)在项目目录rvoice下创建certificate目录和media目录(用于存放历史音频文件或者音频流文件)并配置权限755

cd rvoice
mkdir certificate
mkdir media
chmod 775 certificate/ media/
chmod 775  media/

5)上传Nginx目录下的两个证书文件到服务端的certificate目录下并重名为"server.key"、"server.crt"

cd rvoice/certificate
ls -lh
total 8.0K
-rw-r--r-- 1 root root 3.7K Apr 15 10:48 1_tencentcloud.cdhwdl.com_bundle.crt
-rw-r--r-- 1 root root 1.7K Apr 15 10:48 2_tencentcloud.cdhwdl.com.key
mv 1_tencentcloud.cdhwdl.com_bundle.crt server.crt
mv 2_tencentcloud.cdhwdl.com.key server.key
ls -lh
total 8.0K
-rwxr-xr-x 1 root root 3.7K Apr 15 10:48 server.crt
-rwxr-xr-x 1 root root 1.7K Apr 15 10:48 server.key
  • 实现实时语音识别的服务端Demo
cd rvoice
vi app.js
const fs = require("fs");
const path = require('path');
const https = require('https');

const privateKey  = fs.readFileSync(path.join(__dirname, './certificate/server.key'), 'utf8');
const certificate = fs.readFileSync(path.join(__dirname, './certificate/server.crt'), 'utf8');
const credentials = {key: privateKey, cert: certificate};
const httpsServer = https.createServer(credentials,function(req, res){
    let body = [];
    req.on('data', (chunk) => {
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        var json_ob = JSON.parse(body.trim());
        base64Data = json_ob.base64Buffer;
        seq=json_ob.seq;
        endFlag=json_ob.endFlag;
        var dataBuffer = Buffer.from(base64Data, 'base64');
        var t1 = new Date().getTime();
        var name = t1+".mp3";
        var localPath="./media/"+name
        fs.writeFile(localPath, dataBuffer, function(err) {
            if(err){
                json.Result=err;
                res.end(JSON.stringify(json));
            }
            //引入 SDK 和相关模块
//将 require 中路径替换为项目中 SDK 的真实路径
            const tencentcloud = require("./nodejs_realtime_asr_sdk_v1");
            const Asr = tencentcloud.asrRealtime;
            const Config = tencentcloud.config;


//Config实例的三个参数分别为 SecretId, SecretKey, appId。请前往控制台获取后修改下方参数
            let config = new Config("","",appid);



//设置接口需要参数,具体请参考 实时语音识别接口说明
            let query = {
                engineModelType : '16k_zh',
                resultTextFormat : 0,
                resType : 0,
                voiceFormat : 8,
                // cutLength : 50000,
            }
//创建调用实例
            const asrReq = new Asr(config, query);

//调用方式2:识别某个分片,test.wav 为示例分片
//发送请求时需要用户自行维护3个变量:voiceId:创建后保持不变; seq:递增; endFlag:前面为0,发送尾部分片的请求时设置为1
//需要将"本地文件地址"替换为用户需要识别的文件地址,例:'./test.wav'
            let filePathTestOne = path.resolve(localPath);
            let dataTest = fs.readFileSync(filePathTestOne);
            let vioceId = asrReq.randStr(16);


//发送识别请求,sendRequest 函数最后一个参数为请求返回时触发的回调,可根据业务修改
            asrReq.sendRequest(dataTest, vioceId, seq, endFlag, (error, response, data) => {
                if(error){
                    res.end(JSON.stringify(error));
                }

                res.end(JSON.stringify(JSON.parse(data).text));
            });

        });

    });

});



const SSLPORT = 8000;
httpsServer.listen(SSLPORT, '0.0.0.0', () => {});
  • 实现小程序客户端Demo

创建项目

配置页面

"pages/rvoice/rvoice",

编译生成页面

完善页面Demo

rvoice.js

// pages/rvoice/rvoice.js
const recorderManager = wx.getRecorderManager()  // 获取全局唯一的录音管理器 RecorderManager
const innerAudioContext = wx.createInnerAudioContext()  // 创建内部 audio 上下文 InnerAudioContext 对象。
var init  // 声明一个全局变量,let为局部变量
Page({  // 使用Page函数作为Page构造器来注册一个页面

  /**
   * 页面的初始数据
   */
  data: {
    voiceSize:0, // 音频的大小
    time: 0, // 初始时间
    duration: 60000, // 录音长时间为1分钟
    localFilePath: "",  //录音文件在本地的路径
    status: 0,  // 录音器的状态:开始1,暂停2,继续1,停止3
    actionStatus: 0, //录音播放状态,1为播放状态,0为未播放状态
    seq:0,//语音分片的序号,序号从 0 开始,每次请求递增1, 两个seq之间间隔不能超过6秒。
    endFlag:0, //是否为最后一片,最后一片语音片为 1,其余为 0。
    voiceData:"" //语音阶段数据
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function(options) {

  },

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

  },

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

  },

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

  },

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

  },

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

  },

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

  },

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

  },


  /**开始录音 */
  start: function() {
    var that=this
    clearInterval(init) // 取消之前的计时
    recorderManager.onStart((res) => {  // 监听录音开始事件
      this.setData({
        status: 1 // 录音开始状态为1
      })
    })

    recorderManager.onStop((res) => {  // 监听录音停止事件
      this.setData({
        tempFilePath: res.tempFilePath, // 如果录音停止了,修改本地文件地址
        status: 3
      })
      this.timeCounter(this.data.time) // 取消计时
    })

    const options = { //定义录音参数
      duration: this.data.duration,  // 录音时长 
      format: 'mp3', // 音频格式
      numberOfChannels: 2,
      encodeBitRate:48000,    
      frameSize: 20       
    }
    this.timeCounter()  // 开始计时
    recorderManager.start(options)
    
    recorderManager.onFrameRecorded((res) => {
      const {frameBuffer, isLastFrame} = res
      that.data.endFlag=isLastFrame ? 1 : 0
      const base64Buffer = wx.arrayBufferToBase64(frameBuffer)
      wx.request({
        url: 'https://tencentcloud.cdhwdl.com:8000', //仅为示例,并非真实的接口地址
        method:'post',
        data: {
          base64Buffer: base64Buffer,
          seq:that.data.seq,
          endFlag:that.data.endFlag
        },
        header: {
            'content-type': 'application/json' // 默认值
        },
        success (res) {
             that.setData({
              Words: that.data.voiceData + res.data,
      })
      that.data.voiceData=that.data.voiceData + res.data
        }
    })
  })
  },

  /**
   * 录音暂停
   */
  stop: function() {
    recorderManager.onPause(() => {
      this.setData({
        status: 2
      })
    })
    this.timeCounter(this.data.time) // 取消计时,暂时和停止都是取消计时
    recorderManager.pause() // 暂停录音
  },

  /**
   * 录音继续
   */
  continue: function() {
    this.setData({
      status: 1  // 标记为正在录音状态
    })
    this.timeCounter() // 在之前的计时基础上继续+1计时
    recorderManager.resume()  // 继续录音
  },

  /**
   * 录音停止
   */
  shutoff: function() {
    recorderManager.onStop((res) => {
      this.setData({
        tempFilePath: res.tempFilePath,  // 录音生成文件的本地路径
        status: 3  // 标记录音状态为停止
      })
    })
    this.timeCounter(this.data.time)   // 取消计时
    recorderManager.stop() // 停止录音

  },
  
   
  /**
   * 录音播放
   */
  play: function() {
    innerAudioContext.src = this.data.tempFilePath // 音频资源的地址,用于直接播放
    innerAudioContext.obeyMuteSwitch = false // 是否遵循系统静音开关,默认为 true,当此参数为 false 时,即使用户打开了静音开关,也能继续发出声音

    
    if (this.data.actionStatus == 0) {
      this.setData({
        actionStatus: 1
      })
      innerAudioContext.play()  // 播放音频
    }
  
    innerAudioContext.onEnded(() => {  //监听音频自然播放至结束的事件
      innerAudioContext.stop()  // 停止播放
      this.setData({
        actionStatus: 0
      })
    })
  },

  
  timeCounter: function(time) {  // 定义一个计时器函数
    var that = this
    if (time == undefined) {
   
      init = setInterval(function() { // 设定一个计时器ID。按照指定的周期(以毫秒计)来执行注册的回调函数
        var time = that.data.time + 1; // 每秒钟计时+1
        that.setData({
          time: time
        })
      }, 1000);
    } else {
      clearInterval(init) // 取消计时
      console.log("暂停计时")
    }
  },

  /**
   * 重新录制
   */
  again: function() {
    var that = this
    wx.showModal({  // 显示模态对话框
      title: "重新录音", //提示的标题 
      content: "是否重新录制?", //提示的内容
      success(res) {
        if (res.confirm) { // 点击了确定
          that.setData({  // 重置初始化数据
            time: 0, 
            tempFilePath: "", 
            status: 0,
            actionStatus: 0,
            Words: ""
          })
          innerAudioContext.stop() // 停止音频
        }
      }
    })
  }
})

rvoice.xml

<!--pages/rvoice/rvoice.wxml-->
<view class="REC">
  <view class="time">{{status==0?'录音时长':(status==3?'录音结束':'录音中')}}:{{time}} 秒 ({{duration/1000}}秒)</view>
  <view class="rin">
  <view class="{{status==3 && actionStatus==0?'show':'hide'}}" bindtap="play" hover-class="skip">{{actionStatus==1?'播放中':'播放录音'}}</view>
    <view class="{{status==3?'show':'hide'}}" bindtap="again" hover-class="skip">再次录制</view> 
  </view>
  <view class="anniu">
    <view class="{{status==0?'highlight':'gray'}}" bindtap="start" hover-class="skip">开始</view>
    <view class="{{status==1?'highlight':'gray'}}" bindtap="stop" hover-class="skip">暂停</view>
    <view class="{{status==2?'highlight':'gray'}}" bindtap="continue" hover-class="skip">继续</view>
    <view class="{{(status==1 || status==2)?'highlight':'gray'}}" bindtap="shutoff" hover-class="skip">停止</view>
  </view>
   <view class="progress">
    <progress percent="{{time*(100/(duration/1000))}}"  stroke-width="10" backgroundColor="#fff" border-radius="15" stroke-width="4" color="#7FFF00" active />
  </view>
</view>
<view class=".REC">
  <textarea placeholder="录音完成后点击识别可将音频转文字" auto-focus value="{{ Words }}" />
</view>

rvoice.wxss

/* pages/rvoice/rvoice.wxss */
.REC {
  border-radius: 25rpx;
  background-color: rgb( 199,237,204 );
  padding: 6rpx 0rpx;
  margin: 15rpx 35rpx;
}

.rin {
  justify-content: space-between;
  align-items: center;
  margin: 0rpx 120rpx;
  display: flex;
}

.rin .show {
  background-color: rgb(178, 228, 228);
  padding: 15rpx;
  width: 210rpx;
  border: 5rpx solid rgb(127, 204, 214);
  border-radius: 20rpx;
  font-size: 28rpx;
  display: flex;
  justify-content: center;
  align-items: center;
}

.rin .hide {
  padding: 15rpx;
  align-items: center;
  border-radius: 20rpx;
  display: flex;
  width: 215rpx;
  font-size: 28rpx;
  justify-content: center;
  border: 5rpx solid #eee;
  pointer-events: none;
  background-color: rgba(137, 190, 178, 0.445);
}

.time {
  text-align: center;
  line-height: 75rpx;
  font-size: 28rpx; 
}

.progress {
  margin: 25rpx;
}

.play {
  margin: 0rpx 25rpx;
}

.content {
  line-height: 60rpx;
  font-size: 28rpx;
  display: flex;
  justify-content: center;
}

.anniu {
  display: flex;
  margin: 10rpx 50rpx;
  justify-content: space-between;
}

.highlight {
  display: flex;
  font-size: 28rpx;
  width: 80rpx;
  height: 80rpx;
  justify-content: center;
  border-radius: 50%;
  align-items: center;
  background-color: rgb(107, 194, 53);
  border: 5rpx solid rgb(127, 204, 214);
}

.skip {
  transform: scale(0.9);
}



.anniu .gray {
  pointer-events: none;
  background-color: rgba(137, 190, 178, 0.445);
  display: flex;
  width: 80rpx;
  height: 80rpx;
  font-size: 28rpx;
  justify-content: center;
  align-items: center;
  border-radius: 50%;
  border: 5rpx solid rgb(241, 244, 245); 
}

rvoice.json

{
  "navigationBarTitleText": "实时语音识别在线测试",
  "backgroundColor": "#eeeeee"
}

  • 演示

后台启动服务端入口文件

nohup node app.js &

小程序侧编译后点击预览

微信扫描测试

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 实现一个前后端结构的语音识别小程序服务

    一、实现方式:通过录音管理器 RecorderManager调用手机的录音功能实现音频的在线获取,并将获取到的音频传入到服务端,服务端调用腾讯云“一句话识别”A...

    用户1529147
  • nodejs基础学习

    Node.js是一个开源的、跨平台的JavaScript运行时环境。它是一个流行的工具,几乎适用于任何类型的项目!

    用户1529147
  • 腾讯云智能语音小程序插件实现实时语音识别

    注意:此插件需要小程序的基础库版本在>= 2.10.0,可以通过如下方式查看您当前的小程序基础库版本

    用户1529147
  • 腾讯云TKE使用之ConfigMap挂载

    ConfigMap(Kubernetes简称cm)作为Kubernetes 提供的一种资源,可以将配置信息,跟业务镜像解耦开来,提高业务镜像的移植性。

    白鹏飞
  • linux sed指令详解

    新增多行内容,主要要是用到\或者回车(新增的内容使用单引号,如果要想使用回车来实现新增多行,注意另外一个单引号别写出来,否则就直接执行指令了)来新增多行内容

    我是李超人
  • 480. 二叉树的所有路径递归

    讲真我见到递归真的是害怕,也没办法讲,这也是参考的别人的答案,过两天再让我写我可能就写不出来了,这个看了看理解了一点点,就先放在这里吧,也许写的多了就懂了也不一...

    和蔼的zhxing
  • 腾讯云TKE使用之ConfigMap挂载

    ConfigMap(Kubernetes简称cm)作为Kubernetes 提供的一种资源,可以将配置信息,跟业务镜像解耦开来,提高业务镜像的移植性。

    白鹏飞
  • 文本溢出截断省略

    文本溢出截断省略是比较常见的业务场景,主要分为单行文本溢出截断省略与多行文本溢出截断省略,单行的截断方案比较简单,多行截断相对比较复杂。

    WindrunnerMax
  • Go中使用Seed得到重复随机数的问题

    可能不熟悉seed用法的看到这里会很疑惑,我不是都用了seed吗?为何我随机出来的数字都是一样的?不应该每次都不一样吗?

    SH的全栈笔记
  • Qtcreator调试时变量“无法访问”

    原文链接:https://blog.csdn.net/chyuanrufeng/article/details/861...

    acoolgiser

扫码关注云+社区

领取腾讯云代金券