Просмотр исходного кода

Merge branch 'feat-sy-role-detect' into beta

Carson 2 месяцев назад
Родитель
Сommit
2d02292783

+ 2 - 1
build/webpack.dev.conf.js

@@ -43,7 +43,8 @@ const devWebpackConfig = merge(baseWebpackConfig, {
     quiet: true, // necessary for FriendlyErrorsPlugin
     watchOptions: {
       poll: config.dev.poll,
-    }
+    },
+    https: true // Enable HTTPS
   },
   plugins: [
     new webpack.DefinePlugin({

Разница между файлами не показана из-за своего большого размера
+ 458 - 838
package-lock.json


+ 2 - 0
package.json

@@ -44,8 +44,10 @@
     "pptxgenjs": "^3.12.0",
     "qrcodejs2": "^0.0.2",
     "qs": "^6.10.1",
+    "recorder-core": "^1.3.24040900",
     "relation-graph": "^1.1.0",
     "turndown": "^7.2.0",
+    "uuid": "^10.0.0",
     "v-viewer": "^1.6.4",
     "vant": "^2.12.10",
     "vue": "^2.5.2",

+ 195 - 92
src/components/pages/classroomObservation/components/addNewTeacherVoiceprintDialog.vue

@@ -1,14 +1,11 @@
 <template>
   <div>
-    <el-dialog
-      :center="true"
-      :visible.sync="dialogVisible"
-      width="600px"
-      class="addTemplateDialog"
-    >
+    <el-dialog :center="true" :visible.sync="dialogVisible" width="600px" class="addTemplateDialog" :before-close="close">
       <!-- <div v-if="showDialog == true" class="a-dialog" v-el-drag-dialog> -->
       <div class="a-d-top">
-        <div class="a-d-topTit"><div>新增声纹</div></div>
+        <div class="a-d-topTit">
+          <div>新增声纹</div>
+        </div>
 
         <div class="a-d-t-right">
           <span @click.stop="close()">×</span>
@@ -16,9 +13,9 @@
       </div>
       <div class="a_box">
         <div class="a_b_form">
-          <el-form label-position="top" :model="form" :rules="rules">
+          <el-form ref="form$" label-position="top" :model="form" :rules="rules" :disabled="status === 1">
             <el-form-item class="a_b_f_item" label="教师名称" prop="name">
-              <el-input v-model="form.name" :disabled="![0].includes(status)"></el-input>
+              <el-input v-model="form.name"></el-input>
             </el-form-item>
 
             <el-form-item class="a_b_f_item" label="声纹录制">
@@ -33,45 +30,39 @@
             <span>请使用正常语速朗读以上内容</span>
           </div>
           <div class="a_b_b_bottom">
-            <div class="a_b_b_b_btn" @click.stop="start()" v-if="status===0">
-              <svg
-                width="16"
-                height="22"
-                viewBox="0 0 16 22"
-                fill="none"
-                xmlns="http://www.w3.org/2000/svg"
-              >
-                <path
-                  fill-rule="evenodd"
-                  clip-rule="evenodd"
+            <div class="a_b_b_b_btn" @click.stop="start()" v-if="status === 0">
+              <svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path fill-rule="evenodd" clip-rule="evenodd"
                   d="M8.00009 13.8098C10.5962 13.8098 12.6876 11.6655 12.6876 9.00351V5.30633C12.6876 2.64436 10.5962 0.5 8.00009 0.5C5.40393 0.5 3.31259 2.64436 3.31259 5.30633V9.00351C3.31259 11.6655 5.40393 13.8098 8.00009 13.8098ZM4.75489 5.30633C4.75489 3.45774 6.1972 1.97887 8.00009 1.97887C9.80297 1.97887 11.2453 3.45774 11.2453 5.30633V9.00351C11.2453 10.8521 9.80297 12.331 8.00009 12.331C6.1972 12.331 4.75489 10.8521 4.75489 9.00351V5.30633ZM15.5 10.1132C15.5 9.70651 15.1755 9.37377 14.7788 9.37377C14.4183 9.37377 14.0938 9.66954 14.0577 10.0393C13.5529 13.034 11.0288 15.2892 8 15.2892C4.97115 15.2892 2.44712 13.034 1.94231 10.0393C1.90625 9.66954 1.58173 9.37377 1.22115 9.37377C0.824519 9.37377 0.5 9.70651 0.5 10.1132V10.2241C1.07692 13.6995 3.85337 16.3984 7.27885 16.7311V19.9223H4.15379C3.72896 19.9223 3.38456 20.2755 3.38456 20.7111C3.38456 21.1467 3.72896 21.4998 4.15379 21.4998H11.8461C12.2709 21.4998 12.6153 21.1467 12.6153 20.7111C12.6153 20.2755 12.2709 19.9223 11.8461 19.9223H8.72115V16.7311C12.1466 16.3984 14.9231 13.6995 15.4639 10.2611C15.4639 10.2426 15.473 10.2149 15.482 10.1872L15.482 10.1871C15.491 10.1594 15.5 10.1317 15.5 10.1132Z"
-                  fill="white"
-                />
+                  fill="white" />
               </svg>
               点击录制
             </div>
+            <div class="a_b_b_b_btn" style="background: red;cursor: default" v-else>
+              <svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path fill-rule="evenodd" clip-rule="evenodd"
+                  d="M8.00009 13.8098C10.5962 13.8098 12.6876 11.6655 12.6876 9.00351V5.30633C12.6876 2.64436 10.5962 0.5 8.00009 0.5C5.40393 0.5 3.31259 2.64436 3.31259 5.30633V9.00351C3.31259 11.6655 5.40393 13.8098 8.00009 13.8098ZM4.75489 5.30633C4.75489 3.45774 6.1972 1.97887 8.00009 1.97887C9.80297 1.97887 11.2453 3.45774 11.2453 5.30633V9.00351C11.2453 10.8521 9.80297 12.331 8.00009 12.331C6.1972 12.331 4.75489 10.8521 4.75489 9.00351V5.30633ZM15.5 10.1132C15.5 9.70651 15.1755 9.37377 14.7788 9.37377C14.4183 9.37377 14.0938 9.66954 14.0577 10.0393C13.5529 13.034 11.0288 15.2892 8 15.2892C4.97115 15.2892 2.44712 13.034 1.94231 10.0393C1.90625 9.66954 1.58173 9.37377 1.22115 9.37377C0.824519 9.37377 0.5 9.70651 0.5 10.1132V10.2241C1.07692 13.6995 3.85337 16.3984 7.27885 16.7311V19.9223H4.15379C3.72896 19.9223 3.38456 20.2755 3.38456 20.7111C3.38456 21.1467 3.72896 21.4998 4.15379 21.4998H11.8461C12.2709 21.4998 12.6153 21.1467 12.6153 20.7111C12.6153 20.2755 12.2709 19.9223 11.8461 19.9223H8.72115V16.7311C12.1466 16.3984 14.9231 13.6995 15.4639 10.2611C15.4639 10.2426 15.473 10.2149 15.482 10.1872L15.482 10.1871C15.491 10.1594 15.5 10.1317 15.5 10.1132Z"
+                  fill="white" />
+              </svg>
+              录制中
+              <span>{{ recorderCountdown }}</span>
+            </div>
 
-						<!-- <div class="a_b_b_b_record">
-							<div class="a_b_b_b_r_left">
-								<img src="../../../../assets/icon/classroomObservation/recordLeft.png" alt="">
-								<div>
-									<div>{{recordData.status==0?"录制中":recordData.status==1?"暂停":"结束"}}</div>
-									<span>{{ recordData.time }}</span>
-								</div>
-							</div>
-							<div class="a_b_b_b_r_right">
-								<div class="a_b_b_b_r_r_start">
-									<span></span>
-								</div>
-								<div class="a_b_b_b_r_r_stop">
-									<span></span>
-									<span></span>
-								</div>
-								<div class="a_b_b_b_r_r_end">
-									<span></span>
-								</div>
-							</div>
-						</div> -->
+            <div class="a_b_b_b_record">
+              <div class="a_b_b_b_r_left">
+                <!-- <img src="../../../../assets/icon/classroomObservation/recordLeft.png" alt=""> -->
+                <div>
+                  <div v-if="status === 1">
+                    <el-button @click="stop({ drop: true })">取消录制</el-button>
+                    <el-button :disabled="isRecorderStopDisabled" @click="stop({ drop: false })">停止录制</el-button>
+                  </div>
+                  <div v-else-if="recorderContext.file">
+                    <el-tag style="cursor: pointer" @click="download">{{ recorderContext.file.name }}</el-tag>
+                    <el-button :loading="registerLoading" @click="register()">注册该声纹</el-button>
+                  </div>
+                </div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
@@ -81,17 +72,27 @@
 </template>
 
 <script>
+// import Recorder from 'recorder-core'
+// import Recorder from 'recorder-core/recorder.mp3.min'
+import Recorder from 'recorder-core/recorder.wav.min'
+import * as Sy from '@/lib/shengyang'
+import { saveAs } from 'file-saver';
+
 export default {
+  emits: ['complete'],
   data() {
     return {
       dialogVisible: false,
-      userId: this.$route.query["userid"],
+      userId: this.$route.query["userid"] || '',
+      organizeId: this.$route.query["org"] || '',
       status: 0, //0:初始状态   1:录制
-			recordData:{
-				time:0,
-				status:0,//0 录制中  1暂停  2结束
-			},
-      form: { 
+      recorderContext: {
+        startTime: null,
+        endTime: null,
+        timer: null,
+        file: null,
+      },
+      form: {
         name: ""
       },
       textAreaVale:
@@ -106,21 +107,121 @@ export default {
             message: "长度需在1-20个字符之间"
           }
         ]
-      }
+      },
+      recorder: null,
+      registerLoading: false,
     };
   },
-
+  computed: {
+    recorderCountdown() {
+      const elapsed = this.recorderContext.endTime - this.recorderContext.startTime
+      if (elapsed < 0) {
+        return '00:00'
+      }
+      const minutes = Math.floor(elapsed / 60000)
+      const seconds = Math.floor((elapsed % 60000) / 1000)
+      return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
+    },
+    isRecorderStopDisabled() {
+      return (this.recorderContext.endTime - this.recorderContext.startTime) <= 60 * 1000
+    }
+  },
   methods: {
     open() {
       this.dialogVisible = true;
     },
+    beforeClose(done) {
+      if (this.status === 1) {
+        this.$message.info('请先停止录制')
+        return false
+      }
+      done && done()
+      this.$emit('complete')
+      return true
+    },
     close() {
-      this.dialogVisible = false;
+      if (this.beforeClose()) {
+        this.dialogVisible = false;
+      }
+    },
+    start() {
+      if (!this.form.name) {
+        this.$message.info('请先填写教师名字')
+        return
+      }
+      this.status = 1;
+      this.recorder = Recorder({
+        type: 'wav',
+        sampleRate: 16000,
+        bitRate: 16,
+        disableEnvInFix: false,
+      })
+      this.recorder.open(
+        () => {
+          this.recorder.start()
+          this.recorderContext.startTime = Date.now()
+          this.startTimer()
+        },
+        (msg, isUserNotAllow) => {
+          console.log('Recorder Info: isUserNotAllow', isUserNotAllow, msg)
+        }
+      )
+    },
+    async stop({ drop }) {
+      this.stopTimer()
+      this.recorder.stop(async (blob, duration, mime) => {
+        try {
+          if (drop) {
+            return
+          }
+          if (duration < 60 * 1000) {
+            this.$message.error('录制时间不足一分钟,请重试')
+            return
+          }
+          this.recorderContext.file = new File([blob], `voiceprint_${Date.now()}.wav`, { type: mime });
+
+        } finally {
+          this.status = 0
+          this.recorder = null
+        }
+      })
+    },
+    async register() {
+      this.registerLoading = true
+      try {
+        await Sy.registerVoiceprint({
+          file: this.recorderContext.file, name: this.form.name, userId: this.userId, organizeId: this.organizeId
+        })
+        this.$message.success('注册完成')
+        this.$refs.form$.resetFields()
+        this.recorderContext = {
+          startTime: null,
+          endTime: null,
+          timer: null,
+          file: null,
+        }
+      } finally {
+        this.registerLoading = false
+      }
     },
-		start(){
-			this.status = 1;
-		}
-  }
+    startTimer() {
+      this.recorderContext.timer = setInterval(() => {
+        this.recorderContext.endTime = Date.now()
+      }, 1000)
+    },
+    stopTimer() {
+      if (this.recorderContext.timer) {
+        clearInterval(this.recorderContext.timer)
+        this.recorderContext.timer = null
+      }
+    },
+    download() {
+      if (this.recorderContext.file) {
+        saveAs(this.recorderContext.file, `voiceprint_${Date.now()}.wav`);
+      }
+    }
+  },
+
 };
 </script>
 
