vue 语音转文字使用例子和解析 第三方科大讯飞

一、需求是什么?

需求:在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 语音转文字使用例子和解析 第三方科大讯飞

挂载到全局vue实例上

vue 语音转文字使用例子和解析 第三方科大讯飞

这样就可以在页面this.iatRecorder直接使用方法了

   data() {
    iatRecorder3:null
   },
  mounted() {
     //&#x521D;&#x59CB;&#x5316;&#x8BED;&#x97F3;
     //&#x8FD9;&#x91CC;&#x7528;&#x4E00;&#x4E2A;iatRecorder3&#x63A5;&#x662F;&#x4E3A;&#x4E86;&#x4FDD;&#x6301;&#x5F53;&#x524D;vue&#x65E2;&#x7528;&#x5230;&#x4E86;&#x8BED;&#x97F3; &#x53C8;&#x53EF;&#x4EE5;&#x6709;&#x81EA;&#x5DF1;&#x7684;&#x79C1;&#x6709;&#x65B9;&#x6CD5;
     this.iatRecorder3=this.iatRecorder;
     &#x63A5;&#x6536;&#x8BED;&#x97F3;&#x8F6C;&#x6587;&#x5B57;&#x7ED3;&#x679C;
     this.myData(this.iatRecorder3);
  },
methods: {
     myData(iatRecorder){
            let countInterval
            // &#x72B6;&#x6001;&#x6539;&#x53D8;&#x65F6;&#x5904;&#x7F5A;
            this.iatRecorder3.onWillStatusChange = function(oldStatus, status) {
                // &#x53EF;&#x4EE5;&#x5728;&#x8FD9;&#x91CC;&#x8FDB;&#x884C;&#x9875;&#x9762;&#x4E2D;&#x4E00;&#x4E9B;&#x4EA4;&#x4E92;&#x903B;&#x8F91;&#x5904;&#x7406;&#xFF1A;&#x5012;&#x8BA1;&#x65F6;&#xFF08;&#x542C;&#x5199;&#x53EA;&#x6709;60s&#xFF09;,&#x5F55;&#x97F3;&#x7684;&#x52A8;&#x753B;&#xFF0C;&#x6309;&#x94AE;&#x4EA4;&#x4E92;&#x7B49;
                let senconds = 0
                if (status === 'ing') {
                  console.log('ing')
                    // &#x5012;&#x8BA1;&#x65F6;&#x76F8;&#x5173;
                    countInterval = setInterval(()=>{
                        senconds++
                        if (senconds >= 60) {
                            this.stop()
                            clearInterval(countInterval)
                        }
                    }, 1000)
                } else if (status === 'init') {
                   console.log('init')
                } else {
                    clearInterval(countInterval)

                }
            }
            // &#x76D1;&#x542C;&#x8BC6;&#x522B;&#x7ED3;&#x679C;&#x7684;&#x53D8;&#x5316;
            let that = this
            this.iatRecorder3.onTextChange = function(text) {
               //&#x8F6C;&#x6587;&#x5B57;&#x7ED3;&#x679C;&#x662F;text &#x7136;&#x540E;onSearch&#x662F;&#x4E4B;&#x540E;&#x7684;&#x64CD;&#x4F5C;&#x53EF;&#x6839;&#x636E;&#x81EA;&#x5DF1;&#x60C5;&#x51B5;&#x4FEE;&#x6539;
                //&#x6CE8;&#x610F;&#xFF01;&#xFF01;&#x8FD9;&#x4E2A;&#x65B9;&#x6CD5;&#x4E0D;&#x65AD;&#x4F1A;&#x6709;&#x65B0;&#x7684;&#x7FFB;&#x8BD1;&#x6587;&#x5B57;&#x8FC7;&#x6765;&#x4E0D;&#x662F;&#x4E00;&#x9524;&#x5B50;&#x4E70;&#x5356;O(&#x2229;_&#x2229;)O&#x54C8;&#x54C8;~
                that.onSearch(text)
                that.endmessage = text
            }
    },
    /**
     * &#x8BED;&#x97F3;&#x8F93;&#x5165;&#x5F00;&#x59CB;
     */
    onVoiceStart() {

      clearTimeout(this.timeOutEvent);//&#x6E05;&#x9664;&#x5B9A;&#x65F6;&#x5668;
      this.timeOutEvent = 0;
      this.timeOutEvent = setTimeout(function(){
          //&#x6267;&#x884C;&#x957F;&#x6309;&#x8981;&#x6267;&#x884C;&#x7684;&#x5185;&#x5BB9;&#xFF0C;
          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;
           //&#x53D1;&#x9001;&#x8BED;&#x97F3;&#x6D88;&#x606F;&#x4EE3;&#x7801;
            this.onIMMessageUpdate(this.onIMGetMessage(this.loginUserName, '', val, 0));

        }else{
           this.myMessageData[this.myMessageData.length-1].content=val;
        }

      }

    },
    /**
     * &#x8BED;&#x97F3;&#x8F93;&#x5165;&#x7ED3;&#x675F;
     */
    onVoiceEnd() {
      clearTimeout(this.timeOutEvent);//&#x6E05;&#x9664;&#x5B9A;&#x65F6;&#x5668;
      this.timeOutEvent = 0;
      this.iatRecorder3.stop()

      // &#x6E05;&#x9664;&#x5012;&#x8BA1;&#x65F6;&#x63D0;&#x793A;
      if (null != this.myToast) {
        this.myToast.clear()
      }

      // &#x6E05;&#x9664;&#x8BA1;&#x65F6;&#x5668;
      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)
      // &#x5F55;&#x97F3;&#x8FC7;&#x77ED;&#x63D0;&#x793A;
      if (this.mySecond == 1) {
        Toast.fail('&#x5F55;&#x97F3;&#x8FC7;&#x77ED;')
      }
      // &#x6062;&#x590D;&#x8BA1;&#x65F6;&#x8D77;&#x70B9;
      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/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球