123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- <template>
- <div class="no-select">
- <!-- <button @mousedown="recStart('mp3')" @mouseup="recStop">
- mp3按住录音并实时asr
- </button> -->
- <van-button
- style="margin: 20px 0"
- type="primary"
- size="large"
- @touchstart="recStart('wav')"
- @touchend="recStop"
- >手机端长按</van-button
- >
- <van-button
- type="primary"
- size="large"
- @mousedown="recStart('wav')"
- @mouseup="recStop"
- >电脑端鼠标长按</van-button
- >
- <!-- <button @mousedown="recStart('pcm')" @mouseup="recStop">
- pcm按住录音并实时asr(不支持播放)
- </button> -->
- <div>{{ userTalk }}</div>
- <div>{{ text }}</div>
- <!-- <div class="audioPlay"></div>
- <div class="progress"></div> -->
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onBeforeUnmount } from "vue";
- import axios from "axios";
- import Recorder from "recorder-core";
- import "recorder-core/src/engine/mp3";
- import "recorder-core/src/engine/mp3-engine";
- import "recorder-core/src/engine/wav";
- import "recorder-core/src/engine/pcm";
- //可选的插件支持项,把需要的插件按需引入进来即可
- //import 'recorder-core/src/extensions/waveview'
- import "recorder-core/src/extensions/frequency.histogram.view";
- import "recorder-core/src/extensions/lib.fft";
- const userTalk = ref("");
- let rec;
- let testOutputWavLog = ref(false); // 用于调试是否输出wav格式
- let testSampleRate = ref(16000);
- let testBitRate = ref(16);
- let SendInterval = 300; // 控制实时传输间隔
- let realTimeSendTryType,
- realTimeSendTryEncBusy,
- realTimeSendTryTime = 0,
- realTimeSendTryNumber,
- transferUploadNumberMax,
- realTimeSendTryChunk,
- voicePkgSeq = 0;
- const RealTimeSendTryReset = (type) => {
- realTimeSendTryType = type;
- realTimeSendTryTime = 0;
- realTimeSendTryEncBusy = 0;
- realTimeSendTryNumber = 0;
- transferUploadNumberMax = 0;
- realTimeSendTryChunk = null;
- };
- const RealTimeSendTry = (buffers, bufferSampleRate, isClose) => {
- const t1 = Date.now();
- if (realTimeSendTryTime === 0) {
- realTimeSendTryTime = t1;
- realTimeSendTryEncBusy = 0;
- realTimeSendTryNumber = 0;
- transferUploadNumberMax = 0;
- realTimeSendTryChunk = null;
- }
- if (!isClose && t1 - realTimeSendTryTime < SendInterval) {
- return; // 控制缓冲达到指定间隔才进行传输
- }
- realTimeSendTryTime = t1;
- const number = ++realTimeSendTryNumber;
- let pcm = [],
- pcmSampleRate = 0;
- if (buffers.length > 0) {
- // 借用SampleData函数进行数据的连续处理,采样率转换是顺带的,得到新的pcm数据
- const chunk = Recorder.SampleData(
- buffers,
- bufferSampleRate,
- testSampleRate.value,
- realTimeSendTryChunk,
- { frameType: isClose ? "" : realTimeSendTryType }
- );
- for (
- let i = realTimeSendTryChunk ? realTimeSendTryChunk.index : 0;
- i < chunk.index;
- i++
- ) {
- buffers[i] = null; // 清理已处理完的缓冲数据
- }
- realTimeSendTryChunk = chunk;
- pcm = chunk.data;
- pcmSampleRate = chunk.sampleRate;
- }
- // 如果没有新数据,或者结束时数据量太小,不能进行转码
- if (pcm.length === 0 || (isClose && pcm.length < 2000)) {
- TransferUpload(number, null, 0, null, isClose);
- return;
- }
- // 实时编码队列阻塞处理
- if (!isClose) {
- if (realTimeSendTryEncBusy >= 2) {
- console.log("编码队列阻塞,已丢弃一帧", 1);
- return;
- }
- }
- realTimeSendTryEncBusy++;
- const encStartTime = Date.now();
- const recMock = Recorder({
- type: realTimeSendTryType,
- sampleRate: testSampleRate.value, // 采样率
- bitRate: testBitRate.value, // 比特率
- });
- recMock.mock(pcm, pcmSampleRate);
- recMock.stop(
- (blob, duration) => {
- if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
- blob.encTime = Date.now() - encStartTime;
- TransferUpload(number, blob, duration, recMock, isClose);
- },
- (msg) => {
- if (realTimeSendTryEncBusy) realTimeSendTryEncBusy--;
- console.log("不应该出现的错误:" + msg, 1);
- }
- );
- };
- const TransferUpload = (number, blobOrNull, duration, blobRec, isClose) => {
- transferUploadNumberMax = Math.max(transferUploadNumberMax, number);
- if (blobOrNull) {
- let blob = blobOrNull;
- let encTime = blob.encTime;
- // 发送数据的方式一:Base64文本发送
- let reader = new FileReader();
- reader.onloadend = () => {
- let base64 = (/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1];
- // 可以实现 WebSocket send(base64), WebRTC send(base64), XMLHttpRequest send(base64)
- asrPost("SESSION_IN", base64);
- };
- reader.readAsDataURL(blob);
- // 发送数据的方式二:Blob二进制发送
- // 可以实现 WebSocket send(blob), WebRTC send(blob), XMLHttpRequest send(blob)
- const numberFail =
- number < transferUploadNumberMax
- ? '<span style="color:red">顺序错乱的数据,如果要求不高可以直接丢弃,或者调大SendInterval试试</span>'
- : "";
- const logMsg =
- "No." +
- (number < 100 ? ("000" + number).substr(-3) : number) +
- numberFail;
- console.log(
- blob,
- duration,
- blobRec,
- logMsg + "花" + ("___" + encTime).substr(-3) + "ms"
- );
- // 插入html
- const childDiv = document.createElement("div");
- const button = document.createElement("button");
- childDiv.textContent = `${logMsg + ("___" + encTime).substr(-3) + "ms"}`;
- button.textContent = "播放";
- button.addEventListener("click", () => {
- recPlay(blob);
- });
- childDiv.appendChild(button);
- //document.querySelector(".progress").appendChild(childDiv);
- }
- if (isClose) {
- console.log(
- "No." +
- (number < 100 ? ("000" + number).substr(-3) : number) +
- ":已停止传输"
- );
- }
- };
- const recStart = async (type) => {
- userTalk.value = "";
- text.value = "";
- lastStrIndex = 0;
- await asrPost("SESSION_BEGIN");
- rec.start();
- RealTimeSendTryReset(type);
- };
- const recStop = async () => {
- rec.stop();
- RealTimeSendTry([], 0, true); // 最后一次发送
- await asrPost("SESSION_END");
- voicePkgSeq = 0;
- chatGpt();
- };
- const recPlay = (blob) => {
- const audioPlayElement = document.querySelector(".audioPlay");
- audioPlayElement.innerHTML = "";
- const audio = document.createElement("audio");
- audio.controls = true;
- audioPlayElement.appendChild(audio);
- audio.src = (window.URL || webkitURL).createObjectURL(blob);
- audio.play();
- setTimeout(() => {
- (window.URL || webkitURL).revokeObjectURL(audio.src);
- }, 5000);
- };
- const asrPost = async (eventCodeType = "SESSION_IN", base64) => {
- const wgToken = `C1qziFGlIv3tnCQxcFaStrLuZOO2ZZXjN7FB_G0WlrOLjclfObbSaXAKzl4RWwQBf_0Zhsm0CoVvdVsYMD18iM_LJrxtn7LHJJQuF9UoUuF3fvqOwrG4EF6Z4GahtxtQ2oeaPQBBNKlgVW1xUW7tkhEdXWqzDHPA_I_91Lczk0PI4guhx1c88Hst4-HI8pdMbiUdEJzj3d3a2W06Fa0XA9Q0taAwaRd1k9jUrDVyj9GfS84_SIgJF4SPjWVfsraV79ieb_StgRcUwZjbscGPMlifnJD6F00wwNbxG7AuCHbl3EtMfSed1vuVx3AsizIckwzIVSVRpOGw72cdAMui-I6es9Ozj2ITzSa5KgyXEpX4qCHF1VcCM1wlHLQ_5hLnJIi4r8NsnJPsxMYrTw`;
- const res = await axios.post(
- `https://fls-ai-stg-sit.pingan.com.cn/openapi/ai/voice/asr/v1?channelId=ASP-TEST&sceneId=ASP-TEST&token=${wgToken}`,
- {
- sessionId: "N7FB_G0WlrOLjc",
- eventCodeType,
- voicePkgSeq: ++voicePkgSeq,
- audio: base64,
- format: 16000,
- encoding: "WAV",
- },
- {
- headers: {
- "X-Ai-Asr-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
- },
- }
- );
- console.error(res.data.body.asrResultText);
- userTalk.value = userTalk.value + res.data.body.asrResultText;
- };
- onMounted(() => {
- if (rec) {
- rec.close();
- }
- rec = Recorder({
- type: "unknown",
- onProcess: (buffers, powerLevel, bufferDuration, bufferSampleRate) => {
- RealTimeSendTry(buffers, bufferSampleRate, false);
- },
- });
- const t = setTimeout(() => {
- console.log("无法录音:权限请求被忽略(超时假装手动点击了确认对话框)", 1);
- }, 8000);
- rec.open(
- () => {
- clearTimeout(t);
- },
- (msg, isUserNotAllow) => {
- clearTimeout(t);
- console.log(
- (isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg,
- 1
- );
- }
- );
- });
- onBeforeUnmount(() => {
- if (rec) rec.close();
- });
- ////////
- import PQueue from "p-queue";
- const text = ref("");
- // 创建一个队列实例,设置并发数为 1
- const queue = new PQueue({ concurrency: 1 });
- const queue1 = new PQueue({ concurrency: 1 });
- // 需要tts的文本
- const messageQueue = [];
- // lastStrIndex 用于记录上一个字符串的结束位置
- let lastStrIndex = 0;
- // 将字符串根据标点符号断句分割,并添加到messageQueue中
- const splitMessage = async (str) => {
- 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);
- lastStrIndex = i + 1; // 更新上一个字符串的结束位置
- }
- }
- };
- const playTTS = async (ttsMessage) => {
- //const ttsMessage =
- // "这个错误通常表示浏览器无法识别音频数据的格式或编码,导致无法加载音频源。为了解决这个问题,你可以尝试使用静态的 WAV 格式音频文件以确保能够正常播放,并且避免直接处理原始的音频数据。以下是一个示例代码来加载外部的 WAV 格式音频文件:"; // 替换为你想要播报的文本
- try {
- const wgToken = `C1qziFGlIv3tnCQxcFaStrLuZOO2ZZXjN7FB_G0WlrOLjclfObbSaXAKzl4RWwQBf_0Zhsm0CoVvdVsYMD18iM_LJrxtn7LHJJQuF9UoUuF3fvqOwrG4EF6Z4GahtxtQ2oeaPQBBNKlgVW1xUW7tkhEdXWqzDHPA_I_91Lczk0PI4guhx1c88Hst4-HI8pdMbiUdEJzj3d3a2W06Fa0XA9Q0taAwaRd1k9jUrDVyj9GfS84_SIgJF4SPjWVfsraV79ieb_StgRcUwZjbscGPMlifnJD6F00wwNbxG7AuCHbl3EtMfSed1vuVx3AsizIckwzIVSVRpOGw72cdAMui-I6es9Ozj2ITzSa5KgyXEpX4qCHF1VcCM1wlHLQ_5hLnJIi4r8NsnJPsxMYrTw`;
- const res = await axios.post(
- `https://fls-ai-stg-sit.pingan.com.cn/openapi/ai/voice/tts/v2?channelId=ASP-TEST&sceneId=ASP-TEST&token=${wgToken}`,
- { 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 playAudio = async (audioData) => {
- // const blob = new Blob([audioData], { type: "audio/wav" });
- // const url = URL.createObjectURL(blob);
- // const audio = new Audio(url);
- // audio.onended = () => URL.revokeObjectURL(url);
- // audio.onerror = (error) => console.error("Audio playback error:", error);
- // audio
- // .play()
- // .then(() => console.log("Audio playing"))
- // .catch((error) => console.error("Error playing audio:", error));
- // };
- const playAudio = (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);
- 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 () => {
- const downloadUrl =
- "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
- try {
- const response = await axios.post(
- downloadUrl,
- {
- conversationId: "1976cfe3a5174f9ba768677f789cad7e",
- content: `${userTalk.value} 请输出纯文本的回答,不要使用markdown输出`,
- messageId: Date.now(),
- applicationId: "",
- },
- {
- headers: {
- "Team-Id": "123456",
- Authorization: "9833ae306bde47f8b00b20c18ec809ae",
- "Content-Type": "application/json",
- },
- onDownloadProgress: ({ event }) => {
- //console.log(event);
- const xhr = event.target;
- const { responseText } = xhr;
- // // Always process the final line
- // const lastIndex = responseText.lastIndexOf(
- // "\n",
- // responseText.length - 2
- // );
- // let chunk = responseText;
- // if (lastIndex !== -1) chunk = responseText.substring(lastIndex);
- //console.log(responseText, "chunk====");
- text.value = responseText;
- queue.add(async () => {
- await splitMessage(responseText);
- });
- },
- }
- );
- console.log(response);
- console.log(messageQueue, "messageQueue=======0000");
- } catch (error) {
- console.error("下载文件时出错:", error);
- }
- };
- </script>
- <style scoped>
- .no-select {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
- * {
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -o-user-select: none;
- }
- </style>
|