Skip to content

Commit 5ba8024

Browse files
authored
perf: Optimize voice recording (#2707)
1 parent 378de21 commit 5ba8024

File tree

3 files changed

+150
-95
lines changed

3 files changed

+150
-95
lines changed

ui/src/components/ai-chat/component/chat-input-operate/TouchChat.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
@touchstart="onTouchStart"
99
@touchmove="onTouchMove"
1010
@touchend="onTouchEnd"
11-
:disabled="props.disabled"
11+
:disabled="disabled"
1212
>
13-
按住说话
13+
{{ disabled ? '对话中' : '按住说话' }}
1414
</el-button>
1515
<!-- 使用 custom-class 自定义样式 -->
1616
<transition name="el-fade-in-linear">
@@ -94,10 +94,13 @@ watch(
9494
)
9595
9696
function onTouchStart(event: any) {
97-
emit('TouchStart')
98-
startY.value = event.touches[0].clientY
9997
// 阻止默认滚动行为
10098
event.preventDefault()
99+
if (props.disabled) {
100+
return
101+
}
102+
emit('TouchStart')
103+
startY.value = event.touches[0].clientY
101104
}
102105
function onTouchMove(event: any) {
103106
if (!isTouching.value) return

ui/src/components/ai-chat/component/chat-input-operate/index.vue

Lines changed: 138 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,17 @@
119119
@TouchStart="startRecording"
120120
@TouchEnd="TouchEnd"
121121
:time="recorderTime"
122-
:start="!mediaRecorderStatus"
122+
:start="recorderStatus === 'START'"
123123
:disabled="loading"
124124
/>
125125
<el-input
126126
v-else
127127
ref="quickInputRef"
128128
v-model="inputValue"
129129
:placeholder="
130-
startRecorderTime
130+
recorderStatus === 'START'
131131
? `${$t('chat.inputPlaceholder.speaking')}...`
132-
: recorderLoading
132+
: recorderStatus === 'TRANSCRIBING'
133133
? `${$t('chat.inputPlaceholder.recorderLoading')}...`
134134
: $t('chat.inputPlaceholder.default')
135135
"
@@ -143,8 +143,10 @@
143143
<template v-if="props.applicationDetails.stt_model_enable">
144144
<span v-if="mode === 'mobile'">
145145
<el-button text @click="isMicrophone = !isMicrophone">
146+
<!-- 键盘 -->
146147
<AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon>
147148
<el-icon v-else>
149+
<!-- 录音 -->
148150
<Microphone />
149151
</el-icon>
150152
</el-button>
@@ -154,7 +156,7 @@
154156
:disabled="loading"
155157
text
156158
@click="startRecording"
157-
v-if="mediaRecorderStatus"
159+
v-if="recorderStatus === 'STOP'"
158160
>
159161
<el-icon>
160162
<Microphone />
@@ -165,14 +167,19 @@
165167
<el-text type="info"
166168
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
167169
>
168-
<el-button text type="primary" @click="stopRecording" :loading="recorderLoading">
170+
<el-button
171+
text
172+
type="primary"
173+
@click="stopRecording"
174+
:loading="recorderStatus === 'TRANSCRIBING'"
175+
>
169176
<AppIcon iconName="app-video-stop"></AppIcon>
170177
</el-button>
171178
</div>
172179
</span>
173180
</template>
174181

175-
<template v-if="(!startRecorderTime && !recorderLoading) || mode === 'mobile'">
182+
<template v-if="recorderStatus === 'STOP' || mode === 'mobile'">
176183
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4">
177184
<el-upload
178185
action="#"
@@ -234,7 +241,7 @@
234241
</div>
235242
</template>
236243
<script setup lang="ts">
237-
import { ref, computed, onMounted, nextTick } from 'vue'
244+
import { ref, computed, onMounted, nextTick, watch } from 'vue'
238245
import Recorder from 'recorder-core'
239246
import TouchChat from './TouchChat.vue'
240247
import applicationApi from '@/api/application'
@@ -417,124 +424,147 @@ const uploadFile = async (file: any, fileList: any) => {
417424
}
418425
})
419426
}
420-
427+
// 语音录制任务id
421428
const intervalId = ref<any | null>(null)
429+
// 语音录制开始秒数
422430
const recorderTime = ref(0)
423-
const startRecorderTime = ref(false)
424-
const recorderLoading = ref(false)
431+
// START:开始录音 TRANSCRIBING:转换文字中
432+
const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP')
433+
425434
const inputValue = ref<string>('')
426435
const uploadImageList = ref<Array<any>>([])
427436
const uploadDocumentList = ref<Array<any>>([])
428437
const uploadVideoList = ref<Array<any>>([])
429438
const uploadAudioList = ref<Array<any>>([])
430-
const mediaRecorderStatus = ref(true)
439+
431440
const showDelete = ref('')
432441
433-
// 定义响应式引用
434-
const mediaRecorder = ref<any>(null)
435442
const isDisabledChat = computed(
436443
() => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name))
437444
)
438-
// 移动端语音
445+
// 是否显示移动端语音按钮
439446
const isMicrophone = ref(false)
440-
447+
watch(isMicrophone, (value: boolean) => {
448+
if (value) {
449+
// 如果显示就申请麦克风权限
450+
recorderManage.open()
451+
} else {
452+
// 关闭麦克风
453+
recorderManage.close()
454+
}
455+
})
441456
const TouchEnd = (bool: Boolean) => {
442457
if (bool) {
443458
stopRecording()
459+
recorderStatus.value = 'STOP'
444460
} else {
445461
stopTimer()
446-
mediaRecorder.value.close()
447-
mediaRecorder.value = null
462+
recorderStatus.value = 'STOP'
448463
}
449464
}
450-
451-
// 开始录音
452-
const startRecording = async () => {
453-
try {
454-
// 取消录音控制台日志
455-
Recorder.CLog = function () {}
456-
mediaRecorder.value = new Recorder({
465+
// 取消录音控制台日志
466+
Recorder.CLog = function () {}
467+
468+
class RecorderManage {
469+
recorder?: any
470+
uploadRecording: (blob: Blob, duration: number) => void
471+
constructor(uploadRecording: (blob: Blob, duration: number) => void) {
472+
this.uploadRecording = uploadRecording
473+
}
474+
open() {
475+
const recorder = new Recorder({
457476
type: 'mp3',
458477
bitRate: 128,
459478
sampleRate: 16000
460479
})
461-
462-
mediaRecorder.value.open(
463-
() => {
464-
mediaRecorder.value.start()
465-
mediaRecorderStatus.value = false
480+
if (!this.recorder) {
481+
recorder.open(() => {
482+
this.recorder = recorder
483+
}, this.errorCallBack)
484+
}
485+
}
486+
start() {
487+
if (this.recorder) {
488+
this.recorder.start()
489+
recorderStatus.value = 'START'
490+
handleTimeChange()
491+
} else {
492+
const recorder = new Recorder({
493+
type: 'mp3',
494+
bitRate: 128,
495+
sampleRate: 16000
496+
})
497+
recorder.open(() => {
498+
this.recorder = recorder
499+
recorder.start()
500+
recorderStatus.value = 'START'
466501
handleTimeChange()
467-
},
468-
(err: any) => {
469-
stopTimer()
470-
mediaRecorder.value.close()
471-
MsgAlert(
472-
t('common.tip'),
473-
`${t('chat.tip.recorderTip')}
474-
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
475-
{
502+
}, this.errorCallBack)
503+
}
504+
}
505+
stop() {
506+
if (this.recorder) {
507+
this.recorder.stop(
508+
(blob: Blob, duration: number) => {
509+
if (mode !== 'mobile') {
510+
this.close()
511+
}
512+
this.uploadRecording(blob, duration)
513+
},
514+
(err: any) => {
515+
MsgAlert(t('common.tip'), err, {
476516
confirmButtonText: t('chat.tip.confirm'),
477517
dangerouslyUseHTMLString: true,
478518
customClass: 'record-tip-confirm'
479-
}
480-
)
481-
}
482-
)
483-
} catch (error) {
484-
MsgAlert(
485-
t('common.tip'),
486-
`${t('chat.tip.recorderTip')}
487-
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
488-
{
519+
})
520+
}
521+
)
522+
}
523+
}
524+
close() {
525+
if (this.recorder) {
526+
this.recorder.close()
527+
this.recorder = undefined
528+
}
529+
}
530+
531+
private errorCallBack(err: any, isUserNotAllow: boolean) {
532+
if (isUserNotAllow) {
533+
MsgAlert(t('common.tip'), err, {
489534
confirmButtonText: t('chat.tip.confirm'),
490535
dangerouslyUseHTMLString: true,
491536
customClass: 'record-tip-confirm'
492-
}
493-
)
494-
mediaRecorder.value.close()
495-
stopTimer()
496-
}
497-
}
498-
499-
// 停止录音
500-
const stopRecording = () => {
501-
startRecorderTime.value = false
502-
recorderTime.value = 0
503-
if (mediaRecorder.value) {
504-
mediaRecorderStatus.value = true
505-
mediaRecorder.value.stop(
506-
(blob: Blob, duration: number) => {
507-
// 测试blob是否能正常播放
508-
// const link = document.createElement('a')
509-
// link.href = window.URL.createObjectURL(blob)
510-
// link.download = 'abc.mp3'
511-
// link.click()
512-
uploadRecording(blob) // 上传录音文件
513-
},
514-
(err: any) => {
515-
console.error(`${t('chat.tip.recorderError')}:`, err)
516-
}
517-
)
537+
})
538+
} else {
539+
MsgAlert(
540+
t('common.tip'),
541+
`${err}
542+
<div style="width: 100%;height:1px;border-top:1px var(--el-border-color) var(--el-border-style);margin:10px 0;"></div>
543+
${t('chat.tip.recorderTip')}
544+
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
545+
{
546+
confirmButtonText: t('chat.tip.confirm'),
547+
dangerouslyUseHTMLString: true,
548+
customClass: 'record-tip-confirm'
549+
}
550+
)
551+
}
518552
}
519553
}
520-
521554
// 上传录音文件
522555
const uploadRecording = async (audioBlob: Blob) => {
523556
try {
524557
// 非自动发送切换输入框
525558
if (!props.applicationDetails.stt_autosend) {
526559
isMicrophone.value = false
527560
}
528-
recorderLoading.value = true
529-
561+
recorderStatus.value = 'TRANSCRIBING'
530562
const formData = new FormData()
531563
formData.append('file', audioBlob, 'recording.mp3')
532564
bus.emit('on:transcribing', true)
533565
applicationApi
534566
.postSpeechToText(props.applicationDetails.id as string, formData, localLoading)
535567
.then((response) => {
536-
recorderLoading.value = false
537-
mediaRecorder.value.close()
538568
inputValue.value = typeof response.data === 'string' ? response.data : ''
539569
// 自动发送
540570
if (props.applicationDetails.stt_autosend) {
@@ -546,21 +576,35 @@ const uploadRecording = async (audioBlob: Blob) => {
546576
}
547577
})
548578
.catch((error) => {
549-
recorderLoading.value = false
550579
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
551580
})
552-
.finally(() => bus.emit('on:transcribing', false))
581+
.finally(() => {
582+
recorderStatus.value = 'STOP'
583+
bus.emit('on:transcribing', false)
584+
})
553585
} catch (error) {
554-
recorderLoading.value = false
586+
recorderStatus.value = 'STOP'
555587
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
556588
}
557589
}
590+
const recorderManage = new RecorderManage(uploadRecording)
591+
// 开始录音
592+
const startRecording = () => {
593+
recorderManage.start()
594+
}
595+
596+
// 停止录音
597+
const stopRecording = () => {
598+
recorderManage.stop()
599+
}
558600
559601
const handleTimeChange = () => {
560-
startRecorderTime.value = true
561602
recorderTime.value = 0
603+
if (intervalId.value) {
604+
return
605+
}
562606
intervalId.value = setInterval(() => {
563-
if (!startRecorderTime.value) {
607+
if (recorderStatus.value === 'STOP') {
564608
clearInterval(intervalId.value!)
565609
intervalId.value = null
566610
return
@@ -569,20 +613,21 @@ const handleTimeChange = () => {
569613
recorderTime.value++
570614
571615
if (recorderTime.value === 60) {
572-
stopRecording()
573-
clearInterval(intervalId.value!)
574-
intervalId.value = null
575-
startRecorderTime.value = false
616+
if (mode !== 'mobile') {
617+
stopRecording()
618+
clearInterval(intervalId.value!)
619+
intervalId.value = null
620+
recorderStatus.value = 'STOP'
621+
}
576622
}
577623
}, 1000)
578624
}
579625
// 停止计时的函数
580626
const stopTimer = () => {
581627
if (intervalId.value !== null) {
582628
clearInterval(intervalId.value)
629+
recorderTime.value = 0
583630
intervalId.value = null
584-
startRecorderTime.value = false
585-
mediaRecorderStatus.value = true
586631
}
587632
}
588633
@@ -598,7 +643,9 @@ function autoSendMessage() {
598643
uploadDocumentList.value = []
599644
uploadAudioList.value = []
600645
uploadVideoList.value = []
601-
quickInputRef.value.textareaStyle.height = '45px'
646+
if (quickInputRef.value) {
647+
quickInputRef.value.textareaStyle.height = '45px'
648+
}
602649
}
603650
604651
function sendChatHandle(event?: any) {

0 commit comments

Comments
 (0)