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, + } +}