vue对接讯飞语音识别,websocket语音识别连续流式输出
·
- 首先安装依赖
npm install recorder-core npm install crypto-js - record.js文件(流式输出关键文件)(参考博客:vue使用websocket语音识别连续流式输出_recorder-core-CSDN博客)
//Recorder import Recorder from 'recorder-core' // type import 'recorder-core/src/engine/pcm.js' //可选的扩展支持项 import 'recorder-core/src/extensions/wavesurfer.view.js' export const jsRecorder = (cb) => { var testSampleRate = 16000 var testBitRate = 16 var SendFrameSize = 1280 // 去掉console Recorder.CLog = () => {} //重置环境,每次开始录音时必须先调用此方法,清理环境 var RealTimeSendTryReset = function () { realTimeSendTryChunks = null } var realTimeSendTryNumber var transferUploadNumberMax var realTimeSendTryChunk var realTimeSendTryChunks //=====实时处理核心函数========== var RealTimeSendTry = function (buffers, bufferSampleRate, isClose) { if (realTimeSendTryChunks == null) { realTimeSendTryNumber = 0 transferUploadNumberMax = 0 realTimeSendTryChunk = null realTimeSendTryChunks = [] } //配置有效性检查 if (testBitRate == 16 && SendFrameSize % 2 == 1) { return } var pcm = [], pcmSampleRate = 0 if (buffers.length > 0) { //借用SampleData函数进行数据的连续处理,采样率转换是顺带的,得到新的pcm数据 var chunk = Recorder.SampleData( buffers, bufferSampleRate, testSampleRate, realTimeSendTryChunk ) //清理已处理完的缓冲数据,释放内存以支持长时间录音,最后完成录音时不能调用stop,因为数据已经被清掉了 for ( var i = realTimeSendTryChunk ? realTimeSendTryChunk.index : 0; i < chunk.index - 3; i++ ) { buffers[i] = null } realTimeSendTryChunk = chunk //此时的chunk.data就是原始的音频16位pcm数据(小端LE),直接保存即为16位pcm文件、加个wav头即为wav文件、丢给mp3编码器转一下码即为mp3文件 pcm = chunk.data pcmSampleRate = chunk.sampleRate if (pcmSampleRate != testSampleRate) //除非是onProcess给的bufferSampleRate低于testSampleRate throw new Error( '不应该出现pcm采样率' + pcmSampleRate + '和需要的采样率' + testSampleRate + '不一致' ) } //将pcm数据丢进缓冲,凑够一帧发送,缓冲内的数据可能有多帧,循环切分发送 if (pcm.length > 0) { realTimeSendTryChunks.push({ pcm: pcm, pcmSampleRate: pcmSampleRate }) } //从缓冲中切出一帧数据 var chunkSize = SendFrameSize / (testBitRate / 8) //8位时需要的采样数和帧大小一致,16位时采样数为帧大小的一半 pcm = new Int16Array(chunkSize) pcmSampleRate = 0 var pcmOK = false, pcmLen = 0 for1: for (var i1 = 0; i1 < realTimeSendTryChunks.length; i1++) { chunk = realTimeSendTryChunks[i1] pcmSampleRate = chunk.pcmSampleRate for (var i2 = chunk.offset || 0; i2 < chunk.pcm.length; i2++) { pcm[pcmLen] = chunk.pcm[i2] pcmLen++ //满一帧了,清除已消费掉的缓冲 if (pcmLen == chunkSize) { pcmOK = true chunk.offset = i2 + 1 for (var i3 = 0; i3 < i1; i3++) { realTimeSendTryChunks.splice(0, 1) } break for1 } } } //缓冲的数据不够一帧时,不发送 或者 是结束了 if (!pcmOK) { if (isClose) { var number = ++realTimeSendTryNumber TransferUpload(number, null, 0, null, isClose) } return } //16位pcm格式可以不经过mock转码,直接发送new Blob([pcm.buffer],{type:"audio/pcm"}) 但8位的就必须转码,通用起见,均转码处理,pcm转码速度极快 number = ++realTimeSendTryNumber var encStartTime = Date.now() var recMock = Recorder({ type: 'pcm', sampleRate: testSampleRate, //需要转换成的采样率 bitRate: testBitRate //需要转换成的比特率 }) recMock.mock(pcm, pcmSampleRate) recMock.stop( function (blob, duration) { blob.encTime = Date.now() - encStartTime //转码好就推入传输 TransferUpload(number, blob, duration, recMock, false) //循环调用,继续切分缓冲中的数据帧,直到不够一帧 RealTimeSendTry([], 0, isClose) }, function (msg) { //转码错误?没想到什么时候会产生错误! } ) } //=====数据传输函数========== var TransferUpload = function ( number, blobOrNull, duration, blobRec, isClose ) { // console.log(number, blobOrNull, duration, blobRec, isClose) transferUploadNumberMax = Math.max(transferUploadNumberMax, number) if (blobOrNull) { var blob = blobOrNull var encTime = blob.encTime cb(blob, encTime) } if (isClose) { // console.log('isClose') } } //调用录音 var rec var wave function recStart() { if (rec) { rec.close() } rec = Recorder({ type: 'unknown', onProcess: function ( buffers, powerLevel, bufferDuration, bufferSampleRate ) { //推入实时处理,因为是unknown格式,buffers和rec.buffers是完全相同的,只需清理buffers就能释放内存。 RealTimeSendTry(buffers, bufferSampleRate, false) //可视化图形绘制 // wave.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate); } }) // var t = setTimeout(function () { // console.log('无法录音:权限请求被忽略(超时假装手动点击了确认对话框)', 1) // }, 8000) rec.open(function () { //打开麦克风授权获得相关资源 rec.start() //开始录音 RealTimeSendTryReset() //重置环境,开始录音时必须调用一次 //此处创建这些音频可视化图形绘制浏览器支持妥妥的 // wave = Recorder.WaveSurferView({ // elem: ".speak-wave", // height: 30 //显示宽度 // , scale: 2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊 // , fps: 50 //绘制帧率,不可过高,50-60fps运动性质动画明显会流畅舒适,实际显示帧率达不到这个值也并无太大影响 // , duration: 2500 //当前视图窗口内最大绘制的波形的持续时间,此处决定了移动速率 // , direction: 1 //波形前进方向,取值:1由左往右,-1由右往左 // , position: 0 //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比 // , centerHeight: 1 //中线基础粗细,如果为0不绘制中线,position=±1时应当设为0 // //波形颜色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间 // , linear: [0, "rgba(0,187,17,1)", 0.7, "rgba(255,215,0,1)", 1, "rgba(255,102,0,1)"] // , centerColor: "" //中线css颜色,留空取波形第一个渐变颜色 }); // }, function (msg, isUserNotAllow) { // clearTimeout(t); // console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg, 1); // }); }) } // close function recStop() { rec && rec.close() //直接close掉即可,这个例子不需要获得最终的音频文件 RealTimeSendTry([], 0, true) //最后一次发送 } return { recStart: recStart, recStop: recStop } } - index.js,讯飞websocket url获取,结果处理等
import CryptoJS from 'crypto-js/crypto-js' let resultText = ""; let resultTextTemp = ""; /** * 获取websocket url * 该接口需要后端提供,这里为了方便前端处理 */ export function getWebSocketUrl() { // 请求地址根据语种不同变化 var url = "wss://iat-api.xfyun.cn/v2/iat"; var host = "iat-api.xfyun.cn"; var apiKey = "xfyunApiKey"; var apiSecret = "xfyunApiSecret"; 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}`; return url; } export function 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); } export function renderResult(resultData) { // 识别结束 let jsonData = JSON.parse(resultData); if (jsonData.data && jsonData.data.result) { let data = jsonData.data.result; let str = ""; 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 resultText = resultTextTemp; } // 将结果存储在resultTextTemp中 resultTextTemp = resultText + str; } else { resultText = resultText + str; } return resultTextTemp || resultText || "" } } export function resetResultText(){ resultText = ""; resultTextTemp = ""; } - vue文件
<template> <div class="sound"> <textarea v-model="text" name="" id="" cols="30" rows="10" placeholder="点击按钮开始说话,您的语音将会被转为文本,请允许浏览器获取麦克风权限"></textarea> <div class="btn"> <button @click="startSpeechRecognition()">开始识别</button> <button @click="closeWebsocket()">停止识别</button> </div> </div> </template> <script> import {getWebSocketUrl, renderResult, resetResultText, toBase64} from "@/assets/speech"; import { jsRecorder } from "@/assets/speech/record.js"; //引入录音文件 export default { name: "home", data() { return { isRecording: false, websocket: null,//websocket定义 recStart:null,//开始录音 recStop:null,//结束录音 } }, methods: { startSpeechRecognition() { resetResultText() // 判断麦克风是否打开 navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { //调用websocket方法 // 如果需要加一些图片验证或者其他验证在这里加一些逻辑,成功之后调用websocket this.isRecording=true // 调用录音方法 this.recStart() this.asrWebSocket() }).catch(error => { this.$message.error('麦克风未打开!'); // 麦克风未打开 switch (error.message || error.name) { case 'PERMISSION_DENIED': case 'PermissionDeniedError': console.info('用户拒绝提供信息。') break case 'NOT_SUPPORTED_ERROR': case 'NotSupportedError': console.info('浏览器不支持硬件设备。') break case 'MANDATORY_UNSATISFIED_ERROR': case 'MandatoryUnsatisfiedError': console.info('无法发现指定的硬件设备。') break default: console.info('无法打开麦克风。异常信息:' + (error.code || error.name)) break } }); }, asrWebSocket(){ //使用后端提供的地址 let websocketUrl = getWebSocketUrl() //new 一个websocket this.websocket = new WebSocket(websocketUrl) console.log(this.websocket,'new websocket') this.websocket.onopen = () => { console.log('连接成功') //链接成功之后发送websocket请求参数 this.sendStart() } // 客户端接收服务端返回的数据 this.websocket.onmessage = (evt) => { this.seach = renderResult(evt.data) console.log("返回识别结果吗",this.seach) } this.websocket.onerror = (evt) => { console.error('websocket-asr错误:', evt) } // 关闭连接 this.websocket.onclose = (evt) => { console.log('websocket-asr关闭:', evt) } }, // 请求参数 =====看后端提供的参数信息======== sendStart(){ var params = { common: { app_id: "xfyunAppId", }, business: { language: "zh_cn", domain: "iat", accent: "mandarin", vad_eos: 5000, dwa: "wpgs", }, data: { status: 0, format: "audio/L16;rate=16000", encoding: "raw", }, }; this.websocket.send(JSON.stringify(params)) }, // 关闭websocket closeWebsocket(){ console.log("关闭socket连接") //关闭录音 this.recStop(); // websocket关闭参数 if (this.websocket && this.websocket.readyState == 1){ this.websocket.send( JSON.stringify({ data: { status: 2, format: "audio/L16;rate=16000", encoding: "raw", audio: '', }, }) ) } if (!this.isRecording) return; this.isRecording = false; } }, mounted() { this.getImg() // 使用 jsRecorder 创建一个对象并进行解构操作 const callback = (blob, encTime) => { // 发送音频ArrayBuffer if (this.websocket && this.websocket.readyState == 1 && this.isRecording) { // 创建一个FileReader实例 const reader = new FileReader(); // 定义onload事件处理器 reader.onload = function(e) { // 这里的e.target.result就是一个ArrayBuffer const arrayBuffer = e.target.result; // 发送ArrayBuffer this.websocket.send(JSON.stringify({ data: { status: 1, format: "audio/L16;rate=16000", encoding: "raw", audio: toBase64(new Uint8Array(arrayBuffer)), }, })); }.bind(this); // 使用bind确保this指向正确 // 使用FileReader的readAsArrayBuffer方法读取Blob reader.readAsArrayBuffer(blob); } }; const recorder = jsRecorder(callback) // 开始录音、结束录音方法 this.recStart = recorder.recStart this.recStop = recorder.recStop } } </script>
更多推荐



所有评论(0)