@@ -151,6 +252,7 @@ export default {
   justify-content: center;
   /* text-align: left; */
 }
+
 .a-d-t-right {
   width: 40px;
   height: 40px;
@@ -161,7 +263,7 @@ export default {
   color: black !important;
 }
 
-.a-d-t-right > span {
+.a-d-t-right>span {
   width: 25px;
   height: 25px;
   border-radius: 25px;
@@ -177,7 +279,7 @@ export default {
   color: #adadad;
 }
 
-.addTemplateDialog >>> .el-dialog {
+.addTemplateDialog>>>.el-dialog {
   min-width: 600px;
 
   height: 700px;
@@ -188,7 +290,8 @@ export default {
   /* margin: 0 auto; */
   overflow: hidden;
 }
-.addTemplateDialog >>> .el-dialog__body {
+
+.addTemplateDialog>>>.el-dialog__body {
   height: 100%;
   min-width: 600px;
   flex-shrink: 0;
@@ -196,7 +299,8 @@ export default {
   padding-bottom: 50px;
   padding-top: 10px;
 }
-.addTemplateDialog >>> .el-dialog__header {
+
+.addTemplateDialog>>>.el-dialog__header {
   display: none;
 }
 
@@ -235,7 +339,6 @@ export default {
   width: 100%;
   height: 100%;
   display: flex;
-  justify-content: center;
   align-items: center;
 }
 
@@ -251,7 +354,7 @@ export default {
   cursor: pointer;
 }
 
-.a_b_b_b_btn > svg {
+.a_b_b_b_btn>svg {
   width: 20px;
   height: 20px;
   margin-right: 5px;
@@ -261,7 +364,7 @@ export default {
   margin-bottom: 10px;
 }
 
-.a_b_f_item >>> .el-form-item__label {
+.a_b_f_item>>>.el-form-item__label {
   padding: 0;
   font-size: 20px;
   font-weight: bold;
@@ -276,7 +379,8 @@ export default {
   padding: 10px;
   overflow: auto;
 }
-.a_b_f_itemTextArea > div {
+
+.a_b_f_itemTextArea>div {
   background-color: #f0f2f566;
   border: 1px dashed #f0f2f5;
   border-radius: 4px;
@@ -284,45 +388,44 @@ export default {
   box-sizing: border-box;
   padding: 5px;
   font-size: 16px;
-	line-height: 30px;
+  line-height: 30px;
 }
 
-.a_b_b_b_record{
-	width: 100%;
-	height: 100%;
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
+.a_b_b_b_record {
+  height: 100%;
+  flex: 1;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
 }
 
-.a_b_b_b_r_left{
-	display: flex;
-	align-items: center;
-	justify-content: center;
+.a_b_b_b_r_left {
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
-.a_b_b_b_r_left>div{
-	height: 100%;
-	width: auto;
-	display: flex;
-	flex-direction: column;
-	justify-content: space-between;
-	margin-left: 10px;
+.a_b_b_b_r_left>div {
+  height: 100%;
+  width: auto;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  margin-left: 10px;
 }
 
-.a_b_b_b_r_left>div>div{
-	font-weight: bold;
-	margin-bottom: 5px;
+.a_b_b_b_r_left>div>div {
+  font-weight: bold;
+  margin-bottom: 5px;
 }
 
-.a_b_b_b_r_left>div>span{
-	color: #3681FC;
+.a_b_b_b_r_left>div>span {
+  color: #3681FC;
 }
 
-.a_b_b_b_r_right{
-	display: flex;
-	align-items: center;
-	justify-content: flex-end;
+.a_b_b_b_r_right {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
 }
-
 </style>

Разница между файлами не показана из-за своего большого размера
+ 158 - 677
src/components/pages/classroomObservation/components/chatArea.vue


+ 102 - 0
src/lib/shengyang.js

@@ -0,0 +1,102 @@
+import { v4 as uuid4 } from 'uuid'
+import _ from 'lodash'
+
+export async function registerVoiceprint({ name, userId, organizeId, file }) {
+  // 检查文件的 MIME 类型是否为 WAV 文件
+  if (file.type !== 'audio/wav') {
+    throw new Error('文件类型必须是 WAV');
+  }
+  const formData = new FormData();
+  formData.append('file', file, file.name);
+  const registerRes = await fetch('https://conference.voiceaitech.com/api/convoice/feature/extract', { // 替换为实际的上传URL
+    method: 'POST',
+    body: formData,
+  }).then(response => response.json())
+  if (!_.get(registerRes, ['result', 'featureData'])) {
+    throw new Error("声音文件不达标,请注意文件时长大于1分钟和音量");
+  }
+  const featureData = registerRes.result.featureData
+
+  const insertRes = await fetch('https://gpt4.cocorobo.cn/insert_sy_voiceprint', {
+    method: 'POST',
+    body: JSON.stringify({
+      name,
+      featureData,
+      user_id: userId,
+      organize_id: organizeId,
+    }),
+    headers: {
+      'Content-Type': 'application/json',
+      hwMac: organizeId
+    }
+  }).then(res => res.json())
+  if (!_.get(insertRes, ['FunctionResponse'])) {
+    throw new Error('上传失败')
+  }
+  return featureData
+}
+
+export async function getVoiceprints({ userId, organizeId }) {
+  const res = await fetch('https://gpt4.cocorobo.cn/get_sy_voiceprint', {
+    method: 'POST',
+    body: JSON.stringify({
+      user_id: userId,
+    }),
+    headers: {
+      'Content-Type': 'application/json',
+      hwMac: organizeId
+    }
+  }).then(res => res.json())
+  if (!_.get(res, ['FunctionResponse'])) {
+    throw new Error('获取声纹列表失败')
+  }
+  return JSON.parse(res.FunctionResponse)
+}
+
+export async function createWs({ enroll_infos }) {
+  // 创建一个WebSocket实例
+
+  const ws = new WebSocket(
+    'wss://conference.voiceaitech.com/api/convoice/streaming'
+  );
+  let _resolve
+  // const p = new Promise(resolve => _resolve = resolve)
+  ws.onopen = () => {
+    const data = {
+      eventName: 'start',
+      sessionId: uuid4(),
+      timestamp: new Date().getTime(),
+      convoiceInfo: {
+        options: 2,
+        hotwords: [
+          {
+            weight: 5,
+            hotword: '上课',
+          },
+          // TODO
+        ],
+        enroll_infos,
+      },
+    };
+    ws.send(JSON.stringify(data));
+    // setTimeout(() => {
+    //   _resolve()
+    // }, 3000)
+  };
+  // await p
+  // 处理来自服务器的消息
+  // ws.onmessage = (e) => {
+  //   const res = JSON.parse(e.data);
+  //   if (res?.userdata?.results?.length > 0) {
+  //     this.role_arr = res.userdata.results;
+  //   }
+  //   if (res?.userdata?.last_block) {
+  //     ws.close();
+  //   }
+  // };
+  // 处理错误
+  // ws.onerror = (error) => {
+  //   console.error('WebSocket error:', error);
+  // };
+  return ws
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов