uniapp中使用websocket接入科大讯飞语音听写

给点科大讯飞文档没有的小提示点(demo代码在最下方):

一、关于uniapp的录音api返回数据帧的问题

使用uniapp的录音app时,需要注意recordManager.onFrameRecorded(()=>{})方法,该方法的回调函数根据使用recordManager.start(recordOption)方法时传入的 recordOption配置对象中的frameSize参数决定。该参数决定:每录制指定帧大小的内容后,会回调录制的文件内容 ,不指定则不会回调。暂仅支持 mp3、pcm 格式。 并且由于是当有指定帧大小的文件产生后就马上回调 出来。因此可以直接连接 websocket的发送函数(但是不建议这么做,因为根据要求(采用base64编码,每次发送音频间隔40ms,每次发送音频字节数1280B),建议将处理后的数据在通过websocket传给服务器。)

vue 复制代码
// 录音配置项
const recordOption = {
  duration: 60000, //指定录音的时长,单位 ms,微信可设置的最大值为 600000(10 分钟),科大讯飞最高支持时间为 60000 (1分钟)
  sampleRate: 16000, // 采样率(pc不支持)
  numberOfChannels: 1, // 录音通道数
  // encodeBitRate: 48000, // 编码码率(默认就是48000)
  frameSize: 1, // 指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。暂仅支持 mp3、pcm 格式。
  format: "pcm", // 音频格式,默认是 aac
};

二、关于返回数据为空的可能性

  1. 没开麦克风导致麦克风没有数据
  2. 麦克风数据杂乱,没有识别出来
  3. 没有使用官方demo中提供的渲染函数(renderResult())

三、在相当久的时间之后返回第一条数据的可能性

  1. 没开麦克风导致麦克风没有数据
  2. 麦克风数据杂乱,没有识别出来

四、科大讯飞建议每40ms发一次1.25kB的数据

五、要使用CryptoJS来解析数据

npm 复制代码
npm i CryptoJS

六、关于什么时候断联websocket连接

何时触发断联:

  1. 超过设置的录音时间
  2. 手动关闭录音
  3. 录音时出现未知错误(可能是录音设备导致的问题)

触发断联时的各种情况:

  1. 录音结束后,仍有数据没有发送完毕
  2. 录音结束时,服务器数据没有全部返回

关于上述问题,我们需要知道,什么时候算发送完毕? 答案是:最后一帧的录音 发送之后再返回的数据就是最后一个消息 。 我们无需判断何时发送录音数据的数据队列没有数据,只需要知道科大讯飞返回的消息 中是否为最后一帧

vue 复制代码
//监听科大讯飞消息返回
    uniSocketTask.onMessage((res) => {
      //收到消息
      const message = JSON.parse(res.data);
      //判断是否存在数据
      if (res.data) {
        console.log("收到服务器消息,并开始渲染");
        renderResult(message);
        if (message.code === 0 && message.data.status === 2) {
          //此时此刻我们可以得知当前科大讯飞返回的数据为最后一帧可以执行关闭连接函数
          //该函数为当前页唯一的关闭连接函数
          closeSocket();
        }
        //收到不正常服务器消息,返回错误到控制台
        if (message.code !== 0) {
          closeSocket();
          console.error(message);
        }
      } else {
        console.log("未监听到消息:原因:", JSON.stringify(res));
      }
    });

下述函数为获得录音结果的回调函数 ,其中有个回调参数为**isLastFrame**,为true则可以得知当前录音数据为最后一帧,录音结束之后再返回的数据会自动带上这个值的。

vue 复制代码
    //获得录音结果的回调函数
    recordManager.onFrameRecorded((res) => {
      // console.log("开始产生录音结果:");
      // frameBuffer:	type:[ArrayBuffer]	录音分片结果数据
      // isLastFrame:	type:[Boolean]	当前帧是否正常录音结束前的最后一帧
      const { frameBuffer, isLastFrame } = res;
      // console.log("录音分片结果:", toBase64(frameBuffer));
      // console.log("开始判断当前帧为第几帧");
      // 判断当前录音数据是否为最后一帧
      if (isLastFrame) {
        console.log("当前帧为录音最后一帧");
        status = FRAME.STATUS_LAST_FRAME;
      } 
      //存入心跳数据栈中,再合适的时候发送给科大讯飞
      sendDataStack.push(packFrame(frameBuffer)); //入栈
    });

参考文章:

uniapp - 接入科大讯飞语音评测 - 掘金 uniapp即时聊天 websocket封装(建立连接、断线重连、心跳机制、主动关闭) - 掘金 科大迅飞语音听写(流式版)WebAPI,Web前端、H5调用 语音识别,语音搜索,语音听写 - 掘金

文档:

科大讯飞文档:

语音转写_语音识别技术_录音文件识别-讯飞开放平台 语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心 讯飞开放平台语音识别音频文件格式说明 | 讯飞开放平台文档中心

uniapp文档

获取websocket对象 uni-app官网 录音api uni.getRecorderManager() | uni-app官网

参考代码

