3 次代码提交 2fa7ed84bb ... ce8a7cbb6e

作者 SHA1 备注 提交日期
  hizhangling ce8a7cbb6e [feat][ai聊天][语音合成] 4 天之前
  hizhangling 69c340ed66 [feat][ai聊天][语音合成] 4 天之前
  hizhangling d10edad0e8 [feat][ai聊天][自动滚动到底部] 4 天之前
共有 2 个文件被更改,包括 236 次插入80 次删除
  1. 1 0
      package.json
  2. 235 80
      src/views/xjc-integratedmachine/common/ai/chat.vue

+ 1 - 0
package.json

@@ -28,6 +28,7 @@
     "file-saver": "2.0.5",
     "file-saver": "2.0.5",
     "fuse.js": "6.6.2",
     "fuse.js": "6.6.2",
     "html2canvas": "^1.4.1",
     "html2canvas": "^1.4.1",
+    "js-base64": "^3.7.8",
     "js-beautify": "1.14.11",
     "js-beautify": "1.14.11",
     "js-cookie": "3.0.5",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.3.2",
     "jsencrypt": "3.3.2",

+ 235 - 80
src/views/xjc-integratedmachine/common/ai/chat.vue

@@ -51,7 +51,7 @@
                                     {{expandIndexList.includes(index)?'折叠':'展开'}}
                                     {{expandIndexList.includes(index)?'折叠':'展开'}}
                                 </el-button>
                                 </el-button>
                             </div>
                             </div>
-                            <el-button>播放</el-button>
+                            <el-button @click="ttsStartPlay(item.content)">播放</el-button>
                         </div>
                         </div>
                     </div>
                     </div>
                 </div>
                 </div>
@@ -72,10 +72,10 @@
                     <el-button style="width:140px">退出</el-button>
                     <el-button style="width:140px">退出</el-button>
                 </div>
                 </div>
                 <div class="control-button">
                 <div class="control-button">
-                    <el-button style="width:140px" @click="startVoice">开始</el-button>
+                    <el-button style="width:140px" @click="iatStartVoice">开始</el-button>
                 </div>
                 </div>
                 <div class="control-button">
                 <div class="control-button">
-                    <el-button style="width:140px" @click="closeVoiceOpen">结束</el-button>
+                    <el-button style="width:140px" @click="iatCloseVoiceOpen">结束</el-button>
                 </div>
                 </div>
                 <div class="control-button">
                 <div class="control-button">
                     <el-button style="width:140px" @click="stopMessage">停止回答</el-button>
                     <el-button style="width:140px" @click="stopMessage">停止回答</el-button>
@@ -98,6 +98,8 @@
     // 语音识别
     // 语音识别
     import CryptoJS from 'crypto-js';
     import CryptoJS from 'crypto-js';
     import * as RecorderManager from "/public/ai/iat/dist/index.umd.js"
     import * as RecorderManager from "/public/ai/iat/dist/index.umd.js"
+    // 语音播放
+    import {Base64} from 'js-base64'
     import * as AudioPlayer from "/public/ai/tts/dist/index.umd.js"
     import * as AudioPlayer from "/public/ai/tts/dist/index.umd.js"
 
 
     const loadingHistoryRecord = ref(false)
     const loadingHistoryRecord = ref(false)
