林旭祥 10 miesięcy temu
rodzic
commit
1939d616ee

+ 11 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "gsap": "^3.12.5",
         "pinia": "^2.1.7",
         "socket.io-client": "^2.5.0",
+        "speak-tts": "^2.0.8",
         "vue": "^3.4.21",
         "vue-router": "^4.0.13"
       },
@@ -6102,6 +6103,11 @@
       "deprecated": "Please use @jridgewell/sourcemap-codec instead",
       "dev": true
     },
+    "node_modules/speak-tts": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmmirror.com/speak-tts/-/speak-tts-2.0.8.tgz",
+      "integrity": "sha512-VY6Q6mRjdou6bF+x0LspvM7GJhBxHx8CLyGPTNQQ7jrztiGutyI4QNZn0cA17c4uk0FnFbA4PaMI3skeZ6PiFg=="
+    },
     "node_modules/split-string": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/split-string/-/split-string-3.1.0.tgz",
@@ -11961,6 +11967,11 @@
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
       "dev": true
     },
+    "speak-tts": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmmirror.com/speak-tts/-/speak-tts-2.0.8.tgz",
+      "integrity": "sha512-VY6Q6mRjdou6bF+x0LspvM7GJhBxHx8CLyGPTNQQ7jrztiGutyI4QNZn0cA17c4uk0FnFbA4PaMI3skeZ6PiFg=="
+    },
     "split-string": {
       "version": "3.1.0",
       "resolved": "https://registry.npmmirror.com/split-string/-/split-string-3.1.0.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "gsap": "^3.12.5",
     "pinia": "^2.1.7",
     "socket.io-client": "^2.5.0",
+    "speak-tts": "^2.0.8",
     "vue": "^3.4.21",
     "vue-router": "^4.0.13"
   },

+ 9 - 0
src/api/module/common.ts

@@ -47,5 +47,14 @@ export default {
       method: 'get',
       data: data
     });
+  },
+
+  //获取百度语音token
+  baiduToken: (data: any) => {
+    return req({
+      url: '/bdapi_token',
+      method: 'get',
+      data: data
+    });
   }
 };

+ 1 - 0
src/types/socket.io-client.ts → src/types/declareModule.ts

@@ -1 +1,2 @@
 declare module 'socket.io-client';
+declare module 'speak-tts';

+ 142 - 0
src/utils/speech.ts

