科大讯飞官方文档:语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心

这是demo

这次主要是将demo改写成了vue语法,其他的都没变。

先在public文件下加入了iat,也就是demo文件中的dist文件夹

 

然后还要准备CryptoJS这个第三方的包import CryptoJS from 'crypto-js';,直接npm下载就行。

最后是封装了一个组件,来使用。这个项目使用的是ant的组件库。可自行换成别的。

<script>
import { Close, Voice } from '@icon-park/vue-next';
import CryptoJS from 'crypto-js';
import { message } from 'ant-design-vue';

export default {
  name: 'XfIat',
  components: { Close, Voice },
  data() {
    return {
      // 控制录音弹窗
      voiceOpen: false,
      // 是否开始录音
      startVoiceStatus: false,
      // 识别中状态
      identifyStatus: false,
      recorder: null,
      transcription: '',
      btnStatus: '',
      resultText: '',
      resultTextTemp: '',
      countdownInterval: null,
      iatWS: null,
      recognition: null,
    };
  },
  emits: ['sendMsg'],
  props: {
    buttonDisabled: {
      type: Boolean,
      default: true,
    },
    loading: {
      type: Boolean,
      default: false,
    },
    xfIatKeys: {
      default: {
        APPID: '',
        APIKey: '',
        APISecret: '',
      },
    },
  },
  methods: {
    /**
     * 打开录音
     * @returns {Promise<void>}
     */
    async startVoice() {
      if (this.loading) {
        return;
      }
      this.voiceOpen = true;
      await this.playIatVoice();
    },
    async playIatVoice() {
      if (this.loading) {
        return;
      }
      this.startVoiceStatus = !this.startVoiceStatus;
      // 浏览器自带的识别
      if (this.recognition) {
        if (this.startVoiceStatus) {
          this.recognition.start();
        } else {
          this.recognition.stop();
        }
        return;
      }
      if (this.startVoiceStatus) {
        this.connectWebSocket();
      } else {
        this.recorder.stop();
      }
    },
    /**
     * 关闭录音弹窗
     */
    closeVoiceOpen() {
      this.voiceOpen = false;
      this.startVoiceStatus = false;
      if (this.recorder) {
        this.recorder.stop();
      }
      if (this.recognition) {
        this.recognition.stop();
      }
      this.transcription = '';
    },
    renderResult(resultData) {
      // 识别结束
      const jsonData = JSON.parse(resultData);
      if (jsonData.data && jsonData.data.result) {
        const data = jsonData.data.result;
        let str = '';
        const { ws } = data;
        for (let i = 0; i < ws.length; i += 1) {
          str += ws[i].cw[0].w;
        }
        // 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
        // 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
        if (data.pgs) {
          if (data.pgs === 'apd') {
            // 将resultTextTemp同步给resultText
            this.resultText = this.resultTextTemp;
          }
          // 将结果存储在resultTextTemp中
          this.resultTextTemp = this.resultText + str;
        } else {
          this.resultText += str;
        }
        this.transcription = this.resultTextTemp || this.resultText || '';
      }
      if (jsonData.code === 0 && jsonData.data.status === 2) {
        this.iatWS.close();
      }
      if (jsonData.code !== 0) {
        this.iatWS.close();
        console.error(jsonData);
      }
    },
    connectWebSocket() {
      const websocketUrl = this.getWebSocketUrl();
      if ('WebSocket' in window) {
        this.iatWS = new window.WebSocket(websocketUrl);
      } else if ('MozWebSocket' in window) {
        this.iatWS = new window.MozWebSocket(websocketUrl);
      } else {
        message.error('浏览器不支持WebSocket');
        return;
      }
      this.changeBtnStatus('CONNECTING');
      this.iatWS.onopen = e => {
        console.log('iatWS.onopen', e);
        // 开始录音
        this.recorder.start({
          sampleRate: 16000,
          frameSize: 1280,
        });
        const params = {
          common: {
            app_id: this.xfIatKeys.APPID,
          },
          business: {
            language: 'zh_cn',
            domain: 'iat',
            accent: 'mandarin',
            vad_eos: 5000,
            dwa: 'wpgs',
            nbest: 1,
            wbest: 1,
          },
          data: {
            status: 0,
            format: 'audio/L16;rate=16000',
            encoding: 'raw',
          },
        };
        this.iatWS.send(JSON.stringify(params));
      };
      this.iatWS.onmessage = e => {
        this.renderResult(e.data);
      };
      this.iatWS.onerror = e => {
        console.error(e);
        this.recorder.stop();
        this.changeBtnStatus('CLOSED');
      };
      this.iatWS.onclose = e => {
        console.log(e);
        this.recorder.stop();
        this.changeBtnStatus('CLOSED');
      };
    },
    getWebSocketUrl() {
      const { APIKey, APISecret } = this.xfIatKeys;
      if (!APIKey) {
        message.error('语音识别配置未生效');
        return null;
      }
      // 请求地址根据语种不同变化
      let url = 'wss://iat-api.xfyun.cn/v2/iat';
      const host = 'iat-api.xfyun.cn';
      const apiKey = APIKey;
      const apiSecret = APISecret;
      const date = new Date().toGMTString();
      const algorithm = 'hmac-sha256';
      const headers = 'host date request-line';
      const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
      const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
      const signature = CryptoJS.enc.Base64.stringify(signatureSha);
      const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
      const authorization = btoa(authorizationOrigin);
      url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
      return url;
    },
    countdown() {
      let seconds = 60;
      console.log(`录音中(${seconds}s)`);
      this.countdownInterval = setInterval(() => {
        seconds -= 1;
        if (seconds <= 0) {
          clearInterval(this.countdownInterval);
          this.recorder.stop();
        } else {
          console.log(`录音中(${seconds}s)`);
        }
      }, 1000);
    },
    changeBtnStatus(status) {
      this.btnStatus = status;
      if (status === 'CONNECTING') {
        console.log('建立连接中');
        this.resultText = '';
        this.resultTextTemp = '';
      } else if (status === 'OPEN') {
        if (this.recorder) {
          this.countdown();
        }
      } else if (status === 'CLOSING') {
        console.log('关闭连接中');
      } else if (status === 'CLOSED') {
        console.log('开始录音');
      }
    },
    toBase64(buffer) {
      let binary = '';
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i += 1) {
        binary += String.fromCharCode(bytes[i]);
      }
      return window.btoa(binary);
    },
    sendMsg(text) {
      this.$emit('sendMsg', text);
      this.transcription = '';
    },
  },
  created() {
    // 浏览器自带的识别
    /* if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
      const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
      this.recognition = new SpeechRecognition();
      this.recognition.continuous = true; // 连续识别
      this.recognition.interimResults = true; // 显示中间结果
      this.recognition.lang = 'zh-CN'; // 设置语言为中文(简体)
      // 识别结果
      this.recognition.onresult = event => {
        this.transcription = Array.from(event.results)
          .map(result => result[0].transcript)
          .join('');
      };

      this.recognition.onerror = event => {
        console.error('识别错误:', event.error);
      };

      this.recognition.onend = () => {
        console.log('录音停止了');
        this.startVoiceStatus = false;
      };

      this.recognition.onstart = () => {
        this.changeBtnStatus('OPEN');
      };
      return;
    } */
    this.recorder = new window.RecorderManager('/iat');
    this.recorder.onStart = () => {
      this.changeBtnStatus('OPEN');
    };
    this.recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
      if (this.iatWS.readyState === this.iatWS.OPEN) {
        this.iatWS.send(
          JSON.stringify({
            data: {
              status: isLastFrame ? 2 : 1,
              format: 'audio/L16;rate=16000',
              encoding: 'raw',
              audio: this.toBase64(frameBuffer),
            },
          }),
        );
        if (isLastFrame) {
          this.changeBtnStatus('CLOSING');
        }
      }
    };
    this.recorder.onStop = () => {
      console.log('录音结束,停止定时器');
      clearInterval(this.countdownInterval);
      this.startVoiceStatus = false;
    };
  },
};
</script>

