由于公司业务需求需要实现在微信小程序中实现语音测评功能,因为之前在H5中已经实现了该功能认为在小程序中问题不大,但是在实际开发中遇到了不少坑。

  1. 问题一:语音测评流式版在微信小程序中无法正确的传输数据并获取返回值

    在H5实现时使用 new WebSocket() 实例进行传输数据 并成功的拿到了测评的数据

ws = new WebSocket(url)

   但是在小程序中 wx.connectSocket() 无法将数据流正常的传给科大讯飞的api

wx.connectSocket()

解决方案:使用微信录音功能将录音文件上传服务器,在通过服务端调用科大讯飞api进行语音测评得到结果后返回给前端

小程序代码如下:

// 将数据发送到服务器
  async _sendMp3(path) {
    this.data.toast.linShow({
      icon: "loading",
      title: '解析中请稍后',
      duration: 11111111,
      mask: true
    })
    wx.uploadFile({
      url: `${nodeServer}/xls/postData`, // node 服务接口
      filePath: path, // 录音文件临时地址
      header: {
        "Content-Type": "multipart/form-data" //必须是这个格式
      },
      formData: {
        text: this.data.XLSpeakerTitle //  要对比的文本
      },
      name: "mp3",
      success: async (res) => {
        const {
          Data,
          Code
        } = JSON.parse(res.data);
        if (Code == 200) {
          const score = parseInt(Data.score)
          this.setData({
            showScore: false,
            score,
          });
          // 开始动画
          this.countUp = new WxCountUp('score', score, {}, this);
          this.countUp.start();
          this._AddSpeakItem(score, Data.path);
          // 假如当前得分超过了历史最高分 将历史最高分更新
          this._UpDateMax(score)
        }
      },
      fail: () => {
        this.data.toast.linShow({
          title: '解析失败请,请稍后再试',
        })
      },
      complete: () => {
        this.data.toast.linHide()
      }
    })
  },

node js 服务端代码如下:

const router = require("koa-router")();
const { _init } = require("../ws/index");
const { put } = require("../ali-oss/index");

const { removeFile } = require("../fs");
const { SuccessModel, ErrorModel } = require("../model/resModel");

router.prefix("/xls");
router.post("/postData", async (ctx, next) => {
  const { text } = ctx.request.body;
  // 得到mp3 文件
  const path = ctx.request.files.mp3.path;
  // 得到对比分数
  const { Code, Data, Msg } = await _init(path, text);
  // 将上传的mp3 文件上传到 ali-oss 
  const mp3path = await put(path);
  // 将上传的文件删除 防止上传的录音文件越来越多 导致服务器内存压力
  removeFile(path);
  if (Code == 0) {
    ctx.body = new SuccessModel({
      score: Data,
      path: mp3path.url.split("aliyuncs.com/")[1],
    });
  } else {
    ctx.body = new ErrorModel(Code, Msg ? Msg : "分数解析失败,请稍后再试");
  }
});

科大讯飞语音测评

const CryptoJS = require("crypto-js");
const WebSocket = require("ws");
var fs = require("fs");

const APPID = "xxxxxxxxx";
const API_SECRET = "xxxxxxxxxx";
const API_KEY = "xxxxxxxxxx";
// 系统配置
let xmlParser = require("../util/parser");

const config = {
  // 请求地址
  hostUrl: "wss://ise-api.xfyun.cn/v2/open-ise",
  host: "iat-api.xfyun.cn",
  //在控制台-我的应用-语音评测(流式版)获取
  appid: APPID,
  //在控制台-我的应用-语音评测(流式版)获取
  apiSecret: API_SECRET,
  //在控制台-我的应用-语音评测(流式版)获取
  apiKey: API_KEY,
  file: "./read_sentence_cn.pcm", //请填写您的音频文件路径
  uri: "/v2/open-ise",
  highWaterMark: 1280,
};
// 帧定义
const FRAME = {
  STATUS_FIRST_FRAME: 0,
  STATUS_CONTINUE_FRAME: 1,
  STATUS_LAST_FRAME: 2,
};