@@ -0,0 +1,142 @@
+import Speech from 'speak-tts';
+import http from '@/api';
+let speech: any = null;
+let browserSupport: boolean = false;
+let speechText: string = '';
+
+//初始化语音
+export const initSpeech = () => {
+  speech = new Speech();
+  // 检测浏览器是否支持
+  if (speech.hasBrowserSupport()) {
+    browserSupport = true;
+    speech
+      .init({
+        volume: 1, // 音量
+        lang: 'zh-CN', // 语言
+        rate: 1.5, // 语速
+        pitch: 1.0, // 音调
+        splitSentences: true, // 在句子结束时暂停
+        listeners: {
+          // 事件
+          onvoiceschanged: (voices: any) => {
+            // console.log('事件声音已更改', voices);
+          }
+        }
+      })
+      .then(() => {
+        console.log('语音播报初始化完成');
+      });
+  } else {
+    browserSupport = false;
+    let baiduTok = localStorage.getItem('tok');
+    if (!baiduTok) {
+      http?.common.baiduToken({}).then((res: any) => {
+        let tok = res.token;
+        localStorage.setItem('tok', tok);
+      });
+    }
+  }
+};
+
+//播放语音
+export const speckText = (text: any) => {
+  //console.log('text', text);
+  console.log('speechText', speechText);
+  if (speechText == text) {
+    return false;
+  }
+  speechText = text;
+  let obj: any = {
+    // '请看摄像头进行人脸识别': 'PleaseIdentify.mp3',
+    // '腿部违规': 'LegViolation.mp3',
+    // '手部违规': 'HandViolation.mp3',
+    // '背部违规': 'BackViolation.mp3',
+    // '臀部违规': 'HipViolation.mp3',
+    // '踩线违规': 'LineViolation.mp3',
+    // '肘部违规': 'Elbowiolation.mp3',
+    // '下颌违规': 'HeightViolation.mp3',
+    // '单脚跳违规': 'SingleLegJumpViolation.mp3',
+    // '跳出测试区域违规': 'JumpAreaViolation.mp3',
+    // '您已踩线违规': 'LineViolationed.mp3',
+    // '测试结束,请下一位准备': 'TestEnd.mp3',
+    // '开始': 'Start.mp3',
+    // '测试完成': 'End.mp3',
+    // '请开始测试': 'PleaseStartTest.mp3',
+    // '请准备': 'PleaseReady.mp3',
+    // '该摄像头已启用': 'CameraOpen.mp3',
+    // '该考点已接受请求,请5秒后刷新考点状态后重试.': 'Accepted.mp3',
+    // '工作站登录失败': 'WorkFailLogin.mp3',
+    // '摄像头打开失败': 'openFail.mp3',
+    // '网络错误,请检查网络': 'NetworkError.mp3',
+    // '非法操作': 'IllegalOperation.mp3',
+    // '工作站已断开!': 'WorkDisconnect.mp3',
+    // '无可用工作站': 'NoWork.mp3',
+    // '考试项目不存在': 'NoExam.mp3',
+    // '该场地已被其他页面抢占': 'OtherPages.mp3',
+    // '重连失败,对应工作站已被占用': 'ReconnectionFailed.mp3',
+    // '自动人脸抓取返回result_id不正确': 'ResultIdFalse.mp3',
+    // '系统错误!': 'SystemError.mp3',
+    // '工作站已关闭,请重新上课!': 'WorkDown.mp3',
+    // '初始化成功': 'InitializationSuccessful.mp3',
+    // '有非测试人员进入测试区': 'InterferencePersonnel.mp3',
+    // '布点文件视频尺寸和当前摄像头尺寸不一致': 'MarkInconsistency.mp3',
+    // '各就位,预备': 'Ready.mp3',
+    // '还有30秒,加油!': 'countdown30s.mp3',
+    // '还有10秒,坚持住!': 'countdown10s.mp3',
+    // '5': '5.mp3',
+    // '4': '4.mp3',
+    // '3': '3.mp3',
+    // '2': '2.mp3',
+    // '1': '1.mp3',
+    // '预备和哨声': 'PrepareWhistle.mp3',
+    跑: 'run.mp3',
+    哨声: 'shaosheng.mp3',
+    哨声2: 'shaosheng2.mp3'
+  };
+  console.log('播报', text);
+  if (obj[text]) {
+    //用本地文件
+    let url = `./static/audio/${obj[text]}`;
+    new Audio(url).play();
+    speechText = '';
+  } else {
+    if (browserSupport == true) {
+      //用TTS
+      speech.speak({ text: text.toString() }).then(() => {
+        speech.cancel(); //播放结束后调用
+        speechText = '';
+      });
+    } else {
+      //用百度语音
+      let tok = localStorage.getItem('tok') || '';
+      let url = `https://tsn.baidu.com/text2audio?tex=${encodeURI(text)}&tok=${tok}&cuid=baike&lan=ZH&ctp=1&vol=15&rate=32&per=0&spd=7&pit=4`;
+      new Audio(url).play();
+      speechText = '';
+    }
+  }
+};
+
+//取消播放
+export const speckCancel = () => {
+  if (speech && browserSupport == true) {
+    speech.cancel();
+  }
+};
+
+//小数播报格式
+export const chineseNumber = (num: any) => {
+  const chineseNumbers = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
+  let parts = String(num).split('.');
+  let integerPart = parts[0];
+  let decimalPart = parts[1];
+  let chineseInteger = integerPart;
+  let chineseDecimal = decimalPart
+    ? '点' +
+      decimalPart
+        .split('')
+        .map((digit) => chineseNumbers[parseInt(digit)])
+        .join('')
+    : '';
+  return chineseInteger + chineseDecimal;
+};

