实现思路:利用语音听写实现语音输入---拿到文字后自动调用一句话复刻实现声音输出。最终效果是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><spanstyle="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-popconfirmconfirm-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() { // 开始语音识别要做的动作// 首先要调用扣费APIthis.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 = truelet 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 = falseconsole.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)// 首先要调用扣费APIthis.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 // 主要是这个idthis.$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>