From b716d3c34f7f8f2d1e2b31aa7ac45beb8c18c1b5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 9 Sep 2026 10:13:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=E6=95=B0=E5=AD=97=E4=BA=BA=E3=80=8C?= =?UTF-8?q?=E6=88=91=E7=9A=84=E9=9F=B3=E8=89=B2=E3=80=8D=E5=85=8B=E9=9A=86?= =?UTF-8?q?=E9=9F=B3=E8=89=B2=E8=AF=95=E5=90=AC=E6=94=B9=E7=94=A8=20TTS=20?= =?UTF-8?q?=E5=90=88=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配音库克隆音色能听到,数字人页面听不到,根因: 配音库 useAudioPlayer 的 playVoice() 是调 POST /tts/preview 现合成示例文案拿到 audio_url 再播放(clone 的 preview_url/ audio_url 都为空);数字人面板还停留在直接用 preview_url 直链播放,所以 clone 音色静默返回。 修复: - 克隆音色走 previewTts(与配音库一致)+ 按 voiceId 内存缓存, 同一音色二次试听不重复合成 - 系统预设音色保持原 preview_url/audio_url 直链播放,不改 - 抽取公共 playAudioUrl,保留原 onended/onerror/play().catch 错误提示(含「该音色暂无试听音频」兜底) 不动:播放按钮 disabled/title 逻辑、音色列表布局、其他面板。 --- .../components/PanelVoiceSelector.tsx | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx b/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx index 7b7c8fe74..467ab40dd 100644 --- a/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelVoiceSelector.tsx @@ -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(null) const [previewingId, setPreviewingId] = useState(null) const audioRef = useRef(null) + /** 克隆音色试听合成缓存:voiceId -> url,对齐配音库 useAudioPlayer */ + const previewCacheRef = useRef>(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({