+ 3 - 3
src/utils/ws.ts

@@ -62,7 +62,7 @@ export const initWs = (data: any, callback: any) => {
     if (e.cmd === 'set_exam_state') {
       if (e.data == 3) {
         //关闭遮罩层
-        loading.close();
+        loading?.close();
       }
     }
     //新建测试后返回信息,获取result_id
@@ -298,7 +298,7 @@ export const examEnds = () => {
  */
 const getExit = () => {
   //关闭遮罩层
-  loading.close();
+  loading?.close();
   //清除计时器
   getClearTimer();
   //通知工作站关闭
@@ -308,7 +308,7 @@ const getExit = () => {
   });
   //如果正在连接就关闭
   if (socket?.connected) {
-    socket.close();
+    socket?.close();
   }
 };
 

+ 19 - 3
src/views/train/test.vue

@@ -36,6 +36,7 @@
 </template>
 
 <script setup name="TrainTest" lang="ts">
+import { initSpeech, speckText, speckCancel, chineseNumber } from '@/utils/speech'
 import { initWs, examEnds, openOneTest, startFace, stopFace, faceConfirmOnly, startOneTest, finishOneTest, closeOneTest, suspendFaceRecognitionChannels, resumeFaceRecognitionChannels } from '@/utils/ws'
 import dataDictionary from "@/utils/dataDictionary"
 const { proxy } = getCurrentInstance() as any;
@@ -194,7 +195,7 @@ const getFaceConfirmOnly = () => {
     student_id: faceCheckStu.value.student_id,
     gender: faceCheckStu.value.gender
   }, () => {
-    faceWindowRef.value.close();
+    faceWindowRef?.value.close();
     //不需要按钮的自动进入下一步
     if (needStart.value == false) {
       getStartOneTest();
@@ -232,6 +233,8 @@ const getStartOneTest = () => {
     //计时项目才开
     if (needStart.value == true) {
       getCountdown();
+    } else {
+      speckText(faceCheckStu.value.name + ",请开始测试");
     }
   })
 };
@@ -287,6 +290,7 @@ const confirmExit = () => {
 const getExit = () => {
   getClearTimer();//清除计时器
   examEnds();//通知工作站关闭
+  speckCancel()//停止播报;
   router.push({ path: '/' });//跳转
 };
 
@@ -382,14 +386,16 @@ const getStopCountdown = () => {
 };
 
 /**
- * 停止人脸识别
+ * 人脸窗口
 */
 const getFaceWindow = () => {
+  let txt = parameter.value.gesture ? "请举手看摄像头人脸识别" : "请看摄像头进行人脸识别";
+  speckText(txt);
   faceWindowRef.value.open();
   //然后定时自动关闭
   setTimeout(() => {
     if (examState.value == 41 && faceWindowRef.value.faceState == true) {
-      faceWindowRef.value.close();
+      faceWindowRef?.value.close();
     }
   }, 3000)
 
@@ -483,6 +489,15 @@ const getAchievement = (data: any) => {
     }
   }
   backReason.value = arr;
+
+  if (data.isfinish) {
+    if (['jump'].includes(type) && backReason.value.length) {
+      speckText("请重新测试");
+      return false;
+    }
+    speckText(faceCheckStu?.value.name + "成绩为" + (chineseNumber(count) || 0) + unit.value + ",请下一位准备!" || "");
+  }
+
 };
 
 
@@ -512,6 +527,7 @@ onMounted(() => {
   initWs({ parameter: parameter.value, testTime: testTime.value }, (data: any) => {
     getMessage(data);
   });
+  initSpeech();
 })
 
 onUnmounted(() => {