VUE利用一句话复刻实现变声功能

发布于:2024-12-18 ⋅ 阅读:(61) ⋅ 点赞:(0)

实现思路:利用语音听写实现语音输入---拿到文字后自动调用一句话复刻实现声音输出。最终效果是A输入语音能够转换成B的语音输出。

<template>
  <div class="One-container">
    <div>
      <hr/>
      <!--发音音色列表展示-->
      <el-row :gutter="10" style="border-bottom: 1px dashed grey">
        <el-col :span="12" v-for="item in itemList">
          <el-card style="height: 112px;">
            <img src="../css_assets/7.png" style="width: 15%">
            <div style="display: inline-block;vertical-align: top;margin-left: 20px;width: 120px;">
              {{ item.time.substring(item.time.length - 8) }}<br>
              <span
                  style="padding-left: 5px;padding-right: 5px;  font-size: 12px;background-color: dodgerblue;color: white">
                一句复刻
              </span>
              <span style="margin-left: 10px;font-size: 12px;color: #409EFF;">个性音色</span>
              <br>
              <el-radio v-model="radio" :label=item.number>使用</el-radio>
              <el-popconfirm
                  confirm-button-text="确定"
                  cancel-button-text="取消"
                  icon="el-icon-info"
                  icon-color="red"
                  title="确认要删除吗?"
                  @confirm="confirmDelete(item.id)">
              <span slot="reference"
                    style="background-color: red;color: white;font-size: 12px;padding-left: 5px;padding-right: 5px;cursor: pointer">
                删除
              </span>
              </el-popconfirm>
            </div>
          </el-card>
        </el-col>
      </el-row>
      <!-- 合成界面-->
      <!--  合成文本区域  -->
      <div>
        <el-input type="textarea" v-model="ttsText" rows="12"
                  style="font-family: 'Microsoft YaHei';font-size: medium;font-weight: bold"
                  :maxlength="2000" show-word-limit placeholder="请输入不超过2000个汉字的文本进行合成">
        </el-input>
      </div>
      <br>
      <el-button type="primary" size="medium" @click="voiceSend"><i class="el-icon-microphone"></i>语音输入
      </el-button>
      <el-button type="primary" size="medium" @click="clickTts" style="margin-left:10px;">合成与播放</el-button>
      <el-button type="success" size="medium" @click="clickWav">下载并保存</el-button>

    </div>
  </div>
</template>

<script>
import router from "@/js_router/router";
import * as base64 from 'js-base64'
import CryptoJS from '../js_util/crypto-js/crypto-js.js'
import AudioPlayer from '../../public/player/index.umd.js'
// 初始化录音工具,注意目录
let recorder = new Recorder("../../recorder")
recorder.onStart = () => {
  console.log("开始录音了")
}
recorder.onStop = () => {
  console.log("结束录音了")
}
// 发送中间帧和最后一帧
recorder.onFrameRecorded = ({isLastFrame, frameBuffer}) => {
  if (!isLastFrame && wsFlag) { // 发送中间帧
    const params = {
      data: {
        status: 1,
        format: "audio/L16;rate=16000",
        encoding: "raw",
        audio: toBase64(frameBuffer),
      },
    };
    wsTask.send(JSON.stringify(params)) // 执行发送
  } else {
    if (wsFlag) {
      const params = {
        data: {
          status: 2,
          format: "audio/L16;rate=16000",
          encoding: "raw",
          audio: "",
        },
      };
      console.log("发送最后一帧", params, wsFlag)
      wsTask.send(JSON.stringify(params)) // 执行发送
    }
  }
}

