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
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:
@@ -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