功能优化

This commit is contained in:
2026-02-02 23:16:38 +08:00
parent 42567c457b
commit 409e976209
5 changed files with 76 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="voice-selector"> <div class="voice-selector">
<div v-if="displayedVoices.length === 0" class="empty-voices"> <div v-if="userVoiceCards.length === 0" class="empty-voices">
还没有配音可先在"配音管理"中上传 还没有配音可先在"配音管理"中上传
</div> </div>
@@ -19,31 +19,52 @@
size="small" size="small"
:disabled="!selectedVoiceId" :disabled="!selectedVoiceId"
:loading="previewLoadingVoiceId === selectedVoiceId" :loading="previewLoadingVoiceId === selectedVoiceId"
@click="handlePreviewCurrentVoice" @click="handleSynthesize"
> >
<template #icon> <template #icon>
<SoundOutlined /> <SoundOutlined />
</template> </template>
试听 合成
</a-button> </a-button>
</div> </div>
<!-- APlayer 播放器容器 --> <!-- APlayer 播放器容器 -->
<div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div> <div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div>
<!-- 下载按钮 -->
<a-button
v-if="audioUrl"
type="link"
size="small"
@click="downloadAudio"
class="download-link"
>
下载音频
</a-button>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
import { useVoiceCopyStore } from '@/stores/voiceCopy' import { useVoiceCopyStore } from '@/stores/voiceCopy'
import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS' import { useTTS, TTS_PROVIDERS } from '@/composables/useTTS'
import APlayer from 'aplayer' import APlayer from 'aplayer'
const props = defineProps({
synthText: {
type: String,
default: ''
},
speechRate: {
type: Number,
default: 1.0
}
})
const voiceStore = useVoiceCopyStore() const voiceStore = useVoiceCopyStore()
const emit = defineEmits(['select']) const emit = defineEmits(['select'])
// APlayer 实例
let player = null let player = null
const playerContainer = ref(null) const playerContainer = ref(null)
const audioUrl = ref('') const audioUrl = ref('')
@@ -87,27 +108,21 @@ const defaultCover = `data:image/svg+xml;base64,${btoa(`
// 使用TTS Hook // 使用TTS Hook
const { const {
previewLoadingVoiceId, previewLoadingVoiceId,
playingPreviewVoiceId,
ttsText,
speechRate,
playVoiceSample, playVoiceSample,
setText, setText,
setSpeechRate, setSpeechRate
resetPreviewState
} = useTTS({ } = useTTS({
provider: TTS_PROVIDERS.SILICONFLOW provider: TTS_PROVIDERS.SILICONFLOW
}) })
// 当前选中的音色ID
const selectedVoiceId = ref('') const selectedVoiceId = ref('')
// 从store数据构建音色列表
const userVoiceCards = computed(() => const userVoiceCards = computed(() =>
(voiceStore.profiles || []).map(profile => ({ (voiceStore.profiles || []).map(profile => ({
id: `user-${profile.id}`, id: `user-${profile.id}`,
rawId: profile.id, rawId: profile.id,
name: profile.name || '未命名', name: profile.name || '未命名',
category:'', category: '',
gender: profile.gender || 'female', gender: profile.gender || 'female',
description: profile.note || '我的配音', description: profile.note || '我的配音',
fileUrl: profile.fileUrl, fileUrl: profile.fileUrl,
@@ -117,18 +132,14 @@ const userVoiceCards = computed(() =>
})) }))
) )
const displayedVoices = computed(() => userVoiceCards.value)
// 转换为下拉框选项格式
const voiceOptions = computed(() => const voiceOptions = computed(() =>
displayedVoices.value.map(voice => ({ userVoiceCards.value.map(voice => ({
value: voice.id, value: voice.id,
label: voice.name, label: voice.name,
data: voice // 保存完整数据 data: voice
})) }))
) )
// 音色选择变化处理
const handleVoiceChange = (value, option) => { const handleVoiceChange = (value, option) => {
const voice = option.data const voice = option.data
selectedVoiceId.value = value selectedVoiceId.value = value
@@ -136,26 +147,33 @@ const handleVoiceChange = (value, option) => {
emit('select', voice) emit('select', voice)
} }
// 试听当前选中的音色 const handleSynthesize = () => {
const handlePreviewCurrentVoice = () => {
if (!selectedVoiceId.value) return if (!selectedVoiceId.value) return
const voice = displayedVoices.value.find(v => v.id === selectedVoiceId.value) const voice = userVoiceCards.value.find(v => v.id === selectedVoiceId.value)
if (!voice) return if (!voice) return
currentVoiceName.value = voice.name currentVoiceName.value = voice.name
handlePlayVoiceSample(voice) handlePlayVoiceSample(voice)
} }
// 监听 prop 变化,更新 TTS 参数
watch(() => props.synthText, (newText) => {
setText(newText || '')
}, { immediate: true })
watch(() => props.speechRate, (newRate) => {
setSpeechRate(newRate)
}, { immediate: true })
/** /**
* 处理音色试听 * 处理音色
*/ */
const handlePlayVoiceSample = (voice) => { const handlePlayVoiceSample = (voice) => {
currentVoiceName.value = voice.name currentVoiceName.value = voice.name
playVoiceSample( playVoiceSample(
voice, voice,
(data) => { (data) => {
// 提取音频 URL
const url = data.audioUrl || data.objectUrl const url = data.audioUrl || data.objectUrl
if (!url) { if (!url) {
console.error('无效的音频数据格式', data) console.error('无效的音频数据格式', data)
@@ -186,14 +204,11 @@ const initPlayer = (url) => {
preload: 'auto', preload: 'auto',
volume: 0.7, volume: 0.7,
audio: [{ audio: [{
name: currentVoiceName.value || '语音试听', name: currentVoiceName.value || '语音合成',
artist: '试听', artist: '合成',
url: url, url: url,
cover: defaultCover cover: defaultCover
}], }]
options: {
showDownload: true
}
}) })
player.on('ended', () => { player.on('ended', () => {
@@ -209,6 +224,20 @@ const initPlayer = (url) => {
}) })
} }
/**
* 下载音频
*/
const downloadAudio = () => {
if (!audioUrl.value) return
const link = document.createElement('a')
link.href = audioUrl.value
link.download = `${currentVoiceName.value || '语音合成'}.mp3`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/** /**
* 销毁播放器 * 销毁播放器
*/ */
@@ -227,26 +256,7 @@ const destroyPlayer = () => {
} }
} }
/** defineExpose({})
* 设置要试听的文本(供父组件调用)
* @param {string} text 要试听的文本
*/
const setPreviewText = (text) => {
setText(text)
}
/**
* 设置语速(供父组件调用)
* @param {number} rate 语速倍率
*/
const setPreviewSpeechRate = (rate) => {
setSpeechRate(rate)
}
defineExpose({
setPreviewText,
setPreviewSpeechRate
})
onMounted(async () => { onMounted(async () => {
await voiceStore.refresh() await voiceStore.refresh()
@@ -294,4 +304,11 @@ onBeforeUnmount(() => {
.aplayer-container { .aplayer-container {
margin-top: 12px; margin-top: 12px;
} }
/* 下载链接样式 */
.download-link {
margin-top: 8px;
padding: 0;
font-size: 12px;
}
</style> </style>

View File

@@ -24,7 +24,11 @@
<!-- 音色选择 --> <!-- 音色选择 -->
<div class="section"> <div class="section">
<h3>音色</h3> <h3>音色</h3>
<VoiceSelector ref="voiceSelectorRef" @select="handleVoiceSelect" /> <VoiceSelector
:synth-text="ttsText"
:speech-rate="speechRate"
@select="handleVoiceSelect"
/>
</div> </div>
<!-- TTS 控制 --> <!-- TTS 控制 -->
@@ -300,8 +304,6 @@ import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
import { useIdentifyFaceController } from './hooks/useIdentifyFaceController' import { useIdentifyFaceController } from './hooks/useIdentifyFaceController'
const voiceStore = useVoiceCopyStore() const voiceStore = useVoiceCopyStore()
const voiceSelectorRef: any = ref(null)
const dragOver = ref(false) const dragOver = ref(false)
// ==================== 初始化 Controller ==================== // ==================== 初始化 Controller ====================
@@ -355,12 +357,6 @@ const {
onMounted(async () => { onMounted(async () => {
await voiceStore.refresh() await voiceStore.refresh()
// 设置VoiceSelector的试听文本和语速
if (voiceSelectorRef.value) {
voiceSelectorRef.value.setPreviewText(ttsText.value)
voiceSelectorRef.value.setPreviewSpeechRate(speechRate.value)
}
}) })
</script> </script>

View File

@@ -66,7 +66,7 @@ public class SiliconFlowProvider implements VoiceCloneProvider {
.header("Authorization", "Bearer " + config.getApiKey()) .header("Authorization", "Bearer " + config.getApiKey())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(requestBody) .body(requestBody)
.timeout((int) config.getConnectTimeout().toMillis()) .timeout((int) config.getReadTimeout().toMillis())
.execute(); .execute();
String responseBody = response.body(); String responseBody = response.body();

View File

@@ -64,9 +64,9 @@ public class SiliconFlowProviderConfig extends VoiceProviderProperties.ProviderC
private Duration connectTimeout = Duration.ofSeconds(10); private Duration connectTimeout = Duration.ofSeconds(10);
/** /**
* 读取超时时间(3分钟,提升语音合成成功率) * 读取超时时间(5分钟,提升语音合成成功率)
*/ */
private Duration readTimeout = Duration.ofSeconds(180); private Duration readTimeout = Duration.ofSeconds(300);
/** /**
* 检查是否可用(有 API Key 即可用) * 检查是否可用(有 API Key 即可用)

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.tik.voice.service; package cn.iocoder.yudao.module.tik.voice.service;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray; import cn.hutool.json.JSONArray;