function 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);
};
let wsFlag = false;
let wsTask = {};
const audioPlayer = new AudioPlayer("../../player"); // 播放器
export default {
  name: "One",
  data() {
    return {
      radio: '', // 单选项
      ttsText: "欢迎使用一句话复刻功能,让你的文字发出自己的声音。",
      itemList: [],
      sendForm: {id: 0},
      loading: false,
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {},
      URL: 'wss://iat-api.xfyun.cn/v2/iat',
      resultText: "",
      resultTextTemp: "",
    }
  },
  created() {
    // 查询发音因素列表
    this.selectListPage()
  },
  methods: {
    voiceSend() { // 开始语音识别要做的动作
      // 首先要调用扣费API
      this.user.ability = "语音听写能力" // 标记能力
      this.$http.post("/big/consume_balance", this.user).then(res => {
        if (res.data.code === "200") {
          // 触发父级更新user方法
          this.$emit("person_fff_user", res.data.object)
          this.resultText = "";
          this.resultTextTemp = "";
          this.wsInit();
        } else {
          this.$message.error(res.data.message)
          return false // 这个必须要做
        }
      })
      // 调用扣费API结束
    },
    // 建立ws连接
    async wsInit() {
      //  this.iat = "";
      this.$message.success("请您说出提问内容~")
      let _this = this;
      if (typeof (WebSocket) == 'undefined') {
        console.log('您的浏览器不支持ws...')
      } else {
        console.log('您的浏览器支持ws!!!')
        let reqeustUrl = await _this.getWebSocketUrlIat()
        wsTask = new WebSocket(reqeustUrl);
        // ws的几个事件,在vue中定义
        wsTask.onopen = function () {
          console.log('ws已经打开...')
          wsFlag = true
          let params = { // 第一帧数据
            common: {
              app_id: atob(_this.user.appid),
            },
            business: {
              language: "zh_cn",
              domain: "iat",
              accent: "mandarin",
              vad_eos: 2000,
              dwa: "wpgs",
            },
            data: {
              status: 0,
              format: "audio/L16;rate=16000",
              encoding: "raw",
            },
          };
          console.log("发送第一帧数据...")
          wsTask.send(JSON.stringify(params)) // 执行发送
          // 下面就可以循环发送中间帧了
          // 开始录音
          console.log("开始录音")
          recorder.start({
            sampleRate: 16000,
            frameSize: 1280,
          });
        }
        wsTask.onmessage = function (message) { // 调用第二个API 自动把语音转成文本
          console.log('收到数据===' + message.data)
          let jsonData = JSON.parse(message.data);
          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;
            }
            if (data.pgs) {
              if (data.pgs === "apd") {
                // 将resultTextTemp同步给resultText
                _this.resultText = _this.resultTextTemp;
              }
              // 将结果存储在resultTextTemp中
              _this.resultTextTemp = _this.resultText + str;
            } else {
              _this.resultText = _this.resultText + str;
            }
            _this.ttsText = _this.resultTextTemp || _this.resultText || "";
          }
          // 检测到结束或异常关闭
          if (jsonData.code === 0 && jsonData.data.status === 2) { // 拿到最终的听写文本后,我们会调用大模型
            // alert("执行了")
            recorder.stop();
            _this.$message.success("检测到您2秒没说话,自动结束识别!")
            _this.clickTts();
            wsTask.close();
            wsFlag = false
          }
          if (jsonData.code !== 0) {
            wsTask.close();
            wsFlag = false
            console.error(jsonData);
          }
        }
        // 关闭事件
        wsTask.onclose = function () {
          console.log('ws已关闭...')
        }
        wsTask.onerror = function () {
          console.log('发生错误...')
        }
      }
    }
    ,
