From 4b736615d6608914fbb3cf319d3c62dd156dc14b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 15 Sep 2026 01:09:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(voices):=20=E5=88=87=E6=8D=A2=E5=88=B0?= =?UTF-8?q?=E9=A2=84=E7=BD=AE=E9=9F=B3=E8=89=B2=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E4=B8=8D=E6=94=AF=E6=8C=81=E7=9A=84=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E5=88=B0=E4=B8=AD=E6=96=87=EF=BC=9B=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=83=85=E7=BB=AA=E6=94=B9=20neutral?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/voices/hooks/useTtsSynthesize.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/voices/hooks/useTtsSynthesize.ts b/apps/web/src/pages/voices/hooks/useTtsSynthesize.ts index f5c2e841e..a8ff11ec4 100644 --- a/apps/web/src/pages/voices/hooks/useTtsSynthesize.ts +++ b/apps/web/src/pages/voices/hooks/useTtsSynthesize.ts @@ -7,12 +7,16 @@ import type { TtsClonedVoiceOption } from "../components/tts-modal/VoiceSelector import { DEFAULT_TTS_EMOTION, DEFAULT_TTS_LANGUAGE, + PRESET_TTS_LANGUAGE_OPTIONS, type TtsEmotion, type TtsLanguage, } from "../components/tts-modal/constants" export type TtsStatus = "idle" | "synthesizing" | "done" | "error" +/** 预置音色支持的语言 value 集合 */ +const PRESET_LANG_SET = new Set(PRESET_TTS_LANGUAGE_OPTIONS.map((o) => o.value)) + /** * TTS 合成 Hook * 封装 AI 配音弹窗状态、合成请求、轮询、保存到素材库等逻辑 @@ -43,12 +47,29 @@ export function useTtsSynthesize({ const [ttsError, setTtsError] = useState(null) const ttsTimerRef = useRef | null>(null) + /** 切换音色时:预置音色下自动回退到受支持语言(zh-CN/en) */ + const handleVoiceChange = useCallback( + (voiceId: string) => { + setTtsVoiceId(voiceId) + const isClone = !!voiceId && clonedVoices.some((v) => v.id === voiceId) + if (!isClone && !PRESET_LANG_SET.has(ttsLanguage)) { + setTtsLanguage(DEFAULT_TTS_LANGUAGE) + } + }, + [clonedVoices, ttsLanguage], + ) + /** 开始 AI 配音合成 */ const handleTtsSynthesize = useCallback(async () => { if (!ttsText.trim()) { message.warning("请输入要合成的文本") return } + // 防御:合成前再次校验预置音色语言 + const isClone = !!ttsVoiceId && clonedVoices.some((v) => v.id === ttsVoiceId) + const effectiveLang = + !isClone && !PRESET_LANG_SET.has(ttsLanguage) ? DEFAULT_TTS_LANGUAGE : ttsLanguage + setTtsError(null) setTtsStatus("synthesizing") setTtsAudioUrl(null) @@ -60,7 +81,7 @@ export function useTtsSynthesize({ voice_id: ttsVoiceId || undefined, speed: ttsSpeed, emotion: ttsEmotion, - language: ttsLanguage, + language: effectiveLang, }) setTtsJobId(resp.job_id) @@ -91,7 +112,7 @@ export function useTtsSynthesize({ setTtsStatus("error") setTtsError(msg) } - }, [ttsText, ttsVoiceId, ttsSpeed, ttsEmotion, ttsLanguage]) + }, [ttsText, ttsVoiceId, ttsSpeed, ttsEmotion, ttsLanguage, clonedVoices]) /** 保存 TTS 结果到素材库 */ const handleTtsSave = useCallback(async () => { @@ -160,6 +181,8 @@ export function useTtsSynthesize({ setTtsEmotion, setTtsLanguage, setTtsOpen, + // 覆写 onVoiceChange(带语言回退) + handleVoiceChange, // Actions handleTtsSynthesize, handleTtsSave,