vue 复制代码
// 语音识别模块
<template>
  <div class="asr">
    <!-- <button @touchstart="openMedia" @touchend="stopRecord">长按开启麦克风</button> -->
    <button @click="openMedia">打开麦克风</button>
    <button @click="stopMedia">关闭麦克风</button>
    <p>{{ renderText }}</p>
  </div>
</template>

<script setup>
import { ref } from "vue";
import CryptoJS from "crypto-js";
import UniWebSocket from "@/utils/UniWebSocket.js";
// 科大讯飞接口配置
const config = {
  hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
  host: "iat-api.xfyun.cn",
  appid: "写你自己的",
  apiSecret: "写你自己的",
  apiKey: "写你自己的",
  uri: "/v2/iat",
  highWaterMark: 1280,
  // file: "./16k_10.pcm",
};

// 发给科大讯飞的每一帧的定义
//参考接口调用流程: https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B
const FRAME = {
  STATUS_FIRST_FRAME: 0, //第一帧音频
  STATUS_CONTINUE_FRAME: 1, //中间的音频
  STATUS_LAST_FRAME: 2, //最后一帧音频,最后一帧必须要发送
};
let status = FRAME.STATUS_FIRST_FRAME;

//心跳定时器
let heartTimer = null;
// 心跳数据(发给科大讯飞的数据)发送队列
let sendDataStack = [];

//websocket实例化
let uniSocketTask = null;

//打开麦克风
function openMedia() {
  status = FRAME.STATUS_FIRST_FRAME; //初始化音频状态(设置当前录音状态为首帧)
  connectSocket(); //创建websocket连接
  startRecord(); //打开录音
}
//关闭麦克风
function stopMedia() {
  //关闭录音
  stopRecord();
  //为什么关闭录音的时候不直接关闭websocket连接,这是因为:
  //关闭麦克风的时候会回调出录音最后一帧,并且会触发监听录音结束事件。
  //发送最后一帧后,得到最后一帧返回的消息后,此时此刻才应该关闭websocket连接
}
/**
 * 获取麦克风权限并开始录音(步骤一)
 */
// 录音配置项
const recordOption = {
  duration: 60000, //指定录音的时长,单位 ms,微信可设置的最大值为 600000(10 分钟),科大讯飞最高支持时间为 60000 (1分钟)
  sampleRate: 16000, // 采样率(pc不支持)
  numberOfChannels: 1, // 录音通道数
  // encodeBitRate: 48000, // 编码码率(默认就是48000)
  // frameSize 为指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。
  // 暂仅支持 mp3、pcm 格式。科大讯飞建议:每40ms发送一个节数为1280B的为压缩PCM格式
  frameSize: 1,
  format: "pcm", // 音频格式,默认是 aac
};
const recordManager = uni.getRecorderManager();
//开启录音
const startRecord = () => {
  recordManager.onStart(() => {
    console.log("开始录音");
    // ...
  });
  // recordManager.onPause(() => {
  //   console.log("录音暂停");
  // });
  recordManager.onStop((res) => {
    // tempFilePath	String	录音文件的临时路径
    console.log("录音停止,文件路径为:", res.tempFilePath);
  });
  recordManager.onError((err) => {
    // errMsg	String	错误信息
    console.log("录音出现错误", err);
  });
  //获得录音结果的回调函数
  recordManager.onFrameRecorded((res) => {
    // console.log("开始产生录音结果:");
    // frameBuffer:	type:[ArrayBuffer]	录音分片结果数据
    // isLastFrame:	type:[Boolean]	当前帧是否正常录音结束前的最后一帧
    const { frameBuffer, isLastFrame } = res;
    // console.log("录音分片结果:", toBase64(frameBuffer));
    // console.log("开始判断当前帧为第几帧");
    // 判断当前录音数据是否为最后一帧
    if (isLastFrame) {
      console.log("当前帧为录音最后一帧");
      status = FRAME.STATUS_LAST_FRAME;
    }
    //存入心跳数据栈中,再合适的时候发送给科大讯飞(第四步)
    sendDataStack.push(packFrame(frameBuffer)); //入栈
  });
  recordManager.start(recordOption);
};
//关闭录音
const stopRecord = () => {
  recordManager.stop();
};

//音频转码(步骤二)[把音频片段转换成base64]
function toBase64(buffer) {
  return uni.arrayBufferToBase64(buffer);
}

/**
 * 使用uniapp封装出来的websocketAPI
 */

