评论

微信小程序中使用科大讯飞语音评测,全流程,超简单

微信小程序里使用科大讯飞语音评测服务

1.准备工具文件

新建base64js.js文件到项目中,照着复制就行了,

;(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? module.exports = factory(global)
    : typeof define === 'function' && define.amd
    ? define(factory) : factory(global)
}((
  typeof self !== 'undefined' ? self
    : typeof window !== 'undefined' ? window
    : typeof global !== 'undefined' ? global
      : this
), function(global) {
  'use strict';
  // existing version for noConflict()
  global = global || {};
  var _Base64 = global.Base64;
  var version = "2.5.1";
  // if node.js and NOT React Native, we use Buffer
  var buffer;
  if (typeof module !== 'undefined' && module.exports) {
    try {
      buffer = eval("require('buffer').Buffer");
    } catch (err) {
      buffer = undefined;
    }
  }
  // constants
  var b64chars
    = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  var b64tab = function(bin) {
    var t = {};
    for (var i = 0, l = bin.length; i < l; i++) t[bin.charAt(i)] = i;
    return t;
  }(b64chars);
  var fromCharCode = String.fromCharCode;
  // encoder stuff
  var cb_utob = function(c) {
    if (c.length < 2) {
      var cc = c.charCodeAt(0);
      return cc < 0x80 ? c
        : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
        + fromCharCode(0x80 | (cc & 0x3f)))
          : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
          + fromCharCode(0x80 | ((cc >>>  6) & 0x3f))
          + fromCharCode(0x80 | ( cc         & 0x3f)));
    } else {
      var cc = 0x10000
        + (c.charCodeAt(0) - 0xD800) * 0x400
        + (c.charCodeAt(1) - 0xDC00);
      return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
      + fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
      + fromCharCode(0x80 | ((cc >>>  6) & 0x3f))
      + fromCharCode(0x80 | ( cc         & 0x3f)));
    }
  };
  var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
  var utob = function(u) {
    return u.replace(re_utob, cb_utob);
  };
  var cb_encode = function(ccc) {
    var padlen = [0, 2, 1][ccc.length % 3],
      ord = ccc.charCodeAt(0) << 16
        | ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
        | ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
      chars = [
        b64chars.charAt( ord >>> 18),
        b64chars.charAt((ord >>> 12) & 63),
        padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
        padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
      ];
    return chars.join('');
  };
  var btoa = global.btoa ? function(b) {
    return global.btoa(b);
  } : function(b) {
    return b.replace(/[\s\S]{1,3}/g, cb_encode);
  };
  var _encode = buffer ?
    buffer.from && Uint8Array && buffer.from !== Uint8Array.from
      ? function (u) {
      return (u.constructor === buffer.constructor ? u : buffer.from(u))
        .toString('base64')
    }
      :  function (u) {
      return (u.constructor === buffer.constructor ? u : new  buffer(u))
        .toString('base64')
    }
    : function (u) { return btoa(utob(u)) }
  ;
  var encode = function(u, urisafe) {
    return !urisafe
      ? _encode(String(u))
      : _encode(String(u)).replace(/[+\/]/g, function(m0) {
        return m0 == '+' ? '-' : '_';
      }).replace(/=/g, '');
  };
  var encodeURI = function(u) { return encode(u, true) };
  // decoder stuff
  var re_btou = new RegExp([
    '[\xC0-\xDF][\x80-\xBF]',
    '[\xE0-\xEF][\x80-\xBF]{2}',
    '[\xF0-\xF7][\x80-\xBF]{3}'
  ].join('|'), 'g');
  var cb_btou = function(cccc) {
    switch(cccc.length) {
      case 4:
        var cp = ((0x07 & cccc.charCodeAt(0)) << 18)
            |    ((0x3f & cccc.charCodeAt(1)) << 12)
            |    ((0x3f & cccc.charCodeAt(2)) <<  6)
            |     (0x3f & cccc.charCodeAt(3)),
          offset = cp - 0x10000;
        return (fromCharCode((offset  >>> 10) + 0xD800)
        + fromCharCode((offset & 0x3FF) + 0xDC00));
      case 3:
        return fromCharCode(
          ((0x0f & cccc.charCodeAt(0)) << 12)
          | ((0x3f & cccc.charCodeAt(1)) << 6)
          |  (0x3f & cccc.charCodeAt(2))
        );
      default:
        return  fromCharCode(
          ((0x1f & cccc.charCodeAt(0)) << 6)
          |  (0x3f & cccc.charCodeAt(1))
        );
    }
  };
  var btou = function(b) {
    return b.replace(re_btou, cb_btou);
  };
  var cb_decode = function(cccc) {
    var len = cccc.length,
      padlen = len % 4,
      n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
        | (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
        | (len > 2 ? b64tab[cccc.charAt(2)] <<  6 : 0)
        | (len > 3 ? b64tab[cccc.charAt(3)]       : 0),
      chars = [
        fromCharCode( n >>> 16),
        fromCharCode((n >>>  8) & 0xff),
        fromCharCode( n         & 0xff)
      ];
    chars.length -= [0, 0, 2, 1][padlen];
    return chars.join('');
  };
  var _atob = global.atob ? function(a) {
    return global.atob(a);
  } : function(a){
    return a.replace(/\S{1,4}/g, cb_decode);
  };
  var atob = function(a) {
    return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
  };
  var _decode = buffer ?
    buffer.from && Uint8Array && buffer.from !== Uint8Array.from
      ? function(a) {
      return (a.constructor === buffer.constructor
        ? a : buffer.from(a, 'base64')).toString();
    }
      : function(a) {
      return (a.constructor === buffer.constructor
        ? a : new buffer(a, 'base64')).toString();
    }
    : function(a) { return btou(_atob(a)) };
  var decode = function(a){
    return _decode(
      String(a).replace(/[-_]/g, function(m0) { return m0 == '-' ? '+' : '/' })
        .replace(/[^A-Za-z0-9\+\/]/g, '')
    );
  };
  var noConflict = function() {
    var Base64 = global.Base64;
    global.Base64 = _Base64;
    return Base64;
  };
  // export Base64
  global.Base64 = {
    VERSION: version,
    atob: atob,
    btoa: btoa,
    fromBase64: decode,
    toBase64: encode,
    utob: utob,
    encode: encode,
    encodeURI: encodeURI,
    btou: btou,
    decode: decode,
    noConflict: noConflict,
    __buffer__: buffer
  };
  // if ES5 is available, make Base64.extendString() available
  if (typeof Object.defineProperty === 'function') {
    var noEnum = function(v){
      return {value:v,enumerable:false,writable:true,configurable:true};
    };
    global.Base64.extendString = function () {
      Object.defineProperty(
        String.prototype, 'fromBase64', noEnum(function () {
          return decode(this)
        }));
      Object.defineProperty(
        String.prototype, 'toBase64', noEnum(function (urisafe) {
          return encode(this, urisafe)
        }));
      Object.defineProperty(
        String.prototype, 'toBase64URI', noEnum(function () {
          return encode(this, true)
        }));
    };
  }
  //
  // export Base64 to the namespace
  //
  if (global['Meteor']) { // Meteor.js
    Base64 = global.Base64;
  }
  // module.exports and AMD are mutually exclusive.
  // module.exports has precedence.
  if (typeof module !== 'undefined' && module.exports) {
    module.exports.Base64 = global.Base64;
  }
  else if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define([], function(){ return global.Base64 });
  }
  // that's it!
  return {Base64: global.Base64}
}));

