diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx b/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx index ea9317d83..0326263ca 100644 --- a/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react" import type { UnifiedVoiceItem } from "@/api/voices" +import { LoadingOutlined } from "@ant-design/icons" export interface VoiceSelectorProps { presetVoices: UnifiedVoiceItem[] @@ -7,8 +8,9 @@ export interface VoiceSelectorProps { selectedVoiceId: string loading: boolean playingVoiceId: string | null + loadingSampleVoiceId: string | null onSelect: (voiceId: string) => void - onPlaySample: (voiceId: string, previewUrl: string | null) => void + onPlaySample: (voice: UnifiedVoiceItem) => void } type VoiceTab = "preset" | "clone" @@ -25,6 +27,7 @@ export const VoiceSelector: React.FC = ({ selectedVoiceId, loading, playingVoiceId, + loadingSampleVoiceId, onSelect, onPlaySample, }) => { @@ -70,6 +73,7 @@ export const VoiceSelector: React.FC = ({ {currentList.map((voice) => { const isSelected = selectedVoiceId === voice.voice_id const isPlaying = playingVoiceId === voice.voice_id + const isLoadingSample = loadingSampleVoiceId === voice.voice_id const icon = GENDER_ICON[voice.gender] ?? "✨" return (
= ({ className="tts-voice-item-play" onClick={(e) => { e.stopPropagation() - onPlaySample(voice.voice_id, voice.preview_url ?? voice.audio_url) + onPlaySample(voice) }} - title={isPlaying ? "暂停" : "试听"} + disabled={isLoadingSample} + title={isPlaying ? "暂停" : isLoadingSample ? "合成中" : "试听"} > - {isPlaying ? "⏸" : "▶"} + {isLoadingSample ? : isPlaying ? "⏸" : "▶"}
) diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts b/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts index cc4214ecd..6d3021f55 100644 --- a/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts @@ -3,6 +3,7 @@ import { message } from "antd" import type { TtsConfig, TtsMode } from "../../../types" import { DEFAULT_TTS_CONFIG } from "../../../types" import { previewTts } from "@/api/tts" +import { getVoiceClonePreview } from "@/api/voice-clone" import { fetchVoices, type UnifiedVoiceItem } from "@/api/voices" interface UseTtsPanelOptions { @@ -167,8 +168,10 @@ export const useTtsPanel = ({ open, config, onChange, onClose }: UseTtsPanelOpti /* ── 单音色示例试听 ── */ const [playingVoiceId, setPlayingVoiceId] = useState(null) + const [loadingSampleVoiceId, setLoadingSampleVoiceId] = useState(null) const handlePlayVoiceSample = useCallback( - (voiceId: string, previewUrl: string | null) => { + async (voice: UnifiedVoiceItem) => { + const voiceId = voice.voice_id if (playingVoiceId === voiceId) { audioRef.current?.pause() audioRef.current = null @@ -180,11 +183,34 @@ export const useTtsPanel = ({ open, config, onChange, onClose }: UseTtsPanelOpti audioRef.current.onended = null audioRef.current.pause() } - if (!previewUrl) { + + let audioUrl: string | null = voice.preview_url ?? voice.audio_url + + // 克隆音色:调用试听接口实时合成 + if (voice.type === "clone" && voice.voice_clone_profile_id) { + setLoadingSampleVoiceId(voiceId) + try { + const preview = await getVoiceClonePreview(voice.voice_clone_profile_id) + audioUrl = preview.audio_url + } catch { + if (mountedRef.current) { + message.error("试听音频生成失败,请重试") + } + setLoadingSampleVoiceId(null) + return + } finally { + if (mountedRef.current) { + setLoadingSampleVoiceId(null) + } + } + } + + if (!audioUrl) { message.warning("该音色暂无示例音频") return } - const audio = new Audio(previewUrl) + + const audio = new Audio(audioUrl) audioRef.current = audio audio .play() @@ -230,6 +256,7 @@ export const useTtsPanel = ({ open, config, onChange, onClose }: UseTtsPanelOpti voicesLoading, previewLoading, playingVoiceId, + loadingSampleVoiceId, handleModeChange, handleTextChange, handleVoiceSelect, diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx b/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx index 7bcab1ed4..e3f0ab766 100644 --- a/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx @@ -26,6 +26,7 @@ const TtsPanel: React.FC = ({ open, onClose, config, onChange }) voicesLoading, previewLoading, playingVoiceId, + loadingSampleVoiceId, handleModeChange, handleTextChange, handleVoiceSelect, @@ -91,6 +92,7 @@ const TtsPanel: React.FC = ({ open, onClose, config, onChange }) selectedVoiceId={config.voice_id} loading={voicesLoading} playingVoiceId={playingVoiceId} + loadingSampleVoiceId={loadingSampleVoiceId} onSelect={handleVoiceSelect} onPlaySample={handlePlayVoiceSample} />