From 3466537bfd113d736bedf3e8f46a4d95eb0d7917 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 09:15:24 +0800 Subject: [PATCH 01/25] =?UTF-8?q?refactor(TtsPanel):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=B8=B8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/TtsPanel/constants.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/TtsPanel/constants.ts diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/constants.ts b/apps/web/src/pages/editing-planner/components/TtsPanel/constants.ts new file mode 100644 index 000000000..ea59701f3 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/constants.ts @@ -0,0 +1,18 @@ +import type { TtsMode } from "../types" + +/** 音色卡片分类图标 */ +export const VOICE_CATEGORY_MAP: Record = { + male: { icon: "👨", label: "男声" }, + female: { icon: "👩", label: "女声" }, + young: { icon: "🧑", label: "少年" }, + service: { icon: "🎧", label: "客服" }, + news: { icon: "📰", label: "新闻" }, + emotion: { icon: "🎭", label: "情感" }, +} + +/** 配音模式选项 */ +export const TTS_MODE_OPTIONS: { mode: TtsMode; icon: string; label: string }[] = [ + { mode: "none", icon: "🔇", label: "无配音" }, + { mode: "upload", icon: "📁", label: "上传配音" }, + { mode: "tts", icon: "🤖", label: "TTS 合成" }, +] -- 2.54.0 From 3630907ea4946d858ee0beb3de370b6fa58c67fc Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 09:15:25 +0800 Subject: [PATCH 02/25] =?UTF-8?q?refactor(TtsPanel):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E9=80=BB=E8=BE=91Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/TtsPanel/hooks/useTtsPanel.ts | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts b/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts new file mode 100644 index 000000000..957103856 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/hooks/useTtsPanel.ts @@ -0,0 +1,147 @@ +import { useState, useCallback, useEffect, useRef } from "react" +import { message } from "antd" +import type { TtsConfig, TtsMode } from "../types" +import { DEFAULT_TTS_CONFIG } from "../types" +import { getTtsVoices, previewTts, type TTSVoice } from "@/api/tts" + +interface UseTtsPanelOptions { + open: boolean + config: TtsConfig + onChange: (config: TtsConfig) => void + onClose: () => void +} + +export const useTtsPanel = ({ open, config, onChange, onClose }: UseTtsPanelOptions) => { + /* ── 音色列表 ── */ + const [voices, setVoices] = useState([]) + const [voicesLoading, setVoicesLoading] = useState(false) + + /* ── 试听状态 ── */ + const [previewLoading, setPreviewLoading] = useState(false) + const audioRef = useRef(null) + + /* ── 加载音色列表 ── */ + useEffect(() => { + if (!open) return + setVoicesLoading(true) + getTtsVoices() + .then((v) => setVoices(v)) + .catch(() => message.error("加载音色列表失败")) + .finally(() => setVoicesLoading(false)) + }, [open]) + + /* ── 切换配音模式 ── */ + const handleModeChange = useCallback( + (mode: TtsMode) => { + onChange({ ...config, mode }) + }, + [config, onChange], + ) + + /* ── 文本输入 ── */ + const handleTextChange = useCallback( + (e: React.ChangeEvent) => { + const text = e.target.value.slice(0, 5000) + onChange({ ...config, text }) + }, + [config, onChange], + ) + + /* ── 选择音色 ── */ + const handleVoiceSelect = useCallback( + (voiceId: string) => { + onChange({ ...config, voice_id: voiceId }) + }, + [config, onChange], + ) + + /* ── 语速 ── */ + const handleSpeedChange = useCallback( + (speed: number) => { + onChange({ ...config, speed }) + }, + [config, onChange], + ) + + /* ── 语调 ── */ + const handlePitchChange = useCallback( + (pitch: number) => { + onChange({ ...config, pitch }) + }, + [config, onChange], + ) + + /* ── 音量 ── */ + const handleVolumeChange = useCallback( + (volume: number) => { + onChange({ ...config, volume }) + }, + [config, onChange], + ) + + /* ── 字幕联动 ── */ + const handleSubtitleSyncToggle = useCallback(() => { + onChange({ ...config, subtitle_sync: !config.subtitle_sync }) + }, [config, onChange]) + + /* ── 试听 ── */ + const handlePreview = useCallback(async () => { + if (!config.text.trim()) { + message.warning("请先输入合成文本") + return + } + if (!config.voice_id) { + message.warning("请先选择音色") + return + } + setPreviewLoading(true) + try { + const res = await previewTts({ + text: config.text.slice(0, 200), + voice_id: config.voice_id, + speed: config.speed, + pitch: config.pitch, + }) + audioRef.current?.pause() + const audio = new Audio(res.audio_url) + audioRef.current = audio + audio.play().catch(() => message.error("播放失败")) + audio.onended = () => { + audioRef.current = null + } + message.success("试听播放中") + } catch { + message.error("试听生成失败") + } finally { + setPreviewLoading(false) + } + }, [config]) + + /* ── 重置 ── */ + const handleReset = useCallback(() => { + onChange({ ...DEFAULT_TTS_CONFIG }) + }, [onChange]) + + /* ── 关闭时停止音频 ── */ + const handleClose = useCallback(() => { + audioRef.current?.pause() + audioRef.current = null + onClose() + }, [onClose]) + + return { + voices, + voicesLoading, + previewLoading, + handleModeChange, + handleTextChange, + handleVoiceSelect, + handleSpeedChange, + handlePitchChange, + handleVolumeChange, + handleSubtitleSyncToggle, + handlePreview, + handleReset, + handleClose, + } +} -- 2.54.0 From 194eab7296c68120751bf3c00259e4ff77e6dcc8 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 09:15:25 +0800 Subject: [PATCH 03/25] =?UTF-8?q?refactor(TtsPanel):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E9=9F=B3=E8=89=B2=E9=80=89=E6=8B=A9=E5=99=A8=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TtsPanel/components/VoiceSelector.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx b/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx new file mode 100644 index 000000000..651c50d40 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/components/VoiceSelector.tsx @@ -0,0 +1,45 @@ +import React from "react" +import type { TTSVoice } from "@/api/tts" +import { VOICE_CATEGORY_MAP } from "../constants" + +export interface VoiceSelectorProps { + voices: TTSVoice[] + selectedVoiceId: string + loading: boolean + onSelect: (voiceId: string) => void +} + +export const VoiceSelector: React.FC = ({ + voices, + selectedVoiceId, + loading, + onSelect, +}) => { + const voiceCategories = Object.entries(VOICE_CATEGORY_MAP) + + return ( +
+
+ 选择音色 + {loading && 加载中...} +
+
+ {voiceCategories.map(([cat, info]) => { + const voice = voices.find((v) => v.category === cat) + const isSelected = voice && selectedVoiceId === voice.id + return ( + + ) + })} +
+
+ ) +} -- 2.54.0 From 1bcda88635bf7600416ad6c83c90a074f67a53c6 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 09:15:26 +0800 Subject: [PATCH 04/25] =?UTF-8?q?refactor(TtsPanel):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=BB=91=E5=9D=97=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TtsPanel/components/TtsSlider.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/TtsPanel/components/TtsSlider.tsx diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/components/TtsSlider.tsx b/apps/web/src/pages/editing-planner/components/TtsPanel/components/TtsSlider.tsx new file mode 100644 index 000000000..50bd25d1f --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/components/TtsSlider.tsx @@ -0,0 +1,48 @@ +import React from "react" +import { Slider } from "antd" + +export interface TtsSliderProps { + label: string + value: number + min: number + max: number + step: number + onChange: (val: number) => void + formatter: (val: number) => string + marks?: string[] +} + +export const TtsSlider: React.FC = ({ + label, + value, + min, + max, + step, + onChange, + formatter, + marks, +}) => { + return ( +
+
+ {label} + {formatter(value)} +
+ formatter(v as number) }} + /> + {marks && ( +
+ {marks.map((m, i) => ( + {m} + ))} +
+ )} +
+ ) +} -- 2.54.0 From 5ce1a00e26118e9c2c2227ef2b6212a991c3b4c7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 09:15:26 +0800 Subject: [PATCH 05/25] =?UTF-8?q?refactor(TtsPanel):=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E5=8C=96=E4=B8=BB=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/TtsPanel/index.tsx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx diff --git a/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx b/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx new file mode 100644 index 000000000..75a3ab4f1 --- /dev/null +++ b/apps/web/src/pages/editing-planner/components/TtsPanel/index.tsx @@ -0,0 +1,173 @@ +/** + * TTS 配音面板 — Drawer 形式 + * 配音模式切换 + 文本输入 + 音色选择 + 语速/语调/音量 + 试听 + 字幕联动 + */ +import React from "react" +import { Drawer } from "antd" +import type { TtsConfig } from "../types" + +import { useTtsPanel } from "./hooks/useTtsPanel" +import { VoiceSelector } from "./components/VoiceSelector" +import { TtsSlider } from "./components/TtsSlider" +import { TTS_MODE_OPTIONS } from "./constants" + +export interface TtsPanelProps { + open: boolean + onClose: () => void + /** 当前片段 TTS 配置 */ + config: TtsConfig + onChange: (config: TtsConfig) => void +} + +const TtsPanel: React.FC = ({ open, onClose, config, onChange }) => { + const { + voices, + voicesLoading, + previewLoading, + handleModeChange, + handleTextChange, + handleVoiceSelect, + handleSpeedChange, + handlePitchChange, + handleVolumeChange, + handleSubtitleSyncToggle, + handlePreview, + handleReset, + handleClose, + } = useTtsPanel({ open, config, onChange, onClose }) + + return ( + + {/* ── 配音模式切换 ── */} +
+
配音模式
+
+ {TTS_MODE_OPTIONS.map((m) => ( + + ))} +
+
+ + {/* ── TTS 配置(仅 tts 模式显示) ── */} + {config.mode === "tts" && ( + <> + {/* 文本输入 */} +
+
+ 合成文本 + {config.text.length}/5000 +
+