diff --git a/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx b/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx index 7b7c8fe74..467ab40dd 100644 --- a/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react" import { message } from "antd" import { fetchVoices } from "@/api/voices/voices" +import { previewTts } from "@/api/tts" import type { UnifiedVoiceItem } from "@/api/voices/types" import { type VoiceSource, @@ -44,6 +45,9 @@ export function PanelVoiceSelector({ const [error, setError] = useState(null) const [previewingId, setPreviewingId] = useState(null) const audioRef = useRef(null) + /** 克隆音色试听合成缓存:voiceId -> url,对齐配音库 useAudioPlayer */ + const previewCacheRef = useRef>(new Map()) + const VOICE_PREVIEW_TEXT = "你好呀,欢迎使用小虾智剪,这是我的配音效果,希望你喜欢。" /* 切换来源时重新获取音色列表 */ useEffect(() => { @@ -86,24 +90,15 @@ export function PanelVoiceSelector({ const NO_PREVIEW_TIP = "该音色暂无试听音频,请先用此音色生成一段配音后再试听" - const handlePreview = (voice: UnifiedVoiceItem) => { - const url = voice.preview_url || voice.audio_url - if (!url) { - message.warning(NO_PREVIEW_TIP) - return - } - /* 再次点击当前试听音色 → 停止 */ - if (previewingId === voice.id) { - stopPreview() - return - } + /** 用指定 URL 真实播放(抽取公共) */ + const playAudioUrl = (voiceId: string, url: string) => { if (audioRef.current) { audioRef.current.pause() audioRef.current = null } const audio = new Audio(url) audioRef.current = audio - setPreviewingId(voice.id) + setPreviewingId(voiceId) audio.onended = () => { if (audioRef.current === audio) { audioRef.current = null @@ -123,6 +118,52 @@ export function PanelVoiceSelector({ }) } + const handlePreview = async (voice: UnifiedVoiceItem) => { + /* 再次点击当前试听音色 → 停止 */ + if (previewingId === voice.id) { + stopPreview() + return + } + + /* 克隆音色:preview_url/audio_url 通常为空,需走 POST /tts/preview + * 现合成示例文案再播放,对齐配音库 useAudioPlayer 行为 */ + if (voice.type === "clone") { + const cached = previewCacheRef.current.get(voice.voice_id || voice.id) + if (cached) { + playAudioUrl(voice.id, cached) + return + } + const targetId = voice.voice_id || voice.id + setPreviewingId(voice.id) + try { + const res = await previewTts({ + text: VOICE_PREVIEW_TEXT, + voice_id: targetId, + speed: 1.0, + }) + if (!res.audio_url) { + setPreviewingId(null) + message.error("合成试听失败:未返回音频") + return + } + previewCacheRef.current.set(targetId, res.audio_url) + playAudioUrl(voice.id, res.audio_url) + } catch { + setPreviewingId(null) + // apiClient 拦截器已统一 toast + } + return + } + + /* 系统预设音色:沿用 preview_url/audio_url 直链播放 */ + const url = voice.preview_url || voice.audio_url + if (!url) { + message.warning(NO_PREVIEW_TIP) + return + } + playAudioUrl(voice.id, url) + } + const handleSpeedChange = (value: string) => { const parsed = parseFloat(value) if (Number.isNaN(parsed)) return @@ -187,9 +228,7 @@ export function PanelVoiceSelector({