用npm安装crypto-js xmldom这2个工具,将工具引入到页面里

const CryptoJS = require('crypto-js')
const Base64 = require('../../tools/base64js').Base64;//此路径根据自己的实际路径来
var DOMParser = require('xmldom').DOMParser;

2.初始化用到的变量及函数

const APPID = ''//填自己的,如果考虑安全问题的话,这些数据可以根据后台接口获取
const API_SECRET = ''//填自己的
const API_KEY = ''//填自己的
const recorderManager = wx.getRecorderManager()//初始化录音管理

let audioData = []//存储音频数据的数组
let socketTask = null
let handlerInterval = null


function getWebSocketUrl() {
  console.log('准备开始上传:')
  return new Promise((resolve, reject) => {
    // 请求地址根据语种不同变化
    var url = 'wss://ise-api.xfyun.cn/v2/open-ise'
    var host = 'ise-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/open-ise 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 =Base64.encode(authorizationOrigin)
    url = `${url}?authorization=${authorization}&date=${date}&host=${host}`
    resolve(url)
  })
}

3.完整page代码

Page({
  /**
   * 页面的初始数据
   */
  data: {
    readText:null,
    upReadText:null,
readyFilePath:null,
    audioURL:'', 
    fluencyScore:0,//流畅度
    accuracyScore:0,//准确度
    integrityScore:0,//完整度
    standardScore:0,
    errorText:null,
  },


  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    
  },



  startUpRecord(){
    let that = this
    wx.showLoading({
      title: '正在分析中…',
      mask:true
    })
    
    getWebSocketUrl().then(( url)=>{
      console.log('websocketurl:',url)
      let newURL = encodeURI(url)
      console.log('new websocketurl:',newURL)
      socketTask = wx.connectSocket({
        url: newURL,
      })
      socketTask.onOpen(()=>{
        console.log('打开了socket')
        that.webSocketSend()
      })
      socketTask.onMessage((e)=>{
        // result 在这里做信息处理
        console.log('收到了结果:',e)
        that.result(e.data)
        
      })
      socketTask.onError((err)=>{
        //结束录音
        console.log('socket 出错:',err)
        wx.hideLoading({
          success: (res) => {
            wx.showToast({
              title: '语音评测出错!',
              icon:'error'
            })
          },
        })
      })
      socketTask.onClose(()=>{
        // 结束录音
        console.log('socket 关闭:')
      })
    })
  },
  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])
    }
    // console.log('binary:',Base64.btoa(binary))
    return Base64.btoa(binary)
  },
    // 向webSocket发送数据
    webSocketSend() {
      // console.log('开始发送数据',audioData)
      let that = this
      let audioDataUp = audioData.splice(0, 1)
      var params = {
        common: {
          app_id:APPID,
        },
        business: {
          category: 'read_sentence', // read_syllable/单字朗读,汉语专有 read_word/词语朗读  read_sentence/句子朗读 https://www.xfyun.cn/doc/Ise/IseAPI.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B
          rstcd: 'utf8',
          group: 'pupil',
          sub: 'ise',
          ent: 'en_vip',
          tte: 'utf-8',
          cmd: 'ssb',
          auf: 'audio/L16;rate=16000',
          aus: 1,
          aue: 'lame',
          text: this.data.upReadText//用于评测的文本
        },
        data: {
          status: 0,
          encoding: 'raw',
          data_type: 1,
          data: that.toBase64(audioDataUp[0]),
        },
      }
      console.log(JSON.stringify(params))
      socketTask.send({data: JSON.stringify(params)})
      handlerInterval = setInterval(() => {
        // websocket未连接
        if (!socketTask) {
          clearInterval(handlerInterval)
          return
        }
        // 最后一帧
        if (audioData.length === 0) {
            console.log('数据发送完毕')
            socketTask.send(
              {data:
              JSON.stringify({
                business: {
                  cmd: 'auw',
                  aus: 4,
                  aue: 'lame'
                },
                data: {
                  status: 2,
                  encoding: 'raw',
                  data_type: 1,
                  data: '',
                },
              })}
            )
            audioData = []
            clearInterval(handlerInterval)
          
          return false
        }
        audioDataUp = audioData.splice(0, 1)
        // 中间帧
        // console.log('audioDataUp:',audioDataUp[0])
        socketTask.send(
          {
            data:
          JSON.stringify({
            business: {
              cmd: 'auw',
              aus: 2,
              aue: 'lame'
            },
            data: {
              status: 1,
              encoding: 'raw',
              data_type: 1,
              data: that.toBase64(audioDataUp[0]),
            },
          })}
        )
      }, 40)
    },
    result(resultData) {
      // 识别结束
      let that = this
      console.log('resultData:',resultData)
      let jsonData = JSON.parse(resultData)
      if(jsonData.code ===48195||jsonData.code ==="48195"){
        wx.showModal({
          content:'上传到科大讯飞的评测文本有问题!',
          showCancel:false
        })
      }
      if (jsonData.data && jsonData.data.data) {
        let data = Base64.decode(jsonData.data.data)
        console.log('xml>Data:',data)
        
        const doc=new DOMParser().parseFromString(data,'text/xml');
        let sentence = doc.getElementsByTagName("read_chapter")[0]
        let theSentence = sentence.getElementsByTagName('sentence') ||[]
        console.log('theSentence:',theSentence)
  
        let resultStr = ''
        for(let i1=0;i1<theSentence.length;i1++){
          let item = theSentence[i1]
          let theWord = item.getElementsByTagName('word')||[]
          if(theWord.length>0){
            for(let i2=0;i2<theWord.length;i2++){
              let wt = theWord[i2]
              let flag = false
              let theSyll = wt.getElementsByTagName('syll')||[]
              let flagBools = []
              if(theSyll.length>0){
                
                for(let i3=0;i3<theSyll.length;i3++){
                  let pt = theSyll[i3]
                  let serr_msg = pt.getAttribute('serr_msg')
                  if(Number(serr_msg)!=0){
                    flag = true
                  }
                }
              }
              // flag = flagBools.findIndex(true)
              let wContent = wt.getAttribute('content')
              if (flag) {
                if(wContent!='sil'){
                  resultStr += `<i class="err">${wContent}</i>`
                }
              } else {
                if(wContent!='sil'){
                resultStr += `<i class="normal-res">${wContent}</i>`
                }
              }
  
            }
          }
        }
        
        console.log("resultStr:",resultStr)//此处为返回结果的富文本,可以用 rich-text直接展示,err  normal-res 两个类需要自己定义样式
        
  
        console.log('sentence:',sentence)
        let accuracy_score = sentence.getAttribute("accuracy_score")//准确度
        let fluency_score = sentence.getAttribute("fluency_score")//流畅度
        let integrity_score = sentence.getAttribute("integrity_score")//完整度
        let standard_score = sentence.getAttribute("standard_score")//标准度
        let total_score = sentence.getAttribute("total_score")//总分
  
        let is_rejected = sentence.getAttribute("is_rejected")//是否乱读
        if(is_rejected===false||is_rejected==='false'){
          //乱读,不算入次数,
          
        }else{
          
        }
      }
      if (jsonData.code === 0 && jsonData.data.status === 2) {
        // this.webSocket.close()
        // 在这里结束socket
        socketTask.close()
      }
      if (jsonData.code !== 0) {
        // this.webSocket.close()
        socketTask.close()
        console.log(`${jsonData.code}:${jsonData.message}`)
      }
    },

  readyForXF(){
    this.startUpRecord()
  },
