ChatTts.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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: 4 });
  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. // 按顺序存入音频队列
  144. audioQueue.push({ orderId: currentOrderId, audioData: res.data });
  145. // 仅在没有播放时启动播放
  146. if (!isPlaying) {
  147. await playNextAudio();
  148. }
  149. } catch (error) {
  150. console.error("调用 TTS API 时出错:", error);
  151. }
  152. });
  153. };
  154. // 2. 播放下一个音频
  155. const playNextAudio = async () => {
  156. // 如果播放队列为空,停止播放
  157. if (audioQueue.length === 0) {
  158. isPlaying = false; // 播放完所有音频
  159. audioQueue = []; // 确保列表为空
  160. console.log("所有音频播放完毕.");
  161. return;
  162. }
  163. // 找到队列中最小序号的音频,确保顺序播放
  164. const nextAudio = getNextAudioToPlay();
  165. // 如果找不到下一个应该播放的音频,表示队列还没有按顺序准备好
  166. if (!nextAudio) {
  167. console.log("没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
  168. console.log("当前音频队列:", audioQueue); // 打印当前音频队列
  169. console.log("当前已播放的序列:", playingQueue); // 打印已经播放的音频序列
  170. return;
  171. }
  172. // 打印正在播放的音频序号
  173. console.log(`正在播放音频,序号:${nextAudio.orderId}`);
  174. try {
  175. isPlaying = true; // 标记当前正在播放
  176. playingLock = true; // 锁定播放,避免新的音频插入
  177. // 播放当前音频
  178. await playAudio(nextAudio.audioData);
  179. // 播放完成后,移除已播放的音频
  180. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
  181. playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  182. // 输出音频播放完成的日志
  183. console.log(`音频序号 ${nextAudio.orderId} 播放完成,等待播放下一个包...`);
  184. // 解除锁定,等待下一个音频播放
  185. playingLock = false;
  186. // 使用定时器周期性检查是否可以播放下一个音频
  187. const checkNextAudioTimer = setInterval(async () => {
  188. if (!playingLock) {
  189. await playNextAudio();
  190. clearInterval(checkNextAudioTimer); // 清除定时器
  191. }
  192. }, 300); // 每秒检查一次
  193. } catch (error) {
  194. console.error(`播放音频序号 ${nextAudio.orderId} 时出错:`, error);
  195. // 如果某个音频播放失败,跳过这个音频,继续播放下一个
  196. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId); // 移除报错的音频
  197. playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  198. playingLock = false; // 解除锁定
  199. // 继续播放下一个音频
  200. await playNextAudio();
  201. }
  202. };
  203. // 获取下一个应该播放的音频(保证顺序)
  204. const getNextAudioToPlay = () => {
  205. // 检查音频队列中是否有下一个应该播放的音频
  206. console.log("检查下一个播放的音频...");
  207. for (let i = 0; i < audioQueue.length; i++) {
  208. const audio = audioQueue[i];
  209. // 找到最小的序号,即请求的顺序号
  210. if (audio.orderId === playingQueue.length) {
  211. console.log(`找到了下一个应该播放的音频,序号:${audio.orderId}`);
  212. return audio;
  213. }
  214. }
  215. console.log("没有找到下一个应该播放的音频,返回 null");
  216. return null; // 如果没有找到匹配的音频,返回 null
  217. };
  218. // 播放音频的方法
  219. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  220. const playAudio = (audioData) => {
  221. return new Promise((resolve, reject) => {
  222. const blob = new Blob([audioData], { type: "audio/wav" });
  223. const url = URL.createObjectURL(blob);
  224. fetch(url)
  225. .then((res) => res.arrayBuffer())
  226. .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
  227. .then((audioBuffer) => {
  228. const source = audioContext.createBufferSource();
  229. source.buffer = audioBuffer;
  230. source.connect(audioContext.destination);
  231. source.start(0);
  232. source.onended = () => {
  233. URL.revokeObjectURL(url);
  234. resolve();
  235. };
  236. source.onerror = (error) => {
  237. console.error("音频播放错误:", error);
  238. URL.revokeObjectURL(url);
  239. reject(error);
  240. };
  241. })
  242. .catch((error) => {
  243. console.error("音频加载或解码时出错:", error);
  244. reject(error);
  245. });
  246. });
  247. };
  248. // const playAudio = (audioData) => {
  249. // return new Promise((resolve, reject) => {
  250. // const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  251. // const blob = new Blob([audioData], { type: "audio/wav" });
  252. // const url = URL.createObjectURL(blob);
  253. // const audioBufferSourceNode = audioContext.createBufferSource();
  254. // fetch(url)
  255. // .then((response) => {
  256. // if (!response.ok) {
  257. // console.error("音频文件请求失败: ", response.statusText);
  258. // reject(new Error("音频文件请求失败"));
  259. // return;
  260. // }
  261. // return response.arrayBuffer();
  262. // })
  263. // .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
  264. // .then((audioBuffer) => {
  265. // audioBufferSourceNode.buffer = audioBuffer;
  266. // audioBufferSourceNode.connect(audioContext.destination);
  267. // audioBufferSourceNode.start(0);
  268. // // 当音频播放完毕时,调用 resolve
  269. // audioBufferSourceNode.onended = () => {
  270. // URL.revokeObjectURL(url);
  271. // resolve(); // 播放完成后,继续播放下一个
  272. // };
  273. // audioBufferSourceNode.onerror = (error) => {
  274. // console.error("音频播放错误:", error);
  275. // URL.revokeObjectURL(url);
  276. // reject(error); // 播放失败时,返回错误
  277. // };
  278. // })
  279. // .catch((error) => {
  280. // console.error("音频加载或解码时出错:", error);
  281. // reject(error); // 如果解码或播放出错,reject
  282. // });
  283. // });
  284. // };
  285. // const playTTS1 = async (ttsMessage) => {
  286. // try {
  287. // const res = await axios.post(
  288. // `/openapi-stg/ai/voice/tts/v2`,
  289. // { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
  290. // {
  291. // responseType: "arraybuffer",
  292. // headers: {
  293. // "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  294. // },
  295. // }
  296. // );
  297. // console.log(res.data);
  298. // queue1.add(async () => {
  299. // // 播放获取到的音频
  300. // await playAudio(res.data);
  301. // });
  302. // } catch (error) {
  303. // console.error("Error calling TTS API:", error);
  304. // }
  305. // };
  306. // const playAudio1 = (audioData, options = {}) => {
  307. // return new Promise((resolve, reject) => {
  308. // const blob = new Blob([audioData], { type: "audio/wav" });
  309. // const url = URL.createObjectURL(blob);
  310. // const audio = new Audio(url);
  311. // audio.setAttribute("id", "audio");
  312. // audio.setAttribute("autoplay", "autoplay");
  313. // if (options.volume) {
  314. // audio.volume = options.volume; // 设置音量
  315. // }
  316. // audio.onended = () => {
  317. // URL.revokeObjectURL(url);
  318. // resolve();
  319. // };
  320. // audio.onerror = (error) => {
  321. // URL.revokeObjectURL(url);
  322. // reject(error);
  323. // };
  324. // audio
  325. // .play()
  326. // .then(() => console.log("Audio playing"))
  327. // .catch(reject);
  328. // });
  329. // };
  330. const chatGpt = async (userMessage) => {
  331. const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
  332. i = 0;
  333. queue2.add(() => delay(3000));
  334. try {
  335. const response = await axios.post(
  336. downloadUrl,
  337. {
  338. conversationId: conversationId.value,
  339. content: userMessage,
  340. },
  341. {
  342. headers: {
  343. "Content-Type": "application/json",
  344. },
  345. onDownloadProgress: ({ event }) => {
  346. const xhr = event.target;
  347. let { responseText } = xhr;
  348. console.log("responseText", responseText);
  349. if (responseText.includes("code") && responseText.includes("400")) {
  350. // console.log("responseTextcode", responseText);
  351. // 更新聊天列表
  352. const text = "我没有听清,麻烦在重复一下。";
  353. updateChatList(text, false);
  354. queue.add(async () => {
  355. await splitMessage(text);
  356. });
  357. } else {
  358. // 用于语音播报
  359. queue.add(async () => {
  360. await splitMessage(responseText);
  361. });
  362. queue2.add(async () => await updateChatList(responseText));
  363. //updateChatList(responseText);
  364. }
  365. },
  366. }
  367. );
  368. setTimeout(() => {
  369. handleTaskStatus()
  370. },3000)
  371. //queue2.add(async () => await updateChatList("", false));
  372. } catch (error) {
  373. //updateChatList("", false);
  374. console.error("出错:", error);
  375. }
  376. };
  377. const chatGpt2 = (userMessage) => {
  378. const params = {
  379. conversationId: "2c64eb8b69be432ca0bb9ae55bc78def",
  380. content: userMessage,
  381. };
  382. const ctrlAbout = new AbortController();
  383. fetchEventSource(
  384. "https://fls-ai.pingan.com.cn/openapi/ai/intelligent-tutoring/task/dialogue",
  385. {
  386. method: "POST",
  387. headers: {
  388. "Content-Type": "application/json", // 文本返回格式
  389. },
  390. body: JSON.stringify(params),
  391. signal: ctrlAbout.signal,
  392. openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接
  393. onmessage(res) {
  394. // 操作流式数据
  395. console.log(JSON.parse(res.data), "==========");
  396. },
  397. onclose(res) {
  398. // 关闭流
  399. },
  400. onerror(error) {
  401. // 返回流报错
  402. },
  403. }
  404. );
  405. };
  406. const chatGpt1 = async (userMessage) => {
  407. const downloadUrl =
  408. "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue";
  409. try {
  410. const response = await axios.post(
  411. downloadUrl,
  412. {
  413. conversationId: "1976cfe3a5174f9ba768677f789cad7e",
  414. content: `${userMessage},请输出纯文本的回答,不要使用markdown输出`,
  415. messageId: Date.now(),
  416. applicationId: "",
  417. },
  418. {
  419. headers: {
  420. "Team-Id": "123456",
  421. Authorization: "9b2f86c99b5847739045e6b85f355301",
  422. "Content-Type": "application/json",
  423. },
  424. onDownloadProgress: ({ event }) => {
  425. const xhr = event.target;
  426. const { responseText } = xhr;
  427. console.log(responseText);
  428. // 更新聊天列表
  429. updateChatList(responseText);
  430. // 用于语音播报
  431. queue.add(async () => {
  432. await splitMessage(responseText);
  433. });
  434. },
  435. }
  436. );
  437. updateChatList("", false);
  438. } catch (error) {
  439. updateChatList("", false);
  440. console.error("下载文件时出错:", error);
  441. }
  442. };
  443. const next = () => {
  444. router.push({
  445. name: "result",
  446. query: { conversationId: conversationId.value },
  447. });
  448. };
  449. const loadChatHistory = () => {
  450. const chatHistory = localStorage.getItem("chatHistory");
  451. const chatStatus = localStorage.getItem("status");
  452. if (chatHistory) {
  453. const history = JSON.parse(chatHistory);
  454. history.forEach((item) => {
  455. chatList.value.push(item);
  456. });
  457. }
  458. if(chatStatus){
  459. taskStatus.value = JSON.parse(chatStatus)
  460. }
  461. };
  462. // 查询任务结束状态
  463. const handleTaskStatus = async () => {
  464. const res = await fetchTaskStatus(conversationId.value)
  465. if(res.code == 200){
  466. taskStatus.value = res.body
  467. localStorage.setItem("status", res.body);
  468. }
  469. }
  470. // async function requestMicrophonePermission() {
  471. // try {
  472. // const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  473. // hasPermission.value = true;
  474. // // console.log("🎤 录音权限已授予");
  475. // return stream;
  476. // } catch (error) {
  477. // // console.warn("⛔ 录音权限被拒绝", error);
  478. // retryPermissionRequest();
  479. // }
  480. // }
  481. // async function retryPermissionRequest() {
  482. // const permissionStatus = await navigator.permissions.query({ name: "microphone" });
  483. // if (permissionStatus.state === "denied") {
  484. // Toast("您已拒绝录音权限,请前往浏览器或系统设置手动开启权限。");
  485. // } else {
  486. // requestMicrophonePermission();
  487. // }
  488. // }
  489. // 在组件挂载时调用
  490. onMounted(() => {
  491. loadChatHistory();
  492. // requestMicrophonePermission();
  493. setTimeout(() => {
  494. loading.value = false;
  495. }, 2000);
  496. });
  497. </script>
  498. <template>
  499. <div class="main">
  500. <nav-bar-page title="考试" />
  501. <div class="rate-circle">
  502. <van-circle
  503. :stroke-width="80"
  504. size="70px"
  505. v-model:current-rate="currentRate"
  506. :rate="rate"
  507. :text="text"
  508. />
  509. </div>
  510. <BG />
  511. <div class="loading" v-show="loading">
  512. <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
  513. </div>
  514. <ChatList :chatList="chatList" />
  515. <BottomArea
  516. @startRecord="handleStartRecord"
  517. @stopRecord="handleStopRecord"
  518. v-show="rate < 100 || taskStatus"
  519. />
  520. <div v-show="rate >= 100 || taskStatus" class="next-btn">
  521. <van-button style="width: 100%" @click="next" type="primary"
  522. >已完成对话,下一步</van-button
  523. >
  524. </div>
  525. </div>
  526. </template>
  527. <style>
  528. * {
  529. -webkit-touch-callout: none; /*系统默认菜单被禁用*/
  530. -webkit-user-select: none; /*webkit浏览器*/
  531. -khtml-user-select: none; /*早期浏览器*/
  532. -moz-user-select: none; /*火狐*/
  533. -ms-user-select: none; /*IE10*/
  534. user-select: none;
  535. }
  536. </style>
  537. <style scoped>
  538. .main {
  539. width: 100vw;
  540. height: 100vh;
  541. overflow: hidden;
  542. }
  543. .rate-circle {
  544. position: fixed;
  545. right: 20px;
  546. top: 70px;
  547. z-index: 9999;
  548. }
  549. .next-btn {
  550. position: fixed;
  551. bottom: calc(20px + env(safe-area-inset-bottom));
  552. width: 100%;
  553. box-sizing: border-box;
  554. padding: 0 20px;
  555. }
  556. .loading {
  557. position: fixed;
  558. top: 0;
  559. left: 0;
  560. width: 100%;
  561. height: 100%;
  562. background-color: rgba(0, 0, 0, 0.5);
  563. display: flex;
  564. justify-content: center;
  565. align-items: center;
  566. z-index: 9999;
  567. }
  568. </style>