fix(#1196): TTS面板克隆音色试听改用/voice-clones/{id}/preview接口
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 55s
AI Code Review / AI Code Review (pull_request) Successful in 2m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m14s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m38s
CI/CD Pipeline / CI Gate (pull_request) Successful in 35s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 33s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1102h58m7s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1102h58m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1102h58m32s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1102h58m34s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1102h58m36s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1102h58m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1102h59m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1102h58m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1103h1m13s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 1103h1m17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1103h1m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1103h2m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1103h2m44s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 1103h31m23s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1103h32m13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 1103h34m29s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1103h35m54s

- VoiceSelector: 克隆音色试听按钮加loading态,点击时显示合成中
- useTtsPanel: handlePlayVoiceSample接收完整voice对象,区分preset/clone
- 克隆音色调用/voice-clones/{id}/preview接口实时合成试听音频
- 预设音色保持原有逻辑直接播放preview_url
- 统一错误处理和竞态防护
This commit is contained in:
前端Agent
2026-07-31 14:59:22 +08:00
parent 25bbfdda6b
commit b7ef9fed14
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}
/>