30分钟快速实现小程序语音识别功能


Posted in Javascript onNovember 27, 2018

前言

为了参加某个作秀活动,研究了一波如何结合小程序、科大讯飞实现语音录入、识别的实现。科大讯飞开发文档中只给出 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 接口进行识别。

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
用JavaScript获取网页中的js、css、Flash等文件
Dec 20 Javascript
javascript实现的鼠标链接提示效果生成器代码
Jun 28 Javascript
JavaScript用JQuery呼叫Server端方法示例代码
Sep 03 Javascript
jquery的总体架构分析及实现示例详解
Nov 08 Javascript
快速学习jQuery插件 Cookie插件使用方法
Dec 01 Javascript
JavaScript实现九九乘法表的简单实例
Jun 07 Javascript
Javascript缓存API
Jun 14 Javascript
JavaScript对象的浅拷贝与深拷贝实例分析
Jul 25 Javascript
使用JS获取页面上的所有标签
Oct 18 Javascript
浅谈express.js框架中间件(middleware)
Apr 07 Javascript
ES6的解构赋值实例详解
May 06 Javascript
react 生命周期实例分析
May 18 Javascript
基于Koa2写个脚手架模拟接口服务的方法
Nov 27 #Javascript
Vue实现移动端左右滑动效果的方法
Nov 27 #Javascript
vue2.0移动端滑动事件vue-touch的实例代码
Nov 27 #Javascript
详解vuex 渐进式教程实例代码
Nov 27 #Javascript
解决vue 界面在苹果手机上滑动点击事件等卡顿问题
Nov 27 #Javascript
Node+OCR实现图像文字识别功能
Nov 26 #Javascript
图片文字识别(OCR)插件Ocrad.js教程
Nov 26 #Javascript
You might like
php5 and xml示例
2006/11/22 PHP
php下关于Cannot use a scalar value as an array的解决办法
2010/08/08 PHP
PHPExcel读取Excel文件的实现代码
2011/12/06 PHP
php另类上传图片的方法(PHP用Socket上传图片)
2013/10/30 PHP
wordpress安装过程中遇到中文乱码的处理方法
2015/04/21 PHP
表单提交错误后返回内容消失问题的解决方法(PHP网站)
2015/10/20 PHP
php插入含有特殊符号数据的处理方法
2016/11/24 PHP
php设计模式之组合模式实例详解【星际争霸游戏案例】
2020/03/27 PHP
游戏人文件夹程序 ver 4.03
2006/07/14 Javascript
用正则获取指定路径文件的名称
2007/02/27 Javascript
JQuery AJAX实现目录浏览与编辑的代码
2008/10/21 Javascript
Jquery优化效率 提升性能解决方案
2010/09/06 Javascript
前端必备神器 Snap.svg 弹动效果
2014/11/10 Javascript
jquery插件方式实现table查询功能的简单实例
2016/06/06 Javascript
js操作XML文件的实现方法兼容IE与FireFox
2016/06/25 Javascript
详解XMLHttpRequest(二)响应属性、二进制数据、监测上传下载进度
2016/09/14 Javascript
微信小程序 wxapp内容组件 icon详细介绍
2016/10/31 Javascript
详解js数组的完全随机排列算法
2016/12/16 Javascript
Javascript 链式作用域详细介绍
2017/02/23 Javascript
基于zepto.js实现登录界面
2017/10/09 Javascript
AngularJS下$http服务Post方法传递json参数的实例
2018/03/29 Javascript
Vue起步(无cli)的啊教程详解
2019/04/11 Javascript
理解JavaScript中的Proxy 与 Reflection API
2020/09/21 Javascript
python练习程序批量修改文件名
2014/01/16 Python
python集合用法实例分析
2015/05/30 Python
横向对比分析Python解析XML的四种方式
2016/03/30 Python
深入理解python中的select模块
2017/04/23 Python
python内置函数:lambda、map、filter简单介绍
2017/11/16 Python
Python实现批量修改图片格式和大小的方法【opencv库与PIL库】
2018/12/03 Python
Python对ElasticSearch获取数据及操作
2019/04/24 Python
提升Python效率之使用循环机制代替递归函数
2019/07/23 Python
Pycharm及python安装详细步骤及PyCharm配置整理(推荐)
2020/07/31 Python
德国内衣、泳装和睡衣网上商店:Bigsize Dessous
2018/07/09 全球购物
美国最值得信赖的宠物药房:Allivet
2019/03/23 全球购物
计算机专业自荐信
2014/05/24 职场文书
写给父母的感谢信
2015/01/22 职场文书