// 鉴权签名
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;
}
function getUrl() {
  // 获取当前时间 RFC1123格式
  let date = new Date().toUTCString();
  //科大讯飞远程连接地址
  let wssUrl =
    config.hostUrl +
    "?authorization=" +
    getAuthStr(date) +
    "&date=" +
    encodeURIComponent(date) +
    "&host=" +
    config.host;
  console.log("websocke科大讯飞的地址为", wssUrl);
  return wssUrl;
}
//创建连接并返回数据
function connectSocket() {
  //创建socketTask实例
  if (uniSocketTask === null) {
    uniSocketTask = uni.connectSocket({ url: getUrl(), success() {} });
    //监听连接成功的事件
    uniSocketTask.onOpen(() => {
      console.log("监听到开启连接成功");
      //启动心跳定时器
      onHeartBeat();
    });
    //监听连接关闭的事件
    uniSocketTask.onClose(() => {
      console.log("监听到关闭连接成功");
    });
    uniSocketTask.onError(() => {
      console.log("监听到连接发生错误");
    });
    //监听科大讯飞消息返回
    uniSocketTask.onMessage((res) => {
      //收到消息
      const message = JSON.parse(res.data);
      //判断是否存在数据
      if (res.data) {
        console.log("收到服务器消息,并开始渲染");
        renderResult(message);
        if (message.code === 0 && message.data.status === 2) {
          //此时此刻我们可以得知当前科大讯飞返回的数据为最后一帧可以关闭连接
          //该函数为当前页唯一的关闭连接函数
          closeSocket();
        }
        //收到不正常服务器消息,返回错误到控制它
        if (message.code !== 0) {
          closeSocket();
          console.error(message);
        }
      } else {
        console.log("未监听到消息:原因:", JSON.stringify(res));
      }
    });
  } else {
    console.log("socketTask实例已存在");
  }
}
//关闭连接
function closeSocket() {
  console.log("开始尝试关闭连接");
  // 关闭心跳
  clearInterval(heartTimer);
  // 关闭连接
  uniSocketTask.close();
}

//发送给科大讯飞的每一帧的模板数据格式
let frame = {
  common: {
    app_id: config.appid,
  },
  business: {
    language: "zh_cn",
    domain: "iat",
    accent: "mandarin",
    dwa: "wpgs", // 可选参数,动态修正
    vad_eos: 5000,
  },
  data: {
    status: 0,
    format: "audio/L16;rate=16000",
    encoding: "raw",
    audio: "", //音频内容
  },
};
//封装录音后的每一"帧"数据(步骤三)
function packFrame(audioData) {
  //转载音频数据
  frame.data.audio = toBase64(audioData);
  switch (status) {
    //首条空白消息(第一帧)
    case FRAME.STATUS_FIRST_FRAME:
      // console.log("当前帧为第一帧");
      frame.data.status = 0;
      status = FRAME.STATUS_CONTINUE_FRAME;
      break;
    //正文(中间帧)
    case FRAME.STATUS_CONTINUE_FRAME:
      // console.log("当前帧为中间帧");
      frame.data.status = 1;
      break;
    //传输结束(最后一帧)
    case FRAME.STATUS_LAST_FRAME:
      // console.log("当前帧为最后一帧");
      frame.data.status = 2;
      break;
  }
  return JSON.stringify(frame);
}

//启动心跳连接定时器(第五步发送数据)
function onHeartBeat() {
  let sendData = null; //发送给科大讯飞的每一帧数据
  heartTimer = setInterval(() => {
    //发送队列中有数据的时候执行下述逻辑发送数据,否则不执行下述函数
    if (sendDataStack.length !== 0) {
      sendData = sendDataStack.shift();
      uniSocketTask.send({
        data: sendData,
        success() {
          console.log("发送成功");
        },
        fail() {
          console.log("发送失败");
        },
      });
    }
  }, 40);
}

//根据科大讯飞语音听写识别结果进行渲染(最后一步,根据onMessage()方法返回的数据进行渲染)
let resultText = "";
let resultTextTemp = "";
let renderText = ref("");
function renderResult(jsonData) {
  // console.log("开始执行渲染函数", jsonData);
  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;
    }
    renderText.value = resultTextTemp || resultText || "";
  }
  console.log("渲染后的数据为");
  console.log(renderText.value);
}
</script>

<style scoped lang="scss">
</style>
相关推荐
用户5191495848451 天前
使用Python ConfigParser解析INI配置文件完全指南
人工智能·aigc
小溪彼岸1 天前
分享一个Claude Code宝藏网站Claude Code Templates
aigc·claude
YFCodeDream1 天前
MLLM技术报告 核心创新一览
python·gpt·aigc
蛋先生DX1 天前
RAG 切片利器 LumberChunker 是如何智能地把文档切割成 LLM 爱吃的块
llm·aigc·ai编程
土丁爱吃大米饭1 天前
AIGC工具助力2D游戏美术全流程
aigc·小游戏·游戏开发·ai助力
安思派Anspire1 天前
为何你的RAG系统无法处理复杂问题(二)
aigc·openai·agent
Mintopia1 天前
🧠 可解释性AIGC:Web场景下模型决策透明化的技术路径
前端·javascript·aigc
用户5191495848451 天前
Flutter应用设置插件 - 轻松打开iOS和Android系统设置
人工智能·aigc
墨风如雪2 天前
DeepSeek OCR:用'眼睛'阅读长文本,AI记忆新纪元?
aigc
算家计算2 天前
SAIL-VL2本地部署教程:2B/8B参数媲美大规模模型,为轻量级设备量身打造的多模态大脑
人工智能·开源·aigc