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 相关文章推荐
网页的标准,IMG不支持onload标签怎么办
Jun 29 Javascript
jquery常用技巧及常用方法列表集合
Apr 06 Javascript
json原理分析及实例介绍
Nov 29 Javascript
解析javascript 浏览器关闭事件
Jul 08 Javascript
JS禁用浏览器退格键实现思路及代码
Oct 29 Javascript
JQuery控制DIV的选取实现方法
Sep 18 Javascript
JavaScript使用简单正则表达式的数据验证功能示例
Jan 13 Javascript
Angular 4.x+Ionic3踩坑之Ionic 3.x界面传值详解
Mar 13 Javascript
JS实现自定义弹窗功能
Aug 08 Javascript
手动下载Chrome并解决puppeteer无法使用问题
Nov 12 Javascript
详解一个基于套接字实现长连接的express
Mar 28 Javascript
只有 20 行的 JavaScript 模板引擎实例详解
May 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
日本十大最佳动漫,全都是二次元的神级作品
2019/10/05 日漫
浅谈PDO的rowCount函数
2015/06/18 PHP
php开发工具有哪五款
2015/11/09 PHP
PHP中set_include_path()函数相关用法分析
2016/07/18 PHP
thinkphp5框架实现的自定义扩展类操作示例
2019/05/16 PHP
jquery的index方法实现tab效果
2011/02/16 Javascript
JavaScript调试技巧之console.log()详解
2014/03/19 Javascript
无刷新上传文件并返回自定义值
2015/06/11 Javascript
浅谈JavaScript中变量和函数声明的提升
2016/08/09 Javascript
Jquery组件easyUi实现手风琴(折叠面板)示例
2016/08/23 Javascript
json数据处理及数据绑定
2017/01/25 Javascript
基于JavaScript实现前端数据多条件筛选功能
2020/08/19 Javascript
JavaScript中的高级函数
2018/01/04 Javascript
不得不知的ES6小技巧
2018/07/28 Javascript
详解关于element el-button使用$attrs的一个注意要点
2018/11/09 Javascript
微信小程序3种位置API的使用方法详解
2019/08/05 Javascript
JS实现图片切换特效
2019/12/23 Javascript
Python的Flask框架中配置多个子域名的方法讲解
2016/06/07 Python
Python查找第n个子串的技巧分享
2018/06/27 Python
python代码实现逻辑回归logistic原理
2019/08/07 Python
python实现飞机大战小游戏
2019/11/08 Python
python 项目目录结构设置
2020/02/14 Python
python如何利用Mitmproxy抓包
2020/10/10 Python
英国乐购杂货:Tesco Groceries
2018/11/29 全球购物
Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?
2016/08/18 面试题
京剧自荐信
2014/01/26 职场文书
装饰工程师岗位职责
2014/06/08 职场文书
企业精神口号
2014/06/11 职场文书
教师群众路线剖析材料
2014/09/29 职场文书
2015年全国助残日活动方案
2015/05/04 职场文书
毕业生登记表班级意见
2015/06/05 职场文书
会计专业2019暑假实习报告
2019/06/21 职场文书
详解前端任务构建利器Gulp.js使用指南
2021/04/30 Javascript
PyTorch 如何自动计算梯度
2021/05/23 Python
python图像处理基本操作总结(PIL库、Matplotlib及Numpy)
2021/06/08 Python
Redis字典实现、Hash键冲突及渐进式rehash详解
2021/09/04 Redis