Commit 2a5e9aa6 authored by ali's avatar ali

feat: 更换 icon;修复视频数字人切换视频闪烁问题,回答内容分割不准确问题,优化回答内容麦克风状态联动;新增 llmToTTSSliceLength 配置控制回答内容分割字数

parent fd503c93
buildAssets/icons/icon.png

16.4 KB | W: | H:

buildAssets/icons/icon.png

52.3 KB | W: | H:

buildAssets/icons/icon.png
buildAssets/icons/icon.png
buildAssets/icons/icon.png
buildAssets/icons/icon.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -84,6 +84,11 @@ if (setting.asr.value === 'vosk_asr') { ...@@ -84,6 +84,11 @@ if (setting.asr.value === 'vosk_asr') {
async function changeOpenDevTools() { async function changeOpenDevTools() {
await window.mainApi.send('openDevTools', setting.isOpenDevTools.value) await window.mainApi.send('openDevTools', setting.isOpenDevTools.value)
} }
function clear() {
localStorage.clear()
location.reload()
}
</script> </script>
<template> <template>
<v-app-bar color="#d71b1b" density="compact" class="header"> <v-app-bar color="#d71b1b" density="compact" class="header">
...@@ -181,6 +186,27 @@ async function changeOpenDevTools() { ...@@ -181,6 +186,27 @@ async function changeOpenDevTools() {
:model-value="setting.llmUrl" :model-value="setting.llmUrl"
></v-text-field> ></v-text-field>
<v-slider
v-model="setting.llmToTTSSliceLength.value"
label="TTS 分句长度"
class="align-center"
:max="100"
:min="0"
hide-details
:step="1"
>
<template #append>
<v-text-field
v-model="setting.llmToTTSSliceLength.value"
hide-details
single-line
density="compact"
type="number"
style="width: 80px"
></v-text-field>
</template>
</v-slider>
<v-switch <v-switch
v-model="setting.isFullscreen.value" v-model="setting.isFullscreen.value"
hide-details hide-details
...@@ -202,6 +228,7 @@ async function changeOpenDevTools() { ...@@ -202,6 +228,7 @@ async function changeOpenDevTools() {
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="#d71b1b" text="清除缓存并刷新" @click="clear"></v-btn>
<v-btn text="关闭" @click="isActive.value = false"></v-btn> <v-btn text="关闭" @click="isActive.value = false"></v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
......
...@@ -252,7 +252,7 @@ async function startVoskWsAudioInput() { ...@@ -252,7 +252,7 @@ async function startVoskWsAudioInput() {
return return
} }
await initVoskWS(); await initVoskWS()
sampleRate = 8000 sampleRate = 8000
const mediaStream = await navigator.mediaDevices.getUserMedia({ const mediaStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
...@@ -369,14 +369,18 @@ async function onAsr(question: string) { ...@@ -369,14 +369,18 @@ async function onAsr(question: string) {
answer += text answer += text
isTime && console.time('sliceAnswer') isTime && console.time('sliceAnswer')
isTime = false isTime = false
sliceAnswer += text
if (/[。,?!;,.?!;]/.test(text) && sliceAnswer.length >= 20) { const textArr = text.split('');
console.timeEnd('sliceAnswer') for (let i = 0; i < textArr.length; i++) {
answerArray.push(sliceAnswer) const t = textArr[i];
runTTSTask(answerArray) sliceAnswer += t
sliceAnswer = '' if (/[。,?!;,.?!;]/.test(t) && sliceAnswer.length >= settings.llmToTTSSliceLength) {
isTime = true console.timeEnd('sliceAnswer')
answerArray.push(sliceAnswer)
runTTSTask(answerArray)
sliceAnswer = ''
isTime = true
}
} }
} catch (error) { } catch (error) {
console.log('返回答案错误 -----> ' + JSON.stringify(error)) console.log('返回答案错误 -----> ' + JSON.stringify(error))
......
...@@ -24,8 +24,7 @@ const role = useVideo.list.find((i) => i.url === url) ...@@ -24,8 +24,7 @@ const role = useVideo.list.find((i) => i.url === url)
const microphoneState = ref<'waitInput' | 'input' | 'loading' | 'disabled' | 'reply'>('waitInput') const microphoneState = ref<'waitInput' | 'input' | 'loading' | 'disabled' | 'reply'>('waitInput')
const videoElement = ref<HTMLVideoElement | null>(null) const videoElement = ref<HTMLVideoElement | null>(null)
const videoElement2 = ref<HTMLVideoElement | null>(null) const videoElement2 = ref<HTMLVideoElement | null>(null)
const videos = [videoElement, videoElement2]; const videos = [videoElement, videoElement2]
onMounted(() => { onMounted(() => {
// init(); // init();
...@@ -166,7 +165,7 @@ async function startVoskWsAudioInput() { ...@@ -166,7 +165,7 @@ async function startVoskWsAudioInput() {
} }
initVoskWS() initVoskWS()
sampleRate = 8000 sampleRate = 16000
const mediaStream = await navigator.mediaDevices.getUserMedia({ const mediaStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
echoCancellation: true, echoCancellation: true,
...@@ -184,11 +183,15 @@ async function startVoskWsAudioInput() { ...@@ -184,11 +183,15 @@ async function startVoskWsAudioInput() {
processor.connect(audioContext.destination) processor.connect(audioContext.destination)
processor.onaudioprocess = (audioDataChunk) => { processor.onaudioprocess = (audioDataChunk) => {
if (microphoneState.value === 'loading' || microphoneState.value === 'disabled' || microphoneState.value === 'reply') { if (
return; microphoneState.value === 'loading' ||
microphoneState.value === 'disabled' ||
microphoneState.value === 'reply'
) {
return
} }
postAudio(audioDataChunk); postAudio(audioDataChunk)
} }
await analyzeMicrophoneVolume(mediaStream, (val) => { await analyzeMicrophoneVolume(mediaStream, (val) => {
...@@ -255,24 +258,24 @@ function endAudioInput() { ...@@ -255,24 +258,24 @@ function endAudioInput() {
} }
const canplay = () => { const canplay = () => {
videos[1].value!.style.opacity = '1'; videos[1].value!.style.opacity = '1'
videos[0].value!.style.opacity = '0'; videos[0].value!.style.opacity = '0'
videos[0].value!.pause(); videos[0].value!.pause()
videos[1].value!.play(); videos[1].value!.play()
videos[1].value!.removeEventListener('canplay', canplay); videos[1].value!.removeEventListener('canplay', canplay)
videos.unshift(videos.pop()!); videos.unshift(videos.pop()!)
} }
function loadVideo(url: string) { function loadVideo(url: string) {
videos[1].value!.src = url videos[1].value!.src = url
videos[1].value!.style.opacity = '0'; videos[1].value!.style.opacity = '0'
videos[1].value!.addEventListener('canplay', canplay); videos[1].value!.addEventListener('canplay', canplay)
} }
async function onAsr(question: string) { async function onAsr(question: string) {
console.log('---------------->', question) console.log('---------------->', question)
if (!role) return; if (!role) return
microphoneState.value = 'loading'; microphoneState.value = 'loading'
question = question.replace(/\s/g, '') question = question.replace(/\s/g, '')
for (let i = 0; i < role.qa.length; i++) { for (let i = 0; i < role.qa.length; i++) {
...@@ -280,13 +283,13 @@ async function onAsr(question: string) { ...@@ -280,13 +283,13 @@ async function onAsr(question: string) {
console.log(question + ' : ' + q) console.log(question + ' : ' + q)
if (q.includes(question)) { if (q.includes(question)) {
loadVideo(url) loadVideo(url)
microphoneState.value = 'reply'; microphoneState.value = 'reply'
const videoEle = videos[1].value const videoEle = videos[1].value
videoEle!.loop = false videoEle!.loop = false
videoEle!.muted = false videoEle!.muted = false
videoEle!.onended = () => { videoEle!.onended = () => {
videoEle!.onended = null; videoEle!.onended = null
microphoneState.value = 'input'; microphoneState.value = 'input'
// 是否需要初始化 // 是否需要初始化
} }
return return
...@@ -324,18 +327,23 @@ async function onAsr(question: string) { ...@@ -324,18 +327,23 @@ async function onAsr(question: string) {
answer += text answer += text
isTime && console.time('sliceAnswer') isTime && console.time('sliceAnswer')
isTime = false isTime = false
sliceAnswer += text
if (/[。,?!;,.?!;]/.test(text) && sliceAnswer.length >= 10) { const textArr = text.split('');
console.timeEnd('sliceAnswer') for (let i = 0; i < textArr.length; i++) {
answerArray.push(sliceAnswer) const t = textArr[i];
runTTSTask(answerArray) sliceAnswer += t
sliceAnswer = '' if (/[。,?!;,.?!;]/.test(t) && sliceAnswer.length >= settings.llmToTTSSliceLength) {
isTime = true console.timeEnd('sliceAnswer')
answerArray.push(sliceAnswer)
runTTSTask(answerArray)
sliceAnswer = ''
isTime = true
}
} }
} catch (error) { } catch (error) {
console.log('返回答案错误 -----> ' + JSON.stringify(error)) console.log('返回答案错误 -----> ' + JSON.stringify(error))
microphoneState.value = 'input'; microphoneState.value = 'input'
} }
} }
...@@ -360,11 +368,11 @@ async function runTTSTask(tasks: string[]) { ...@@ -360,11 +368,11 @@ async function runTTSTask(tasks: string[]) {
while (tasks.length) { while (tasks.length) {
const task = tasks.shift() const task = tasks.shift()
if (!task) break if (!task) break
if (task.length < 1) continue if (task.trim().length < 1) continue
console.time(task + ' TTS: ') console.time(task + ' TTS: ')
microphoneState.value = 'loading'; microphoneState.value = 'loading'
const res = await localTTS({ const res = await localTTS({
url: settings.ttsHost, url: settings.ttsHost,
text: task, text: task,
...@@ -395,21 +403,21 @@ async function runAudioPlay() { ...@@ -395,21 +403,21 @@ async function runAudioPlay() {
const audio = ttsAudios.shift() const audio = ttsAudios.shift()
if (!audio) { if (!audio) {
isPlayRunning = false; isPlayRunning = false
videos[0].value!.pause(); videos[0].value!.pause()
!isTTSRunning && (microphoneState.value = 'input'); !isTTSRunning && (microphoneState.value = 'input')
return return
} }
audio.onended = () => { audio.onended = () => {
isPlayRunning = false isPlayRunning = false
runAudioPlay() runAudioPlay()
} }
await audio.play(); await audio.play()
loadVideo(new URL('/libai/10.mp4', import.meta.url).href) loadVideo(new URL('/libai/10.mp4', import.meta.url).href)
videos[1].value!.loop = true videos[1].value!.loop = true
videos[1].value!.muted = true videos[1].value!.muted = true
microphoneState.value = 'reply'; microphoneState.value = 'reply'
} }
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
...@@ -425,7 +433,6 @@ async function xfTTS(text: string) { ...@@ -425,7 +433,6 @@ async function xfTTS(text: string) {
}) })
console.log('----------------> tts:', res) console.log('----------------> tts:', res)
} }
</script> </script>
<template> <template>
...@@ -444,7 +451,11 @@ async function xfTTS(text: string) { ...@@ -444,7 +451,11 @@ async function xfTTS(text: string) {
color="#fff" color="#fff"
variant="elevated" variant="elevated"
size="x-large" size="x-large"
:disabled="microphoneState === 'loading' || microphoneState === 'disabled' || microphoneState === 'reply'" :disabled="
microphoneState === 'loading' ||
microphoneState === 'disabled' ||
microphoneState === 'reply'
"
@pointerdown="startVoskWsAudioInput" @pointerdown="startVoskWsAudioInput"
> >
<v-icon v-if="microphoneState === 'waitInput'" icon="mdi-microphone"></v-icon> <v-icon v-if="microphoneState === 'waitInput'" icon="mdi-microphone"></v-icon>
...@@ -512,13 +523,15 @@ async function xfTTS(text: string) { ...@@ -512,13 +523,15 @@ async function xfTTS(text: string) {
border-radius: 36%; border-radius: 36%;
} }
.video-ele, .video-ele2 { .video-ele,
.video-ele2 {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0; opacity: 0;
} }
.video-ele.active, .video-ele2.active { .video-ele.active,
.video-ele2.active {
opacity: 1; opacity: 1;
} }
......
...@@ -25,6 +25,7 @@ export type ISettings = { ...@@ -25,6 +25,7 @@ export type ISettings = {
isFullscreen: 'yes' | 'no' isFullscreen: 'yes' | 'no'
isOpenDevTools: boolean isOpenDevTools: boolean
llmUrl: string llmUrl: string
llmToTTSSliceLength: number
voskWsLUrl: string voskWsLUrl: string
} }
...@@ -58,7 +59,8 @@ const useSettingsStore = defineStore('settings', { ...@@ -58,7 +59,8 @@ const useSettingsStore = defineStore('settings', {
selectSource: '', selectSource: '',
isFullscreen: 'no', isFullscreen: 'no',
isOpenDevTools: false, isOpenDevTools: false,
llmUrl: 'ws://127.0.0.1:9001/api/v1/stream', llmUrl: 'ws://127.0.0.1:9899/api/v1/stream',
llmToTTSSliceLength: 20,
voskWsLUrl: 'ws://127.0.0.1:2700' voskWsLUrl: 'ws://127.0.0.1:2700'
}) as ISettings, }) as ISettings,
getters: {}, getters: {},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment