|
@@ -14,11 +14,15 @@
|
|
|
<!-- 语音角色选择 -->
|
|
|
<div class="config-item">
|
|
|
<label>语音角色</label>
|
|
|
- <select v-model="voiceConfig.role" @change="handleConfigChange">
|
|
|
- <option value="female">女声</option>
|
|
|
- <option value="male">男声</option>
|
|
|
- <option value="child">童声</option>
|
|
|
- </select>
|
|
|
+ <div class="voice-select-container">
|
|
|
+ <select v-model="voiceConfig.role" @change="handleConfigChange">
|
|
|
+ <option v-for="voice in voiceList"
|
|
|
+ :key="voice.id"
|
|
|
+ :value="voice.id">
|
|
|
+ {{ voice.name+'_'+voice.gender }} ({{ getLanguageName(voice.language) }})
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 语速调节 -->
|
|
@@ -38,31 +42,101 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 语言选择 -->
|
|
|
- <div class="config-item">
|
|
|
- <label>语言</label>
|
|
|
+ <div class="config-item" style="display: flex;justify-content: space-around;">
|
|
|
+ <button
|
|
|
+ class="preview-button"
|
|
|
+ @click="handlePreviewVoice"
|
|
|
+ :disabled="isPreviewPlaying">
|
|
|
+ {{ isPreviewPlaying ? '播放中...' : '试听' }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="switch-button"
|
|
|
+ @click="handleSwitchVoice"
|
|
|
+ :disabled="switchingVoice">
|
|
|
+ {{ switchingVoice ? '切换中...' : '切换语音' }}
|
|
|
+ </button>
|
|
|
+ <!-- <label>语言</label>
|
|
|
<select v-model="voiceConfig.language" @change="handleConfigChange">
|
|
|
<option value="zh">中文</option>
|
|
|
<option value="en">英文</option>
|
|
|
<option value="ja">日语</option>
|
|
|
- </select>
|
|
|
+ </select> -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive } from 'vue';
|
|
|
+import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
|
|
import { useStore } from 'vuex';
|
|
|
+import axios from 'axios';
|
|
|
|
|
|
const store = useStore();
|
|
|
const isPanelOpen = ref(false);
|
|
|
+const voiceList = ref([]);
|
|
|
|
|
|
const voiceConfig = reactive({
|
|
|
- role: 'female',
|
|
|
+ role: '',
|
|
|
speed: 1,
|
|
|
language: 'zh'
|
|
|
});
|
|
|
|
|
|
+const switchingVoice = ref(false);
|
|
|
+const isPreviewPlaying = ref(false);
|
|
|
+const audioElement = ref(null);
|
|
|
+
|
|
|
+// 获取当前语音配置
|
|
|
+const fetchCurrentVoiceConfig = async () => {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/chatbot/current_voice_config`);
|
|
|
+ const currentConfig = response.data.data;
|
|
|
+ console.log('Current config:', currentConfig); // 添加日志
|
|
|
+ if (currentConfig && currentConfig.voice_name && voiceList.value.length > 0) {
|
|
|
+ // 在 voiceList 中查找匹配的语音
|
|
|
+ const matchedVoice = voiceList.value.find(voice => voice.name === currentConfig.voice_name);
|
|
|
+ console.log('Matched voice:', matchedVoice); // 添加日志
|
|
|
+ if (matchedVoice) {
|
|
|
+ voiceConfig.role = matchedVoice.id;
|
|
|
+ voiceConfig.speed = currentConfig.speed || 1;
|
|
|
+ voiceConfig.language = matchedVoice.language;
|
|
|
+ // 触发配置更新
|
|
|
+ handleConfigChange();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取当前语音配置失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取语音列表
|
|
|
+const fetchVoiceList = async () => {
|
|
|
+ try {
|
|
|
+ const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/chatbot/list_voices`);
|
|
|
+ const voices = response.data.data.voices;
|
|
|
+ voiceList.value = Object.values(voices).flat();
|
|
|
+ console.log('Voice list:', voiceList.value); // 添加日志
|
|
|
+ // 获取语音列表后再获取当前配置
|
|
|
+ await fetchCurrentVoiceConfig();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取语音列表失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取语言名称
|
|
|
+const getLanguageName = (language) => {
|
|
|
+ const languageMap = {
|
|
|
+ 'zh-cn': '中文',
|
|
|
+ 'en-us': '英文',
|
|
|
+ 'ja-jp': '日语',
|
|
|
+ 'ko-kr': '韩语'
|
|
|
+ };
|
|
|
+ return languageMap[language] || language;
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchVoiceList();
|
|
|
+});
|
|
|
+
|
|
|
const togglePanel = () => {
|
|
|
isPanelOpen.value = !isPanelOpen.value;
|
|
|
};
|
|
@@ -70,6 +144,87 @@ const togglePanel = () => {
|
|
|
const handleConfigChange = () => {
|
|
|
store.commit('updateVoiceConfig', { ...voiceConfig });
|
|
|
};
|
|
|
+
|
|
|
+// 添加切换语音的方法
|
|
|
+const handleSwitchVoice = async () => {
|
|
|
+ if (!voiceConfig.role) return;
|
|
|
+
|
|
|
+ switchingVoice.value = true;
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('voice_id', voiceConfig.role);
|
|
|
+
|
|
|
+ await axios.post(
|
|
|
+ `${import.meta.env.VITE_BASE_AI_API}/switch_voice_config`,
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 切换成功后可以添加提示
|
|
|
+ /* alert('语音切换成功'); */
|
|
|
+ } catch (error) {
|
|
|
+ console.error('切换语音失败:', error);
|
|
|
+ alert('语音切换失败');
|
|
|
+ } finally {
|
|
|
+ switchingVoice.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 添加试听功能
|
|
|
+const handlePreviewVoice = async () => {
|
|
|
+ if (!voiceConfig.role || isPreviewPlaying.value) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ isPreviewPlaying.value = true;
|
|
|
+
|
|
|
+ // 构建预览URL
|
|
|
+ const previewUrl = `${import.meta.env.VITE_BASE_AI_API}/get_voice_preview?voice_id=${voiceConfig.role}&text=你好&style=chat`;
|
|
|
+
|
|
|
+ // 如果已存在音频元素,先停止并移除
|
|
|
+ if (audioElement.value) {
|
|
|
+ audioElement.value.pause();
|
|
|
+ audioElement.value = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的音频元素
|
|
|
+ const audio = new Audio(previewUrl);
|
|
|
+ audioElement.value = audio;
|
|
|
+
|
|
|
+ // 监听播放结束事件
|
|
|
+ audio.onended = () => {
|
|
|
+ isPreviewPlaying.value = false;
|
|
|
+ audioElement.value = null;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 监听错误事件
|
|
|
+ audio.onerror = () => {
|
|
|
+ console.error('音频播放失败');
|
|
|
+ isPreviewPlaying.value = false;
|
|
|
+ audioElement.value = null;
|
|
|
+ alert('试听失败');
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始播放
|
|
|
+ await audio.play();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('试听失败:', error);
|
|
|
+ isPreviewPlaying.value = false;
|
|
|
+ alert('试听失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 组件卸载时清理音频
|
|
|
+onUnmounted(() => {
|
|
|
+ if (audioElement.value) {
|
|
|
+ audioElement.value.pause();
|
|
|
+ audioElement.value = null;
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
@@ -162,4 +317,72 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
min-width: 36px;
|
|
|
}
|
|
|
+
|
|
|
+.voice-select-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.switch-button {
|
|
|
+ padding: 4px 15px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.switch-button:hover {
|
|
|
+ color: #1677ff;
|
|
|
+ border-color: #1677ff;
|
|
|
+}
|
|
|
+
|
|
|
+.switch-button:disabled {
|
|
|
+ color: rgba(0, 0, 0, 0.25);
|
|
|
+ border-color: #d9d9d9;
|
|
|
+ background: #f5f5f5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+select {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0; /* 防止select溢出 */
|
|
|
+}
|
|
|
+
|
|
|
+.preview-button {
|
|
|
+ padding: 4px 15px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ background: #fff;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-button:hover {
|
|
|
+ color: #1677ff;
|
|
|
+ border-color: #1677ff;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-button:disabled {
|
|
|
+ color: rgba(0, 0, 0, 0.25);
|
|
|
+ border-color: #d9d9d9;
|
|
|
+ background: #f5f5f5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.voice-select-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+select {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
</style>
|