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 相关文章推荐
对google个性主页的拖拽效果的js的完整注释[转]
Apr 10 Javascript
javascript 类方法定义还是有点区别
Apr 15 Javascript
JavaScript 浮点数运算 精度问题
Oct 06 Javascript
使用JQuery进行跨域请求
Jan 25 Javascript
同一页面多个商品倒计时JS 基于面向对象的javascript
Feb 16 Javascript
jquery选择器-根据多个属性选择示例代码
Oct 21 Javascript
11种ASP连接数据库的方法
Sep 18 Javascript
基于jQuery的表单填充实例
Aug 22 jQuery
React注册倒计时功能的实现
Sep 06 Javascript
vue 点击按钮实现动态挂载子组件的方法
Sep 07 Javascript
有趣的JavaScript隐式类型转换操作实例分析
May 02 Javascript
JavaScript的一些小技巧分享
Jan 06 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
Windows下的PHP5.0详解
2006/11/18 PHP
约瑟夫环问题的PHP实现 使用PHP数组内部指针操作函数
2010/10/12 PHP
PHP里的单例类写法实例
2015/06/25 PHP
一波PHP中cURL库的常见用法代码示例
2016/05/06 PHP
php实现的网页版剪刀石头布游戏示例
2016/11/25 PHP
JavaScript 拾碎[三] 使用className属性
2010/10/16 Javascript
form表单中去掉默认的enter键提交并绑定js方法实现代码
2013/04/01 Javascript
原生js实现半透明遮罩层效果具体代码
2013/06/06 Javascript
js用正则表达式来验证表单(比较齐全的资源)
2013/11/17 Javascript
详解js界面跳转与值传递
2016/11/22 Javascript
bootstrap table实例详解
2017/01/06 Javascript
详解React-Native全球化多语言切换工具库react-native-i18n
2017/11/03 Javascript
webpack+vue+express(hot)热启动调试简单配置方法
2018/09/19 Javascript
Vue.js 中的 v-cloak 指令及使用详解
2018/11/19 Javascript
vue+elementUI实现表格关键字筛选高亮
2020/10/26 Javascript
小程序两种滚动公告栏的实现方法
2019/09/17 Javascript
用Javascript实现发送短信验证码间隔功能
2021/02/08 Javascript
[08:47]DOTA2每周TOP10 精彩击杀集锦vol.6
2014/06/25 DOTA
[54:33]2018DOTA2亚洲邀请赛小组赛 A组加赛 Liquid vs Optic
2018/04/03 DOTA
[26:52]LGD vs EG 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
python中readline判断文件读取结束的方法
2014/11/08 Python
python函数中return后的语句一定不会执行吗?
2017/07/06 Python
12个步骤教你理解Python装饰器
2019/07/01 Python
Python使用微信接入图灵机器人过程解析
2019/11/04 Python
Python PyQt5模块实现窗口GUI界面代码实例
2020/05/12 Python
通过实例解析Python RPC实现原理及方法
2020/07/07 Python
Python直接赋值及深浅拷贝原理详解
2020/09/05 Python
使用py-spy解决scrapy卡死的问题方法
2020/09/29 Python
css3的动画特效之动画序列(animation)
2017/12/22 HTML / CSS
检测浏览器对HTML5和CSS3支持度的方法
2015/06/25 HTML / CSS
学习十八大坚定理想信念心得体会
2014/03/11 职场文书
幼儿园中班评语大全
2014/04/17 职场文书
办理房产证委托书
2014/09/18 职场文书
2015年度个人思想工作总结
2015/04/08 职场文书
老干部局2015年度工作总结
2015/10/22 职场文书
2019通用版新员工入职培训方案!
2019/07/11 职场文书