perf: Optimize voice recording (#2707)

This commit is contained in:
shaohuzhang1 2025-03-27 19:53:35 +08:00 committed by GitHub
parent 378de21fa2
commit 5ba802482f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 149 additions and 94 deletions

View File

@ -8,9 +8,9 @@
@touchstart="onTouchStart" @touchstart="onTouchStart"
@touchmove="onTouchMove" @touchmove="onTouchMove"
@touchend="onTouchEnd" @touchend="onTouchEnd"
:disabled="props.disabled" :disabled="disabled"
> >
按住说话 {{ disabled ? '对话中' : '按住说话' }}
</el-button> </el-button>
<!-- 使用 custom-class 自定义样式 --> <!-- 使用 custom-class 自定义样式 -->
<transition name="el-fade-in-linear"> <transition name="el-fade-in-linear">
@ -94,10 +94,13 @@ watch(
) )
function onTouchStart(event: any) { function onTouchStart(event: any) {
emit('TouchStart')
startY.value = event.touches[0].clientY
// //
event.preventDefault() event.preventDefault()
if (props.disabled) {
return
}
emit('TouchStart')
startY.value = event.touches[0].clientY
} }
function onTouchMove(event: any) { function onTouchMove(event: any) {
if (!isTouching.value) return if (!isTouching.value) return

View File

@ -119,7 +119,7 @@
@TouchStart="startRecording" @TouchStart="startRecording"
@TouchEnd="TouchEnd" @TouchEnd="TouchEnd"
:time="recorderTime" :time="recorderTime"
:start="!mediaRecorderStatus" :start="recorderStatus === 'START'"
:disabled="loading" :disabled="loading"
/> />
<el-input <el-input
@ -127,9 +127,9 @@
ref="quickInputRef" ref="quickInputRef"
v-model="inputValue" v-model="inputValue"
:placeholder=" :placeholder="
startRecorderTime recorderStatus === 'START'
? `${$t('chat.inputPlaceholder.speaking')}...` ? `${$t('chat.inputPlaceholder.speaking')}...`
: recorderLoading : recorderStatus === 'TRANSCRIBING'
? `${$t('chat.inputPlaceholder.recorderLoading')}...` ? `${$t('chat.inputPlaceholder.recorderLoading')}...`
: $t('chat.inputPlaceholder.default') : $t('chat.inputPlaceholder.default')
" "
@ -143,8 +143,10 @@
<template v-if="props.applicationDetails.stt_model_enable"> <template v-if="props.applicationDetails.stt_model_enable">
<span v-if="mode === 'mobile'"> <span v-if="mode === 'mobile'">
<el-button text @click="isMicrophone = !isMicrophone"> <el-button text @click="isMicrophone = !isMicrophone">
<!-- 键盘 -->
<AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon> <AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon>
<el-icon v-else> <el-icon v-else>
<!-- 录音 -->
<Microphone /> <Microphone />
</el-icon> </el-icon>
</el-button> </el-button>
@ -154,7 +156,7 @@
:disabled="loading" :disabled="loading"
text text
@click="startRecording" @click="startRecording"
v-if="mediaRecorderStatus" v-if="recorderStatus === 'STOP'"
> >
<el-icon> <el-icon>
<Microphone /> <Microphone />
@ -165,14 +167,19 @@
<el-text type="info" <el-text type="info"
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text >00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
> >
<el-button text type="primary" @click="stopRecording" :loading="recorderLoading"> <el-button
text
type="primary"
@click="stopRecording"
:loading="recorderStatus === 'TRANSCRIBING'"
>
<AppIcon iconName="app-video-stop"></AppIcon> <AppIcon iconName="app-video-stop"></AppIcon>
</el-button> </el-button>
</div> </div>
</span> </span>
</template> </template>
<template v-if="(!startRecorderTime && !recorderLoading) || mode === 'mobile'"> <template v-if="recorderStatus === 'STOP' || mode === 'mobile'">
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4"> <span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4">
<el-upload <el-upload
action="#" action="#"
@ -234,7 +241,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue' import { ref, computed, onMounted, nextTick, watch } from 'vue'
import Recorder from 'recorder-core' import Recorder from 'recorder-core'
import TouchChat from './TouchChat.vue' import TouchChat from './TouchChat.vue'
import applicationApi from '@/api/application' import applicationApi from '@/api/application'
@ -417,107 +424,133 @@ const uploadFile = async (file: any, fileList: any) => {
} }
}) })
} }
// id
const intervalId = ref<any | null>(null) const intervalId = ref<any | null>(null)
//
const recorderTime = ref(0) const recorderTime = ref(0)
const startRecorderTime = ref(false) // START: TRANSCRIBING:
const recorderLoading = ref(false) const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP')
const inputValue = ref<string>('') const inputValue = ref<string>('')
const uploadImageList = ref<Array<any>>([]) const uploadImageList = ref<Array<any>>([])
const uploadDocumentList = ref<Array<any>>([]) const uploadDocumentList = ref<Array<any>>([])
const uploadVideoList = ref<Array<any>>([]) const uploadVideoList = ref<Array<any>>([])
const uploadAudioList = ref<Array<any>>([]) const uploadAudioList = ref<Array<any>>([])
const mediaRecorderStatus = ref(true)
const showDelete = ref('') const showDelete = ref('')
//
const mediaRecorder = ref<any>(null)
const isDisabledChat = computed( const isDisabledChat = computed(
() => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name)) () => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name))
) )
// //
const isMicrophone = ref(false) const isMicrophone = ref(false)
watch(isMicrophone, (value: boolean) => {
if (value) {
//
recorderManage.open()
} else {
//
recorderManage.close()
}
})
const TouchEnd = (bool: Boolean) => { const TouchEnd = (bool: Boolean) => {
if (bool) { if (bool) {
stopRecording() stopRecording()
recorderStatus.value = 'STOP'
} else { } else {
stopTimer() stopTimer()
mediaRecorder.value.close() recorderStatus.value = 'STOP'
mediaRecorder.value = null
} }
} }
//
Recorder.CLog = function () {}
// class RecorderManage {
const startRecording = async () => { recorder?: any
try { uploadRecording: (blob: Blob, duration: number) => void
// constructor(uploadRecording: (blob: Blob, duration: number) => void) {
Recorder.CLog = function () {} this.uploadRecording = uploadRecording
mediaRecorder.value = new Recorder({ }
open() {
const recorder = new Recorder({
type: 'mp3', type: 'mp3',
bitRate: 128, bitRate: 128,
sampleRate: 16000 sampleRate: 16000
}) })
if (!this.recorder) {
mediaRecorder.value.open( recorder.open(() => {
() => { this.recorder = recorder
mediaRecorder.value.start() }, this.errorCallBack)
mediaRecorderStatus.value = false }
}
start() {
if (this.recorder) {
this.recorder.start()
recorderStatus.value = 'START'
handleTimeChange()
} else {
const recorder = new Recorder({
type: 'mp3',
bitRate: 128,
sampleRate: 16000
})
recorder.open(() => {
this.recorder = recorder
recorder.start()
recorderStatus.value = 'START'
handleTimeChange() handleTimeChange()
}, }, this.errorCallBack)
(err: any) => { }
stopTimer() }
mediaRecorder.value.close() stop() {
MsgAlert( if (this.recorder) {
t('common.tip'), this.recorder.stop(
`${t('chat.tip.recorderTip')} (blob: Blob, duration: number) => {
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`, if (mode !== 'mobile') {
{ this.close()
}
this.uploadRecording(blob, duration)
},
(err: any) => {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'), confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm' customClass: 'record-tip-confirm'
} })
) }
} )
) }
} catch (error) { }
MsgAlert( close() {
t('common.tip'), if (this.recorder) {
`${t('chat.tip.recorderTip')} this.recorder.close()
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`, this.recorder = undefined
{ }
}
private errorCallBack(err: any, isUserNotAllow: boolean) {
if (isUserNotAllow) {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'), confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm' customClass: 'record-tip-confirm'
} })
) } else {
mediaRecorder.value.close() MsgAlert(
stopTimer() t('common.tip'),
`${err}
<div style="width: 100%;height:1px;border-top:1px var(--el-border-color) var(--el-border-style);margin:10px 0;"></div>
${t('chat.tip.recorderTip')}
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
{
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
}
)
}
} }
} }
//
const stopRecording = () => {
startRecorderTime.value = false
recorderTime.value = 0
if (mediaRecorder.value) {
mediaRecorderStatus.value = true
mediaRecorder.value.stop(
(blob: Blob, duration: number) => {
// blob
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = 'abc.mp3'
// link.click()
uploadRecording(blob) //
},
(err: any) => {
console.error(`${t('chat.tip.recorderError')}:`, err)
}
)
}
}
// //
const uploadRecording = async (audioBlob: Blob) => { const uploadRecording = async (audioBlob: Blob) => {
try { try {
@ -525,16 +558,13 @@ const uploadRecording = async (audioBlob: Blob) => {
if (!props.applicationDetails.stt_autosend) { if (!props.applicationDetails.stt_autosend) {
isMicrophone.value = false isMicrophone.value = false
} }
recorderLoading.value = true recorderStatus.value = 'TRANSCRIBING'
const formData = new FormData() const formData = new FormData()
formData.append('file', audioBlob, 'recording.mp3') formData.append('file', audioBlob, 'recording.mp3')
bus.emit('on:transcribing', true) bus.emit('on:transcribing', true)
applicationApi applicationApi
.postSpeechToText(props.applicationDetails.id as string, formData, localLoading) .postSpeechToText(props.applicationDetails.id as string, formData, localLoading)
.then((response) => { .then((response) => {
recorderLoading.value = false
mediaRecorder.value.close()
inputValue.value = typeof response.data === 'string' ? response.data : '' inputValue.value = typeof response.data === 'string' ? response.data : ''
// //
if (props.applicationDetails.stt_autosend) { if (props.applicationDetails.stt_autosend) {
@ -546,21 +576,35 @@ const uploadRecording = async (audioBlob: Blob) => {
} }
}) })
.catch((error) => { .catch((error) => {
recorderLoading.value = false
console.error(`${t('chat.uploadFile.errorMessage')}:`, error) console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
}) })
.finally(() => bus.emit('on:transcribing', false)) .finally(() => {
recorderStatus.value = 'STOP'
bus.emit('on:transcribing', false)
})
} catch (error) { } catch (error) {
recorderLoading.value = false recorderStatus.value = 'STOP'
console.error(`${t('chat.uploadFile.errorMessage')}:`, error) console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
} }
} }
const recorderManage = new RecorderManage(uploadRecording)
//
const startRecording = () => {
recorderManage.start()
}
//
const stopRecording = () => {
recorderManage.stop()
}
const handleTimeChange = () => { const handleTimeChange = () => {
startRecorderTime.value = true
recorderTime.value = 0 recorderTime.value = 0
if (intervalId.value) {
return
}
intervalId.value = setInterval(() => { intervalId.value = setInterval(() => {
if (!startRecorderTime.value) { if (recorderStatus.value === 'STOP') {
clearInterval(intervalId.value!) clearInterval(intervalId.value!)
intervalId.value = null intervalId.value = null
return return
@ -569,10 +613,12 @@ const handleTimeChange = () => {
recorderTime.value++ recorderTime.value++
if (recorderTime.value === 60) { if (recorderTime.value === 60) {
stopRecording() if (mode !== 'mobile') {
clearInterval(intervalId.value!) stopRecording()
intervalId.value = null clearInterval(intervalId.value!)
startRecorderTime.value = false intervalId.value = null
recorderStatus.value = 'STOP'
}
} }
}, 1000) }, 1000)
} }
@ -580,9 +626,8 @@ const handleTimeChange = () => {
const stopTimer = () => { const stopTimer = () => {
if (intervalId.value !== null) { if (intervalId.value !== null) {
clearInterval(intervalId.value) clearInterval(intervalId.value)
recorderTime.value = 0
intervalId.value = null intervalId.value = null
startRecorderTime.value = false
mediaRecorderStatus.value = true
} }
} }
@ -598,7 +643,9 @@ function autoSendMessage() {
uploadDocumentList.value = [] uploadDocumentList.value = []
uploadAudioList.value = [] uploadAudioList.value = []
uploadVideoList.value = [] uploadVideoList.value = []
quickInputRef.value.textareaStyle.height = '45px' if (quickInputRef.value) {
quickInputRef.value.textareaStyle.height = '45px'
}
} }
function sendChatHandle(event?: any) { function sendChatHandle(event?: any) {

View File

@ -527,6 +527,11 @@ onMounted(() => {
window.sendMessage = sendMessage window.sendMessage = sendMessage
bus.on('on:transcribing', (status: boolean) => { bus.on('on:transcribing', (status: boolean) => {
transcribing.value = status transcribing.value = status
nextTick(() => {
if (scorll.value) {
scrollDiv.value.setScrollTop(getMaxHeight())
}
})
}) })
}) })