// 设置当前临时状态为初始化
let status = FRAME.STATUS_FIRST_FRAME;
let ws;
const _init = (path, text) => {
  // 一定要初始化状态
  status = FRAME.STATUS_FIRST_FRAME;
  // 合成websocket接口,一次请求成功,未中断链接情况下,再次发送新请求导致 10140 错误
  const url = getWsUrl();
  console.log("url-----------", url);
  ws = new WebSocket(url);
  return new Promise((resolve, reject) => {
    // 连接建立完毕,读取数据进行识别
    ws.on("open", (event) => {
      console.log("websocket connect!");
      // config.file
      var readerStream = fs.createReadStream(path, {
        highWaterMark: config.highWaterMark,
      });
      readerStream.on("data", function (chunk) {
        send(chunk, text);
      });
      // 最终帧发送结束
      readerStream.on("end", function () {
        status = FRAME.STATUS_LAST_FRAME;
        send("");
      });
    });
    // 得到识别结果后进行处理,仅供参考,具体业务具体对待
    ws.on("message", (data, err) => {
      if (err) {
        ws.close();
        return;
      }
      res = JSON.parse(data);
      if (res.code != 0) {
        ws.close();
        // reject(`error code ${res.code}, reason ${res.message}`);
        reject({ Code: res.code, Msg: res.message });
        return;
      }
      if (res.data.status == 2) {
        const { data } = res.data;
        let b = new Buffer(data, "base64");
        let iseResult = `最终识别结果: ${b.toString()}`;
        const scroeData = xmlParser.parse(iseResult, {
          attributeNamePrefix: "",
          ignoreAttributes: false,
        });
        resolve({
          Code: 0,
          Data:
            scroeData.xml_result.read_sentence.rec_paper.read_sentence
              .total_score,
        });
      }
    });
    // 资源释放
    ws.on("close", () => {
      console.log("connect close!-------------------------");
    });
    // 建连错误
    ws.on("error", (err) => {
      reject("websocket connect err: " + err);
      console.log("websocket connect err: " + err);
    });
  });
};
// 获取长连接地址
function getWsUrl() {
  // 获取当前时间 RFC1123格式
  let date = new Date().toUTCString();
  const wssUrl =
    config.hostUrl +
    "?authorization=" +
    getAuthStr(date) +
    "&date=" +
    date +
    "&host=" +
    config.host;
  return wssUrl;
}
// 传输数据
function send(data, text = "今天天气怎么样") {
  let frame = "";
  switch (status) {
    case FRAME.STATUS_FIRST_FRAME:
      // 第一次数据发送:
      frame = {
        common: { app_id: config.appid },
        business: {
          // 	服务类型指定 ise(开放评测)
          sub: "ise",
          // 中文:cn_vip 英文:en_vip
          ent: "cn_vip",
          // 题型:句子朗读
          category: "read_sentence",
          // 待评测文本编码 utf-8
          text: `\uFEFF${text}`,
          // 待评测文本编码 utf-8 gbk
          tte: "utf-8",
          // 跳过ttp直接使用ssb中的文本进行评测(使用时结合cmd参数查看),默认值true
          ttp_skip: true,
          cmd: "ssb",
          aue: "lame",
          auf: "audio/L16;rate=16000",
        },
        data: { status: 0 },
      };
      ws.send(JSON.stringify(frame));
      // 后续数据发送
      frame = {
        common: { app_id: config.appid },
        business: { aus: 1, cmd: "auw", aue: "lame" },
        data: { status: 1, data: data.toString("base64") },
      };
      status = FRAME.STATUS_CONTINUE_FRAME;
      break;
    case FRAME.STATUS_CONTINUE_FRAME:
      frame = {
        common: { app_id: config.appid },
        business: { aus: 2, cmd: "auw", aue: "lame" },
        data: { status: 1, data: data.toString("base64") },
      };
      break;
    case FRAME.STATUS_LAST_FRAME:
      frame = {
        common: { app_id: config.appid },
        business: { aus: 4, cmd: "auw", aue: "lame" },
        data: { status: 2, data: data.toString("base64") },
      };
      break;
  }
  ws.send(JSON.stringify(frame));
}
// 鉴权签名
function getAuthStr(date) {
  let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`;
  let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
  let signature = CryptoJS.enc.Base64.stringify(signatureSha);
  let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
  let authStr = CryptoJS.enc.Base64.stringify(
    CryptoJS.enc.Utf8.parse(authorizationOrigin)
  );
  return authStr;
}

module.exports = { _init };

 2.问题2:文本朗读功能,刚开始做的时候我们是使用科大讯飞的语音合成功能,由于科大讯飞是收费的 推荐大家使用微信小程序同声传译插件很好用而且是免费,如果日使用量很大的话可以申请大额数量,缺点是朗读的文字有大小限制,我测试了下大概是200 字左右,如果超过两百字需要手动截取,分批合成

WechatSI 使用微信同声传译插件
WechatSI.textToSpeech({
      lang: "zh_CN",
      tts: true,
      content: this.data.XLSpeakerTitle,
      success: (res) => {
        this.data.WechatSIStatus = "success"
        this._palyBGAduio(res.filename, "演示")
      },
      fail: (err) => {
        this.setData({
          isPlay: false,
          isPlayDemo: false
        })
        this.data.toast.linShow({
          title: err.msg,
        })
      }
    })

3.问题三实现最高分回放功能,这里我的解决方案是在解决方案一的的同时,将上传到服务器的录音文件同时上传到阿里云OSS 返回给前端阿里云的图片地址和当前语音的测评分数,前端判断当前得分是否超过最高分,如果超过最高分将最高分更新

阿里云OSS 上传如下:

const OSS = require("ali-oss");

const client = new OSS({
  bucket: "xlxtimg",
  // region以杭州为例(oss-cn-hangzhou),其他region按实际情况填写。
  region: "oss-cn-beijing",
  // 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录RAM控制台创建RAM账号。
  accessKeyId: "xxxxxxxx",
  accessKeySecret: "xxxxxxxxxxxxxxxxxx",
});

async function put(file) {
  try {
    //object-name可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
    let result = await client.put(`xlsLee/${file}`, file);
    return result;
  } catch (e) {
    console.log(e);
  }
}
module.exports = { put };

 以上就是我实现微信小程序进行语音测评 并将语音文件长传阿里云的实践过程