//开始录音
startVoiceRecord(){
  if(this.data.isPlaying){
    return
  }
  let that = this
  that.setData({recordState:'recording'})
  recorderManager.onStart(() => {
    console.log('recorder start')
    that.countDownStart()
  })
  recorderManager.onPause(() => {
    console.log('recorder pause')
  })
  recorderManager.onStop((res) => {
    console.log('recorder stop', res)
    const { tempFilePath } = res
    console.log('录音结束:',tempFilePath)
    // that.readyForUp(tempFilePath)//此方法可将录音文件上传到自己服务器或者云存储
    that.readyForXF()
    // that.testBuffer(tempFilePath)
    that.setData({recordState:'ready',readyFilePath:tempFilePath})
    that.countDownReset()
  })
  recorderManager.onFrameRecorded((res) => {
    const { frameBuffer } = res
    console.log('frameBuffer.byteLength', frameBuffer.byteLength)
    let u8Arr = new Uint8Array(frameBuffer)
    audioData.push(u8Arr)
  })
  
  
  const options = {
    duration: 180000,
    sampleRate: 16000,
    numberOfChannels: 1,
    encodeBitRate: 44100,
    frameSize: 2,
    format: 'mp3',
  }
  
  recorderManager.start(options)
},
//结束录音
stopVoiceRecord(){
  recorderManager.stop()
},
countDownStart() {
  const countDown = this.selectComponent('.control-count-down');
  countDown.start();
},


