一、需求是什么?
需求:在vue的h5页面上使用科大讯飞的语音转文字功能 /聊天页面发送语音消息点击朗读
二、使用步骤
1.放入js文件(科大讯飞官方例子文件)
代码如下(示例):
/**
* Created by lycheng on 2019/8/1.
*
* 语音听写流式 WebAPI 接口调用示例 接口文档(必看):https://doc.xfyun.cn/rest_api/语音听写(流式版).html
* webapi 听写服务参考帖子(必看):http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=38947&extra=
* 语音听写流式WebAPI 服务,热词使用方式:登陆开放平台https://www.xfyun.cn/后,找到控制台--我的应用---语音听写---个性化热词,上传热词
* 注意:热词只能在识别的时候会增加热词的识别权重,需要注意的是增加相应词条的识别率,但并不是绝对的,具体效果以您测试为准。
* 错误码链接:
* https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E9%94%99%E8%AF%AF%E7%A0%81
* https://www.xfyun.cn/document/error-code (code返回错误码时必看)
* 语音听写流式WebAPI 服务,方言或小语种试用方法:登陆开放平台https://www.xfyun.cn/后,在控制台--语音听写(流式)--方言/语种处添加
* 添加后会显示该方言/语种的参数值
*
*/
// 1. websocket连接:判断浏览器是否兼容,获取websocket url并连接,这里为了方便本地生成websocket url
// 2. 获取浏览器录音权限:判断浏览器是否兼容,获取浏览器录音权限,
// 3. js获取浏览器录音数据
// 4. 将录音数据处理为文档要求的数据格式:采样率16k或8K、位长16bit、单声道;该操作属于纯数据处理,使用webWork处理
// 5. 根据要求(采用base64编码,每次发送音频间隔40ms,每次发送音频字节数1280B)将处理后的数据通过websocket传给服务器,
// 6. 实时接收websocket返回的数据并进行处理
// ps: 该示例用到了es6中的一些语法,建议在chrome下运行
import CryptoJS from 'crypto-js'
import TransWorker from '@/js/transcode.worker.js'
import './index.css'
//import {getAuthUrl} from '@/api/websocketUrl.js'
let transWorker = new TransWorker()
//APPID,APISecret,APIKey在控制台-我的应用-语音听写(流式版)页面获取
const APPID = '5ea8d439'
const API_SECRET = 'f723e5fc58653672607a525aad6d22a'//6
const API_KEY = '41252ac4bd31676c7944905ecfaffa8'//b
/**
* 获取websocket url
* 该接口需要后端提供,这里为了方便前端处理
*/
function getWebSocketUrl() {
return new Promise((resolve, reject) => {
// 请求地址根据语种不同变化
var url = 'wss://iat-api.xfyun.cn/v2/iat'
var host = 'iat-api.xfyun.cn'
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/iat HTTP/1.1
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
var signature = CryptoJS.enc.Base64.stringify(signatureSha)
var authorizationOrigin = api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"
var authorization = btoa(authorizationOrigin)
url = ${url}?authorization=${authorization}&date=${date}&host=${host}
resolve(url)
})
}
class IatRecorder {
constructor({ language, accent, appId } = {}) {
let self = this
this.status = 'null'
this.language = language || 'zh_cn'
this.accent = accent || 'mandarin'
this.appId = appId || APPID
// 记录音频数据
this.audioData = []
// 记录听写结果
this.resultText = ''
//保存录音
this.leftDataList = [],
this.rightDataList = [];
//保存录音 end
// wpgs下的听写结果需要中间状态辅助记录
this.resultTextTemp = ''
transWorker.onmessage = function (event) {
self.audioData.push(...event.data)
}
}
// 修改录音听写状态
setStatus(status) {
this.onWillStatusChange && this.status !== status && this.onWillStatusChange(this.status, status)
this.status = status
}
setResultText({ resultText, resultTextTemp } = {}) {
console.log(resultText+'-----'+resultTextTemp)
this.onTextChange && this.onTextChange(resultTextTemp || resultText || '')
resultText !== undefined && (this.resultText = resultText)
resultTextTemp !== undefined && (this.resultTextTemp = resultTextTemp)
}
// 修改听写参数
setParams({ language, accent } = {}) {
language && (this.language = language)
accent && (this.accent = accent)
}
// 连接websocket
connectWebSocket() {
return getWebSocketUrl().then(url => {
let iatWS
if ('WebSocket' in window) {
iatWS = new WebSocket(url)
} else if ('MozWebSocket' in window) {
iatWS = new MozWebSocket(url)
} else {
alert('浏览器不支持WebSocket')
return
}
this.webSocket = iatWS
this.setStatus('init')
iatWS.onopen = e => {
this.setStatus('ing')
// 重新开始录音
setTimeout(() => {
this.webSocketSend()
}, 500)
}
iatWS.onmessage = e => {
this.result(e.data)
}
iatWS.onerror = e => {
this.recorderStop()
}
iatWS.onclose = e => {
this.recorderStop()
}
})
}
// 初始化浏览器录音
recorderInit() {
navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
// 创建音频环境
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
this.audioContext.resume()
if (!this.audioContext) {
alert('浏览器不支持webAudioApi相关接口')
return
}
} catch (e) {
if (!this.audioContext) {
alert('浏览器不支持webAudioApi相关接口')
return
}
}
// 获取浏览器录音权限
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
audio: true,
video: false,
})
.then(stream => {
getMediaSuccess(stream)
})
.catch(e => {
getMediaFail(e)
})
} else if (navigator.getUserMedia) {
navigator.getUserMedia(
{
audio: true,
video: false,
},
stream => {
getMediaSuccess(stream)
},
function(e) {
getMediaFail(e)
}
)
} else {
if (navigator.userAgent.toLowerCase().match(/chrome/) && location.origin.indexOf('https://') < 0) {
alert('chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限')
} else {
alert('无法获取浏览器录音功能,请升级浏览器或使用chrome')
}
this.audioContext && this.audioContext.close()
return
}
var that=this;
// 获取浏览器录音权限成功的回调
let getMediaSuccess = stream => {
// console.log('getMediaSuccess')
// 创建一个用于通过JavaScript直接处理音频
//新增保存录音
//新增保存录音end
this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 2, 2)
//保存录音
//保存录音 end
this.scriptProcessor.onaudioprocess = e => {
// 去处理音频数据
if (this.status === 'ing') {
// debugger
transWorker.postMessage(e.inputBuffer.getChannelData(0))
//保存录音
let leftChannelData = e.inputBuffer.getChannelData(0),
rightChannelData = e.inputBuffer.getChannelData(1);
// 需要克隆一下
that.leftDataList.push(leftChannelData.slice(0));
that.rightDataList.push(rightChannelData.slice(0));
//保存录音 end
}
}
// 创建一个新的MediaStreamAudioSourceNode 对象,使来自MediaStream的音频可以被播放和操作
this.mediaSource = this.audioContext.createMediaStreamSource(stream)
// 连接
this.mediaSource.connect(this.scriptProcessor)
this.scriptProcessor.connect(this.audioContext.destination)
this.connectWebSocket()
}
let getMediaFail = (e) => {
alert('请求麦克风失败')
console.log(e)
this.audioContext && this.audioContext.close()
this.audioContext = undefined
// 关闭websocket
if (this.webSocket && this.webSocket.readyState === 1) {
this.webSocket.close()
}
}
}
recorderStart() {
if (!this.audioContext) {
this.recorderInit()
} else {
this.audioContext.resume()
this.connectWebSocket()
}
}
// 暂停录音
recorderStop() {
// safari下suspend后再次resume录音内容将是空白,设置safari下不做suspend
if (!(/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgen))){
this.audioContext && this.audioContext.suspend()
}
this.setStatus('end')
//发送语音并保存点击播放
// if(window.location.href.indexOf('instantMessagingChatWorkerMain')!=-1 && this.leftDataList.length!=0){
// this.stopRecord(this.leftDataList,this.rightDataList);
// }
}
//
mergeArray (list) {
let length = list.length * list[0].length;
let data = new Float32Array(length),
offset = 0;
for (let i = 0; i < list.length; i++) {
data.set(list[i], offset);
offset += list[i].length;
}
return data;
}
stopRecord (leftDataList,rightDataList) {
// 停止录音
let leftData = this.mergeArray(leftDataList),
rightData = this.mergeArray(rightDataList),
audioData=this.interleaveLeftAndRight (leftData, rightData),
wavBuffer = this.createWavFile(audioData);
this.playRecord(wavBuffer);
}
interleaveLeftAndRight (left, right) {
let totalLength = left.length + right.length;
let data = new Float32Array(totalLength);
for (let i = 0; i < left.length; i++) {
let k = i * 2;
data[k] = left[i];
data[k + 1] = right[i];
}
return data;
}
createWavFile (audioData) {
const WAV_HEAD_SIZE = 44;
let buffer = new ArrayBuffer(audioData.length * 2 + WAV_HEAD_SIZE),
// 需要用一个view来操控buffer
view = new DataView(buffer);
// 写入wav头部信息
// RIFF chunk descriptor/identifier
this.writeUTFBytes(view, 0, 'RIFF');
// RIFF chunk length
view.setUint32(4, 44 + audioData.length * 2, true);
// RIFF type
this.writeUTFBytes(view, 8, 'WAVE');
// format chunk identifier
// FMT sub-chunk
this.writeUTFBytes(view, 12, 'fmt ');
// format chunk length
view.setUint32(16, 16, true);
// sample format (raw)
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
// sample rate
view.setUint32(24, 44100, true);
// byte rate (sample rate * block align)
view.setUint32(28, 44100 * 2, true);
// block align (channel count * bytes per sample)
view.setUint16(32, 2 * 2, true);
// bits per sample
view.setUint16(34, 16, true);
// data sub-chunk
// data chunk identifier
this.writeUTFBytes(view, 36, 'data');
// data chunk length
view.setUint32(40, audioData.length * 2, true);
// 写入PCM数据
let length = audioData.length;
let index = 44;
let volume = 1;
for (let i = 0; i < length; i++) {
view.setInt16(index, audioData[i] * (0x7FFF * volume), true);
index += 2;
}
return buffer;
}
writeUTFBytes (view, offset, string) {
var lng = string.length;
for (var i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
playRecord (arrayBuffer) {
let blob = new Blob([new Uint8Array(arrayBuffer)]);
let blobUrl = URL.createObjectURL(blob);
localStorage.setItem('audioURLnew',blobUrl);
this.leftDataList=[];
this.rightDataList=[];
}
// 处理音频数据
// transAudioData(audioData) {
// audioData = transAudioData.transaction(audioData)
// this.audioData.push(...audioData)
// }
// 对处理后的音频数据进行base64编码,
toBase64(buffer) {
var binary = ''
var bytes = new Uint8Array(buffer)
var len = bytes.byteLength
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i])
}
return window.btoa(binary)
}
// 向webSocket发送数据
webSocketSend() {
if (this.webSocket.readyState !== 1) {
return
}
let audioData = this.audioData.splice(0, 1280)
var params = {
common: {
app_id: this.appId,
},
business: {
language: this.language, //小语种可在控制台--语音听写(流式)--方言/语种处添加试用
domain: 'iat',
accent: this.accent, //中文方言可在控制台--语音听写(流式)--方言/语种处添加试用
vad_eos: 5000,
dwa: 'wpgs', //为使该功能生效,需到控制台开通动态修正功能(该功能免费)
},
data: {
status: 0,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: this.toBase64(audioData),
},
}
this.webSocket.send(JSON.stringify(params))
this.handlerInterval = setInterval(() => {
// websocket未连接
if (this.webSocket.readyState !== 1) {
this.audioData = []
clearInterval(this.handlerInterval)
return
}
if (this.audioData.length === 0) {
if (this.status === 'end') {
this.webSocket.send(
JSON.stringify({
data: {
status: 2,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: '',
},
})
)
this.audioData = []
clearInterval(this.handlerInterval)
}
return false
}
audioData = this.audioData.splice(0, 1280)
// 中间帧
this.webSocket.send(
JSON.stringify({
data: {
status: 1,
format: 'audio/L16;rate=16000',
encoding: 'raw',
audio: this.toBase64(audioData),
},
})
)
}, 40)
}
result(resultData) {
// 识别结束
let jsonData = JSON.parse(resultData)
if (jsonData.data && jsonData.data.result) {
let data = jsonData.data.result
let str = ''
let resultStr = ''
let ws = data.ws
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w
}
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
if (data.pgs) {
if (data.pgs === 'apd') {
// 将resultTextTemp同步给resultText
this.setResultText({
resultText: this.resultTextTemp,
})
}
// 将结果存储在resultTextTemp中
this.setResultText({
resultTextTemp: this.resultText + str,
})
} else {
this.setResultText({
resultText: this.resultText + str,
})
}
}
if (jsonData.code === 0 && jsonData.data.status === 2) {
this.webSocket.close()
localStorage.setItem('voiceFun','end');
}
if (jsonData.code !== 0) {
this.webSocket.close();
localStorage.setItem('voiceFun','end');
}
}
start() {
this.recorderStart()
this.setResultText({ resultText: '', resultTextTemp: '' })
}
stop() {
this.recorderStop()
}
}
export{
IatRecorder
}
2.实例化语音并挂在到vue全局实例上
代码如下(示例):
import {IatRecorder} from '@/js/index.js' export var iatRecorder=new IatRecorder();
挂载到全局vue实例上
这样就可以在页面this.iatRecorder直接使用方法了
data() {
iatRecorder3:null
},
mounted() {
//初始化语音
//这里用一个iatRecorder3接是为了保持当前vue既用到了语音 又可以有自己的私有方法
this.iatRecorder3=this.iatRecorder;
接收语音转文字结果
this.myData(this.iatRecorder3);
},
methods: {
myData(iatRecorder){
let countInterval
// 状态改变时处罚
this.iatRecorder3.onWillStatusChange = function(oldStatus, status) {
// 可以在这里进行页面中一些交互逻辑处理:倒计时(听写只有60s),录音的动画,按钮交互等
let senconds = 0
if (status === 'ing') {
console.log('ing')
// 倒计时相关
countInterval = setInterval(()=>{
senconds++
if (senconds >= 60) {
this.stop()
clearInterval(countInterval)
}
}, 1000)
} else if (status === 'init') {
console.log('init')
} else {
clearInterval(countInterval)
}
}
// 监听识别结果的变化
let that = this
this.iatRecorder3.onTextChange = function(text) {
//转文字结果是text 然后onSearch是之后的操作可根据自己情况修改
//注意!!这个方法不断会有新的翻译文字过来不是一锤子买卖O(∩_∩)O哈哈~
that.onSearch(text)
that.endmessage = text
}
},
/**
* 语音输入开始
*/
onVoiceStart() {
clearTimeout(this.timeOutEvent);//清除定时器
this.timeOutEvent = 0;
this.timeOutEvent = setTimeout(function(){
//执行长按要执行的内容,
if (that.iatRecorder3.status === 'ing') {
that.iatRecorder3.stop()
} else {
that.iatRecorder3.start()
}
},150);
this.myToast = Toast.loading({
duration: 0,
forbidClick: true,
message: 请讲话\n ${this.mySecond}s
})
this.myTimer = setInterval(() => {
this.mySecond += 1
this.myToast.message = 请讲话\n ${this.mySecond}s
}, 1000)
},
onSearch(val){
if(val!=''){
if(!this.messageSendflag){
this.messageSendflag=true;
//发送语音消息代码
this.onIMMessageUpdate(this.onIMGetMessage(this.loginUserName, '', val, 0));
}else{
this.myMessageData[this.myMessageData.length-1].content=val;
}
}
},
/**
* 语音输入结束
*/
onVoiceEnd() {
clearTimeout(this.timeOutEvent);//清除定时器
this.timeOutEvent = 0;
this.iatRecorder3.stop()
// 清除倒计时提示
if (null != this.myToast) {
this.myToast.clear()
}
// 清除计时器
if (null != this.myTimer) {
clearInterval(this.myTimer)
}
let that = this;
this.myTimer=setInterval(() => {
if(localStorage.getItem('voiceFun')=='end' ){
clearInterval(this.myTimer)
if(that.endmessage!=''){
const params = {message: that.endmessage, account: that.loginUserName}
that.replyRobot(params);
if(document.querySelectorAll(".audio-player").length!=0){
document.querySelectorAll(".audio-player")[document.querySelectorAll(".audio-player").length-1].src=localStorage.getItem('audioURLnew');
}else{
document.querySelectorAll(".audio-player")[0].src=localStorage.getItem('audioURLnew');
}
}
}
}, 1000)
// 录音过短提示
if (this.mySecond == 1) {
Toast.fail('录音过短')
}
// 恢复计时起点
this.mySecond = 1
}
}
总结
以上内容可以从语音转换为文本。
[En]
The above can be converted from voice to text.
Original: https://blog.csdn.net/zyz_3362/article/details/109023354
Author: 不宅的程序园
Title: vue 语音转文字使用例子和解析 第三方科大讯飞
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/525035/
转载文章受原作者版权保护。转载请注明原作者出处!