fix: AI数字人「我的音色」克隆音色试听改用 TTS 合成 #1815
@@ -5,6 +5,7 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { message } from "antd"
|
||||
import { fetchVoices } from "@/api/voices/voices"
|
||||
import { previewTts } from "@/api/tts"
|
||||
import type { UnifiedVoiceItem } from "@/api/voices/types"
|
||||
import {
|
||||
type VoiceSource,
|
||||
@@ -44,6 +45,9 @@ export function PanelVoiceSelector({
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [previewingId, setPreviewingId] = useState<string | null>(null)
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||
/** 克隆音色试听合成缓存:voiceId -> url,对齐配音库 useAudioPlayer */
|
||||
const previewCacheRef = useRef<Map<string, string>>(new Map())
|
||||
const VOICE_PREVIEW_TEXT = "你好呀,欢迎使用小虾智剪,这是我的配音效果,希望你喜欢。"
|
||||
|
||||
/* 切换来源时重新获取音色列表 */
|
||||
useEffect(() => {
|
||||
@@ -86,24 +90,15 @@ export function PanelVoiceSelector({
|
||||
|
||||
const NO_PREVIEW_TIP = "该音色暂无试听音频,请先用此音色生成一段配音后再试听"
|
||||
|
||||
const handlePreview = (voice: UnifiedVoiceItem) => {
|
||||
const url = voice.preview_url || voice.audio_url
|
||||
if (!url) {
|
||||
message.warning(NO_PREVIEW_TIP)
|
||||
return
|
||||
}
|
||||
/* 再次点击当前试听音色 → 停止 */
|
||||
if (previewingId === voice.id) {
|
||||
stopPreview()
|
||||
return
|
||||
}
|
||||
/** 用指定 URL 真实播放(抽取公共) */
|
||||
const playAudioUrl = (voiceId: string, url: string) => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause()
|
||||
audioRef.current = null
|
||||
}
|
||||
const audio = new Audio(url)
|
||||
audioRef.current = audio
|
||||
setPreviewingId(voice.id)
|
||||
setPreviewingId(voiceId)
|
||||
audio.onended = () => {
|
||||
if (audioRef.current === audio) {
|
||||
audioRef.current = null
|
||||
@@ -123,6 +118,52 @@ export function PanelVoiceSelector({
|
||||
})
|
||||
}
|
||||
|
||||
const handlePreview = async (voice: UnifiedVoiceItem) => {
|
||||
/* 再次点击当前试听音色 → 停止 */
|
||||
if (previewingId === voice.id) {
|
||||
stopPreview()
|
||||
return
|
||||
}
|
||||
|
||||
/* 克隆音色:preview_url/audio_url 通常为空,需走 POST /tts/preview
|
||||
* 现合成示例文案再播放,对齐配音库 useAudioPlayer 行为 */
|
||||
if (voice.type === "clone") {
|
||||
const cached = previewCacheRef.current.get(voice.voice_id || voice.id)
|
||||
if (cached) {
|
||||
playAudioUrl(voice.id, cached)
|
||||
return
|
||||
}
|
||||
const targetId = voice.voice_id || voice.id
|
||||
setPreviewingId(voice.id)
|
||||
try {
|
||||
const res = await previewTts({
|
||||
text: VOICE_PREVIEW_TEXT,
|
||||
voice_id: targetId,
|
||||
speed: 1.0,
|
||||
})
|
||||
if (!res.audio_url) {
|
||||
setPreviewingId(null)
|
||||
message.error("合成试听失败:未返回音频")
|
||||
return
|
||||
}
|
||||
previewCacheRef.current.set(targetId, res.audio_url)
|
||||
playAudioUrl(voice.id, res.audio_url)
|
||||
} catch {
|
||||
setPreviewingId(null)
|
||||
// apiClient 拦截器已统一 toast
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/* 系统预设音色:沿用 preview_url/audio_url 直链播放 */
|
||||
const url = voice.preview_url || voice.audio_url
|
||||
if (!url) {
|
||||
message.warning(NO_PREVIEW_TIP)
|
||||
return
|
||||
}
|
||||
playAudioUrl(voice.id, url)
|
||||
}
|
||||
|
||||
const handleSpeedChange = (value: string) => {
|
||||
const parsed = parseFloat(value)
|
||||
if (Number.isNaN(parsed)) return
|
||||
@@ -187,9 +228,7 @@ export function PanelVoiceSelector({
|
||||
<button
|
||||
type="button"
|
||||
className="aa-voice-card__preview"
|
||||
title={
|
||||
!previewUrl ? NO_PREVIEW_TIP : previewingId === voice.id ? "停止试听" : "试听"
|
||||
}
|
||||
title={previewingId === voice.id ? "停止试听" : "试听"}
|
||||
disabled={!previewUrl}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
Reference in New Issue
Block a user