b7ef9fed14
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 55s
AI Code Review / AI Code Review (pull_request) Successful in 2m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m14s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m38s
CI/CD Pipeline / CI Gate (pull_request) Successful in 35s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 33s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1102h58m7s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1102h58m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1102h58m32s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1102h58m34s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1102h58m36s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1102h58m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1102h59m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1102h58m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1103h1m13s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 1103h1m17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1103h1m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1103h2m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1103h2m44s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 1103h31m23s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1103h32m13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 1103h34m29s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1103h35m54s
- VoiceSelector: 克隆音色试听按钮加loading态,点击时显示合成中
- useTtsPanel: handlePlayVoiceSample接收完整voice对象,区分preset/clone
- 克隆音色调用/voice-clones/{id}/preview接口实时合成试听音频
- 预设音色保持原有逻辑直接播放preview_url
- 统一错误处理和竞态防护
181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
/**
|
|
* 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<TtsPanelProps> = ({ open, onClose, config, onChange }) => {
|
|
const {
|
|
presetVoices,
|
|
cloneVoices,
|
|
voicesLoading,
|
|
previewLoading,
|
|
playingVoiceId,
|
|
loadingSampleVoiceId,
|
|
handleModeChange,
|
|
handleTextChange,
|
|
handleVoiceSelect,
|
|
handlePlayVoiceSample,
|
|
handleSpeedChange,
|
|
handlePitchChange,
|
|
handleVolumeChange,
|
|
handleSubtitleSyncToggle,
|
|
handlePreview,
|
|
handleReset,
|
|
handleClose,
|
|
} = useTtsPanel({ open, config, onChange, onClose })
|
|
|
|
return (
|
|
<Drawer
|
|
title="🎙️ TTS 配音"
|
|
placement="right"
|
|
width={400}
|
|
open={open}
|
|
onClose={handleClose}
|
|
className="tts-panel-drawer"
|
|
>
|
|
{/* ── 配音模式切换 ── */}
|
|
<div className="tts-mode-section">
|
|
<div className="tts-mode-label">配音模式</div>
|
|
<div className="tts-mode-group">
|
|
{TTS_MODE_OPTIONS.map((m) => (
|
|
<button
|
|
key={m.mode}
|
|
className={`tts-mode-btn${config.mode === m.mode ? " active" : ""}`}
|
|
onClick={() => handleModeChange(m.mode)}
|
|
>
|
|
<span className="tts-mode-icon">{m.icon}</span>
|
|
<span className="tts-mode-text">{m.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── TTS 配置(仅 tts 模式显示) ── */}
|
|
{config.mode === "tts" && (
|
|
<>
|
|
{/* 文本输入 */}
|
|
<div className="tts-text-section">
|
|
<div className="tts-text-header">
|
|
<span className="tts-text-label">合成文本</span>
|
|
<span className="tts-text-count">{config.text.length}/5000</span>
|
|
</div>
|
|
<textarea
|
|
className="tts-text-input"
|
|
placeholder="请输入需要合成的文本内容..."
|
|
value={config.text}
|
|
onChange={handleTextChange}
|
|
maxLength={5000}
|
|
rows={5}
|
|
/>
|
|
</div>
|
|
|
|
{/* 音色选择 */}
|
|
<VoiceSelector
|
|
presetVoices={presetVoices}
|
|
cloneVoices={cloneVoices}
|
|
selectedVoiceId={config.voice_id}
|
|
loading={voicesLoading}
|
|
playingVoiceId={playingVoiceId}
|
|
loadingSampleVoiceId={loadingSampleVoiceId}
|
|
onSelect={handleVoiceSelect}
|
|
onPlaySample={handlePlayVoiceSample}
|
|
/>
|
|
|
|
{/* 语速 */}
|
|
<TtsSlider
|
|
label="语速"
|
|
value={config.speed}
|
|
min={0.5}
|
|
max={2.0}
|
|
step={0.05}
|
|
onChange={handleSpeedChange}
|
|
formatter={(v) => `${v.toFixed(2)}x`}
|
|
marks={["0.5x", "1.0x", "2.0x"]}
|
|
/>
|
|
|
|
{/* 语调 */}
|
|
<TtsSlider
|
|
label="语调"
|
|
value={config.pitch}
|
|
min={-12}
|
|
max={12}
|
|
step={1}
|
|
onChange={handlePitchChange}
|
|
formatter={(v) => `${v > 0 ? "+" : ""}${v} 半音`}
|
|
marks={["-12", "0", "+12"]}
|
|
/>
|
|
|
|
{/* 音量 */}
|
|
<TtsSlider
|
|
label="音量"
|
|
value={config.volume}
|
|
min={0}
|
|
max={100}
|
|
step={1}
|
|
onChange={handleVolumeChange}
|
|
formatter={(v) => `${v}%`}
|
|
/>
|
|
|
|
{/* 试听按钮 */}
|
|
<div className="tts-preview-section">
|
|
<button className="tts-preview-btn" onClick={handlePreview} disabled={previewLoading}>
|
|
{previewLoading ? "⏳ 生成中..." : "🔊 试听"}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 字幕联动 */}
|
|
<div className="tts-subtitle-section">
|
|
<div className="tts-subtitle-info">
|
|
<span className="tts-subtitle-label">字幕联动</span>
|
|
<span className="tts-subtitle-desc">
|
|
{config.subtitle_sync ? "TTS 文本自动同步到字幕" : "字幕需手动编辑"}
|
|
</span>
|
|
</div>
|
|
<div
|
|
className={`ep-toggle${config.subtitle_sync ? " active" : ""}`}
|
|
onClick={handleSubtitleSyncToggle}
|
|
>
|
|
<div className="ep-toggle-knob" />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ── 上传配音模式提示 ── */}
|
|
{config.mode === "upload" && (
|
|
<div className="tts-upload-hint">
|
|
<p>请在右侧面板的「配音素材」中选择已上传的配音文件。</p>
|
|
<p>如需上传新配音,请前往配音库页面。</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── 底部操作 ── */}
|
|
{config.mode === "tts" && (
|
|
<div className="tts-footer">
|
|
<button className="tts-reset-btn" onClick={handleReset}>
|
|
重置默认
|
|
</button>
|
|
</div>
|
|
)}
|
|
</Drawer>
|
|
)
|
|
}
|
|
|
|
export default TtsPanel
|