|
<script setup lang="ts"> |
|
|
|
import router from "@/router.ts"; |
|
import { useSettingsStore } from "@/stores/config.ts"; |
|
import { onMounted, onUnmounted, ref, reactive, computed, watch, h } from "vue"; |
|
import { Modal } from 'ant-design-vue'; |
|
import { SoundTwoTone, SoundOutlined } from "@ant-design/icons-vue"; |
|
import axios from "axios"; |
|
import PromptText from "./Components/PromptText.vue"; |
|
|
|
const base_url = axios.defaults.baseURL |
|
|
|
const settingsStore = useSettingsStore() |
|
|
|
import setting from "@/assets/setting.png" |
|
|
|
|
|
onMounted(async () => { |
|
await fetchASRLanguages(); |
|
await fetchTTSRoles(); |
|
}); |
|
|
|
const chatAction = async () => { |
|
const state = await startAudioChat(); |
|
if (!state) { |
|
console.error('Failed to start audio chat system service'); |
|
|
|
Modal.error({ |
|
title: 'Error', |
|
content: 'Failed to start audio chat system service', |
|
}); |
|
return; |
|
} |
|
router.replace('/home') |
|
} |
|
const chatLoading = ref<boolean>(false); |
|
|
|
const startAudioChat = async () => { |
|
try { |
|
chatLoading.value = true; |
|
const response = await fetch(`${base_url}/system/start`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
enable_echo_cancellation: echoCancel.value |
|
}) |
|
}); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
const data = await response.json(); |
|
console.log('ASR Instance started successfully:', data); |
|
return true; |
|
} catch (error) { |
|
console.error('Error starting ASR instance:', error); |
|
return false; |
|
} finally { |
|
chatLoading.value = false; |
|
} |
|
} |
|
|
|
|
|
const voiceModelOpen = ref<boolean>(false); |
|
const modalLoading = ref<boolean>(false); |
|
|
|
const handleVoiceModalCancel = () => { |
|
voiceModelOpen.value = false; |
|
role.value = settingsStore.$state.role; |
|
language.value = settingsStore.$state.language; |
|
}; |
|
|
|
const handleVoiceModalSubmit = async () => { |
|
console.log('Selected Language:', language.value); |
|
console.log('Selected Role:', role.value); |
|
console.log('Echo Cancel:', echoCancel.value); |
|
settingsStore.$state.language = language.value; |
|
settingsStore.$state.role = role.value || ''; |
|
settingsStore.$state.echoCancel = echoCancel.value; |
|
|
|
await pushConfig(settingsStore.$state.role); |
|
}; |
|
|
|
const pushConfig = async (model_id: string) => { |
|
try { |
|
modalLoading.value = true; |
|
const response = await fetch(`${base_url}/tts/models/load`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
"model_id": model_id, |
|
}) |
|
}); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
const data = await response.json(); |
|
console.log('Config pushed successfully:', data); |
|
|
|
const response2 = await fetch(`${base_url}/asr/instance/create`, { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
"language": language.value, |
|
}) |
|
}); |
|
if (!response2.ok) { |
|
throw new Error(`HTTP error! status: ${response2.status}`); |
|
} |
|
const data2 = await response2.json(); |
|
console.log('ASR Language set successfully:', data2); |
|
|
|
} catch (err) { |
|
console.error('Error pushing config:', err); |
|
Modal.error({ |
|
title: 'Error', |
|
content: "Error config: " + JSON.stringify(err), |
|
}); |
|
} finally { |
|
modalLoading.value = false; |
|
voiceModelOpen.value = false; |
|
} |
|
|
|
console.log('Selected Language:', language.value); |
|
console.log('Selected Role:', role.value); |
|
} |
|
|
|
|
|
const language = ref<string>(settingsStore.$state.language || 'zh'); |
|
const languages = reactive([]); |
|
const languageOptions = { |
|
'zh': 'Chinese', |
|
'en': 'English', |
|
'auto': 'Auto', |
|
}; |
|
const role = ref<string>(settingsStore.$state.role || ''); |
|
const roles = reactive([]) |
|
const echoCancel = ref<boolean>(settingsStore.$state.echoCancel ?? true); |
|
|
|
const radioStyle = reactive({ |
|
display: 'flex', |
|
height: '40px', |
|
lineHeight: '40px', |
|
fontSize: '16px', |
|
marginBottom: '8px', |
|
}); |
|
|
|
const filteredRoles = computed(() => { |
|
const is_chinese = language.value == 'zh'; |
|
return roles.filter(ro => ro['is_chinese_voice'] == is_chinese); |
|
}); |
|
|
|
watch( |
|
() => language.value, |
|
(newLang) => { |
|
|
|
if (filteredRoles.value.length > 0) { |
|
const current_role_id = settingsStore.$state.role; |
|
const current_role = filteredRoles.value.find(ro => ro['id'] == current_role_id); |
|
if (current_role) { |
|
role.value = current_role_id; |
|
} else { |
|
role.value = filteredRoles.value[0]['id']; |
|
} |
|
} else { |
|
role.value = ""; |
|
} |
|
} |
|
); |
|
|
|
|
|
const fetchTTSRoles = async () => { |
|
try { |
|
const response = await fetch(`${base_url}/tts/models`); |
|
const data = await response.json() |
|
if (data && data.models) { |
|
|
|
roles.splice(0, data.length, ...data.models) |
|
console.log('Fetched TTS Roles:', roles); |
|
|
|
if (data.current_model_id) { |
|
role.value = data.current_model_id; |
|
} |
|
} |
|
} catch (error) { |
|
console.error('Error fetching TTS roles:', error); |
|
} |
|
}; |
|
|
|
const fetchASRLanguages = async () => { |
|
try { |
|
const response = await fetch(`${base_url}/asr/languages`); |
|
const data = await response.json(); |
|
if (data && data.languages) { |
|
|
|
languages.splice(0, languages.length, ...data.languages); |
|
console.log('Fetched ASR Languages:', data.languages); |
|
|
|
if (data.current_asr_language) { |
|
language.value = data.current_asr_language; |
|
} |
|
} |
|
} catch (error) { |
|
console.error('Error fetching ASR languages:', error); |
|
} |
|
}; |
|
|
|
const togglePopover = (item: string) => { |
|
popoverVisible.value = !popoverVisible.value; |
|
if (item == 'voice') { |
|
voiceModelOpen.value = true; |
|
} else if (item == 'prompt') { |
|
promptModelOpen.value = true; |
|
} |
|
}; |
|
|
|
const popoverVisible = ref<boolean>(false); |
|
const promptModelOpen = ref<boolean>(false); |
|
|
|
|
|
const currentPlayingId = ref<string | null>(null); |
|
const currentAudio = ref<HTMLAudioElement | null>(null); |
|
|
|
|
|
const playRefAudio = async (id: string, e: Event) => { |
|
console.log('Playing reference audio for role:', id); |
|
|
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
|
|
try { |
|
|
|
if (currentPlayingId.value === id && currentAudio.value) { |
|
currentAudio.value.pause(); |
|
currentAudio.value = null; |
|
currentPlayingId.value = null; |
|
console.log('Audio stopped'); |
|
return; |
|
} |
|
|
|
|
|
if (currentAudio.value) { |
|
currentAudio.value.pause(); |
|
currentAudio.value = null; |
|
} |
|
|
|
|
|
const audio = new Audio(`${base_url}/tts/models/${id}/reference-audio`); |
|
|
|
|
|
audio.addEventListener('ended', () => { |
|
currentPlayingId.value = null; |
|
currentAudio.value = null; |
|
}); |
|
|
|
audio.addEventListener('error', (error) => { |
|
console.error('Audio playback error:', error); |
|
currentPlayingId.value = null; |
|
currentAudio.value = null; |
|
Modal.error({ |
|
title: 'Error', |
|
content: 'Failed to play reference audio', |
|
}); |
|
}); |
|
|
|
|
|
await audio.play(); |
|
currentPlayingId.value = id; |
|
currentAudio.value = audio; |
|
console.log('Audio played successfully'); |
|
|
|
} catch (error) { |
|
console.error('Error playing audio:', error); |
|
currentPlayingId.value = null; |
|
currentAudio.value = null; |
|
Modal.error({ |
|
title: 'Error', |
|
content: 'Failed to play reference audio', |
|
}); |
|
} |
|
}; |
|
|
|
|
|
onUnmounted(() => { |
|
if (currentAudio.value) { |
|
currentAudio.value.pause(); |
|
currentAudio.value = null; |
|
} |
|
currentPlayingId.value = null; |
|
}); |
|
|
|
|
|
const isPlaying = (id: string) => { |
|
return currentPlayingId.value === id; |
|
}; |
|
|
|
</script> |
|
|
|
<template> |
|
<div class="welcome-wrapper"> |
|
<div class="content"> |
|
<div class="inner-content"> |
|
<div class="text-box"> |
|
<div class="title"> |
|
欢迎使用 |
|
</div> |
|
<div class="sub-title"> |
|
点击下方按钮开始对话 |
|
</div> |
|
</div> |
|
<div class="btn-box"> |
|
<a-button @click="chatAction" block :loading="chatLoading" type="primary" size="large"> |
|
<span>开始对话</span> |
|
</a-button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="actions"> |
|
|
|
|
|
<a-button v-if="false" type="text" @click="voiceModelOpen = true" |
|
style="width:44px; height: 44px; margin-right:24px;margin-bottom: 24px;"> |
|
<template #icon> |
|
<img :src="setting" width="28" height="28" alt="settings" /> |
|
</template> |
|
</a-button> |
|
<a-popover v-if="true" v-model:open="popoverVisible" trigger="click" ok-text="Yes" cancel-text="No" placement="bottomRight"> |
|
<template #content> |
|
<div class="custom-popover-list"> |
|
<div class="custom-popover-item" @click="togglePopover('voice')"> |
|
选择音色</div> |
|
<div class="custom-popover-item" @click="togglePopover('prompt')">Prompt调试</div> |
|
</div> |
|
</template> |
|
<img :src="setting" alt="item actions" style="width: 28px; height: 28px; margin-right:24px;margin-top: 16px;"> |
|
</a-popover> |
|
</div> |
|
|
|
<a-modal v-model:open="voiceModelOpen" :title="null" :mask-closable="false" :closable="false" centered> |
|
<template #footer> |
|
<a-button key="back" @click="handleVoiceModalCancel">Cancel</a-button> |
|
<a-button key="submit" type="primary" :loading="modalLoading" @click="handleVoiceModalSubmit">Submit</a-button> |
|
</template> |
|
<div class="languages"> |
|
<div class="echo-cancel-item"> |
|
<div style="display: flex; justify-content: space-between; align-items: center;"> |
|
<p style="margin: 0;">Enable Echo Cancellation:</p> |
|
<a-switch v-model:checked="echoCancel" /> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="languages"> |
|
<div class="language-item"> |
|
<p>Select Language:</p> |
|
<a-select v-model:value="language" style="width: 100%;"> |
|
<a-select-option v-for="lan in languages" :value="lan" :key="lan"> |
|
{{ languageOptions[lan] }} |
|
</a-select-option> |
|
</a-select> |
|
</div> |
|
</div> |
|
<div class="languages"> |
|
<div class="role-item"> |
|
<p>Select voice Role:</p> |
|
<a-radio-group size="large" v-model:value="role"> |
|
<a-radio v-for="r in filteredRoles" :style="radioStyle" :value="r['id']" :key="r['id']"> |
|
<div style="display: flex; justify-content: space-between; align-items: center; width:450px;"> |
|
{{ r['character_name'] }} |
|
<a-button |
|
:key="r['id']" |
|
type="text" |
|
@click="playRefAudio(r['id'], $event)" |
|
class="audio-play-btn" |
|
:class="{ 'playing': isPlaying(r['id']) }" |
|
> |
|
<SoundTwoTone |
|
v-if="isPlaying(r['id'])" |
|
style="font-size: 18px; color: #52c41a;" |
|
class="playing-icon" |
|
/> |
|
<SoundOutlined |
|
v-else |
|
style="font-size: 18px; color: #1890ff;" |
|
/> |
|
</a-button> |
|
</div> |
|
|
|
</a-radio> |
|
</a-radio-group> |
|
|
|
</div> |
|
</div> |
|
</a-modal> |
|
|
|
<PromptText v-model:open="promptModelOpen" /> |
|
</div> |
|
</template> |
|
|
|
<style lang="scss" scoped> |
|
|
|
.languages { |
|
margin-top: 24px; |
|
margin-bottom: 24px; |
|
|
|
p { |
|
font-size: 16px; |
|
font-weight: 500; |
|
margin-bottom: 8px; |
|
} |
|
} |
|
|
|
.audio-play-btn { |
|
padding: 0px 8px; |
|
padding-top:2px; |
|
border-radius: 4px; |
|
transition: all 0.2s; |
|
height: 40px; |
|
|
|
&:hover { |
|
background-color: #f0f0f0; |
|
} |
|
|
|
&.playing { |
|
background-color: #f6ffed; |
|
border-color: #1890ff; |
|
|
|
.playing-icon { |
|
animation: pulse 1.5s infinite; |
|
} |
|
} |
|
} |
|
|
|
@keyframes pulse { |
|
0% { |
|
opacity: 1; |
|
transform: scale(1); |
|
} |
|
50% { |
|
opacity: 0.7; |
|
transform: scale(1.1); |
|
} |
|
100% { |
|
opacity: 1; |
|
transform: scale(1); |
|
} |
|
} |
|
|
|
.btn-groups { |
|
margin-top: 36px; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.custom-popover-list { |
|
width: 92px; |
|
margin: 0; |
|
.custom-popover-item { |
|
font-size: 14px; |
|
line-height: 36px; |
|
font-weight: 500; |
|
color: #1e1e1e; |
|
cursor: pointer; |
|
border-radius: 4px; |
|
padding: 0 8px; |
|
margin: 0px -8px; |
|
transition: background 0.2s; |
|
} |
|
.custom-popover-item:hover, .custom-popover-item:focus { |
|
background: #e5e7eb; |
|
} |
|
} |
|
|
|
|
|
.welcome-wrapper { |
|
width: 100%; |
|
height: 100%; |
|
background-image: url('@/assets/bg.png'); |
|
background-repeat: no-repeat; |
|
background-attachment: fixed; |
|
background-size: cover; |
|
background-position: center; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: space-between; |
|
color: #fff; |
|
|
|
.content { |
|
width: 100%; |
|
height: 80vh; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-around; |
|
margin-top: 64px; |
|
|
|
.inner-content { |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
text-align: center; |
|
padding: 20px; |
|
|
|
.text-box { |
|
color: #000; |
|
margin-bottom: 36px; |
|
|
|
.title { |
|
font-size: 24px; |
|
font-weight: 600; |
|
margin-bottom: 24px; |
|
} |
|
|
|
.sub-title { |
|
font-size: 15px; |
|
margin-top: 10px; |
|
} |
|
} |
|
.btn-box { |
|
width: 224px; |
|
height: 80px; |
|
} |
|
} |
|
} |
|
|
|
.actions { |
|
width: 100%;; |
|
height: 64px; |
|
|
|
display: flex; |
|
justify-content: flex-end; |
|
} |
|
} |
|
</style> |
|
|