最近接受了一个新的需求,希望制作一个基于微信的英语语音评价页面。即点击录音按钮,用户录音说出预设的英文,根据用户的发音给出对应的评价。
以下是简单的Demo:
最近接受了一个新的需求,希望制作一个基于微信的英语语音评价页面。即点击录音按钮,用户录音说出预设的英文,根据用户的发音给出对应的评价。
以下是示例二维码,使用微信扫一扫即可查看:
功能列表
- ☑ 录音
- ☑ 录音动画
- ☑ 录音播放
- ☑ 英语语音评价(部分实现)
- ☑ 只允许微信客户端打开
零 技术选型
录音方面
可供选择的方案有两个:
使用HTML5接口 -getUserMedia()
;- 微信开放平台-
微信JS-SDK
;
由于主要应用场景是在移动端,此API只能在iOS 11+
与Android 5-6.X
及以上可用,兼容性感人,故舍弃此方案。所以此次录音实现基于微信开放平台提供的微信JS-SDK
。
英语语音识别
因为主要是基于微信平台,所以要求语音识别平台需要提供Web Api。
语音识别方面功能,主要有两种技术路线。
- 专门着力于语音识别及相关产业的技术平台,例如
讯飞
以及调研中发现的驰声
。
优势:专业并且提供语音评测相关功能;
劣势:花费昂贵; - AI开放平台,因为各大厂商布局AI,免费提供语音识别相关的接口。
优势:免费,API清晰;
劣势:并非为专门为教育定制,无语音评测功能;
结合项目的实际情况,决定使用第二种方案。(主要是因为经费有限……)
大厂提供的免费方案主要有:
- 百度AI
限制:50000次/天免费
格式支持:pcm(不压缩)、wav(不压缩,pcm编码)、amr(压缩格式);固定16k 采样率; 腾讯AI开发平台
语音参数:必须符合16k或8K采样率、16bit采样位数、单声道
语音格式:PCM、WAV、AMR、 SILK
其他:目前只支持汉语腾讯云智能语音服务
语音参数:必须符合16k或8K采样率、16bit采样位数、单声道
语音格式:通用标准格式,例如 mp3,wma,wav 等微信公众平台AI开放接口
语音参数:16k,单声道,最大1M
语音格式:mp3- 微信公众平台JS-SDK智能接口
其他:目前只支持汉语
大厂竞争果然系列,大鹅厂光语音服务肉眼可见的就折腾了这么多。(大雾)
经过一番折腾,最终可以形成两种方案:
微信JS-SDK音频接口录音
->上传到微信临时素材
->下载到服务器
->转换录音文件格式
->百度AI语音识别返回结果
->与预置的文件比对
->返回比对结果
优势:识别结果准
劣势:慢(因为无法直接获取用户的录音,需要从微信公众平台的临时素材
中转,且录音文件格式与百度AI可识别格式不一致,所以整个流程下来太慢);微信公众号需要企业认证
其他:至于为什么不选用腾讯系列,因为腾讯系列语音服务没有调通。。。微信JS-SDK音频接口录音
->调用JS-SDK智能接口返回识别结果
->结果转为拼音
->与预置的文件比对
->返回比对结果
优势:返回结果迅速、方法简单
劣势:识别结果不太准确(因为JS-SDK智能接口
不只是单单根据语音直接转换,还会在结果的基础上进行一定程度的联想,话说为啥不能增加个语言选择参数。)
本次整个方案使用方案2。
一 微信JS-SDK环境准备
写在前边:此处的开发环境不是指本地的开发环境,单指使用微信JS-SDK
所需完成的一系列的获取AccessToken
、jsapi_ticket
等前置条件。
开发环境
云服务器:腾讯云 · 小程序(特价,买了个折腾)
后台语音:PHP · CodeIgniter(小程序PHP样例使用CI框架)
1)公众号配置
前置的公众号申请等就不再赘述,如果要正常使用微信JS-SDK
的功能,需要在公众号配置一些内容。
配置IP白名单
通过微信公众平台 开发 -> 基本配置 -> IP白名单 进行设置,将开发环境的IP配置到IP白名单。
注1. 如果不配置白名单将无法获取access_token
,并在返回结果中返回40164
错误;
注2. 因为是在腾讯云 · 小程序
主机开发环境
下折腾的,该环境如果一周不更新新的代码会暂时关闭,IP也会发生变化,所以建议每周更新一下代码;
配置JS接口安全域名
通过微信公众平台 设置 -> 公众号设置 -> 功能设置 -> JS接口安全域名 进行设置,将JS接口安全域名写入。
注1. 一个公众号最多可以配置3个安全域名,需使用字母、数字及“-”的组合,不支持IP地址、端口号及短链域名,且域名必须经过备案;
注2. 需要将MP_verify_qEwAJiPuWerKftkO.txt
(可在配置JS接口安全域名处自行下载)放到配置域名的根目录,并确保可以访问到。腾讯云 · 小程序
默认样例使用的CI框架,需要放到\server
下;
注3. 如不配置JS接口安全域名,则无法成功调用JS-SDK;
2)获取access_token
access_token
是公众号的全局唯一接口调用凭据,调用公众号的各个接口时都需要使用。获取access_token
需要appid
与appsecret
。微信公众平台的access_token
有效期为7200s (2小时)
,每天最高可调用上限为2000次。因此获取access_token
需要做到:
- 定时刷新(刷新间隔大于1分钟,小于120分);
- 全局缓存
access_token
;
Show me the code
1 | class className extends CI_Controller { |
Talk is cheap
- 因为使用的是CI框架,将文件写到
server\application\controllers\
下可直接通过域名+文件名
访问到该接口,默认执行文件中的index
中的方法; - 代码中的基本逻辑通过
build_access_token()
方法获取access_token
,并存储到wxtken.txt
,通过read_token()
方法读取access_token
;
获取access_token的详细情况见官方API。
3)获取jsapi_ticket
jsapi_ticket
是公众号用于调用微信JS接口的临时票据,通过access_token来获取。微信公众平台的jsapi_ticket
有效期为7200s (2小时)
,每天最高可调用上限为1000000次。因此同样在全局缓存。
Show me the code
1 | public function get_jsapi_ticket() { |
Talk is cheap
- 写到跟获取
access_token
同一文件中,以便同时刷新; - 同之前的代码中逻辑类似,通过
get_jsapi_ticket()
方法获取jsapi_ticket
,并存储到wxjsapiTicket.txt
,通过read_jsapi_ticket()
方法读取jsapi_ticket
;
获取access_token的详细情况见官方API。
4)刷新access_token及jsapi_ticket
由于微信公众平台的access_token
与jsapi_ticket
有两个小时有效期,故需要定期刷新。基本思路有如下三个:
PHP定时执行任务;服务器定时任务;- 定时访问URL;
1.PHP定时执行任务
主要使用死循环,执行一次时间,使用sleep()
函数休眠一段时间,如下代码:
1 | ignore_user_abort();//即使Client断开(如关掉浏览器),PHP脚本也可以继续执行. |
缺点:缺点严重,启动之后,无法控制。而且一直消耗服务器资源,容易被杀死;
2.服务器定时任务
windows平台的计划任务或者是Unix平台的Crontab
都有定时执行php脚本或者访问URL的方法,但是由于使用的腾讯云 · 小程序
使用的是Wafer
一体化解决方案,无法直接访问远端服务器,故此方法放弃。
3. 定时访问URL
我们这次定时刷新access_token
及jsapi_ticket
采用的就是此方法,腾讯云平台
,有个免费的功能云拨测
可定时访问某个URL,并且在无法访问时,将预警信息发送给某个设定好的用户组。
将我们之前写好的获取access_token
及jsapi_ticket
方法,写到index()方法下,将URL填到拨测地址中,定时刷新,搞定。
注1. 云拨测最长的周期为半个小时,而且每次拨测可能访问地址5-6次,其实更稳妥的方法是在数据库中设置标志位,防治过度刷新,但是每天2000次的限额完全够用,就暂时未做此功能。
5)生成JS-SDK配置信息
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用,配置信息需要的参数如下:
1 | wx.config({ |
其中的appId
、jsApiList
已知,timestamp
、nonceStr
动态生成,signature
由算法生产。其中关于signature
的算法官方API描述如下:
签名算法
签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
即signature=sha1(string1)。 示例:
noncestr=Wm3WZYTPz0wzccnW
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
timestamp=1414587457
url=http://mp.weixin.qq.com?params=value步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value
步骤2. 对string1进行sha1签名,得到signature:
0f9de62fce790f9a083d5c99e95740ceb90c27ed
注意事项
- 签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
- 签名用的url必须是调用JS接口页面的完整URL。
- 出于安全考虑,开发者必须在服务器端实现签名的逻辑。
Show me the code
1 | public function get_signpackage(){ |
Talk is cheap
- 一定要注意,签名用的url必须是调用JS接口页面的完整URL,这里通过前端POST获取调用页的URL;
- 返回值为json格式,前端通过ajax获取;
- 因为采用了CI框架,前端ajax请求地址为
域名
/weapp
/此段代码的文件名
/get_signpackage
。
微信JS-SDK说明见官方API。
至此,使用微信公众平台JS-SDK的前置条件均已准备完毕。
二 实现录音按钮动画
基本的交互逻辑如下图演示:
此处来实现长按录音按钮的动画。基本思路是:
- 通过CSS3的
transition
属性实现record突变的平滑变小、平滑变大; - 通过CSS3的
keyframes
动画与伪类配合完成环形进度动画;
Show me the code
1 | <div class="content"> |
Talk is cheap
录音按钮动画原理如上图分层,其中:before
层添加动画为旋转180度,时间为30s,与此同时:after
层添加动画为旋转360度,时间为60s,即前30s两个图层同时旋转,当30s后:after
层继续旋转,:before
层保持位置不变,使整个右侧环形显示。.cover
层添加动画为前30s覆盖整个左侧,后30s隐藏。 整个动画由最顶部.icon
覆盖,使整个动画过程显示为一个环形。
三 实现录音及录音播放功能
开始是实现录音及播放的相关功能。主要流程是引入JS文件
、通过config接口注入权限验证配置
、通过ready接口处理成功验证
、撰写录音代码逻辑
、撰写录音播放代码逻辑
。
1)引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js
Show me the code
1 | requirejs.config({ |
Talk is cheap
- 此次使用AMD模式
requirejs
引入相关文件; - 这里引入多个文件,之后的代码需要使用;
注1. 支持使用 AMD/CMD 标准模块加载方法加载,也支持直接使用直接引用;
注2. 调用之前需要完成配置JS接口安全域名
。
2)通过config接口注入权限验证配置
通过ajax请求之前完成的生成JS-SDK配置信息
接口,获取到相关的配置内容,另外jsApiList
接口列表需要根据业务需求自行添加。
Show me the code
1 | $.ajax({ |
Talk is cheap
- 用
post
传入当前页面URL,因为签名算法必须是使用调用页的地址; - 此次功能只用到如代码中的几个API,更多API详见官方API;
3)通过ready接口处理成功验证
1 | wx.ready(function(){ |
4)撰写录音代码逻辑
创建一个对象R,封装录音、播放以及翻译等过程。监听录音按钮的touchstart
事件启动录音,监听touchend
时间结束录音。
Show me the code
1 | /* Javascript Code*/ |
1 | /* CSS Code*/ |
Talk is cheap
- 构建了
insertContent()
方法构建页面,使用scrollTop()
方法使填充的新的对话框出现再最下边; - 构建了
spoint
与epoint
两个参数,判断录音时间; - 构建
recode()
方法,使用setInterval()
方法,限制录音超过60s后停止(因为微信JS-SDK限制录音时长最多为60s); - 构建
feedback
参数,为之后翻译提供反馈; - 使用伪类实现对话前后的音频时长;
已知兼容性问题
- 部分华为手机,长按后弹出弹出菜单,检测
document
的oncontextmenu
事件,阻止默认事件e.preventDefault()
; - 微信开发者工具调试时,超过60s后会因为
alert()
会触发一次touchend
事件,真正抬手后又会触发一次touchend
,真机运行时无此问题;
5)撰写录音播放代码逻辑
在构建页面时将localid写到对应对话语句中,通过该localid对应相应的录音。
Show me the code
1 | $(document).on('touchstart', '.iPlayVoice', function() { |
Talk is cheap
- 使用
$(document).on('touchstart', '.iPlayVoice', function() {})
为.iPlayVoice
动态绑定事件; - 使用
playing
类名,控制播放时的状态;
四 实现语音评价功能
开篇的技术选型时已经将前因后果说明了。现在就写借助微信JS-SDK
中的wx.translateVoice()
方法实现语音评价功能的具体实现。具体流程为引入示例json
、获取语音翻译结果
、语音结果转为拼音
、结果比对
、反馈评价
。
1)引入示例json
将示例的数据写成json,用requirejs
引入。
Show me the code
1 | var word = { |
Talk is cheap
content
数据项,标识的是引导语;matched
项标识的是匹配内容,通过“,”分隔多个匹配内容,以提高匹配度;
2)获取语音翻译结果
Show me the code
1 | wx.translateVoice({ |
Talk is cheap
翻译接口主要依靠localId
来完成一系列的工作,成功后返回一段json格式的数据。
3)语音结果转为拼音
此步骤主要将返回的内容转换成拼音。借助的是@sxei(小茗同学)的一个库,地址为github。
因为只需要转换成无声掉的拼音,那么只需要引入pinyin_dict_notone.js
与pinyinUtil.js
两个文件,使用pinyinUtil.getPinyin('')
方法将汉字转化成拼音。
4)结果比对
比对语音翻译的拼音与预置的信息的拼音进行比对,返回匹配程度。因为预置的结果有多个,取其中匹配程度最高的的一项。
Show me the code
1 | var str_User = pinyinUtil.getPinyin(res.translateResult.split("。")[0]), |
Talk is cheap
- 返回的json数据,返回结果的key为translateResult;
- 返回的结果有“。”,故需要使用
res.translateResult.split("。")[0]
将“。”排除; - 使用了三个自定义方法,
strSimilarity2Percent()
返回匹配程度、Trim()
排除字符串中的空格、arrayMax()
返回数组中的最大值。相关方法存放在unit.js
中;
1 | /** |
5)反馈评价
根据评价结果的情况,分为三档:
matchedNum >= 0.8 ———-
great
0.8 > matchedNum >= 0.6 –good
matchedNum < 0.6 ———–normal
同时在此时对小于0.5s的录音予以忽略。
Show me the code
1 | translate: function() { //结束录音并识别语音 |
Talk is cheap
使用parseInt(Math.random() * 5)
生成随机数,使反馈语随机生成;
五 限制只允许微信客户端打开
检测客户端版本的micromessenger
值,微信用的是浏览器内核是这个。
Show me the code
1 | /** |
Talk is cheap
判断如果是微信浏览器,对对象R
进行初始化,如果不是,返回请在微信客户端打开;
总结
絮絮叨叨终于总结好了。过段时间用小程序对该功能进行重写。