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 动态数据下的锚点错位问题解决方法
Dec 24 Javascript
jquery 可拖拽的窗体控件实现代码
Mar 21 Javascript
JS维吉尼亚密码算法实现代码
Nov 09 Javascript
jQuery '行 4954 错误: 不支持该属性或方法' 的问题解决方法
Jan 19 Javascript
jquery toolbar与网页浮动工具条具体实现代码
Jan 12 Javascript
jQuery实现的淡入淡出二级菜单效果代码
Sep 15 Javascript
AngularJS入门教程之数据绑定用法示例
Nov 01 Javascript
Vue.js组件tree实现无限级树形菜单
Dec 02 Javascript
Angular的$http的ajax的请求操作(推荐)
Jan 10 Javascript
bootstrap中的 form表单属性role="form"的作用详解
Jan 20 Javascript
vue.js的提示组件
Mar 02 Javascript
详解angular2.x创建项目入门指令
Oct 11 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
帅气的琦玉老师
2020/03/02 日漫
PHP页面间传递参数实例代码
2008/06/05 PHP
php 3行代码的分页算法(求起始页和结束页)
2009/10/21 PHP
那些年一起学习的PHP(一)
2012/03/21 PHP
PHP删除数组中的特定元素的代码
2012/06/28 PHP
PHP两种去掉数组重复值的方法比较
2014/06/19 PHP
用Div仿showModalDialog模式菜单的效果的代码
2007/03/05 Javascript
javascript温习的一些笔记 基础常用知识小结
2011/06/22 Javascript
Jquery Change与bind事件代码
2011/09/29 Javascript
JS的Document属性和方法小结
2013/09/17 Javascript
解析offsetHeight,clientHeight,scrollHeight之间的区别
2013/11/20 Javascript
浅谈JavaScript异常处理语句
2015/06/26 Javascript
页面内容排序插件jSort使用方法
2015/10/10 Javascript
JavaScript Array对象详解
2016/03/01 Javascript
jQuery ajaxSubmit 实现ajax提交表单局部刷新
2016/07/04 Javascript
require、backbone等重构手机图片查看器
2016/11/17 Javascript
如何用JS/HTML将时间戳转换为“xx天前”的形式
2017/02/06 Javascript
JavaScript 用fetch 实现异步下载文件功能
2017/07/21 Javascript
说说AngularJS中的$parse和$eval的用法
2017/09/14 Javascript
javaScript实现游戏倒计时功能
2018/11/17 Javascript
CountUp.js实现数字滚动增值效果
2019/10/17 Javascript
[07:52]2014DOTA2 TI逗比武士游V社解说背后的故事
2014/07/10 DOTA
python使用KNN算法手写体识别
2018/02/01 Python
Python 常用模块 re 使用方法详解
2019/06/06 Python
python数组循环处理方法
2019/08/26 Python
python函数局部变量、全局变量、递归知识点总结
2019/11/15 Python
python如何实现图片压缩
2020/09/11 Python
HTML5中FileReader接口使用方法实例详解
2017/08/26 HTML / CSS
汽车驾驶求职信
2013/10/25 职场文书
后勤主管工作职责
2013/12/07 职场文书
学校岗位设置方案
2014/01/16 职场文书
七年级历史教学反思
2014/02/05 职场文书
禁烟标语大全
2014/06/11 职场文书
地方白酒代理协议书
2014/10/25 职场文书
入党积极分子党支部意见
2015/06/02 职场文书
家长反馈意见及建议
2015/06/03 职场文书