@@ -132,7 +134,7 @@
         aiChatRecordList(queryForm).then(resp =>{
         aiChatRecordList(queryForm).then(resp =>{
             chatRecordList.value = resp.rows;
             chatRecordList.value = resp.rows;
             loadingHistoryRecord.value = false
             loadingHistoryRecord.value = false
-            setInterval(()=>{
+            setTimeout(()=>{
                 scrollToBottom();
                 scrollToBottom();
             }, 100);
             }, 100);
             if(resp.total == 0){
             if(resp.total == 0){
@@ -268,22 +270,22 @@
 
 
     /* 语音识别 ↓*/
     /* 语音识别 ↓*/
     // 控制录音弹窗
     // 控制录音弹窗
-    let voiceOpen = false
+    let iatVoiceOpen = false
     // 是否开始录音
     // 是否开始录音
-    let startVoiceStatus = false
+    let iatStartVoiceStatus = false
     // 识别中状态
     // 识别中状态
-    let identifyStatus = false
+    let iatIdentifyStatus = false
     // 录音对象
     // 录音对象
-    let recorder = null
-    let transcription = ''
-    let btnStatus = ''
-    let resultText = ''
-    let resultTextTemp = ''
-    let countdownInterval = null
+    let iatRecorder = null
+    let iatTranscription = ''
+    let iatBtnStatus = ''
+    let iatResultText = ''
+    let iatResultTextTemp = ''
+    let iatCountdownInterval = null
     let iatWS = null
     let iatWS = null
-    let recognition = null
-    let buttonDisabled = true
-    let loading = false
+    let iatRecognition = null
+    let iatButtonDisabled = true
+    let iatLoading = false
     let xfIatKeys = {
     let xfIatKeys = {
         APPID: '5a2643f4',
         APPID: '5a2643f4',
         APISecret: 'MTg4MWIzY2VmYTg2YTEwMjliMDY1N2Iz',
         APISecret: 'MTg4MWIzY2VmYTg2YTEwMjliMDY1N2Iz',
@@ -292,12 +294,12 @@
     // 初始化语音识别
     // 初始化语音识别
     function initSpeechRecognition(){
     function initSpeechRecognition(){
         // 初始化录音
         // 初始化录音
-        recorder = new window.RecorderManager('/ai/iat/dist');
+        iatRecorder = new window.RecorderManager('/ai/iat/dist');
         // 开始录音
         // 开始录音
-        recorder.onStart = () => {
-            changeBtnStatus('OPEN');
+        iatRecorder.onStart = () => {
+            iatChangeBtnStatus('OPEN');
         };
         };
-        recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
+        iatRecorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
             if (iatWS.readyState === iatWS.OPEN) {
             if (iatWS.readyState === iatWS.OPEN) {
                 iatWS.send(
                 iatWS.send(
                     JSON.stringify({
                     JSON.stringify({
@@ -310,63 +312,63 @@
                     }),
                     }),
                 );
                 );
                 if (isLastFrame) {
                 if (isLastFrame) {
-                    changeBtnStatus('CLOSING');
+                    iatChangeBtnStatus('CLOSING');
                 }
                 }
             }
             }
         };
         };
-        recorder.onStop = () => {
+        iatRecorder.onStop = () => {
             console.log('录音结束,停止定时器');
             console.log('录音结束,停止定时器');
-            clearInterval(countdownInterval);
-            startVoiceStatus = false;
+            clearInterval(iatCountdownInterval);
+            iatStartVoiceStatus = false;
         };
         };
 
 
     }
     }
 
 
-    async function startVoice() {
-        if (loading) {
+    async function iatStartVoice() {
+        if (iatLoading) {
             return;
             return;
         }
         }
-        voiceOpen = true;
-        await playIatVoice();
+        iatVoiceOpen = true;
+        await iatPlayIatVoice();
     }
     }
 
 
-    async function playIatVoice() {
-        if (loading) {
+    async function iatPlayIatVoice() {
+        if (iatLoading) {
             return;
             return;
         }
         }
-        startVoiceStatus = !startVoiceStatus;
+        iatStartVoiceStatus = !iatStartVoiceStatus;
         // 浏览器自带的识别
         // 浏览器自带的识别
-        if (recognition) {
-            if (startVoiceStatus) {
-                recognition.start();
+        if (iatRecognition) {
+            if (iatStartVoiceStatus) {
+                iatRecognition.start();
             } else {
             } else {
-                recognition.stop();
+                iatRecognition.stop();
             }
             }
             return;
             return;
         }
         }
-        if (startVoiceStatus) {
-            connectWebSocket();
+        if (iatStartVoiceStatus) {
+            iatConnectWebSocket();
         } else {
         } else {
-            recorder.stop();
+            iatRecorder.stop();
         }
         }
     }
     }
 
 
     /**
     /**
      * 关闭录音弹窗
      * 关闭录音弹窗
      */
      */
-    function closeVoiceOpen() {
-        voiceOpen = false;
-        startVoiceStatus = false;
-        if (recorder) {
-            recorder.stop();
+    function iatCloseVoiceOpen() {
+        iatVoiceOpen = false;
+        iatStartVoiceStatus = false;
+        if (iatRecorder) {
+            iatRecorder.stop();
         }
         }
-        if (recognition) {
-            recognition.stop();
+        if (iatRecognition) {
+            iatRecognition.stop();
         }
         }
-        transcription = '';
+        iatTranscription = '';
     }
     }
 
 
-    function renderResult(resultData) {
+    function iatRenderResult(resultData) {
         // 识别结束
         // 识别结束
         const jsonData = JSON.parse(resultData);
         const jsonData = JSON.parse(resultData);
         if (jsonData.data && jsonData.data.result) {
         if (jsonData.data && jsonData.data.result) {
@@ -381,15 +383,14 @@
             if (data.pgs) {
             if (data.pgs) {
                 if (data.pgs === 'apd') {
                 if (data.pgs === 'apd') {
                     // 将resultTextTemp同步给resultText
                     // 将resultTextTemp同步给resultText
-                    resultText = resultTextTemp;
+                    iatResultText = iatResultTextTemp;
                 }
                 }
                 // 将结果存储在resultTextTemp中
                 // 将结果存储在resultTextTemp中
-                resultTextTemp = resultText + str;
+                iatResultTextTemp = iatResultText + str;
             } else {
             } else {
-                resultText += str;
+                iatResultText += str;
             }
             }
-            inputMessage.value = resultTextTemp || resultText || '';
-            console.log("识别:"+inputMessage.value);
+            inputMessage.value = iatResultTextTemp || iatResultText || '';
         }
         }
         if (jsonData.code === 0 && jsonData.data.status === 2) {
         if (jsonData.code === 0 && jsonData.data.status === 2) {
             iatWS.close();
             iatWS.close();
@@ -400,8 +401,8 @@
         }
         }
     }
     }
 
 
-    function connectWebSocket() {
-        const websocketUrl = getWebSocketUrl();
+    function iatConnectWebSocket() {
+        const websocketUrl = iatGetWebSocketUrl();
         if ('WebSocket' in window) {
         if ('WebSocket' in window) {
             iatWS = new window.WebSocket(websocketUrl);
             iatWS = new window.WebSocket(websocketUrl);
         } else if ('MozWebSocket' in window) {
         } else if ('MozWebSocket' in window) {
@@ -410,11 +411,11 @@
             message.error('浏览器不支持WebSocket');
             message.error('浏览器不支持WebSocket');
             return;
             return;
         }
         }
-        changeBtnStatus('CONNECTING');
+        iatChangeBtnStatus('CONNECTING');
         iatWS.onopen = e => {
         iatWS.onopen = e => {
             console.log('iatWS.onopen', e);
             console.log('iatWS.onopen', e);
             // 开始录音
             // 开始录音
-            recorder.start({
+            iatRecorder.start({
                 sampleRate: 16000,
                 sampleRate: 16000,
                 frameSize: 1280,
                 frameSize: 1280,
             });
             });
@@ -440,21 +441,21 @@
             iatWS.send(JSON.stringify(params));
             iatWS.send(JSON.stringify(params));
         };
         };
         iatWS.onmessage = e => {
         iatWS.onmessage = e => {
-            renderResult(e.data);
+            iatRenderResult(e.data);
         };
         };
         iatWS.onerror = e => {
         iatWS.onerror = e => {
             console.error(e);
             console.error(e);
-            recorder.stop();
-            changeBtnStatus('CLOSED');
+            iatRecorder.stop();
+            iatChangeBtnStatus('CLOSED');
         };
         };
         iatWS.onclose = e => {
         iatWS.onclose = e => {
             console.log(e);
             console.log(e);
-            recorder.stop();
-            changeBtnStatus('CLOSED');
+            iatRecorder.stop();
+            iatChangeBtnStatus('CLOSED');
         };
         };
     }
     }
 
 
-    function getWebSocketUrl() {
+    function iatGetWebSocketUrl() {
         const { APIKey, APISecret } = xfIatKeys;
         const { APIKey, APISecret } = xfIatKeys;
         if (!APIKey) {
         if (!APIKey) {
             message.error('语音识别配置未生效');
             message.error('语音识别配置未生效');
@@ -477,29 +478,29 @@
         return url;
         return url;
     }
     }
 
 
-    function countdown() {
+    function iatCountdown() {
         let seconds = 60;
         let seconds = 60;
         console.log(`录音中(${seconds}s)`);
         console.log(`录音中(${seconds}s)`);
-        countdownInterval = setInterval(() => {
+        iatCountdownInterval = setInterval(() => {
             seconds -= 1;
             seconds -= 1;
             if (seconds <= 0) {
             if (seconds <= 0) {
-                clearInterval(countdownInterval);
-                recorder.stop();
+                clearInterval(iatCountdownInterval);
+                iatRecorder.stop();
             } else {
             } else {
                 console.log(`录音中(${seconds}s)`);
                 console.log(`录音中(${seconds}s)`);
             }
             }
         }, 1000);
         }, 1000);
     }
     }
 
 
-    function changeBtnStatus(status) {
-        btnStatus = status;
+    function iatChangeBtnStatus(status) {
+        iatBtnStatus = status;
         if (status === 'CONNECTING') {
         if (status === 'CONNECTING') {
             console.log('建立连接中');
             console.log('建立连接中');
-            resultText = '';
-            resultTextTemp = '';
+            iatResultText = '';
+            iatResultTextTemp = '';
         } else if (status === 'OPEN') {
         } else if (status === 'OPEN') {
-            if (recorder) {
-                countdown();
+            if (iatRecorder) {
+                iatCountdown();
             }
             }
         } else if (status === 'CLOSING') {
         } else if (status === 'CLOSING') {
             console.log('关闭连接中');
             console.log('关闭连接中');
@@ -518,31 +519,185 @@
         return window.btoa(binary);
         return window.btoa(binary);
     }
     }
 
 
-    // 发送消息
-    function sendMsg(text) {
-        // $emit('sendMsg', text);
-        // transcription = '';
-    }
     /* 语音识别 ↑*/
     /* 语音识别 ↑*/
 
 
     /*语音合成 ↓*/
     /*语音合成 ↓*/
+    // 合成对象
+    let audioPlayer = null
+    // 按钮状态:"UNDEFINED" "CONNECTING" "PLAY" "STOP"
+    let btnStatus = "UNDEFINED";
+    //
+    let ttsWS = null;
+
+    function initSpeechSynthesis(){
+        // 初始化语音播放
+        audioPlayer = new window.AudioPlayer("/ai/tts/dist");
+        // 开始播放
+        audioPlayer.onPlay = () => {
+            changeBtnStatus("PLAY");
+        };
+        audioPlayer.onStop = (audioDatas) => {
+            console.log('=======>',audioDatas);
+            btnStatus === "PLAY" && changeBtnStatus("STOP");
+        };
+    }
+
+    function changeBtnStatus(status) {
+        btnStatus = status;
+        if (status === "UNDEFINED") {
+            console.log("立即合成");
+        } else if (status === "CONNECTING") {
+            console.log("正在合成");
+        } else if (status === "PLAY") {
+            console.log("停止播放");
+        } else if (status === "STOP") {
+            console.log("重新播放");
+        }
+    }
+
+    function getWebSocketUrl() {
+        const { APIKey, APISecret } = xfIatKeys;
+        if (!APIKey) {
+            message.error('语音合成配置未生效');
+            return null;
+        }
+        var url = "wss://tts-api.xfyun.cn/v2/tts";
+        var host = location.host;
+        const apiKey = APIKey;
+        const apiSecret = APISecret;
+        var date = new Date().toGMTString();
+        var algorithm = "hmac-sha256";
+        var headers = "host date request-line";
+        var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`;
+        var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
+        var signature = CryptoJS.enc.Base64.stringify(signatureSha);
+        var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
+        var authorization = btoa(authorizationOrigin);
+        url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
+        return url;
+    }
 
 
+    function encodeText(text, type) {
+        if (type === "unicode") {
+            let buf = new ArrayBuffer(text.length * 4);
+            let bufView = new Uint16Array(buf);
+            for (let i = 0, strlen = text.length; i < strlen; i++) {
+                bufView[i] = text.charCodeAt(i);
+            }
+            let binary = "";
+            let bytes = new Uint8Array(buf);
+            let len = bytes.byteLength;
+            for (let i = 0; i < len; i++) {
+                binary += String.fromCharCode(bytes[i]);
+            }
+            return window.btoa(binary);
+        } else {
+            return Base64.encode(text);
+        }
+    }
 
 
-    function initSpeechsynthesis(){
+    function ttsStartPlay(text) {
+        if (btnStatus === "UNDEFINED") {
+            // 开始合成
+            connectWebSocket(text.replace(/<\/?.+?\/?>/g,''));
+        } else if (btnStatus === "CONNECTING") {
+            // 停止合成
+            changeBtnStatus("UNDEFINED");
+            ttsWS?.close();
+            audioPlayer.reset();
+            return;
+        } else if (btnStatus === "PLAY") {
+            audioPlayer.stop();
+        } else if (btnStatus === "STOP") {
+            audioPlayer.play();
+        }
+    };
+
+    function connectWebSocket(text) {
+        const url = getWebSocketUrl();
+        if ("WebSocket" in window) {
+            ttsWS = new WebSocket(url);
+        } else if ("MozWebSocket" in window) {
+            ttsWS = new MozWebSocket(url);
+        } else {
+            alert("浏览器不支持WebSocket");
+            return;
+        }
+        changeBtnStatus("CONNECTING");
 
 
+        ttsWS.onopen = (e) => {
+            audioPlayer.start({
+                autoPlay: true,
+                sampleRate: 16000,
+                resumePlayDuration: 1000
+            });
+            changeBtnStatus("PLAY");
+            var tte = "UTF8";
+            var params = {
+                common: {
+                    app_id: xfIatKeys.APPID,
+                },
+                business: {
+                    aue: "raw",
+                    auf: "audio/L16;rate=16000",
+                    vcn: "xiaoyan",
+                    // vcn: "aisbabyxu",
+                    // vcn: "aisjinger",
+                    // vcn: "aisxping",
+                    // vcn: "aisjinger",
+                    // vcn: "x4_lingxiaoyao_em",
+                    // vcn: "x4_lingxiaoyao_en",
+                    speed: +50,
+                    volume: +50,
+                    pitch: +50,
+                    bgs: 1,
+                    tte,
+                },
+                data: {
+                    status: 2,
+                    text: encodeText(text, tte),
+                },
+            };
+            ttsWS.send(JSON.stringify(params));
+        };
+        ttsWS.onmessage = (e) => {
+            let jsonData = JSON.parse(e.data);
+            // 合成失败
+            if (jsonData.code !== 0) {
+                console.error(jsonData);
+                changeBtnStatus("UNDEFINED");
+                return;
+            }
+            audioPlayer.postMessage({
+                type: "base64",
+                data: jsonData.data.audio,
+                isLastData: jsonData.data.status === 2,
+            });
+            if (jsonData.code === 0 && jsonData.data.status === 2) {
+                ttsWS.close();
+            }
+        };
+        ttsWS.onerror = (e) => {
+            console.error(e);
+        };
+        ttsWS.onclose = (e) => {
+            // console.log(e);
+        };
     }
     }
 
 
     /*语音合成 ↑*/
     /*语音合成 ↑*/
 
 
     let chatContainerRef = ref(null)
     let chatContainerRef = ref(null)
     function scrollToBottom(){
     function scrollToBottom(){
-        chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight+20;
+        if(chatContainerRef.value.scrollHeight){
+            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight+20;
+        }
     }
     }
 
 
     onMounted(()=>{
     onMounted(()=>{
         nextTick(()=>{
         nextTick(()=>{
             initSpeechRecognition();
             initSpeechRecognition();
-            initSpeechsynthesis();
+            initSpeechSynthesis();
             list();
             list();
         })
         })