功能优化
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 即可用)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user