|
@@ -1,31 +1,37 @@
|
|
|
<template>
|
|
|
- <div class="audio">
|
|
|
+ <div class="flex items-center">
|
|
|
<template v-if="state.isStart">
|
|
|
<el-tooltip content="停止语音输入" placement="top">
|
|
|
- <div class="audio-main-voice __hover" @click="onStop">
|
|
|
- <div v-for="item in 4" ref="ref_bars"/>
|
|
|
+ <div class="voice __hover" @click="onStop">
|
|
|
+ <div v-for="item in 4" ref="ref_bars" />
|
|
|
</div>
|
|
|
</el-tooltip>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
<el-tooltip content="语音输入" placement="top">
|
|
|
- <div class="audio-main __hover" @click="onStart">
|
|
|
- <img src="@/views/smart-ask-answer/assistant-2/imgs/audio.png"/>
|
|
|
+ <div class="__hover h-6 w-6" @click="onStart">
|
|
|
+ <img class="h-full w-full" src="@/assets/images/chat/audio.png" />
|
|
|
</div>
|
|
|
</el-tooltip>
|
|
|
</template>
|
|
|
<template v-if="state.isStart">
|
|
|
- <div class="duration">
|
|
|
- {{durationCpt}}
|
|
|
+ <div class="ml-1.5 text-sm text-[#4f4f4f]">
|
|
|
+ {{ durationCpt }}
|
|
|
</div>
|
|
|
</template>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from "vue";
|
|
|
-import {ElMessage} from "element-plus";
|
|
|
-import {audioToText} from "@/views/smart-ask-answer/assistant-2/dify/share";
|
|
|
+import {
|
|
|
+ computed,
|
|
|
+ getCurrentInstance,
|
|
|
+ onMounted,
|
|
|
+ reactive,
|
|
|
+ ref,
|
|
|
+ watch,
|
|
|
+} from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
|
const emit = defineEmits(['onLoading', 'onAudio'])
|
|
|
const props = defineProps({})
|
|
@@ -49,14 +55,15 @@ const onStart = async () => {
|
|
|
state.isStart = true
|
|
|
try {
|
|
|
// 请求麦克风权限
|
|
|
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
|
|
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
- state.analyser = audioContext.createAnalyser();
|
|
|
- state.analyser.fftSize = 256;
|
|
|
- const microphone = audioContext.createMediaStreamSource(stream);
|
|
|
- microphone.connect(state.analyser);
|
|
|
- updateVolumeBars();
|
|
|
+ const audioContext = new (window.AudioContext ||
|
|
|
+ window.webkitAudioContext)()
|
|
|
+ state.analyser = audioContext.createAnalyser()
|
|
|
+ state.analyser.fftSize = 256
|
|
|
+ const microphone = audioContext.createMediaStreamSource(stream)
|
|
|
+ microphone.connect(state.analyser)
|
|
|
+ updateVolumeBars()
|
|
|
|
|
|
state.mediaRecorder = new MediaRecorder(stream)
|
|
|
const audioChunks: any = []
|
|
@@ -65,20 +72,20 @@ const onStart = async () => {
|
|
|
}
|
|
|
state.mediaRecorder.onstop = async () => {
|
|
|
clearInterval(state.timer)
|
|
|
- state.audioBlob = new Blob(audioChunks, {type: 'audio/mp3'})
|
|
|
+ state.audioBlob = new Blob(audioChunks, { type: 'audio/mp3' })
|
|
|
// this.audioUrl = URL.createObjectURL(this.audioBlob)
|
|
|
- stream.getTracks().forEach(track => track.stop())
|
|
|
+ stream.getTracks().forEach((track) => track.stop())
|
|
|
if (microphone) {
|
|
|
- microphone.disconnect();
|
|
|
+ microphone.disconnect()
|
|
|
}
|
|
|
if (audioContext && audioContext.state !== 'closed') {
|
|
|
- audioContext.close();
|
|
|
+ audioContext.close()
|
|
|
}
|
|
|
- cancelAnimationFrame(state.animationId);
|
|
|
+ cancelAnimationFrame(state.animationId)
|
|
|
// 重置柱状图
|
|
|
- ref_bars.value.forEach(bar => {
|
|
|
- bar.style.height = '4px';
|
|
|
- });
|
|
|
+ ref_bars.value.forEach((bar) => {
|
|
|
+ bar.style.height = '4px'
|
|
|
+ })
|
|
|
if (!state.audioBlob) {
|
|
|
ElMessage.error('没有可上传的录音文件')
|
|
|
return
|
|
@@ -86,8 +93,15 @@ const onStart = async () => {
|
|
|
try {
|
|
|
const formData = new FormData()
|
|
|
formData.append('file', state.audioBlob)
|
|
|
- const audioResponse = await audioToText(`/installed-apps/${import.meta.env.VITE_DIFY_APPID}/audio-to-text`, false, formData)
|
|
|
- emit('onAudio', audioResponse.text)
|
|
|
+ // const audioResponse = await audioToText(
|
|
|
+ // `/installed-apps/${import.meta.env.VITE_DIFY_APPID}/audio-to-text`,
|
|
|
+ // false,
|
|
|
+ // formData,
|
|
|
+ // )
|
|
|
+ emit(
|
|
|
+ 'onAudio',
|
|
|
+ '语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容语音内容',
|
|
|
+ )
|
|
|
} catch (err) {
|
|
|
emit('onAudio', '')
|
|
|
ElMessage.error('上传错误:' + err)
|
|
@@ -103,7 +117,6 @@ const onStart = async () => {
|
|
|
state.timer = setInterval(() => {
|
|
|
state.duration = Math.floor((Date.now() - startTime) / 1000)
|
|
|
}, 1000)
|
|
|
-
|
|
|
} catch (err: any) {
|
|
|
ElMessage.error('无法访问麦克风: ' + err.message)
|
|
|
console.error('录音错误:', err)
|
|
@@ -117,80 +130,69 @@ const onStop = async () => {
|
|
|
}
|
|
|
}
|
|
|
const updateVolumeBars = () => {
|
|
|
- if (!state.isStart) return;
|
|
|
+ if (!state.isStart) return
|
|
|
|
|
|
- const array = new Uint8Array(state.analyser.frequencyBinCount);
|
|
|
- state.analyser.getByteFrequencyData(array);
|
|
|
- let sum = 0;
|
|
|
+ const array = new Uint8Array(state.analyser.frequencyBinCount)
|
|
|
+ state.analyser.getByteFrequencyData(array)
|
|
|
+ let sum = 0
|
|
|
for (let i = 0; i < array.length; i++) {
|
|
|
- sum += array[i];
|
|
|
+ sum += array[i]
|
|
|
}
|
|
|
|
|
|
- const average = sum / array.length;
|
|
|
- const baseVolume = Math.min(1, average / 70);
|
|
|
+ const average = sum / array.length
|
|
|
+ const baseVolume = Math.min(1, average / 70)
|
|
|
|
|
|
// 更新相位
|
|
|
- state.phase += 0.2;
|
|
|
- if (state.phase > Math.PI * 2) state.phase -= Math.PI * 2;
|
|
|
+ state.phase += 0.2
|
|
|
+ if (state.phase > Math.PI * 2) state.phase -= Math.PI * 2
|
|
|
|
|
|
// 更新每个柱子的高度
|
|
|
ref_bars.value.forEach((bar, index) => {
|
|
|
// 每个柱子有轻微相位差
|
|
|
- const barPhase = state.phase + (index * Math.PI / 3);
|
|
|
+ const barPhase = state.phase + (index * Math.PI) / 3
|
|
|
// 波浪因子 (0.5-1.5范围)
|
|
|
- const waveFactor = 0.8 + Math.sin(barPhase) * 0.2;
|
|
|
+ const waveFactor = 0.8 + Math.sin(barPhase) * 0.2
|
|
|
|
|
|
// 基础高度 + 音量影响 * 波浪因子
|
|
|
- const height = 4 + (baseVolume * ((index > 0 && index < ref_bars.value.length - 1) ? 15 : 5)) * waveFactor;
|
|
|
- bar.style.height = `${height}px`;
|
|
|
- });
|
|
|
+ const height =
|
|
|
+ 4 +
|
|
|
+ baseVolume *
|
|
|
+ (index > 0 && index < ref_bars.value.length - 1 ? 15 : 5) *
|
|
|
+ waveFactor
|
|
|
+ bar.style.height = `${height}px`
|
|
|
+ })
|
|
|
|
|
|
- state.animationId = requestAnimationFrame(updateVolumeBars);
|
|
|
+ state.animationId = requestAnimationFrame(updateVolumeBars)
|
|
|
}
|
|
|
const calculateBarLevels = (volume) => {
|
|
|
// 根据音量计算4个柱子的高度比例
|
|
|
// 无声音时全部为0,有声音时从低到高依次点亮
|
|
|
- const thresholds = [0.25, 0.5, 0.75, 1.0];
|
|
|
- return thresholds.map(t => Math.max(0, Math.min(1, (volume - (t - 0.25)) * 4)));
|
|
|
+ const thresholds = [0.25, 0.5, 0.75, 1.0]
|
|
|
+ return thresholds.map((t) =>
|
|
|
+ Math.max(0, Math.min(1, (volume - (t - 0.25)) * 4)),
|
|
|
+ )
|
|
|
}
|
|
|
-onMounted(() => {
|
|
|
-})
|
|
|
+onMounted(() => {})
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.audio {
|
|
|
+.voice {
|
|
|
+ background-color: rgba(var(--czr-main-color-rgb), 0.1);
|
|
|
+ gap: 2px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 8px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- .audio-main, .audio-main-voice {
|
|
|
- width: 32px;
|
|
|
- height: 32px;
|
|
|
- border-radius: 8px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
- .audio-main {
|
|
|
- &:hover {
|
|
|
- background-color: rgba(0, 0, 0, 0.04);
|
|
|
+ justify-content: center;
|
|
|
+ > div {
|
|
|
+ width: 2px;
|
|
|
+ height: 4px;
|
|
|
+ background-color: var(--czr-main-color);
|
|
|
+ transition: height 0.15s ease-out;
|
|
|
+ &:nth-child(2) {
|
|
|
+ margin-right: 1px;
|
|
|
}
|
|
|
}
|
|
|
- .audio-main-voice {
|
|
|
- background-color: rgba(0,87,255,0.1);
|
|
|
- gap: 2px;
|
|
|
- >div {
|
|
|
- width: 2px;
|
|
|
- height: 4px;
|
|
|
- background-color: #06f;
|
|
|
- transition: height 0.15s ease-out;
|
|
|
- &:nth-child(2) {
|
|
|
- margin-right: 1px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- .duration {
|
|
|
- color: #4f4f4f;
|
|
|
- font-size: 14px;
|
|
|
- margin-left: 6px;
|
|
|
- }
|
|
|
}
|
|
|
</style>
|