feat: 优化
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"ai": "^6.0.39",
|
"ai": "^6.0.39",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
|
"aplayer": "^1.10.1",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"path-to-regexp": "^6.3.0",
|
"path-to-regexp": "^6.3.0",
|
||||||
|
|||||||
@@ -27,19 +27,32 @@
|
|||||||
试听
|
试听
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- APlayer 播放器容器 -->
|
||||||
|
<div v-if="audioUrl" ref="playerContainer" class="aplayer-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick } 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'
|
||||||
|
|
||||||
const voiceStore = useVoiceCopyStore()
|
const voiceStore = useVoiceCopyStore()
|
||||||
|
|
||||||
const emit = defineEmits(['select'])
|
const emit = defineEmits(['select'])
|
||||||
|
|
||||||
// 使用TTS Hook,默认使用Qwen供应商
|
// APlayer 实例
|
||||||
|
let player = null
|
||||||
|
const playerContainer = ref(null)
|
||||||
|
const audioUrl = ref('')
|
||||||
|
const currentVoiceName = ref('')
|
||||||
|
|
||||||
|
// 默认封面图片(Base64 SVG)
|
||||||
|
const defaultCover = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMzY4MmY2IiBmaWxsLW9wYWNpdHk9IjAuMSIvPjxwL3N2Zz4='
|
||||||
|
|
||||||
|
// 使用TTS Hook
|
||||||
const {
|
const {
|
||||||
previewLoadingVoiceId,
|
previewLoadingVoiceId,
|
||||||
playingPreviewVoiceId,
|
playingPreviewVoiceId,
|
||||||
@@ -87,6 +100,7 @@ const voiceOptions = computed(() =>
|
|||||||
const handleVoiceChange = (value, option) => {
|
const handleVoiceChange = (value, option) => {
|
||||||
const voice = option.data
|
const voice = option.data
|
||||||
selectedVoiceId.value = value
|
selectedVoiceId.value = value
|
||||||
|
currentVoiceName.value = voice.name
|
||||||
emit('select', voice)
|
emit('select', voice)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,27 +111,90 @@ const handlePreviewCurrentVoice = () => {
|
|||||||
const voice = displayedVoices.value.find(v => v.id === selectedVoiceId.value)
|
const voice = displayedVoices.value.find(v => v.id === selectedVoiceId.value)
|
||||||
if (!voice) return
|
if (!voice) return
|
||||||
|
|
||||||
|
currentVoiceName.value = voice.name
|
||||||
handlePlayVoiceSample(voice)
|
handlePlayVoiceSample(voice)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理音色试听
|
* 处理音色试听
|
||||||
* 使用Hook提供的playVoiceSample方法
|
|
||||||
*/
|
*/
|
||||||
const handlePlayVoiceSample = (voice) => {
|
const handlePlayVoiceSample = (voice) => {
|
||||||
|
currentVoiceName.value = voice.name
|
||||||
playVoiceSample(
|
playVoiceSample(
|
||||||
voice,
|
voice,
|
||||||
(audioData) => {
|
(data) => {
|
||||||
// 成功回调
|
// 提取音频 URL
|
||||||
console.log('音频播放成功', audioData)
|
const url = data.audioUrl || data.objectUrl
|
||||||
|
if (!url) {
|
||||||
|
console.error('无效的音频数据格式', data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initPlayer(url)
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 错误回调
|
|
||||||
console.error('音频播放失败', error)
|
console.error('音频播放失败', error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 APlayer
|
||||||
|
*/
|
||||||
|
const initPlayer = (url) => {
|
||||||
|
destroyPlayer()
|
||||||
|
audioUrl.value = url
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (!playerContainer.value) return
|
||||||
|
|
||||||
|
player = new APlayer({
|
||||||
|
container: playerContainer.value,
|
||||||
|
autoplay: true,
|
||||||
|
theme: '#3b82f6',
|
||||||
|
preload: 'auto',
|
||||||
|
volume: 0.7,
|
||||||
|
audio: [{
|
||||||
|
name: currentVoiceName.value || '语音试听',
|
||||||
|
artist: '试听',
|
||||||
|
url: url,
|
||||||
|
cover: defaultCover
|
||||||
|
}],
|
||||||
|
options: {
|
||||||
|
showDownload: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('ended', () => {
|
||||||
|
if (audioUrl.value?.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(audioUrl.value)
|
||||||
|
}
|
||||||
|
audioUrl.value = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('error', (e) => {
|
||||||
|
console.error('APlayer 播放错误:', e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁播放器
|
||||||
|
*/
|
||||||
|
const destroyPlayer = () => {
|
||||||
|
if (player) {
|
||||||
|
try {
|
||||||
|
player.destroy()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('销毁播放器失败:', e)
|
||||||
|
}
|
||||||
|
player = null
|
||||||
|
}
|
||||||
|
if (audioUrl.value) {
|
||||||
|
URL.revokeObjectURL(audioUrl.value)
|
||||||
|
audioUrl.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置要试听的文本(供父组件调用)
|
* 设置要试听的文本(供父组件调用)
|
||||||
* @param {string} text 要试听的文本
|
* @param {string} text 要试听的文本
|
||||||
@@ -142,6 +219,10 @@ defineExpose({
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await voiceStore.refresh()
|
await voiceStore.refresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyPlayer()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -176,4 +257,9 @@ onMounted(async () => {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* APlayer 容器样式 */
|
||||||
|
.aplayer-container {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
|||||||
import Antd from 'ant-design-vue'
|
import Antd from 'ant-design-vue'
|
||||||
import 'normalize.css'
|
import 'normalize.css'
|
||||||
import 'ant-design-vue/dist/reset.css'
|
import 'ant-design-vue/dist/reset.css'
|
||||||
|
import 'aplayer/dist/APlayer.min.css'
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^25.0.6",
|
"@types/node": "^25.0.6",
|
||||||
|
"aplayer": "^1.10.1",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
|
|||||||
@@ -499,9 +499,9 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
|||||||
log.info("[previewVoice][试听,voiceConfigId={}, voiceId={}, userId={}]",
|
log.info("[previewVoice][试听,voiceConfigId={}, voiceId={}, userId={}]",
|
||||||
voiceConfigId, reqVO.getVoiceId(), userId);
|
voiceConfigId, reqVO.getVoiceId(), userId);
|
||||||
|
|
||||||
String voiceId = null;
|
String voiceId;
|
||||||
String fileUrl = null;
|
String fileUrl;
|
||||||
String referenceText = null;
|
String referenceText;
|
||||||
|
|
||||||
// 1. 通过语音URL合成
|
// 1. 通过语音URL合成
|
||||||
if (StrUtil.isNotBlank(reqVO.getFileUrl()) && StrUtil.isNotBlank(reqVO.getTranscriptionText())) {
|
if (StrUtil.isNotBlank(reqVO.getFileUrl()) && StrUtil.isNotBlank(reqVO.getTranscriptionText())) {
|
||||||
@@ -510,6 +510,7 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
|||||||
? reqVO.getFileUrl()
|
? reqVO.getFileUrl()
|
||||||
: fileApi.presignGetUrl(rawFileUrl, PRESIGN_URL_EXPIRATION_SECONDS);
|
: fileApi.presignGetUrl(rawFileUrl, PRESIGN_URL_EXPIRATION_SECONDS);
|
||||||
referenceText = reqVO.getTranscriptionText();
|
referenceText = reqVO.getTranscriptionText();
|
||||||
|
voiceId = null;
|
||||||
}
|
}
|
||||||
// 2. 用户配音
|
// 2. 用户配音
|
||||||
else if (voiceConfigId != null) {
|
else if (voiceConfigId != null) {
|
||||||
@@ -518,8 +519,10 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
|||||||
throw exception(VOICE_NOT_EXISTS, "配音不存在");
|
throw exception(VOICE_NOT_EXISTS, "配音不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StrUtil.isNotBlank(voice.getVoiceId())) {
|
|
||||||
voiceId = voice.getVoiceId();
|
voiceId = voice.getVoiceId();
|
||||||
|
if (StrUtil.isNotBlank(voiceId)) {
|
||||||
|
fileUrl = null;
|
||||||
|
referenceText = null;
|
||||||
} else {
|
} else {
|
||||||
FileDO fileDO = fileMapper.selectById(voice.getFileId());
|
FileDO fileDO = fileMapper.selectById(voice.getFileId());
|
||||||
if (fileDO == null) {
|
if (fileDO == null) {
|
||||||
@@ -538,14 +541,14 @@ public class TikUserVoiceServiceImpl implements TikUserVoiceService {
|
|||||||
if (StrUtil.isBlank(voiceId)) {
|
if (StrUtil.isBlank(voiceId)) {
|
||||||
throw exception(VOICE_NOT_EXISTS, "系统配音音色ID不能为空");
|
throw exception(VOICE_NOT_EXISTS, "系统配音音色ID不能为空");
|
||||||
}
|
}
|
||||||
|
fileUrl = null;
|
||||||
|
referenceText = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一处理:使用前端传入的 inputText,否则使用默认试听文本
|
|
||||||
String finalText = StrUtil.blankToDefault(reqVO.getInputText(), getPreviewText());
|
String finalText = StrUtil.blankToDefault(reqVO.getInputText(), getPreviewText());
|
||||||
|
|
||||||
String instruction = reqVO.getInstruction();
|
String instruction = reqVO.getInstruction();
|
||||||
Float speechRate = reqVO.getSpeechRate() != null ? reqVO.getSpeechRate() : 1.0f;
|
Float speechRate = ObjectUtil.defaultIfNull(reqVO.getSpeechRate(), 1.0f);
|
||||||
Float volume = reqVO.getVolume() != null ? reqVO.getVolume() : 0f;
|
Float volume = ObjectUtil.defaultIfNull(reqVO.getVolume(), 0f);
|
||||||
String audioFormat = StrUtil.blankToDefault(reqVO.getAudioFormat(), "mp3");
|
String audioFormat = StrUtil.blankToDefault(reqVO.getAudioFormat(), "mp3");
|
||||||
|
|
||||||
// 缓存
|
// 缓存
|
||||||
|
|||||||
Reference in New Issue
Block a user