fix(#1196): TTS面板克隆音色试听改用/voice-clones/{id}/preview接口 #1205
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user