countDownPause() {
  const countDown = this.selectComponent('.control-count-down');
  countDown.pause();
},


countDownReset() {
  const countDown = this.selectComponent('.control-count-down');
  countDown.reset();
},
//倒计时结束  停止录音
onTimeCountfinished(){
  recorderManager.stop()
},
readyForUp(filePath){
//  自定义上传文件方法,可将音频文件上传到自己服务器,或者oss
},

})


主要用到的页面上的控制组件贴出来了,可以完成流程验证

<van-icon bindtap="startVoiceRecord" size="50px" name="play-circle-o" color="#004EFF"/>
<van-icon bindtap="stopVoiceRecord"  size="50px" name="stop-circle-o" color="#004EFF"/>
<van-count-down
  class="control-count-down"
  millisecond
  time="{{ 180000 }}"
  auto-start="{{ false }}"
  format="mm:ss"
  bind:finish="onTimeCountfinished"
/>


主要代码就这些,祝大家早日调试成功!

最后一次编辑于  10-27  
点赞 1
收藏
评论

2 个评论

  • Cherish every day
    Cherish every day
    11小时前

    调用完api 反馈的结果如下:

    "{"header":{"code":0,"message":"success","sid":"iat000e3286@dx1934f73c47bb93f882","status":2},"payload":{"result":{"compress":"raw","encoding":"utf8","format":"json","seq":1,"status":2,"text":"eyJzbiI6MSwibHMiOnRydWUsImJnIjowLCJlZCI6MCwid3MiOlt7ImJnIjowLCJjdyI6W3sic2MiOjAuMDAsInciOiIifV19XX0="}}}"

    11小时前
    赞同
    回复
  • Cherish every day
    Cherish every day
    11小时前

    你好,我最近调用了大模型多语种语音识别 API ,微信小程序录音,按照api要求发送并接受结果,但是将 API 返回的 Base64 字符串转换为 UTF-8 文本时遇到了一些问题,我是这样写的,麻烦你帮我看看,  这是socket 接受的反馈信息,

    result(resultData) {

        try {

          // 检查 resultData 是否为字符串

          if (typeof resultData === 'string'{

            // 如果是字符串,则解析为对象

            resultData = JSON.parse(resultData);

          }

          var temp = JSON.parse(resultData.data);

          console.log('收到的数据:', temp.payload.result.text); // 打印解析后的对象

          var decodedString = base64.decode(temp.payload.result.text);

          console.log('解码后结果:', decodedString); // 打印解析后的对象

          const utf8Text = this.base64ToUtf8(decodedString);

          console.log(utf8Text);

        } catch (error{

          console.error('JSON 解析错误:', error);

        }

      },

      base64ToUtf8: function (base64String) {

        // 解码 Base64 字符串

        const binaryString = base64.decode(base64String);


        // 创建一个 Uint8Array 来存储二进制数据

        const len = binaryString.length;

        const bytes = new Uint8Array(len);


        for (let i = 0; i < len; i++{

          bytes[i= binaryString.charCodeAt(i);

        }


        // 使用 TextDecoder 将 Uint8Array 转换为 UTF-8 字符串

        const decoder = new TextDecoder('utf-8');

        const utf8String = decoder.decode(bytes);


        return utf8String;

      },但是还是报错


    11小时前
    赞同
    回复
登录 后发表内容