|
@@ -9,12 +9,14 @@ import axios from "axios";
|
|
|
import { fetchEventSource } from "@microsoft/fetch-event-source";
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
import delay from "delay";
|
|
|
+import { fetchTaskStatus } from "./utils/api";
|
|
|
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
const conversationId = computed(() => route.query.taskId); // 会话ID
|
|
|
const round = computed(() => route.query.round); // 轮次
|
|
|
const sessionId = ref("");
|
|
|
+const taskStatus = ref(false)
|
|
|
|
|
|
const currentRate = ref(0);
|
|
|
const currentRound = computed(() => {
|
|
@@ -53,19 +55,10 @@ const updateChatList = async (message = "", loading = true) => {
|
|
|
clearInterval(timer);
|
|
|
}
|
|
|
}, 50);
|
|
|
-
|
|
|
- // chatList.value = chatList.value.map((item) =>
|
|
|
- // item.loading
|
|
|
- // ? message
|
|
|
- // ? { ...item, text: message, loading }
|
|
|
- // : { ...item, loading }
|
|
|
- // : item
|
|
|
- // );
|
|
|
- // localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
|
|
|
};
|
|
|
|
|
|
// 创建一个队列实例,设置并发数为 1
|
|
|
-const queue = new PQueue({ concurrency: 1 });
|
|
|
+const queue = new PQueue({ concurrency: 4 });
|
|
|
const queue1 = new PQueue({ concurrency: 1 });
|
|
|
const queue2 = new PQueue({ concurrency: 1 });
|
|
|
|
|
@@ -77,13 +70,13 @@ let lastStrIndex = 0;
|
|
|
|
|
|
// 将字符串根据标点符号断句分割,并添加到messageQueue中
|
|
|
const splitMessage = async (str) => {
|
|
|
- const punctuation = ["。", "!", "?", ";", ":"];
|
|
|
+ const punctuation = ["。","," ,"!", "?", ";", ":"];
|
|
|
|
|
|
for (let i = lastStrIndex; i < str.length; i++) {
|
|
|
if (punctuation.includes(str[i])) {
|
|
|
const message = str.slice(lastStrIndex, i + 1);
|
|
|
console.log(message, "==========");
|
|
|
- await playTTS(message);
|
|
|
+ playTTS(message);
|
|
|
lastStrIndex = i + 1; // 更新上一个字符串的结束位置
|
|
|
}
|
|
|
}
|
|
@@ -125,114 +118,229 @@ const handleStopRecord = (message) => {
|
|
|
sessionId.value = generateSessionId();
|
|
|
chatGpt(message);
|
|
|
};
|
|
|
+let audioQueue = []; // 音频队列,保存按请求顺序的音频数据
|
|
|
+let playingQueue = []; // 用来记录当前正在播放的音频序号
|
|
|
+let isPlaying = false; // 当前是否正在播放
|
|
|
+let orderId = 0; // 请求顺序号
|
|
|
+
|
|
|
+// 用来控制正在播放音频的锁定机制
|
|
|
+let playingLock = false; // 标识是否正在播放
|
|
|
+
|
|
|
+// 1. 发起 TTS 请求
|
|
|
+// const playTTS = async (ttsMessage) => {
|
|
|
+// const currentOrderId = orderId++; // 获取当前请求的顺序号
|
|
|
+
|
|
|
+// try {
|
|
|
+// // 发起请求
|
|
|
+// const res = await axios.get(
|
|
|
+// `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
|
|
|
+// { responseType: "arraybuffer" }
|
|
|
+// );
|
|
|
+
|
|
|
+// // 将音频数据和顺序号一起存入 audioQueue
|
|
|
+// audioQueue.push({ orderId: currentOrderId, audioData: res.data });
|
|
|
+
|
|
|
+// // 如果当前没有音频在播放,开始播放
|
|
|
+// if (!isPlaying && !playingLock) {
|
|
|
+// await playNextAudio(); // 开始播放
|
|
|
+// }
|
|
|
+// } catch (error) {
|
|
|
+// console.error("调用 TTS API 时出错:", error);
|
|
|
+// }
|
|
|
+// };
|
|
|
+const playTTS = async (ttsMessage) => {
|
|
|
+ queue.add(async () => {
|
|
|
+ const currentOrderId = orderId++;
|
|
|
|
|
|
-const playTTS1 = async (ttsMessage) => {
|
|
|
- try {
|
|
|
- const res = await axios.post(
|
|
|
- `/openapi-stg/ai/voice/tts/v2`,
|
|
|
- { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
|
|
|
- {
|
|
|
- responseType: "arraybuffer",
|
|
|
- headers: {
|
|
|
- "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
|
|
|
- },
|
|
|
+ try {
|
|
|
+ const res = await axios.get(
|
|
|
+ `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
|
|
|
+ { responseType: "arraybuffer" }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 按顺序存入音频队列
|
|
|
+ audioQueue.push({ orderId: currentOrderId, audioData: res.data });
|
|
|
+
|
|
|
+ // 仅在没有播放时启动播放
|
|
|
+ if (!isPlaying) {
|
|
|
+ await playNextAudio();
|
|
|
}
|
|
|
- );
|
|
|
- console.log(res.data);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("调用 TTS API 时出错:", error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+// 2. 播放下一个音频
|
|
|
+const playNextAudio = async () => {
|
|
|
+ // 如果播放队列为空,停止播放
|
|
|
+ if (audioQueue.length === 0) {
|
|
|
+ isPlaying = false; // 播放完所有音频
|
|
|
+ console.log("所有音频播放完毕.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- queue1.add(async () => {
|
|
|
- // 播放获取到的音频
|
|
|
- await playAudio(res.data);
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- console.error("Error calling TTS API:", error);
|
|
|
+ // 找到队列中最小序号的音频,确保顺序播放
|
|
|
+ const nextAudio = getNextAudioToPlay();
|
|
|
+
|
|
|
+ // 如果找不到下一个应该播放的音频,表示队列还没有按顺序准备好
|
|
|
+ if (!nextAudio) {
|
|
|
+ console.log("没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
|
|
|
+ console.log("当前音频队列:", audioQueue); // 打印当前音频队列
|
|
|
+ console.log("当前已播放的序列:", playingQueue); // 打印已经播放的音频序列
|
|
|
+ return;
|
|
|
}
|
|
|
-};
|
|
|
|
|
|
-const playTTS = async (ttsMessage) => {
|
|
|
+ // 打印正在播放的音频序号
|
|
|
+ console.log(`正在播放音频,序号:${nextAudio.orderId}`);
|
|
|
+
|
|
|
try {
|
|
|
- const res = await axios.get(
|
|
|
- `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
|
|
|
- {
|
|
|
- responseType: "arraybuffer",
|
|
|
- }
|
|
|
- );
|
|
|
- console.log(res.data);
|
|
|
+ isPlaying = true; // 标记当前正在播放
|
|
|
+ playingLock = true; // 锁定播放,避免新的音频插入
|
|
|
|
|
|
- queue1.add(async () => {
|
|
|
- // 播放获取到的音频
|
|
|
- await playAudio(res.data);
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- console.error("Error calling TTS API:", error);
|
|
|
- }
|
|
|
-};
|
|
|
+ // 播放当前音频
|
|
|
+ await playAudio(nextAudio.audioData);
|
|
|
|
|
|
-const playAudio1 = (audioData, options = {}) => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const blob = new Blob([audioData], { type: "audio/wav" });
|
|
|
- const url = URL.createObjectURL(blob);
|
|
|
- const audio = new Audio(url);
|
|
|
+ // 播放完成后,移除已播放的音频
|
|
|
+ audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
|
|
|
+ playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
|
|
|
|
|
|
- audio.setAttribute("id", "audio");
|
|
|
- audio.setAttribute("autoplay", "autoplay");
|
|
|
+ // 输出音频播放完成的日志
|
|
|
+ console.log(`音频序号 ${nextAudio.orderId} 播放完成,等待播放下一个包...`);
|
|
|
|
|
|
- if (options.volume) {
|
|
|
- audio.volume = options.volume; // 设置音量
|
|
|
- }
|
|
|
+ // 解除锁定,等待下一个音频播放
|
|
|
+ playingLock = false;
|
|
|
|
|
|
- audio.onended = () => {
|
|
|
- URL.revokeObjectURL(url);
|
|
|
- resolve();
|
|
|
- };
|
|
|
+ // 使用定时器周期性检查是否可以播放下一个音频
|
|
|
+ const checkNextAudioTimer = setInterval(async () => {
|
|
|
+ if (!playingLock) {
|
|
|
+ await playNextAudio();
|
|
|
+ clearInterval(checkNextAudioTimer); // 清除定时器
|
|
|
+ }
|
|
|
+ }, 300); // 每秒检查一次
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`播放音频序号 ${nextAudio.orderId} 时出错:`, error);
|
|
|
+ // 如果某个音频播放失败,跳过这个音频,继续播放下一个
|
|
|
+ audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId); // 移除报错的音频
|
|
|
+ playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
|
|
|
+ playingLock = false; // 解除锁定
|
|
|
+ // 继续播放下一个音频
|
|
|
+ await playNextAudio();
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- audio.onerror = (error) => {
|
|
|
- URL.revokeObjectURL(url);
|
|
|
- reject(error);
|
|
|
- };
|
|
|
|
|
|
- audio
|
|
|
- .play()
|
|
|
- .then(() => console.log("Audio playing"))
|
|
|
- .catch(reject);
|
|
|
- });
|
|
|
+// 获取下一个应该播放的音频(保证顺序)
|
|
|
+const getNextAudioToPlay = () => {
|
|
|
+ // 检查音频队列中是否有下一个应该播放的音频
|
|
|
+ console.log("检查下一个播放的音频...");
|
|
|
+ for (let i = 0; i < audioQueue.length; i++) {
|
|
|
+ const audio = audioQueue[i];
|
|
|
+ // 找到最小的序号,即请求的顺序号
|
|
|
+ if (audio.orderId === playingQueue.length) {
|
|
|
+ console.log(`找到了下一个应该播放的音频,序号:${audio.orderId}`);
|
|
|
+ return audio;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.log("没有找到下一个应该播放的音频,返回 null");
|
|
|
+ return null; // 如果没有找到匹配的音频,返回 null
|
|
|
};
|
|
|
|
|
|
-const playAudio = (audioData, options = {}) => {
|
|
|
+// 播放音频的方法
|
|
|
+const playAudio = (audioData) => {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
- const audioContext = new (window.AudioContext ||
|
|
|
- window.webkitAudioContext)();
|
|
|
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
const blob = new Blob([audioData], { type: "audio/wav" });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const audioBufferSourceNode = audioContext.createBufferSource();
|
|
|
|
|
|
fetch(url)
|
|
|
- .then((response) => response.arrayBuffer())
|
|
|
- .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
|
|
|
- .then((audioBuffer) => {
|
|
|
- audioBufferSourceNode.buffer = audioBuffer;
|
|
|
- audioBufferSourceNode.connect(audioContext.destination);
|
|
|
-
|
|
|
- if (options.volume) {
|
|
|
- audioBufferSourceNode.gain.value = options.volume; // 设置音量
|
|
|
- }
|
|
|
-
|
|
|
- audioBufferSourceNode.start(0);
|
|
|
-
|
|
|
- audioBufferSourceNode.onended = () => {
|
|
|
- URL.revokeObjectURL(url);
|
|
|
- resolve();
|
|
|
- };
|
|
|
-
|
|
|
- audioBufferSourceNode.onerror = (error) => {
|
|
|
- URL.revokeObjectURL(url);
|
|
|
- reject(error);
|
|
|
- };
|
|
|
- })
|
|
|
- .catch(reject);
|
|
|
+ .then((response) => {
|
|
|
+ if (!response.ok) {
|
|
|
+ console.error("音频文件请求失败: ", response.statusText);
|
|
|
+ reject(new Error("音频文件请求失败"));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return response.arrayBuffer();
|
|
|
+ })
|
|
|
+ .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
|
|
|
+ .then((audioBuffer) => {
|
|
|
+ audioBufferSourceNode.buffer = audioBuffer;
|
|
|
+ audioBufferSourceNode.connect(audioContext.destination);
|
|
|
+
|
|
|
+ audioBufferSourceNode.start(0);
|
|
|
+
|
|
|
+ // 当音频播放完毕时,调用 resolve
|
|
|
+ audioBufferSourceNode.onended = () => {
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ resolve(); // 播放完成后,继续播放下一个
|
|
|
+ };
|
|
|
+
|
|
|
+ audioBufferSourceNode.onerror = (error) => {
|
|
|
+ console.error("音频播放错误:", error);
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+ reject(error); // 播放失败时,返回错误
|
|
|
+ };
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error("音频加载或解码时出错:", error);
|
|
|
+ reject(error); // 如果解码或播放出错,reject
|
|
|
+ });
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+// const playTTS1 = async (ttsMessage) => {
|
|
|
+// try {
|
|
|
+// const res = await axios.post(
|
|
|
+// `/openapi-stg/ai/voice/tts/v2`,
|
|
|
+// { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
|
|
|
+// {
|
|
|
+// responseType: "arraybuffer",
|
|
|
+// headers: {
|
|
|
+// "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
|
|
|
+// },
|
|
|
+// }
|
|
|
+// );
|
|
|
+// console.log(res.data);
|
|
|
+
|
|
|
+// queue1.add(async () => {
|
|
|
+// // 播放获取到的音频
|
|
|
+// await playAudio(res.data);
|
|
|
+// });
|
|
|
+// } catch (error) {
|
|
|
+// console.error("Error calling TTS API:", error);
|
|
|
+// }
|
|
|
+// };
|
|
|
+
|
|
|
+// const playAudio1 = (audioData, options = {}) => {
|
|
|
+// return new Promise((resolve, reject) => {
|
|
|
+// const blob = new Blob([audioData], { type: "audio/wav" });
|
|
|
+// const url = URL.createObjectURL(blob);
|
|
|
+// const audio = new Audio(url);
|
|
|
+
|
|
|
+// audio.setAttribute("id", "audio");
|
|
|
+// audio.setAttribute("autoplay", "autoplay");
|
|
|
+
|
|
|
+// if (options.volume) {
|
|
|
+// audio.volume = options.volume; // 设置音量
|
|
|
+// }
|
|
|
+
|
|
|
+// audio.onended = () => {
|
|
|
+// URL.revokeObjectURL(url);
|
|
|
+// resolve();
|
|
|
+// };
|
|
|
+
|
|
|
+// audio.onerror = (error) => {
|
|
|
+// URL.revokeObjectURL(url);
|
|
|
+// reject(error);
|
|
|
+// };
|
|
|
+
|
|
|
+// audio
|
|
|
+// .play()
|
|
|
+// .then(() => console.log("Audio playing"))
|
|
|
+// .catch(reject);
|
|
|
+// });
|
|
|
+// };
|
|
|
+
|
|
|
const chatGpt = async (userMessage) => {
|
|
|
const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
|
|
|
i = 0;
|
|
@@ -265,38 +373,17 @@ const chatGpt = async (userMessage) => {
|
|
|
queue.add(async () => {
|
|
|
await splitMessage(responseText);
|
|
|
});
|
|
|
- // const timer = setInterval(() => {
|
|
|
- // i = i + 1;
|
|
|
- // //updateChatList(responseText.substring(0, i));
|
|
|
- // queue2.add(
|
|
|
- // async () => await updateChatList(responseText.substring(0, i))
|
|
|
- // );
|
|
|
- // if (i >= responseText.length) {
|
|
|
- // clearInterval(timer);
|
|
|
- // }
|
|
|
- // console.log(
|
|
|
- // i,
|
|
|
- // responseText.substring(i),
|
|
|
- // "=======responseText.substring(i)"
|
|
|
- // );
|
|
|
- // if (
|
|
|
- // responseText.substring(i).indexOf("[DONE]") === 0 ||
|
|
|
- // responseText.substring(i).indexOf("[DONE]") === 1
|
|
|
- // ) {
|
|
|
- // console.log("========done===========");
|
|
|
- // updateChatList("", false);
|
|
|
- // // 保存聊天记录到本地
|
|
|
- // const chatHistory = JSON.stringify(chatList.value);
|
|
|
- // localStorage.setItem("chatHistory", chatHistory);
|
|
|
- // }
|
|
|
- //}, 200);
|
|
|
-
|
|
|
+
|
|
|
queue2.add(async () => await updateChatList(responseText));
|
|
|
+
|
|
|
//updateChatList(responseText);
|
|
|
}
|
|
|
},
|
|
|
}
|
|
|
);
|
|
|
+ setTimeout(() => {
|
|
|
+ handleTaskStatus()
|
|
|
+ },3000)
|
|
|
//queue2.add(async () => await updateChatList("", false));
|
|
|
} catch (error) {
|
|
|
//updateChatList("", false);
|
|
@@ -380,23 +467,35 @@ const next = () => {
|
|
|
};
|
|
|
|
|
|
const loadChatHistory = () => {
|
|
|
+
|
|
|
const chatHistory = localStorage.getItem("chatHistory");
|
|
|
+ const chatStatus = localStorage.getItem("status");
|
|
|
if (chatHistory) {
|
|
|
const history = JSON.parse(chatHistory);
|
|
|
history.forEach((item) => {
|
|
|
chatList.value.push(item);
|
|
|
});
|
|
|
}
|
|
|
+ if(chatStatus){
|
|
|
+ taskStatus.value = JSON.parse(chatStatus)
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
+// 查询任务结束状态
|
|
|
+
|
|
|
+const handleTaskStatus = async () => {
|
|
|
+ const res = await fetchTaskStatus(conversationId.value)
|
|
|
+ if(res.code == 200){
|
|
|
+ taskStatus.value = res.body
|
|
|
+ localStorage.setItem("status", res.body);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 在组件挂载时调用
|
|
|
onMounted(() => {
|
|
|
loadChatHistory();
|
|
|
});
|
|
|
|
|
|
-// onBeforeUnmount(() => {
|
|
|
-// localStorage.removeItem("chatHistory");
|
|
|
-// });
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
@@ -416,9 +515,9 @@ onMounted(() => {
|
|
|
<BottomArea
|
|
|
@startRecord="handleStartRecord"
|
|
|
@stopRecord="handleStopRecord"
|
|
|
- v-show="rate < 100"
|
|
|
+ v-show="rate < 100 || taskStatus"
|
|
|
/>
|
|
|
- <div v-show="rate >= 100" class="next-btn">
|
|
|
+ <div v-show="rate >= 100 || taskStatus" class="next-btn">
|
|
|
<van-button style="width: 100%" @click="next" type="primary"
|
|
|
>已完成对话,下一步</van-button
|
|
|
>
|