fix(#1196): TTS面板克隆音色试听改用/voice-clones/{id}/preview接口 #1205

Merged
xiaoxia merged 1 commits from fix/tts-clone-voice-preview-1196 into develop 2026-08-01 16:29:54 +08:00
3 changed files with 41 additions and 7 deletions
@@ -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<VoiceSelectorProps> = ({
selectedVoiceId,
loading,
playingVoiceId,
loadingSampleVoiceId,
onSelect,
onPlaySample,
}) => {
@@ -70,6 +73,7 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
{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 (
<div
@@ -86,11 +90,12 @@ export const VoiceSelector: React.FC<VoiceSelectorProps> = ({
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 ? <LoadingOutlined /> : isPlaying ? "⏸" : "▶"}
</button>
</div>
)
@@ -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<string | null>(null)
const [loadingSampleVoiceId, setLoadingSampleVoiceId] = useState<string | null>(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,
@@ -26,6 +26,7 @@ const TtsPanel: React.FC<TtsPanelProps> = ({ open, onClose, config, onChange })
voicesLoading,
previewLoading,
playingVoiceId,
loadingSampleVoiceId,
handleModeChange,
handleTextChange,
handleVoiceSelect,
@@ -91,6 +92,7 @@ const TtsPanel: React.FC<TtsPanelProps> = ({ open, onClose, config, onChange })
selectedVoiceId={config.voice_id}
loading={voicesLoading}
playingVoiceId={playingVoiceId}
loadingSampleVoiceId={loadingSampleVoiceId}
onSelect={handleVoiceSelect}
onPlaySample={handlePlayVoiceSample}
/>