// 获取鉴权地址与参数
    getWebSocketUrlIat() {
      return new Promise((resolve, reject) => {
        // 请求地址根据语种不同变化
        var url = this.URL;
        var host = "iat-api.xfyun.cn";
        var apiKeyName = "api_key";
        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, atob(this.user.apisecret));
        var signature = CryptoJS.enc.Base64.stringify(signatureSha);
        var authorizationOrigin =
            `${apiKeyName}="${atob(this.user.apikey)}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
        var authorization = base64.encode(authorizationOrigin);
        url = `${url}?authorization=${authorization}&date=${encodeURI(date)}&host=${host}`;
        console.log(url)
        resolve(url); // 主要是返回地址
      });
    }
    ,
    clickTts() {
      // console.log(this.user)
      // 首先要调用扣费API
      this.user.ability = "一句话复刻合成" // 标记能力
      this.$http.post("/big/consume_balance", this.user).then(res => {
        if (res.data.code === "200") {
          // 触发父级更新user方法
          this.$emit("person_fff_user", res.data.object)
          this.doWsWork(); // 调用ws链接方法
        } else {
          this.$message.error(res.data.message)
          return false // 这个必须要做
        }
      })
      // 调用扣费API结束
    },
    doWsWork() {
      const url = this.getWebSocketUrl(atob(this.user.apikey), atob(this.user.apisecret));
      if ("WebSocket" in window) {
        this.ttsWS = new WebSocket(url);
      } else if ("MozWebSocket" in window) {
        this.ttsWS = new MozWebSocket(url);
      } else {
        alert("浏览器不支持WebSocket");
        return;
      }
      this.ttsWS.onopen = (e) => {
        console.log("链接成功...")
        audioPlayer.start({
          autoPlay: true,
          sampleRate: 16000,
          resumePlayDuration: 1000
        });
        let text = this.ttsText;
        let tte = document.getElementById("tte") ? "unicode" : "UTF8";
        let params = {
          "payload": {
            "text": {
              "compress": "raw",
              "format": "plain",
              "text": this.encodeText(text, tte),
              "encoding": "utf8",
              "seq": 0,
              "status": 2
            }
          },
          "parameter": {
            "tts": {
              "vcn": "x5_clone",
              "volume": 50,
              "rhy": 0,
              "pybuffer": 1,
              "pybuf": {
                "compress": "raw",
                "format": "plain",
                "encoding": "utf8"
              },
              "pitch": 50,
              "audio": {
                "sample_rate": 16000,
                "channels": 1,
                "encoding": "raw",
                "bit_depth": 16
              },
              "speed": 50
            }
          },
          "header": {
            "res_id": this.radio,
            "app_id": atob(this.user.appid),
            "status": 2
          }
        }
        this.ttsWS.send(JSON.stringify(params));
        console.log("发送成功...")
      };
      this.ttsWS.onmessage = (e) => {
        let jsonData = JSON.parse(e.data);
        console.log("合成返回的数据" + JSON.stringify(jsonData));
        // 合成失败
        if (jsonData.header.code !== 0) {
          console.error(jsonData);
          return;
        }
        if (jsonData.hasOwnProperty("payload")) {
          audioPlayer.postMessage({
            type: "base64",
            data: jsonData.payload.audio.audio,
            isLastData: jsonData.header.status === 2,
          });
        }

        if (jsonData.header.code === 0 && jsonData.header.status === 2) {
          this.ttsWS.close();
        }
      };
      this.ttsWS.onerror = (e) => {
        console.error(e);
      };
      this.ttsWS.onclose = (e) => {
        console.log(e + "链接已关闭");
      };
    },
    getWebSocketUrl(apiKey, apiSecret) {
      let url = "wss://cbm01.cn-huabei-1.xf-yun.com/v1/private/s06a6b848";
      let host = location.host;
      let date = new Date().toGMTString();
      let algorithm = "hmac-sha256";
      let headers = "host date request-line";
      let signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v1/private/s06a6b848 HTTP/1.1`;
      let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
      let signature = CryptoJS.enc.Base64.stringify(signatureSha);
      let authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
      let authorization = btoa(authorizationOrigin);
      url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
      return url;
    },
    // 文本编码
    encodeText(text, type) {
      if (type === "unicode") {
        let buf = new ArrayBuffer(text.length * 4);
        let bufView = new Uint16Array(buf);
        for (let i = 0, strlen = text.length; i < strlen; i++) {
          bufView[i] = text.charCodeAt(i);
        }
        let binary = "";
        let bytes = new Uint8Array(buf);
        let len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
          binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary);
      } else {
        return base64.encode(text);
      }
    },
    clickWav() {
      const blob = audioPlayer.getAudioDataBlob("wav")
      if (!blob) {
        return
      }
      let defaultName = new Date().getTime();
      let node = document.createElement("a");
      node.href = window.URL.createObjectURL(blob);
      node.download = `${defaultName}.wav`;
      node.click();
      node.remove();
    },
    /*确认删除*/
    confirmDelete(id) {
      // console.log(id)
      this.sendForm.id = id // 主要是这个id
      this.$http.post("/timbre/delete", this.sendForm).then(res => {
        if (res.data.code === "200") {
          this.$message.success('删除成功')
        } else {
          this.$message.error('删除失败,' + res.data.message)
        }
        this.selectListPage()
      })
    },
    selectListPage() {
      this.$http.post("/timbre/list_timbre", {name: this.user.name}).then(res => {
        if (res.data.code === "200") {
          // console.log(res)
          this.itemList = res.data.object
        } else {
          this.$message.error('查询失败,' + res.data.code)
          router.push("/login")
        }
      })
    }
  }
}
</script>

<style scoped>
</style>


网站公告

今日签到

点亮在社区的每一天
去签到