实现思路:利用语音听写实现语音输入---拿到文字后自动调用一句话复刻实现声音输出。最终效果是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>