perf: Optimize voice recording (#2707)
This commit is contained in:
parent
378de21fa2
commit
5ba802482f
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user