<template>
  <a-tooltip placement="top">
    <template #title>
      <span style="font-size: 12px;font-weight: 300">语音输入</span>
    </template>
    <div
        @click="startVoice"
        :class="['send-btn',buttonDisabled ? 'inactive' : 'active']">
      <voice
          theme="outline"
          size="24"
          :strokeWidth="3"
          class="icon"/>
    </div>
  </a-tooltip>

  <div class="voice-box" v-if="voiceOpen">
    <div class="voice-body">
      <div class="close-box">
        <close theme="outline" class="close-icon" size="16" @click.stop="closeVoiceOpen"/>
      </div>
      <div class="textarea-box">
        <a-textarea
            v-model:value="transcription"
            :rows="2"
            :bordered="false"
            style="padding: 0;resize: none;"
            class="custom-textarea-wrapper"
        />
        <div class="text-center-box">
          <div class="font-medium">{{ identifyStatus ? '识别中' : '想问什么,说来听听...' }}</div>
          <div class="tip-text">点击下方语音图标可{{ startVoiceStatus ? '停止' : '开始' }}录音</div>
        </div>
      </div>
      <div class="action-box">
        <a-button type="text" :disabled="!transcription||startVoiceStatus" @click="transcription=''">清空</a-button>
        <div class="start-voice-box" :style="loading?'cursor: not-allowed':''">
          <voice
              @click.stop="playIatVoice"
              theme="outline"
              size="20"
              :strokeWidth="3"
              class="start-voice"/>
        </div>
        <a-button type="text" :disabled="!transcription||startVoiceStatus" @click="sendMsg(transcription)">发送</a-button>
        <div class="water-ripples" :style="{opacity:startVoiceStatus?1:0}">
          <div class="circle"></div>
          <div class="circle circle1"></div>
          <div class="circle circle2"></div>
          <div class="circle circle3"></div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped lang="less">
