|
@@ -3,8 +3,10 @@
|
|
|
// import animationData from './robot.json'
|
|
|
import { Microphone } from '@element-plus/icons-vue'
|
|
|
import placeImg from './place.png'
|
|
|
+import { TTSRecorder } from './sdk/chat'
|
|
|
+import { audioPlayer, connectTtsWS } from './sdk/tts'
|
|
|
+import { connectIatWS, iatStatus, recorder } from './sdk/iat'
|
|
|
import { qa_audio, qa_text } from './qa.ts'
|
|
|
-
|
|
|
// const anim = ref(null)
|
|
|
|
|
|
// onMounted(() => {
|
|
@@ -17,6 +19,10 @@ import { qa_audio, qa_text } from './qa.ts'
|
|
|
// })
|
|
|
// })
|
|
|
|
|
|
+const isWorking = ref(false)
|
|
|
+
|
|
|
+const bigModel = $ref(new TTSRecorder())
|
|
|
+
|
|
|
const videoRef = ref<HTMLVideoElement>()
|
|
|
const videoLoaded = ref(false)
|
|
|
function handleLoadedMetadata() {
|
|
@@ -31,6 +37,7 @@ function pauseVideoPlay() {
|
|
|
console.log('pauseVideoPlay')
|
|
|
videoRef.value!.pause()
|
|
|
videoRef.value!.currentTime = 0
|
|
|
+ isWorking.value = false
|
|
|
}
|
|
|
|
|
|
const ifDrawer = ref(false)
|
|
@@ -49,9 +56,24 @@ audioEl.addEventListener('ended', () => {
|
|
|
pauseVideoPlay()
|
|
|
})
|
|
|
|
|
|
-function handleSendMessage(q?: string, isInput?: boolean) {
|
|
|
- if (isInput)
|
|
|
- return
|
|
|
+function sleep(ms: number) {
|
|
|
+ return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
+}
|
|
|
+
|
|
|
+async function handleSendMessage(q?: string, isInput?: boolean) {
|
|
|
+ isWorking.value = true
|
|
|
+ console.log('handleSendMessage', q, isInput)
|
|
|
+ console.log('iatStatus', iatStatus.value)
|
|
|
+
|
|
|
+ if (iatStatus.value === 'OPEN') {
|
|
|
+ recorder.stop()
|
|
|
+ // 录音机关闭后,需要等待一段时间,才能清空输入框
|
|
|
+ await sleep(1000)
|
|
|
+ }
|
|
|
+ if (isInput) {
|
|
|
+ console.log('清空输入框')
|
|
|
+ questionInput.value = ''
|
|
|
+ }
|
|
|
if (!q)
|
|
|
return
|
|
|
const t = Date.now()
|
|
@@ -61,19 +83,18 @@ function handleSendMessage(q?: string, isInput?: boolean) {
|
|
|
d: encodeURIComponent(q),
|
|
|
t,
|
|
|
})
|
|
|
- questionInput.value = ''
|
|
|
- setTimeout(() => {
|
|
|
- const answer = {
|
|
|
+ nextTick(() => {
|
|
|
+ scrollbarRef.value.setScrollTop(99999999)
|
|
|
+ })
|
|
|
+ if (!isInput && qa_text[q]) {
|
|
|
+ const i = chatList.value.push({
|
|
|
left: true,
|
|
|
name: 'AI助教',
|
|
|
d: '',
|
|
|
t: t + 1,
|
|
|
- }
|
|
|
- const i = chatList.value.push(answer)
|
|
|
- nextTick(() => {
|
|
|
- scrollbarRef.value.setScrollTop(99999999)
|
|
|
})
|
|
|
startVideoPlay()
|
|
|
+
|
|
|
const fullAnswer = qa_text[q].replace(/(\r\n|\n|\r)/gm, '<br />') ?? ''
|
|
|
const fullAnswerList = fullAnswer.match(/.{1,3}/g) ?? []
|
|
|
const answerAudio = qa_audio[q]
|
|
@@ -92,8 +113,54 @@ function handleSendMessage(q?: string, isInput?: boolean) {
|
|
|
nextTick(() => {
|
|
|
scrollbarRef.value.setScrollTop(99999999)
|
|
|
})
|
|
|
- }, 100)
|
|
|
- }, 1000)
|
|
|
+ }, 400)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!['init', 'endPlay', 'errorTTS'].includes(bigModel.status)) {
|
|
|
+ // chatList.value.push({
|
|
|
+ // left: true,
|
|
|
+ // name: 'AI助教',
|
|
|
+ // d: encodeURIComponent(''),
|
|
|
+ // t,
|
|
|
+ // })
|
|
|
+ isWorking.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ bigModel.start(q)
|
|
|
+ const i = chatList.value.push({
|
|
|
+ left: true,
|
|
|
+ name: 'AI助教',
|
|
|
+ d: '思考中',
|
|
|
+ t: t + 1,
|
|
|
+ })
|
|
|
+ // bigModel.onWillResultChange = (resultData) => {
|
|
|
+ // chatList.value[i - 1].d += encodeURIComponent(resultData)
|
|
|
+ // nextTick(() => {
|
|
|
+ // scrollbarRef.value.setScrollTop(99999999)
|
|
|
+ // })
|
|
|
+ // }
|
|
|
+ bigModel.onWillResultFinish = (resultData) => {
|
|
|
+ chatList.value[i - 1].d = ''
|
|
|
+
|
|
|
+ console.log('handleStartAudioTts', resultData)
|
|
|
+ const fullAnswerList = resultData.match(/.{1,3}/g) ?? []
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ if (fullAnswerList.length === 0) {
|
|
|
+ clearInterval(timer)
|
|
|
+ // pauseVideoPlay()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const item = fullAnswerList!.shift()
|
|
|
+ chatList.value[i - 1].d += encodeURIComponent(item)
|
|
|
+ nextTick(() => {
|
|
|
+ scrollbarRef.value.setScrollTop(99999999)
|
|
|
+ })
|
|
|
+ }, 400)
|
|
|
+ startVideoPlay()
|
|
|
+ handleStartAudioTts(resultData, i - 1)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const optionInputs = [
|
|
@@ -101,6 +168,41 @@ const optionInputs = [
|
|
|
'如何锻炼英语口语的发音',
|
|
|
'哪些学习的记忆方法较好',
|
|
|
]
|
|
|
+
|
|
|
+// 音频播放结束
|
|
|
+audioPlayer.onStop = () => {
|
|
|
+ console.log('audioPlayer.onStop')
|
|
|
+ pauseVideoPlay()
|
|
|
+}
|
|
|
+
|
|
|
+function handleStartAudioTts(text: string, idx: number) {
|
|
|
+ console.log('handleStartAudioTts')
|
|
|
+
|
|
|
+ const ttsWS = connectTtsWS(text)
|
|
|
+ ttsWS.onWillResultFinish = (audioPlayer: any) => {
|
|
|
+ console.log('read finsih : ', audioPlayer.getAudioDataBlob('wav'))
|
|
|
+ chatList.value[idx].audio = URL.createObjectURL(audioPlayer.getAudioDataBlob('wav'))
|
|
|
+ }
|
|
|
+}
|
|
|
+// 语音识别
|
|
|
+function handleStartIat() {
|
|
|
+ if (iatStatus.value === 'UNDEFINED' || iatStatus.value === 'CLOSED') {
|
|
|
+ const iatWS = connectIatWS()
|
|
|
+ iatWS.onWillResultStart = () => {
|
|
|
+ questionInput.value = ''
|
|
|
+ }
|
|
|
+ iatWS.onWillResultChange = (resultData) => {
|
|
|
+ console.log('onWillResultChange', resultData)
|
|
|
+ questionInput.value = resultData
|
|
|
+ }
|
|
|
+ iatWS.onWillResultFinish = (resultData) => {
|
|
|
+ console.log('onWillResultFinish', resultData)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (iatStatus.value === 'OPEN') {
|
|
|
+ recorder.stop()
|
|
|
+ }
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
@@ -112,7 +214,7 @@ const optionInputs = [
|
|
|
<img :src="placeImg" alt="">
|
|
|
</div>
|
|
|
<el-drawer
|
|
|
- v-model="ifDrawer" direction="rtl" append-to-body :size="800" title="AI助教" :z-index="201"
|
|
|
+ v-model="ifDrawer" direction="rtl" append-to-body :size="880" title="AI助教" :z-index="201"
|
|
|
style="background-color: #f0f0f0;" class="no-mb"
|
|
|
>
|
|
|
<div class="flex">
|
|
@@ -122,7 +224,7 @@ const optionInputs = [
|
|
|
class="h-full flex items-center justify-center" loop muted @loadedmetadata="handleLoadedMetadata"
|
|
|
/>
|
|
|
</div>
|
|
|
- <div class="w-360px flex flex-col justify-between rounded bg-white px-10px py-10px">
|
|
|
+ <div class="w-440px flex flex-col justify-between rounded bg-white px-10px py-10px">
|
|
|
<el-scrollbar ref="scrollbarRef" height="680px" class="px-14px">
|
|
|
<div class="flex flex-col gap-4">
|
|
|
<div class="rounded-4px bg-hex-f0f0f0 p-14px text-gray-500">
|
|
@@ -143,17 +245,31 @@ const optionInputs = [
|
|
|
</div>
|
|
|
</el-scrollbar>
|
|
|
<div class="flex items-start justify-between space-x-14px">
|
|
|
- <div
|
|
|
+ <!-- <div
|
|
|
class="h-32px w-32px flex flex-none cursor-pointer items-center justify-center rounded-16px bg-[#626aef] text-white"
|
|
|
>
|
|
|
<Microphone class="h-20px w-20px" />
|
|
|
- </div>
|
|
|
-
|
|
|
+ </div> -->
|
|
|
+ <el-tooltip
|
|
|
+ class=""
|
|
|
+ effect="dark"
|
|
|
+ content="一次语音识别时长不能超过60秒"
|
|
|
+ placement="top-start"
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ :icon="Microphone" circle :color="(iatStatus === 'UNDEFINED' || iatStatus === 'CLOSED') ? '#626aef' : '#f56c6c'"
|
|
|
+ :disabled="isWorking" style="--color: #fff;"
|
|
|
+ @click="handleStartIat"
|
|
|
+ />
|
|
|
+ </el-tooltip>
|
|
|
<el-input
|
|
|
- v-model="questionInput" disabled size="large" type="textarea" placeholder="请输入您的问题"
|
|
|
+ v-model="questionInput" size="large" type="textarea" placeholder="请输入您的问题"
|
|
|
:autosize="{ minRows: 1, maxRows: 4 }"
|
|
|
/>
|
|
|
- <el-button color="#626aef" @click="handleSendMessage(questionInput, true)">
|
|
|
+ <el-button
|
|
|
+ color="#626aef" :disabled="isWorking"
|
|
|
+ @click="handleSendMessage(questionInput, true)"
|
|
|
+ >
|
|
|
发送
|
|
|
</el-button>
|
|
|
</div>
|