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 相关文章推荐
fancybox modal的完美解决(右上的X)
Oct 30 Javascript
js随机颜色代码的多种实现方式
Apr 23 Javascript
js中top的作用深入剖析
Mar 04 Javascript
使用原生js写的一个简单slider
Apr 29 Javascript
使用JavaScript获取电池状态的方法
May 03 Javascript
简述JavaScript的正则表达式中test()方法的使用
Jun 16 Javascript
Javascript封装id、class与元素选择器方法示例
Mar 13 Javascript
vue中如何使用ztree
Feb 06 Javascript
vue父组件向子组件传递多个数据的实例
Mar 01 Javascript
JavaScript ES6常用基础知识总结
Feb 09 Javascript
微信小程序实现可长按移动控件
Nov 01 Javascript
基于vue实现微博三方登录流程解析
Nov 04 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
欧美媒体选出10年前最流行的17部动画
2017/01/18 日漫
人大复印资料处理程序_查询篇
2006/10/09 PHP
PHP令牌 Token改进版
2008/07/18 PHP
PHP 读取大文件的X行到Y行内容的实现代码
2013/06/24 PHP
php使用Jpgraph绘制简单X-Y坐标图的方法
2015/06/10 PHP
php通过会话控制实现身份验证实例
2016/10/18 PHP
简单的js分页脚本
2009/05/21 Javascript
使用jquery mobile做幻灯播放效果实现步骤
2013/01/04 Javascript
js简易namespace管理器 实例代码
2013/06/21 Javascript
如何书写高质量jQuery代码(使用jquery性能问题)
2014/06/30 Javascript
Bootstrap精简教程
2015/11/27 Javascript
jQuery实现为LI列表前3行设置样式的方法【2种方法】
2016/09/04 Javascript
Underscore之Array_动力节点Java学院整理
2017/07/10 Javascript
JS中LocalStorage与SessionStorage五种循序渐进的使用方法
2017/07/12 Javascript
JS返回页面时自动回滚到历史浏览位置
2018/09/26 Javascript
过滤器vue.filters的使用方法实现
2019/09/18 Javascript
JavaScript冒泡算法原理与实现方法深入理解
2020/06/04 Javascript
vue2和vue3的v-if与v-for优先级对比学习
2020/10/10 Javascript
[15:20]DOTA2亚洲邀请赛总决赛开幕式表演:羽泉献唱
2017/04/05 DOTA
[28:05]完美世界DOTA2联赛循环赛Inki vs DeMonsTer 第一场 10月30日
2020/10/31 DOTA
在Python中的Django框架中进行字符串翻译
2015/07/27 Python
详解Python 协程的详细用法使用和例子
2018/06/15 Python
关于python中密码加盐的学习体会小结
2019/07/15 Python
python爬虫解决验证码的思路及示例
2019/08/01 Python
python 使用递归回溯完美解决八皇后的问题
2020/02/26 Python
python 实用工具状态机transitions
2020/11/21 Python
英国高端食品和葡萄酒超市:Waitrose
2016/08/23 全球购物
美国珠宝网上商店:Jeulia
2016/09/01 全球购物
马来西亚网上花店:FlowerAdvisor马来西亚
2020/01/03 全球购物
小学庆六一活动方案
2014/02/28 职场文书
预备党员承诺书
2014/03/25 职场文书
暑假社会实践心得体会
2014/09/02 职场文书
2014年培训工作总结范文
2014/11/27 职场文书
计划生育个人总结
2015/03/02 职场文书
详解Spring Security中的HttpBasic登录验证模式
2022/03/17 Java/Android
postgreSQL数据库基础知识介绍
2022/04/12 PostgreSQL