uniapp - 接入科大讯飞语音评测
Posted GitLqr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了uniapp - 接入科大讯飞语音评测相关的知识,希望对你有一定的参考价值。
欢迎关注微信公众号:FSA全栈行动 👋
一、简介
科大讯飞语音评测可以对字、词、句、篇章等题型进行多维度评分(准确度、流畅度、完整度、声韵调型等),支持中文和英文。最新的流式版使用 webSocket 调用接口,开发者可以边录音边上边音频数据(录音与评测同时进行),可以缩短用户等待评测结果的时间,大大提高用户体验。
科大讯飞语音评测提供了多种平台的 SDK 与 Demo,但是没有提供微信小程序版本的 SDK 与 Demo,不过,科大讯飞语音评测【WebApi】有提供浏览器版本的 Demo,理论上可以对它进行适当的改造,就能运行到微信小程序环境下,所以本文的主要内容就是对科大讯飞语音评测【WebApi】的浏览器版本 Demo 进行改造和封装。
温馨提示:着急伸手拿最终成品的,可以直接滚到 【文章末尾】 查看。
二、分析
- API 文档调用示例:https://www.xfyun.cn/doc/Ise/IseAPI.html#调用示例
- 语音评测流式 API demo js 语言:https://xfyun-doc.cn-bj.ufileos.com/static/16546792913730431/ise_ws_js_demo.zip
打开科大讯飞语音评测 API 文档页面,在 调用示例
章节处,找到 语音评测流式API demo js语言
并下载,解压后使用 vscode 打开,查看 index.js
,可以看到开头有如下注释:
// 1. websocket连接:判断浏览器是否兼容,获取websocket url并连接,这里为了方便本地生成websocket url
// 2. 获取浏览器录音权限:判断浏览器是否兼容,获取浏览器录音权限,
// 3. js获取浏览器录音数据
// 4. 将录音数据处理为文档要求的数据格式:采样率16k或8K、位长16bit、单声道;该操作属于纯数据处理,使用webWork处理
// 5. 根据要求(采用base64编码,每次发送音频间隔40ms,每次发送音频字节数1280B)将处理后的数据通过websocket传给服务器,
// 6. 实时接收websocket返回的数据并进行处理
// ps: 该示例用到了es6中的一些语法,建议在chrome下运行
通过这个注释说明,可以直观的了解到该 js demo 的大致流程,下面会按照注释里的顺序,逐个分析,并对应到微信小程序里的实现。
注意:因为本人使用的是 uniapp 开发,所以以下关于微信小程序代码实现的部分,
并非
传统的js + wxss + wxml
,而是vue3 + typescript
。
1、创建 webSocket
官方 demo 是运行在浏览器环境下的,而不同的浏览器对 webSocket 的创建方式不太一样,所以这里做兼容:
// 连接websocket
connectWebSocket()
return getWebSocketUrl().then(url =>
let iseWS
if ('WebSocket' in window)
iseWS = new WebSocket(url)
else if ('MozWebSocket' in window)
iseWS = new MozWebSocket(url)
else
alert('浏览器不支持WebSocket')
return
this.webSocket = iseWS
iseWS.onopen = e =>
...
iseWS.onmessage = e =>
...
iseWS.onerror = e =>
...
iseWS.onclose = e =>
...
)
微信小程序内创建 webSocket 就很简单了,使用 uni.connectSocket
即可:
/* socket相关 */
private socketTask: UniApp.SocketTask | null = null;
async connect()
const url = await this.getWebSocketUrl();
const newUrl = encodeURI(url);
this.socketTask = uni.connectSocket(
url: newUrl,
// 如果希望返回一个 socketTask 对象,需要至少传入 success / fail / complete 参数中的一个
complete: () => ,
);
this.socketTask.onOpen((res) =>
...
);
this.socketTask.onMessage((res) =>
...
);
this.socketTask.onError((err) =>
...
);
this.socketTask.onClose(() =>
...
);
注意:这里有一个坑,
uni.connectSocket()
如果不指定success/fail/complete
参数中的一个,则返回的不是 SocketTask,而是一个 Promise!我们需要的是 SocketTask,所以这里指定了一个空的 complete 回调函数。
- 论坛帖子:https://ask.dcloud.net.cn/question/63162
- 官方文档:https://uniapp.dcloud.net.cn/api/request/websocket.html
上述代码中有一处十分重要的函数调用,即 getWebSocketUrl()
,它负责生成科大讯飞语音评测【WebApi】的 webSocket 链接,涉及到参数加密:
import CryptoJS from "crypto-js";
import Base64 from "js/base64js.js";
/**
* 获取websocket url
* 该接口需要后端提供,这里为了方便前端处理
*/
function getWebSocketUrl()
return new Promise((resolve, reject) =>
// 请求地址根据语种不同变化
var url = "wss://ise-api.xfyun.cn/v2/open-ise";
var host = "ise-api.xfyun.cn";
var apiKeyName = "api_key";
var apiKey = API_KEY;
var apiSecret = API_SECRET;
var date = new Date().toGMTString();
var algorithm = "hmac-sha256";
var headers = "host date request-line";
var signatureOrigin = `host: $host\\ndate: $date\\nGET /v2/open-ise HTTP/1.1`;
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
var authorizationOrigin = `$apiKeyName="$apiKey", algorithm="$algorithm", headers="$headers", signature="$signature"`;
var authorization = btoa(authorizationOrigin);
url = `$url?authorization=$authorization&date=$date&host=$host`;
resolve(url);
);
js/base64js.js
是 demo 工程里 js 目录下的一个文件(采用 CommonJs 导入规范),而 crypto-js
是使用 npm 安装的一个第三方模块,理论上只需要将这 2 个模块直接拷贝或 npm 安装集成到项目中就好了,但是我的这个工程用的构建工具是 Vite
,只支持 ES 模块导入规范,并且项目中使用了 TypeScript,所以,为了更好的编码体验,这里替换为支持 ES 模块导入规范且支持 TypeScript 的另外 2 个模块(js-base64
、crypto-es
),与 demo 中的那 2 个模块功能完全相同:
// npm i crypto-es -S
// npm i js-base64 -S
import CryptoES from "crypto-es";
import Base64 from "js-base64";
/**
* @returns 生成wss链接
*/
protected getWebSocketUrl(): Promise<string>
if (this.apiKey === "" || this.apiSecret === "")
throw new Error("apiKey、apiSecret must not be empty !!!");
return new Promise<string>((resolve, reject) =>
// 请求地址根据语种不同变化
let url = "wss://ise-api.xfyun.cn/v2/open-ise";
const host = "ise-api.xfyun.cn";
const date = (new Date() as any).toGMTString();
const apiKeyName = "api_key";
const algorithm = "hmac-sha256";
const headers = "host date request-line";
const signatureOrigin = `host: $host\\ndate: $date\\nGET /v2/open-ise HTTP/1.1`;
const signatureSha = CryptoES.HmacSHA256(signatureOrigin, this.apiSecret);
const signature = CryptoES.enc.Base64.stringify(signatureSha);
const authorizationOrigin = `$apiKeyName="$this.apiKey", algorithm="$algorithm", headers="$headers", signature="$signature"`;
const authorization = Base64.encode(authorizationOrigin);
url = `$url?authorization=$authorization&date=$date&host=$host`;
resolve(url);
);
Date#toGMTString()
是一个过时的方法,在 TypeScript 中不被识别,从而导致编译不通过,除了像上述代码中通过强转 any
来规避外,还可以在 src/env.d.ts
文件中进行如下声明解决,interface Date toGMTString(): string;
,如果工程中频繁使用该方法的话,建议用第二种方法,这样就不必每处都写一次强转代码了。
注意:
getWebSocketUrl()
函数内在生成 webSocket url 时,会使用到API_KEY
和API_SECRET
,这 2 个参数需要你自己注册一个科大讯飞的开发者账号之后,在开发者账号后台获取,为了防止被别人盗用,这个 url 的生成逻辑应该放置到自己的业务后端去实现。
2、录音上下文与权限
录音上下文的创建,以及获取录音权限的方式在不同浏览器环境中各不相同,所以官方 Demo 中做了大量兼容判断:
// 初始化浏览器录音
recorderInit()
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
// 创建音频环境
try
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
this.audioContext.resume()
catch (e)
if (!this.audioContext)
alert('浏览器不支持webAudioApi相关接口')
return
// 获取浏览器录音权限
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
navigator.mediaDevices
...
.then(stream => getMediaSuccess(stream) )
.catch(e => getMediaFail(e) )
else if (navigator.getUserMedia)
navigator.getUserMedia(
...,
stream => getMediaSuccess(stream) ,
function(e) getMediaFail(e)
)
else
...
alert('无法获取浏览器录音功能,请升级浏览器或使用chrome')
this.audioContext && this.audioContext.close()
return
// 获取浏览器录音权限成功的回调
let getMediaSuccess = stream =>
console.log('getMediaSuccess')
// 创建一个用于通过javascript直接处理音频
this.scriptProcessor = this.audioContext.createScriptProcessor(0, 1, 1)
this.scriptProcessor.onaudioprocess = e =>
// 去处理音频数据
...
// 创建一个新的MediaStreamAudiosourceNode 对象,使来自MediaStream的音频可以被播放和操作
this.mediaSource = this.audioContext.createMediaStreamSource(stream)
// 连接
this.mediaSource.connect(this.scriptProcessor)
this.scriptProcessor.connect(this.audioContext.destination)
...
let getMediaFail = (e) =>
alert('请求麦克风失败')
this.audioContext && this.audioContext.close()
this.audioContext = undefined
...
而在微信小程序环境下,获取录音上下文与权限就简单多了,通过 uni.getRecorderManager()
即可获取录音上下文(管理器),然后调用 start(option)
时会自动询问用户是否授权录音权限:
const recordManager = uni.getRecorderManager();
/**
* 开始录音
*/
const startRecord = () =>
recordManager.onStart(() =>
console.log("recorder start");
...
);
recordManager.onPause(() =>
console.log("recorder pause");
);
recordManager.onStop((res) =>
// tempFilePath String 录音文件的临时路径
console.log("recorder stop", res);
...
);
recordManager.onError((err) =>
// errMsg String 错误信息
console.log("recorder err", err);
);
recordManager.onFrameRecorded((res) =>
// frameBuffer ArrayBuffer 录音分片结果数据
// isLastFrame Boolean 当前帧是否正常录音结束前的最后一帧
const frameBuffer = res;
...
);
recordManager.start(option);
;
3、获取录音数据
浏览器的录音数据是通过 scriptProcessor.onaudioprocess
回调函数获得的:
// 获取浏览器录音权限成功的回调
let getMediaSuccess = (stream) =>
...
this.scriptProcessor.onaudioprocess = (e) =>
// 去处理音频数据
transWorker.postMessage(e.inputBuffer.getChannelData(0));
;
...
;
微信小程序的录音数据是通过 recordManager.onFrameRecorded()
回调函数获得:
recordManager.onFrameRecorded((res) =>
// frameBuffer ArrayBuffer 录音分片结果数据
// isLastFrame Boolean 当前帧是否正常录音结束前的最后一帧
const frameBuffer = res;
pushAudioData(frameBuffer); // 将每一帧音频保存起来
);
4、录音数据格式
从上面的 语音评测接口要求
中可以知道,科大讯飞语音评测接口对录音数据的格式是有要求的:
内容 | 说明 |
---|---|
音频属性 | 采样率 16k、位长 16bit、单声道 |
音频格式 | pcm、wav、mp3(需更改 aue 的值为 lame)、speex-wb; |
音频大小 | 音频数据发送会话时长不能超过 5 分钟 |
官方 Demo 中,通过 scriptProcessor.onaudioprocess
回调拿到的录音数据是双声道的 PCM 数据,而接口要求的是单声道,所以通过代码 e.inputBuffer.getChannelData(0)
提取出第 1 个声道的数据,并交给 transWorker 去处理成 采样率 16k
、位长 16bit
的 PCM 数据:
// index.js
this.scriptProcessor.onaudioprocess = (e) =>
// 去处理音频数据
transWorker.postMessage(e.inputBuffer.getChannelData(0));
;
// transcode.worker.js
(function()
self.onmessage = function(e)
transAudioData.transcode(e.data)
let transAudioData =
transcode(audioData)
let output = transAudioData.to16kHz(audioData)
output = transAudioData.to16BitPCM(output)
...
,
to16kHz(audioData)
...
return newData
,
to16BitPCM(input)
...
return dataView
,
)()
微信小程序中在启动录音时需要传入一个配置参数 option
,在这个 option
中我们可以指定采样率、通道数等配置,之后通过 recordManager.onFrameRecorded()
回调拿到的就已经是 采样率 16k
、位长 16bit
的单声道 PCM 数据了:
const recordManager = uni.getRecorderManager();
const option =
duration: duration, // 录音的时长,单位 ms,最大值 600000(10 分钟)
sampleRate: 16000, // 采样率(pc不支持)
numberOfChannels: 1, // 录音通道数
// encodeBitRate: 48000, // 编码码率(默认就是48000)
frameSize: 1, // 指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。暂仅支持 mp3、pcm 格式。
format: "pcm", // 音频格式,默认是 aac
const startRecord = () =>
...
recordManager.onFrameRecorded((res) =>
// frameBuffer ArrayBuffer 录音分片结果数据
// isLastFrame Boolean 当前帧是否正常录音结束前的最后一帧
const frameBuffer = res;
pushAudioData(frameBuffer); // 将每一帧音频保存起来
);
recordManager.start(option);
;
注意:微信小程序录音时长最大值为 10 分钟,而科大讯飞语音评测录音时长最大值为 5 分钟!!
5、发送 webSocket 数据
官方 Demo 中,使用 1 个 audioData 数组来存放每一帧经过 transWorker 处理过的 PCM "散装"
数据(音频流数据);在 webSocket 连接开启的 500ms 之后,开始发送音频流数据:
class IseRecorder
constructor( language, accent, appId = )
// 记录音频数据
this.audioData = []
transWorker.onmessage = function (event)
self.audioData.push(...event.data) // GitLqr: 注意这里是 "散装" 的
...
recorderStart()
this.audioContext.resume()
this.connectWebSocket()
// 连接we以上是关于uniapp - 接入科大讯飞语音评测的主要内容,如果未能解决你的问题,请参考以下文章