From 7a51c523778bb61697d687013411b4bd89707ec9 Mon Sep 17 00:00:00 2001 From: saas-frontend-agent Date: Mon, 14 Sep 2026 18:17:38 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(frontend):=20#1897=20=E5=A3=B0=E9=9F=B3?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E9=A1=B5=E5=A2=9E=E5=8A=A0=E6=83=85=E7=BB=AA?= =?UTF-8?q?/=E8=AF=AD=E9=80=9F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/voice-clone/clones.ts: getVoiceClonePreview 增加 options 参数(speed/emotion),通过 searchParams 透传 - 新增 VoiceClonePreviewPanel 组件:音色选择下拉、试听文本框、语速滑块(复用 SpeedControl)、情绪下拉(复用 EmotionControl)、试听按钮,试听完成后可直接播放音频 - VoiceClone.tsx:PageHead 下方、卡片网格上方插入试听面板(仅当存在就绪音色时显示) - voice-clone.css:新增试听面板样式(复用现有 CSS 变量,风格与页面一致) - 语速 0.5x-2.0x 默认1.0x;情绪 natural/excited/calm/friendly 默认natural - 不加语言选择(克隆音色语言由训练时确定) - 参数真实传递到后端 preview 接口 --- apps/web/src/api/voice-clone/clones.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/src/api/voice-clone/clones.ts b/apps/web/src/api/voice-clone/clones.ts index f63e71124..a5e64fdc8 100644 --- a/apps/web/src/api/voice-clone/clones.ts +++ b/apps/web/src/api/voice-clone/clones.ts @@ -96,9 +96,12 @@ export const retryVoiceClone = async (id: string): Promise => export const getVoiceClonePreview = async ( cloneId: string, text?: string, + options?: { speed?: number; emotion?: string }, ): Promise => { const searchParams = new URLSearchParams() if (text) searchParams.set("text", text) + if (options?.speed !== undefined) searchParams.set("speed", String(options.speed)) + if (options?.emotion) searchParams.set("emotion", options.emotion) const qs = searchParams.toString() const response = await apiClient.get( `/voice-clones/${cloneId}/preview${qs ? `?${qs}` : ""}`, -- 2.54.0 From b69f3200be9e4a7b1804b886c7f486646f86c4d9 Mon Sep 17 00:00:00 2001 From: saas-frontend-agent Date: Mon, 14 Sep 2026 18:17:50 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(frontend):=20#1897=20=E5=A3=B0=E9=9F=B3?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E9=A1=B5=E5=A2=9E=E5=8A=A0=E6=83=85=E7=BB=AA?= =?UTF-8?q?/=E8=AF=AD=E9=80=9F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/voice-clone/clones.ts: getVoiceClonePreview 增加 options 参数(speed/emotion),通过 searchParams 透传 - 新增 VoiceClonePreviewPanel 组件:音色选择下拉、试听文本框、语速滑块(复用 SpeedControl)、情绪下拉(复用 EmotionControl)、试听按钮,试听完成后可直接播放音频 - VoiceClone.tsx:PageHead 下方、卡片网格上方插入试听面板(仅当存在就绪音色时显示) - voice-clone.css:新增试听面板样式(复用现有 CSS 变量,风格与页面一致) - 语速 0.5x-2.0x 默认1.0x;情绪 natural/excited/calm/friendly 默认natural - 不加语言选择(克隆音色语言由训练时确定) - 参数真实传递到后端 preview 接口 --- .../components/VoiceClonePreviewPanel.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 apps/web/src/pages/voice-clone/components/VoiceClonePreviewPanel.tsx diff --git a/apps/web/src/pages/voice-clone/components/VoiceClonePreviewPanel.tsx b/apps/web/src/pages/voice-clone/components/VoiceClonePreviewPanel.tsx new file mode 100644 index 000000000..8a7185ce5 --- /dev/null +++ b/apps/web/src/pages/voice-clone/components/VoiceClonePreviewPanel.tsx @@ -0,0 +1,133 @@ +/** + * 克隆音色试听面板 + * - 选择就绪音色、输入试听文本、调节语速/情绪,点试听 + * - 复用配音库 TTS 弹窗的 SpeedControl / EmotionControl 组件 + */ +import React, { useState, useRef, useCallback } from "react" +import { message } from "antd" +import { SoundOutlined, LoadingOutlined } from "@ant-design/icons" +import { getVoiceClonePreview, type VoiceClone } from "@/api/voice-clone" +import SpeedControl from "@/pages/voices/components/tts-modal/SpeedControl" +import EmotionControl from "@/pages/voices/components/tts-modal/EmotionControl" +import { DEFAULT_TTS_EMOTION, type TtsEmotion } from "@/pages/voices/components/tts-modal/constants" +import { TTS_CONFIG } from "@/pages/voices/components/tts-modal/types" + +interface VoiceClonePreviewPanelProps { + voices: VoiceClone[] +} + +const DEFAULT_PREVIEW_TEXT = "你好呀,欢迎使用小虾智剪,这是我的声音效果,希望你喜欢。" + +const VoiceClonePreviewPanel: React.FC = ({ voices }) => { + const readyVoices = voices.filter((v) => v.status === "ready") + + const [selectedId, setSelectedId] = useState(readyVoices[0]?.id ?? "") + const [previewText, setPreviewText] = useState(DEFAULT_PREVIEW_TEXT) + const [speed, setSpeed] = useState(TTS_CONFIG.DEFAULT_SPEED) + const [emotion, setEmotion] = useState(DEFAULT_TTS_EMOTION) + const [previewing, setPreviewing] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const audioRef = useRef(null) + + const handlePreview = useCallback(async () => { + if (!selectedId) { + message.warning("请先选择要试听的音色") + return + } + const text = previewText.trim() + if (!text) { + message.warning("请输入试听文本") + return + } + if (audioRef.current) { + audioRef.current.pause() + audioRef.current = null + } + setPreviewing(true) + setAudioUrl(null) + try { + const res = await getVoiceClonePreview(selectedId, text, { speed, emotion }) + setAudioUrl(res.audio_url) + const audio = new Audio(res.audio_url) + audioRef.current = audio + audio.play().catch(() => { + message.error("播放失败,请重试") + }) + } catch (err) { + const msg = err instanceof Error ? err.message : "试听失败" + message.error(msg) + } finally { + setPreviewing(false) + } + }, [selectedId, previewText, speed, emotion]) + + if (readyVoices.length === 0) return null + + return ( +
+
+ + 音色试听 +
+ +
+
+ + +
+ +
+ +