ChatTts.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <script setup>
  2. import { ref, computed, onMounted, onBeforeUnmount } from "vue";
  3. import BG from "./components/BG.vue";
  4. import BottomArea from "./components/BottomArea.vue";
  5. import ChatList from "./components/ChatList.vue";
  6. import NavBarPage from "../components/NavBarPage.vue";
  7. import PQueue from "p-queue";
  8. import axios from "axios";
  9. import { fetchEventSource } from "@microsoft/fetch-event-source";
  10. import { useRoute, useRouter } from "vue-router";
  11. import delay from "delay";
  12. import { fetchTaskStatus } from "./utils/api";
  13. const route = useRoute();
  14. const router = useRouter();
  15. const conversationId = computed(() => route.query.taskId); // 会话ID
  16. const round = computed(() => route.query.round); // 轮次
  17. const sessionId = ref("");
  18. const taskStatus = ref(false)
  19. const loading = ref(true); //初始化进入的加载状态
  20. const currentRate = ref(0);
  21. const currentRound = computed(() => {
  22. const filteredMessages = chatList.value.filter(
  23. (message) =>
  24. message.text !== "我没有听清,麻烦在重复一下。" && message.text !== ""
  25. );
  26. return Math.floor(filteredMessages.length / 2);
  27. });
  28. // 当前进度百分比整数部分
  29. const rate = computed(() => {
  30. return Math.floor((currentRound.value / round.value) * 100);
  31. });
  32. const text = computed(() => `${currentRound.value} / ${round.value}`);
  33. // 对话记录
  34. const chatList = ref([]);
  35. let i = 0;
  36. // 我 我是 我是中国人 我是中国人的英雄 我是中国人的英雄。[
  37. const updateChatList = async (message = "", loading = true) => {
  38. const timer = setInterval(() => {
  39. i = i + 1;
  40. chatList.value = chatList.value.map((item, index) => {
  41. if (index == chatList.value.length - 1) {
  42. return {
  43. ...item,
  44. text: message.substring(0, i),
  45. };
  46. } else {
  47. return item;
  48. }
  49. });
  50. localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
  51. if (i >= message.length) {
  52. clearInterval(timer);
  53. }
  54. }, 50);
  55. };
  56. // 创建一个队列实例,设置并发数为 1
  57. const queue = new PQueue({ concurrency: 3 });
  58. const queue1 = new PQueue({ concurrency: 1 });
  59. const queue2 = new PQueue({ concurrency: 1 });
  60. // 需要tts的文本队列
  61. const messageQueue = [];
  62. // lastStrIndex 用于记录上一个字符串的结束位置
  63. let lastStrIndex = 0;
  64. // 将字符串根据标点符号断句分割,并添加到messageQueue中
  65. const splitMessage = async (str) => {
  66. const punctuation = ["。","," ,"!", "?", ";", ":"];
  67. for (let i = lastStrIndex; i < str.length; i++) {
  68. if (punctuation.includes(str[i])) {
  69. const message = str.slice(lastStrIndex, i + 1);
  70. console.log(message, "==========");
  71. playTTS(message);
  72. lastStrIndex = i + 1; // 更新上一个字符串的结束位置
  73. }
  74. }
  75. };
  76. const handleStartRecord = async () => {
  77. queue.clear();
  78. queue1.clear();
  79. };
  80. const generateSessionId = () => {
  81. // 获取当前时间戳
  82. const timestamp = Date.now().toString();
  83. // 生成一个随机数
  84. const randomNum = Math.floor(Math.random() * 10000);
  85. // 将时间戳和随机数拼接成会话ID
  86. const sessionId = `${timestamp}${randomNum}`;
  87. return sessionId;
  88. };
  89. // 发送消息
  90. const handleStopRecord = (message) => {
  91. lastStrIndex = 0; // 重置
  92. chatList.value = [
  93. ...chatList.value,
  94. {
  95. id: Date.now(),
  96. type: "user",
  97. text: message,
  98. loading: false,
  99. },
  100. {
  101. id: Date.now(),
  102. type: "gpt",
  103. text: "",
  104. loading: true,
  105. },
  106. ];
  107. sessionId.value = generateSessionId();
  108. chatGpt(message);
  109. };
  110. let audioQueue = []; // 音频队列,保存按请求顺序的音频数据
  111. let playingQueue = []; // 用来记录当前正在播放的音频序号
  112. let isPlaying = false; // 当前是否正在播放
  113. let orderId = 0; // 请求顺序号
  114. // 用来控制正在播放音频的锁定机制
  115. let playingLock = false; // 标识是否正在播放
  116. // 1. 发起 TTS 请求
  117. // const playTTS = async (ttsMessage) => {
  118. // const currentOrderId = orderId++; // 获取当前请求的顺序号
  119. // try {
  120. // // 发起请求
  121. // const res = await axios.get(
  122. // `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
  123. // { responseType: "arraybuffer" }
  124. // );
  125. // // 将音频数据和顺序号一起存入 audioQueue
  126. // audioQueue.push({ orderId: currentOrderId, audioData: res.data });
  127. // // 如果当前没有音频在播放,开始播放
  128. // if (!isPlaying && !playingLock) {
  129. // await playNextAudio(); // 开始播放
  130. // }
  131. // } catch (error) {
  132. // console.error("调用 TTS API 时出错:", error);
  133. // }
  134. // };
  135. const playTTS = async (ttsMessage) => {
  136. queue.add(async () => {
  137. const currentOrderId = orderId++;
  138. try {
  139. const res = await axios.get(
  140. `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
  141. { responseType: "arraybuffer" }
  142. );
  143. if (!res.data || res.data.byteLength === 0) {
  144. console.error("音频数据无效,无法播放.");
  145. return;
  146. }
  147. // 按顺序存入音频队列
  148. audioQueue.push({ orderId: currentOrderId, audioData: res.data });
  149. // 仅在没有播放时启动播放
  150. if (!isPlaying) {
  151. await playNextAudio();
  152. }
  153. } catch (error) {
  154. console.error("调用 TTS API 时出错:", error);
  155. }
  156. });
  157. };
  158. // 2. 播放下一个音频
  159. const playNextAudio = async () => {
  160. // 如果播放队列为空,停止播放
  161. if (audioQueue.length === 0) {
  162. isPlaying = false; // 播放完所有音频
  163. // audioQueue = []; // 确保列表为空
  164. console.log("所有音频播放完毕.");
  165. return;
  166. }
  167. // 找到队列中最小序号的音频,确保顺序播放
  168. const nextAudio = getNextAudioToPlay();
  169. console.log(nextAudio,"没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
  170. // 如果找不到下一个应该播放的音频,表示队列还没有按顺序准备好
  171. if (!nextAudio) {
  172. console.log("没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
  173. console.log("当前音频队列:", audioQueue); // 打印当前音频队列
  174. console.log("当前已播放的序列:", playingQueue); // 打印已经播放的音频序列
  175. return;
  176. }
  177. // 打印正在播放的音频序号
  178. console.log(`正在播放音频,序号:${nextAudio.orderId}`);
  179. try {
  180. isPlaying = true; // 标记当前正在播放
  181. playingLock = true; // 锁定播放,避免新的音频插入
  182. // 播放当前音频
  183. await playAudio(nextAudio.audioData);
  184. // 播放完成后,移除已播放的音频
  185. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
  186. playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  187. // 输出音频播放完成的日志
  188. console.log(`音频序号 ${nextAudio.orderId} 播放完成,等待播放下一个包...`);
  189. // 解除锁定,等待下一个音频播放
  190. playingLock = false;
  191. // 使用定时器周期性检查是否可以播放下一个音频
  192. const checkNextAudioTimer = setInterval(async () => {
  193. if (!playingLock) {
  194. await playNextAudio();
  195. clearInterval(checkNextAudioTimer); // 清除定时器
  196. }
  197. }, 300); // 每秒检查一次
  198. } catch (error) {
  199. console.error(`播放音频序号 ${nextAudio.orderId} 时出错:`, error);
  200. // 如果某个音频播放失败,跳过这个音频,继续播放下一个
  201. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId); // 移除报错的音频
  202. playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  203. playingLock = false; // 解除锁定
  204. // 继续播放下一个音频
  205. await playNextAudio();
  206. }
  207. };
  208. // 获取下一个应该播放的音频(保证顺序)
  209. const getNextAudioToPlay = () => {
  210. // 检查音频队列中是否有下一个应该播放的音频
  211. console.log("检查下一个播放的音频...");
  212. for (let i = 0; i < audioQueue.length; i++) {
  213. const audio = audioQueue[i];
  214. // 找到最小的序号,即请求的顺序号
  215. if (audio.orderId === playingQueue.length) {
  216. console.log(`找到了下一个应该播放的音频,序号:${audio.orderId}`);
  217. return audio;
  218. }
  219. }
  220. console.log("没有找到下一个应该播放的音频,返回 null");
  221. return null; // 如果没有找到匹配的音频,返回 null
  222. };
  223. // 播放音频的方法
  224. const playAudio = (audioData) => {
  225. return new Promise((resolve, reject) => {
  226. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  227. const blob = new Blob([audioData], { type: "audio/wav" });
  228. const url = URL.createObjectURL(blob);
  229. const audioBufferSourceNode = audioContext.createBufferSource();
  230. fetch(url)
  231. .then((response) => {
  232. if (!response.ok) {
  233. console.error("音频文件请求失败: ", response.statusText);
  234. reject(new Error("音频文件请求失败"));
  235. return;
  236. }
  237. return response.arrayBuffer();
  238. })
  239. .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
  240. .then((audioBuffer) => {
  241. audioBufferSourceNode.buffer = audioBuffer;
  242. audioBufferSourceNode.connect(audioContext.destination);
  243. audioBufferSourceNode.start(0);
  244. // 当音频播放完毕时,调用 resolve
  245. audioBufferSourceNode.onended = () => {
  246. URL.revokeObjectURL(url);
  247. resolve(); // 播放完成后,继续播放下一个
  248. };
  249. audioBufferSourceNode.onerror = (error) => {
  250. console.error("音频播放错误:", error);
  251. URL.revokeObjectURL(url);
  252. reject(error); // 播放失败时,返回错误
  253. };
  254. })
  255. .catch((error) => {
  256. console.error("音频加载或解码时出错:", error);
  257. reject(error); // 如果解码或播放出错,reject
  258. });
  259. });
  260. };
  261. // const playTTS1 = async (ttsMessage) => {
  262. // try {
  263. // const res = await axios.post(
  264. // `/openapi-stg/ai/voice/tts/v2`,
  265. // { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
  266. // {
  267. // responseType: "arraybuffer",
  268. // headers: {
  269. // "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  270. // },
  271. // }
  272. // );
  273. // console.log(res.data);
  274. // queue1.add(async () => {
  275. // // 播放获取到的音频
  276. // await playAudio(res.data);
  277. // });
  278. // } catch (error) {
  279. // console.error("Error calling TTS API:", error);
  280. // }
  281. // };
  282. // const playAudio1 = (audioData, options = {}) => {
  283. // return new Promise((resolve, reject) => {
  284. // const blob = new Blob([audioData], { type: "audio/wav" });
  285. // const url = URL.createObjectURL(blob);
  286. // const audio = new Audio(url);
  287. // audio.setAttribute("id", "audio");
  288. // audio.setAttribute("autoplay", "autoplay");
  289. // if (options.volume) {
  290. // audio.volume = options.volume; // 设置音量
  291. // }
  292. // audio.onended = () => {
  293. // URL.revokeObjectURL(url);
  294. // resolve();
  295. // };
  296. // audio.onerror = (error) => {
  297. // URL.revokeObjectURL(url);
  298. // reject(error);
  299. // };
  300. // audio
  301. // .play()
  302. // .then(() => console.log("Audio playing"))
  303. // .catch(reject);
  304. // });
  305. // };
  306. const chatGpt = async (userMessage) => {
  307. const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
  308. i = 0;
  309. queue2.add(() => delay(3000));
  310. try {
  311. const response = await axios.post(
  312. downloadUrl,
  313. {
  314. conversationId: conversationId.value,
  315. content: userMessage,
  316. },
  317. {
  318. headers: {
  319. "Content-Type": "application/json",
  320. },
  321. onDownloadProgress: ({ event }) => {
  322. const xhr = event.target;
  323. let { responseText } = xhr;
  324. console.log("responseText", responseText);
  325. if (responseText.includes("code") && responseText.includes("400")) {
  326. // console.log("responseTextcode", responseText);
  327. // 更新聊天列表
  328. const text = "我没有听清,麻烦在重复一下。";
  329. updateChatList(text, false);
  330. queue.add(async () => {
  331. await splitMessage(text);
  332. });
  333. } else {
  334. // 用于语音播报
  335. queue.add(async () => {
  336. await splitMessage(responseText);
  337. });
  338. queue2.add(async () => await updateChatList(responseText));
  339. //updateChatList(responseText);
  340. }
  341. },
  342. }
  343. );
  344. setTimeout(() => {
  345. audioQueue=[]
  346. playingQueue=[]
  347. handleTaskStatus()
  348. },3000)
  349. //queue2.add(async () => await updateChatList("", false));
  350. } catch (error) {
  351. //updateChatList("", false);
  352. console.error("出错:", error);
  353. }
  354. };
  355. const chatGpt2 = (userMessage) => {
  356. const params = {
  357. conversationId: "2c64eb8b69be432ca0bb9ae55bc78def",
  358. content: userMessage,
  359. };
  360. const ctrlAbout = new AbortController();
  361. fetchEventSource(
  362. "https://fls-ai.pingan.com.cn/openapi/ai/intelligent-tutoring/task/dialogue",
  363. {
  364. method: "POST",
  365. headers: {
  366. "Content-Type": "application/json", // 文本返回格式
  367. },
  368. body: JSON.stringify(params),
  369. signal: ctrlAbout.signal,
  370. openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接
  371. onmessage(res) {
  372. // 操作流式数据
  373. console.log(JSON.parse(res.data), "==========");
  374. },
  375. onclose(res) {
  376. // 关闭流
  377. },
  378. onerror(error) {
  379. // 返回流报错
  380. },
  381. }
  382. );
  383. };
  384. const chatGpt1 = async (userMessage) => {
  385. const downloadUrl =
  386. "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue";
  387. try {
  388. const response = await axios.post(
  389. downloadUrl,
  390. {
  391. conversationId: "1976cfe3a5174f9ba768677f789cad7e",
  392. content: `${userMessage},请输出纯文本的回答,不要使用markdown输出`,
  393. messageId: Date.now(),
  394. applicationId: "",
  395. },
  396. {
  397. headers: {
  398. "Team-Id": "123456",
  399. Authorization: "9b2f86c99b5847739045e6b85f355301",
  400. "Content-Type": "application/json",
  401. },
  402. onDownloadProgress: ({ event }) => {
  403. const xhr = event.target;
  404. const { responseText } = xhr;
  405. console.log(responseText);
  406. // 更新聊天列表
  407. updateChatList(responseText);
  408. // 用于语音播报
  409. queue.add(async () => {
  410. await splitMessage(responseText);
  411. });
  412. },
  413. }
  414. );
  415. updateChatList("", false);
  416. } catch (error) {
  417. updateChatList("", false);
  418. console.error("下载文件时出错:", error);
  419. }
  420. };
  421. const next = () => {
  422. router.push({
  423. name: "result",
  424. query: { conversationId: conversationId.value },
  425. });
  426. };
  427. const loadChatHistory = () => {
  428. const chatHistory = localStorage.getItem("chatHistory");
  429. const chatStatus = localStorage.getItem("status");
  430. if (chatHistory) {
  431. const history = JSON.parse(chatHistory);
  432. history.forEach((item) => {
  433. chatList.value.push(item);
  434. });
  435. }
  436. if(chatStatus){
  437. taskStatus.value = JSON.parse(chatStatus)
  438. }
  439. };
  440. // 查询任务结束状态
  441. const handleTaskStatus = async () => {
  442. const res = await fetchTaskStatus(conversationId.value)
  443. if(res.code == 200){
  444. taskStatus.value = res.body
  445. localStorage.setItem("status", res.body);
  446. }
  447. }
  448. // 在组件挂载时调用
  449. onMounted(() => {
  450. loadChatHistory();
  451. setTimeout(() => {
  452. loading.value = false;
  453. }, 2000);
  454. });
  455. </script>
  456. <template>
  457. <div class="main">
  458. <nav-bar-page title="考试" />
  459. <div class="rate-circle">
  460. <van-circle
  461. :stroke-width="80"
  462. size="70px"
  463. v-model:current-rate="currentRate"
  464. :rate="rate"
  465. :text="text"
  466. />
  467. </div>
  468. <BG />
  469. <div class="loading" v-show="loading">
  470. <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
  471. </div>
  472. <ChatList :chatList="chatList" />
  473. <BottomArea
  474. @startRecord="handleStartRecord"
  475. @stopRecord="handleStopRecord"
  476. v-show="rate < 100 || taskStatus"
  477. />
  478. <div v-show="rate >= 100 || taskStatus" class="next-btn">
  479. <van-button style="width: 100%" @click="next" type="primary"
  480. >已完成对话,下一步</van-button
  481. >
  482. </div>
  483. </div>
  484. </template>
  485. <style>
  486. * {
  487. -webkit-touch-callout: none; /*系统默认菜单被禁用*/
  488. -webkit-user-select: none; /*webkit浏览器*/
  489. -khtml-user-select: none; /*早期浏览器*/
  490. -moz-user-select: none; /*火狐*/
  491. -ms-user-select: none; /*IE10*/
  492. user-select: none;
  493. }
  494. </style>
  495. <style scoped>
  496. .main {
  497. width: 100vw;
  498. height: 100vh;
  499. overflow: hidden;
  500. }
  501. .rate-circle {
  502. position: fixed;
  503. right: 20px;
  504. top: 70px;
  505. z-index: 9999;
  506. }
  507. .next-btn {
  508. position: fixed;
  509. bottom: calc(20px + env(safe-area-inset-bottom));
  510. width: 100%;
  511. box-sizing: border-box;
  512. padding: 0 20px;
  513. }
  514. .loading {
  515. position: fixed;
  516. top: 0;
  517. left: 0;
  518. width: 100%;
  519. height: 100%;
  520. background-color: rgba(0, 0, 0, 0.5);
  521. display: flex;
  522. justify-content: center;
  523. align-items: center;
  524. z-index: 9999;
  525. }
  526. </style>