.send-btn {
  //margin-left: 1rem;
  height: 2.25rem;
  width: 2.25rem;
  cursor: pointer;
  border-radius: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 0.15s;

  &:hover {
    background-color: #f2f3f5;

    .icon {
      margin: 2px 2px 0 0;
      color: #7f5df6;
    }
  }
}

.inactive {
  .icon {
    color: #b4becf;
    margin: 2px 2px 0 0;
  }
}

.active {
  .icon {
    margin: 2px 2px 0 0;
    color: #7f5df6;
  }
}

.voice-box {
  width: 100%;
  position: fixed;
  bottom: 5px;
  left: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;

  .voice-body {
    position: absolute;
    bottom: 0;
    width: 64%;
    height: 11rem;
    border-radius: 1rem;
    padding: 1rem 1.25rem;
    font-size: .875rem;
    line-height: 1.5rem;
    box-shadow: 0 8px 32px rgba(0, 0, 0, .16);
    box-sizing: border-box;
    background-color: #fff;
    border: 1px solid #e4e7ed;
    color: #303133;
    display: flex;
    flex-direction: column;
    justify-content: space-between;

    .close-box {
      position: absolute;
      right: 1rem;
      top: 1rem;

      .close-icon {
        color: #596780;
        float: right;
        cursor: pointer;
      }
    }

    .textarea-box {
      padding-right: 1.25rem;

      .custom-textarea-wrapper {
        max-height: 90px; /* 设置最大高度以激活滚动条 */
        overflow: auto; /* 启用滚动条 */
      }

      /* WebKit 浏览器的滚动条样式 */

      .custom-textarea-wrapper ::-webkit-scrollbar {
        width: 8px; /* 设置滚动条的宽度 */
      }

      .custom-textarea-wrapper ::-webkit-scrollbar-track {
        background: transparent; /* 滚动条轨道背景 */
      }

      .custom-textarea-wrapper ::-webkit-scrollbar-thumb {
        background: #f2f3f5; /* 滚动条滑块的颜色 */
        border-radius: 4px; /* 滚动条滑块的圆角 */
      }

      .custom-textarea-wrapper ::-webkit-scrollbar-thumb:hover {
        background: #555; /* 滚动条滑块在悬停时的颜色 */
      }

      /* Firefox 的滚动条样式 */

      .custom-textarea-wrapper {
        scrollbar-width: thin; /* 滚动条宽度: thin/auto */
        scrollbar-color: #f2f3f5 transparent; /* 滚动条颜色 (滑块 背景) */
      }

      .text-center-box {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;

        .font-medium {
          font-weight: 500;
        }

        .tip-text {
          font-size: .75rem;
          line-height: 1rem;
          margin-top: 0.35rem;
          color: #9DA3AF;
        }
      }
    }

    .action-box {
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      gap: 2.5rem;

      .start-voice-box {
        width: 2.25rem;
        height: 2.25rem;
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: rgb(124 92 252);
        border-radius: 50%;
        cursor: pointer;
        z-index: 3;

        .start-voice {
          color: #fff;
        }
      }

      .start-voice-box:hover {
        opacity: 0.8;
      }

      .water-ripples {
        z-index: 2;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 28px;
        height: 28px;

        .circle {
          width: 28px;
          height: 28px;
          background: rgb(124, 92, 252);
          border-radius: 100%;
          position: absolute;
        }

        .circle1 {
          animation-delay: 0s;
          animation-name: waterCircle;
          animation-duration: 2s;
          animation-iteration-count: infinite;
          animation-timing-function: linear;
        }

        .circle2 {
          animation-delay: 1s;
          animation-name: waterCircle;
          animation-duration: 2s;
          animation-iteration-count: infinite;
          animation-timing-function: linear;
        }

        .circle3 {
          animation-delay: 2s;
          animation-name: waterCircle;
          animation-duration: 2s;
          animation-iteration-count: infinite;
          animation-timing-function: linear;
        }

        @keyframes waterCircle {
          0% {
            transform: scale(1);
            opacity: .5;
          }
          25% {
            transform: scale(1.25);
            opacity: .375;
          }
          50% {
            transform: scale(1.5);
            opacity: .25;
          }
          75% {
            transform: scale(1.75);
            opacity: .125;
          }
          100% {
            transform: scale(2);
            opacity: .05;
          }
        }
      }

      .ant-btn-text {
        color: #909399;
      }

      .ant-btn-text:disabled {
        background-color: transparent;
        border-color: transparent;
        color: #c8c9cc;
      }

      .ant-btn-text:not(:disabled):hover {
        background-color: transparent;
        border-color: transparent;
        color: #c8c9cc;
      }
    }
  }
}
@media (max-width: 1279px) {
  .voice-body{
    width: 76% !important;
  }
}
@media (max-width: 1023px) {
  .voice-body{
    width: calc(100% - 1rem) !important;
  }
}
</style>

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