AsrDemo.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <div class="no-select">
  3. <!-- <button @mousedown="recStart('mp3')" @mouseup="recStop">
  4. mp3按住录音并实时asr
  5. </button> -->
  6. <van-button
  7. style="margin: 20px 0"
  8. type="primary"
  9. size="large"
  10. @touchstart="recStart('wav')"
  11. @touchend="recStop"
  12. >手机端长按</van-button
  13. >
  14. <van-button
  15. type="primary"
  16. size="large"
  17. @mousedown="recStart('wav')"
  18. @mouseup="recStop"
  19. >电脑端鼠标长按</van-button
  20. >
  21. <!-- <button @mousedown="recStart('pcm')" @mouseup="recStop">
  22. pcm按住录音并实时asr(不支持播放)
  23. </button> -->
  24. <div>{{ userTalk }}</div>
  25. <div>{{ text }}</div>
  26. <!-- <div class="audioPlay"></div>
  27. <div class="progress"></div> -->
  28. </div>
  29. </template>
  30. <script setup>
  31. import { ref, onMounted, onBeforeUnmount } from "vue";
  32. import axios from "axios";
  33. import Recorder from "recorder-core";
  34. import "recorder-core/src/engine/mp3";
  35. import "recorder-core/src/engine/mp3-engine";
  36. import "recorder-core/src/engine/wav";
  37. import "recorder-core/src/engine/pcm";
  38. //可选的插件支持项,把需要的插件按需引入进来即可
  39. //import 'recorder-core/src/extensions/waveview'
  40. import "recorder-core/src/extensions/frequency.histogram.view";
  41. import "recorder-core/src/extensions/lib.fft";
  42. const userTalk = ref("");
  43. let rec;
  44. let testOutputWavLog = ref(false); // 用于调试是否输出wav格式
  45. let testSampleRate = ref(16000);
  46. let testBitRate = ref(16);
  47. let SendInterval = 300; // 控制实时传输间隔
  48. let realTimeSendTryType,
  49. realTimeSendTryEncBusy,
  50. realTimeSendTryTime = 0,
  51. realTimeSendTryNumber,
  52. transferUploadNumberMax,
  53. realTimeSendTryChunk,
  54. voicePkgSeq = 0;
  55. const RealTimeSendTryReset = (type) => {
  56. realTimeSendTryType = type;
  57. realTimeSendTryTime = 0;
  58. realTimeSendTryEncBusy = 0;
  59. realTimeSendTryNumber = 0;
  60. transferUploadNumberMax = 0;
  61. realTimeSendTryChunk = null;
  62. };
  63. const RealTimeSendTry = (buffers, bufferSampleRate, isClose) => {
  64. const t1 = Date.now();
  65. if (realTimeSendTryTime === 0) {
  66. realTimeSendTryTime = t1;
  67. realTimeSendTryEncBusy = 0;
  68. realTimeSendTryNumber = 0;
  69. transferUploadNumberMax = 0;
  70. realTimeSendTryChunk = null;
  71. }
  72. if (!isClose && t1 - realTimeSendTryTime < SendInterval) {
  73. return; // 控制缓冲达到指定间隔才进行传输
  74. }
  75. realTimeSendTryTime = t1;
  76. const number = ++realTimeSendTryNumber;
  77. let pcm = [],
  78. pcmSampleRate = 0;
  79. if (buffers.length > 0) {
  80. // 借用SampleData函数进行数据的连续处理,采样率转换是顺带的,得到新的pcm数据
  81. const chunk = Recorder.SampleData(
  82. buffers,
  83. bufferSampleRate,
  84. testSampleRate.value,
  85. realTimeSendTryChunk,
  86. { frameType: isClose ? "" : realTimeSendTryType }
  87. );
  88. for (
  89. let i = realTimeSendTryChunk ? realTimeSendTryChunk.index : 0;
  90. i < chunk.index;
  91. i++
  92. ) {
  93. buffers[i] = null; // 清理已处理完的缓冲数据
  94. }
  95. realTimeSendTryChunk = chunk;
  96. pcm = chunk.data;
  97. pcmSampleRate = chunk.sampleRate;
  98. }
  99. // 如果没有新数据,或者结束时数据量太小,不能进行转码
  100. if (pcm.length === 0 || (isClose && pcm.length < 2000)) {
  101. TransferUpload(number, null, 0, null, isClose);
  102. return;
  103. }
  104. // 实时编码队列阻塞处理
  105. if (!isClose) {
  106. if (realTimeSendTryEncBusy >= 2) {
  107. console.log("编码队列阻塞,已丢弃一帧", 1);
  108. return;
  109. }
  110. }
  111. realTimeSendTryEncBusy++;
  112. const encStartTime = Date.now();
  113. const recMock = Recorder({
  114. type: realTimeSendTryType,
  115. sampleRate: testSampleRate.value, // 采样率
  116. bitRate: testBitRate.value, // 比特率
  117. });
  118. recMock.mock(pcm, pcmSampleRate);
  119. recMock.stop(
  120. (blob, duration) => {
  121. if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
  122. blob.encTime = Date.now() - encStartTime;
  123. TransferUpload(number, blob, duration, recMock, isClose);
  124. },
  125. (msg) => {
  126. if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
  127. console.log("不应该出现的错误:" + msg, 1);
  128. }
  129. );
  130. };
  131. const TransferUpload = (number, blobOrNull, duration, blobRec, isClose) => {
  132. transferUploadNumberMax = Math.max(transferUploadNumberMax, number);
  133. if (blobOrNull) {
  134. let blob = blobOrNull;
  135. let encTime = blob.encTime;
  136. // 发送数据的方式一:Base64文本发送
  137. let reader = new FileReader();
  138. reader.onloadend = () => {
  139. let base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1];
  140. // 可以实现 WebSocket send(base64), WebRTC send(base64), XMLHttpRequest send(base64)
  141. asrPost("SESSION_IN", base64);
  142. };
  143. reader.readAsDataURL(blob);
  144. // 发送数据的方式二:Blob二进制发送
  145. // 可以实现 WebSocket send(blob), WebRTC send(blob), XMLHttpRequest send(blob)
  146. const numberFail =
  147. number < transferUploadNumberMax
  148. ? '<span style="color:red">顺序错乱的数据,如果要求不高可以直接丢弃,或者调大SendInterval试试</span>'
  149. : "";
  150. const logMsg =
  151. "No." +
  152. (number < 100 ? ("000" + number).substr(-3) : number) +
  153. numberFail;
  154. console.log(
  155. blob,
  156. duration,
  157. blobRec,
  158. logMsg + "花" + ("___" + encTime).substr(-3) + "ms"
  159. );
  160. // 插入html
  161. const childDiv = document.createElement("div");
  162. const button = document.createElement("button");
  163. childDiv.textContent = `${logMsg + ("___" + encTime).substr(-3) + "ms"}`;
  164. button.textContent = "播放";
  165. button.addEventListener("click", () => {
  166. recPlay(blob);
  167. });
  168. childDiv.appendChild(button);
  169. //document.querySelector(".progress").appendChild(childDiv);
  170. }
  171. if (isClose) {
  172. console.log(
  173. "No." +
  174. (number < 100 ? ("000" + number).substr(-3) : number) +
  175. ":已停止传输"
  176. );
  177. }
  178. };
  179. const recStart = async (type) => {
  180. userTalk.value = "";
  181. text.value = "";
  182. lastStrIndex = 0;
  183. await asrPost("SESSION_BEGIN");
  184. rec.start();
  185. RealTimeSendTryReset(type);
  186. };
  187. const recStop = async () => {
  188. rec.stop();
  189. RealTimeSendTry([], 0, true); // 最后一次发送
  190. await asrPost("SESSION_END");
  191. voicePkgSeq = 0;
  192. chatGpt();
  193. };
  194. const recPlay = (blob) => {
  195. const audioPlayElement = document.querySelector(".audioPlay");
  196. audioPlayElement.innerHTML = "";
  197. const audio = document.createElement("audio");
  198. audio.controls = true;
  199. audioPlayElement.appendChild(audio);
  200. audio.src = (window.URL || webkitURL).createObjectURL(blob);
  201. audio.play();
  202. setTimeout(() => {
  203. (window.URL || webkitURL).revokeObjectURL(audio.src);
  204. }, 5000);
  205. };
  206. const asrPost = async (eventCodeType = "SESSION_IN", base64) => {
  207. const wgToken = `C1qziFGlIv3tnCQxcFaStrLuZOO2ZZXjN7FB_G0WlrOLjclfObbSaXAKzl4RWwQBf_0Zhsm0CoVvdVsYMD18iM_LJrxtn7LHJJQuF9UoUuF3fvqOwrG4EF6Z4GahtxtQ2oeaPQBBNKlgVW1xUW7tkhEdXWqzDHPA_I_91Lczk0PI4guhx1c88Hst4-HI8pdMbiUdEJzj3d3a2W06Fa0XA9Q0taAwaRd1k9jUrDVyj9GfS84_SIgJF4SPjWVfsraV79ieb_StgRcUwZjbscGPMlifnJD6F00wwNbxG7AuCHbl3EtMfSed1vuVx3AsizIckwzIVSVRpOGw72cdAMui-I6es9Ozj2ITzSa5KgyXEpX4qCHF1VcCM1wlHLQ_5hLnJIi4r8NsnJPsxMYrTw`;
  208. const res = await axios.post(
  209. `https://fls-ai-stg-sit.pingan.com.cn/openapi/ai/voice/asr/v1?channelId=ASP-TEST&sceneId=ASP-TEST&token=${wgToken}`,
  210. {
  211. sessionId: "N7FB_G0WlrOLjc",
  212. eventCodeType,
  213. voicePkgSeq: ++voicePkgSeq,
  214. audio: base64,
  215. format: 16000,
  216. encoding: "WAV",
  217. },
  218. {
  219. headers: {
  220. "X-Ai-Asr-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  221. },
  222. }
  223. );
  224. console.error(res.data.body.asrResultText);
  225. userTalk.value = userTalk.value + res.data.body.asrResultText;
  226. };
  227. onMounted(() => {
  228. if (rec) {
  229. rec.close();
  230. }
  231. rec = Recorder({
  232. type: "unknown",
  233. onProcess: (buffers, powerLevel, bufferDuration, bufferSampleRate) => {
  234. RealTimeSendTry(buffers, bufferSampleRate, false);
  235. },
  236. });
  237. const t = setTimeout(() => {
  238. console.log("无法录音:权限请求被忽略(超时假装手动点击了确认对话框)", 1);
  239. }, 8000);
  240. rec.open(
  241. () => {
  242. clearTimeout(t);
  243. },
  244. (msg, isUserNotAllow) => {
  245. clearTimeout(t);
  246. console.log(
  247. (isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg,
  248. 1
  249. );
  250. }
  251. );
  252. });
  253. onBeforeUnmount(() => {
  254. if (rec) rec.close();
  255. });
  256. ////////
  257. import PQueue from "p-queue";
  258. const text = ref("");
  259. // 创建一个队列实例,设置并发数为 1
  260. const queue = new PQueue({ concurrency: 1 });
  261. const queue1 = new PQueue({ concurrency: 1 });
  262. // 需要tts的文本
  263. const messageQueue = [];
  264. // lastStrIndex 用于记录上一个字符串的结束位置
  265. let lastStrIndex = 0;
  266. // 将字符串根据标点符号断句分割,并添加到messageQueue中
  267. const splitMessage = async (str) => {
  268. const punctuation = ["。", "!", "?", ";", ":", ","];
  269. for (let i = lastStrIndex; i < str.length; i++) {
  270. if (punctuation.includes(str[i])) {
  271. const message = str.slice(lastStrIndex, i + 1);
  272. console.log(message, "==========");
  273. await playTTS(message);
  274. lastStrIndex = i + 1; // 更新上一个字符串的结束位置
  275. }
  276. }
  277. };
  278. const playTTS = async (ttsMessage) => {
  279. //const ttsMessage =
  280. // "这个错误通常表示浏览器无法识别音频数据的格式或编码,导致无法加载音频源。为了解决这个问题,你可以尝试使用静态的 WAV 格式音频文件以确保能够正常播放,并且避免直接处理原始的音频数据。以下是一个示例代码来加载外部的 WAV 格式音频文件:"; // 替换为你想要播报的文本
  281. try {
  282. const wgToken = `C1qziFGlIv3tnCQxcFaStrLuZOO2ZZXjN7FB_G0WlrOLjclfObbSaXAKzl4RWwQBf_0Zhsm0CoVvdVsYMD18iM_LJrxtn7LHJJQuF9UoUuF3fvqOwrG4EF6Z4GahtxtQ2oeaPQBBNKlgVW1xUW7tkhEdXWqzDHPA_I_91Lczk0PI4guhx1c88Hst4-HI8pdMbiUdEJzj3d3a2W06Fa0XA9Q0taAwaRd1k9jUrDVyj9GfS84_SIgJF4SPjWVfsraV79ieb_StgRcUwZjbscGPMlifnJD6F00wwNbxG7AuCHbl3EtMfSed1vuVx3AsizIckwzIVSVRpOGw72cdAMui-I6es9Ozj2ITzSa5KgyXEpX4qCHF1VcCM1wlHLQ_5hLnJIi4r8NsnJPsxMYrTw`;
  283. const res = await axios.post(
  284. `https://fls-ai-stg-sit.pingan.com.cn/openapi/ai/voice/tts/v2?channelId=ASP-TEST&sceneId=ASP-TEST&token=${wgToken}`,
  285. { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
  286. {
  287. responseType: "arraybuffer",
  288. headers: {
  289. "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  290. },
  291. }
  292. );
  293. console.log(res.data);
  294. queue1.add(async () => {
  295. // 播放获取到的音频
  296. await playAudio(res.data);
  297. });
  298. } catch (error) {
  299. console.error("Error calling TTS API:", error);
  300. }
  301. };
  302. // const playAudio = async (audioData) => {
  303. // const blob = new Blob([audioData], { type: "audio/wav" });
  304. // const url = URL.createObjectURL(blob);
  305. // const audio = new Audio(url);
  306. // audio.onended = () => URL.revokeObjectURL(url);
  307. // audio.onerror = (error) => console.error("Audio playback error:", error);
  308. // audio
  309. // .play()
  310. // .then(() => console.log("Audio playing"))
  311. // .catch((error) => console.error("Error playing audio:", error));
  312. // };
  313. const playAudio = (audioData, options = {}) => {
  314. return new Promise((resolve, reject) => {
  315. const blob = new Blob([audioData], { type: "audio/wav" });
  316. const url = URL.createObjectURL(blob);
  317. const audio = new Audio(url);
  318. if (options.volume) {
  319. audio.volume = options.volume; // 设置音量
  320. }
  321. audio.onended = () => {
  322. URL.revokeObjectURL(url);
  323. resolve();
  324. };
  325. audio.onerror = (error) => {
  326. URL.revokeObjectURL(url);
  327. reject(error);
  328. };
  329. audio
  330. .play()
  331. .then(() => console.log("Audio playing"))
  332. .catch(reject);
  333. });
  334. };
  335. const chatGpt = async () => {
  336. const downloadUrl =
  337. "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue?token=mVcexzY_mjtGAL5_exPlmAyfOJxuuEthWY1mk9tUFC_HwceY58uRZ2WDhz7-ttexCdUtFN8C7V636_jIq6fzaSfqIj8OQyhUPKPMa2eZjLlblT77ySqBt_lYM6iEAhrj7-raGmySMmkLS4Rqh651Ak2tqmUbjS64cqv5ofMsuadOCg1J-CtLFt7NeSoU4N3Kpm5MJ_4sOFBhQGfBym88dcwxosFl9LbvhpyleXFf6fOZkkOj0l2X8Nr2pfNjYs3_VOmCQxrxXh1XZ_a1v9qj5_rA9k9wGNNQfmr2JwJTUT4V9NwtNq94gNFt8C0J6MWKVE2eyr25Rke8tkKu3CGNNmspmEFpr6LavPlaWnWOIh9CRJ1cIDB70pg_JD2l0nPTkPbtaTQaIGTz"; // 替换为实际的下载URL
  338. try {
  339. const response = await axios.post(
  340. downloadUrl,
  341. {
  342. conversationId: "1976cfe3a5174f9ba768677f789cad7e",
  343. content: `${userTalk.value} 请输出纯文本的回答,不要使用markdown输出`,
  344. messageId: Date.now(),
  345. applicationId: "",
  346. },
  347. {
  348. headers: {
  349. "Team-Id": "123456",
  350. Authorization: "9833ae306bde47f8b00b20c18ec809ae",
  351. "Content-Type": "application/json",
  352. },
  353. onDownloadProgress: ({ event }) => {
  354. //console.log(event);
  355. const xhr = event.target;
  356. const { responseText } = xhr;
  357. // // Always process the final line
  358. // const lastIndex = responseText.lastIndexOf(
  359. // "\n",
  360. // responseText.length - 2
  361. // );
  362. // let chunk = responseText;
  363. // if (lastIndex !== -1) chunk = responseText.substring(lastIndex);
  364. //console.log(responseText, "chunk====");
  365. text.value = responseText;
  366. queue.add(async () => {
  367. await splitMessage(responseText);
  368. });
  369. },
  370. }
  371. );
  372. console.log(response);
  373. console.log(messageQueue, "messageQueue=======0000");
  374. } catch (error) {
  375. console.error("下载文件时出错:", error);
  376. }
  377. };
  378. </script>
  379. <style scoped>
  380. .no-select {
  381. -webkit-user-select: none;
  382. -moz-user-select: none;
  383. -ms-user-select: none;
  384. user-select: none;
  385. }
  386. * {
  387. -webkit-touch-callout: none;
  388. -webkit-user-select: none;
  389. -khtml-user-select: none;
  390. -moz-user-select: none;
  391. -ms-user-select: none;
  392. user-select: none;
  393. -o-user-select: none;
  394. }
  395. </style>