## 🎙️ 配音前置重构 — 第一阶段 (Day 23) ### 概述 将配音从视频生成流程中独立出来,实现"先生成配音 → 选中配音 → 再选素材 → 生成视频"的新工作流。用户可以独立管理配音(生成/试听/改名/删除/选择),并在选中配音后看到时长信息,为第二阶段的素材时间轴编排奠定数据基础。 **旧流程**: 文案 + 选素材 → 一键生成(内联 TTS → Whisper → 均分 → LipSync → 合成) **新流程**: 文案 → 配音方式 → **生成配音** → 选中配音 → 选素材 → 背景音乐 → 生成视频 --- ### 一、后端:新增 `generated_audios` 模块 #### 模块结构 ``` backend/app/modules/generated_audios/ ├── __init__.py ├── router.py # 5 个 API 端点 ├── schemas.py # 请求/响应模型 └── service.py # 生成/列表/删除/改名 ``` #### API 端点 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/generated-audios/generate` | 异步生成配音(返回 task_id) | | GET | `/api/generated-audios/tasks/{task_id}` | 轮询生成进度 | | GET | `/api/generated-audios` | 列出用户所有配音 | | DELETE | `/api/generated-audios/{audio_id}` | 删除配音 | | PUT | `/api/generated-audios/{audio_id}` | 改名 | #### 存储方案 - Supabase 存储桶:`generated-audios`(启动时自动创建) - 音频文件:`{user_id}/{timestamp}_audio.wav` - 元数据文件:`{user_id}/{timestamp}_audio.json`(含 display_name、text、tts_mode、duration_sec 等) #### 生成流程 复用现有 `TTSService` / `voice_clone_service` / `task_store`: ``` POST /generate → 创建 task → BackgroundTask: 1. edgetts → TTSService.generate_audio() voiceclone → 下载 ref_audio → voice_clone_service.generate_audio() 2. ffprobe 获取时长 3. 上传 .wav + .json 到 generated-audios 桶 4. 更新 task(status=completed, output={audio_id, duration_sec, ...}) ``` --- ### 二、后端:修改视频生成 workflow #### `GenerateRequest` 新增字段 ```python generated_audio_id: Optional[str] = None # 预生成配音 ID(存在时跳过内联 TTS) ``` #### `workflow.py` TTS 阶段新增分支 ```python if req.generated_audio_id: # 下载预生成配音 + 从元数据读取 language elif req.tts_mode == "voiceclone": # 原有声音克隆逻辑 else: # 原有 EdgeTTS 逻辑 ``` 向后兼容:不传 `generated_audio_id` 时,原有内联 TTS 流程不受影响。 --- ### 三、前端:新增配音列表 hook + 面板 #### `useGeneratedAudios.ts` - 状态:`generatedAudios[]`、`selectedAudio`、`isGeneratingAudio`、`audioTask` - 方法:`fetchGeneratedAudios()`、`generateAudio()`、`deleteAudio()`、`renameAudio()`、`selectAudio()` - 轮询:生成后 1s 轮询 task 状态,完成后自动刷新列表并选中最新配音 - 独立于视频生成的 TaskContext(不互相干扰) #### `GeneratedAudiosPanel.tsx` - 每条配音:播放/暂停、名称、时长、重命名、删除 - 选中态:`border-purple-500 bg-purple-500/20` - 内嵌进度条(生成中显示) - 底部显示选中配音的原始文案(截断) - 播放逻辑自包含于面板内(`new Audio()` + play/pause toggle) --- ### 四、前端:UI 面板重排序 **旧顺序**: MaterialSelector → ScriptEditor → TitleSubtitle → VoiceSelector → BgmPanel → GenerateActionBar **新顺序**: 1. ScriptEditor(文案编辑) 2. TitleSubtitlePanel(标题与字幕样式) 3. VoiceSelector(配音方式) 4. **GeneratedAudiosPanel**(配音列表)← 新增 5. MaterialSelector(视频素材)← 后移,需选中配音才解锁 6. BgmPanel(背景音乐) 7. GenerateActionBar(生成视频) #### 素材区门控 未选中配音时,素材区显示半透明遮罩 + "请先生成并选中配音"提示。素材上传/预览/改名/删除始终可用,仅选择勾选被遮罩。 #### 时长信息 选中配音后,MaterialSelector 顶部显示: ``` 当前配音: 45.2 秒 | 已选 3 个素材(自动均分每段 ~15.1 秒) ``` #### 生成按钮条件更新 ```typescript // 旧条件 disabled={isGenerating || selectedMaterials.length === 0 || (ttsMode === "voiceclone" && !selectedRefAudio)} // 新条件 disabled={isGenerating || selectedMaterials.length === 0 || !selectedAudio} ``` --- ### 五、持久化 `useHomePersistence` 新增 `selectedAudioId` 的 localStorage 读写,刷新页面后恢复选中的配音。 --- ### 涉及文件汇总 #### 后端新增 | 文件 | 说明 | |------|------| | `backend/app/modules/generated_audios/__init__.py` | 模块标记 | | `backend/app/modules/generated_audios/router.py` | 5 个 API 端点 | | `backend/app/modules/generated_audios/service.py` | 生成/列表/删除/改名 | | `backend/app/modules/generated_audios/schemas.py` | 请求/响应模型 | #### 后端修改 | 文件 | 变更 | |------|------| | `backend/app/main.py` | 注册 generated_audios 路由 | | `backend/app/services/storage.py` | 新增 `BUCKET_GENERATED_AUDIOS`,启动时自动创建桶 | | `backend/app/modules/videos/schemas.py` | `GenerateRequest` 新增 `generated_audio_id` 字段 | | `backend/app/modules/videos/workflow.py` | TTS 阶段新增预生成音频分支 | #### 前端新增 | 文件 | 说明 | |------|------| | `frontend/src/features/home/model/useGeneratedAudios.ts` | 配音列表 hook | | `frontend/src/features/home/ui/GeneratedAudiosPanel.tsx` | 配音列表面板 | #### 前端修改 | 文件 | 变更 | |------|------| | `frontend/src/features/home/ui/HomePage.tsx` | 面板重排序 + 素材区门控 + 插入 GeneratedAudiosPanel | | `frontend/src/features/home/ui/MaterialSelector.tsx` | 新增 `selectedAudioDuration` prop + 时长信息显示 | | `frontend/src/features/home/ui/GenerateActionBar.tsx` | 禁用条件改为 `!selectedAudio` | | `frontend/src/features/home/model/useHomeController.ts` | 集成 useGeneratedAudios、新增 handleGenerateAudio、修改 handleGenerate 使用 generated_audio_id | | `frontend/src/features/home/model/useHomePersistence.ts` | 新增 selectedAudioId 持久化 | --- ## 🎞️ 素材时间轴编排 — 第二阶段 (Day 23) ### 概述 在第一阶段"配音前置"基础上,新增**时间轴编辑器**,用户可以: 1. 在音频波形上查看各素材块的时长分配 2. 拖拽分割线调整每段素材的时长(无缝铺满,调整一段自动压缩/扩展相邻段) 3. 为每段素材设置**源视频截取起点**(从视频任意位置开始,而非始终从头) **旧行为**: 多素材时自动均分(`_split_equal`),无法控制每段时长和源视频起始点 **新行为**: 时间轴编辑器可视化分配 + 拖拽调整 + ClipTrimmer 截取设置 --- ### 一、后端改动 #### 1.1 新增 `CustomAssignment` 模型 ```python # backend/app/modules/videos/schemas.py class CustomAssignment(BaseModel): material_path: str start: float # 音频时间轴起点 end: float # 音频时间轴终点 source_start: float = 0.0 # 源视频截取起点 ``` `GenerateRequest` 新增 `custom_assignments: Optional[List[CustomAssignment]] = None`。存在时跳过 Whisper 均分,直接使用用户定义的分配。 #### 1.2 `prepare_segment` 支持 `source_start` ```python def prepare_segment(self, video_path, target_duration, output_path, target_resolution=None, source_start: float = 0.0): ``` 关键逻辑: - `source_start > 0` 时使用 `-ss` 快速 seek,并强制重编码(避免 stream copy 关键帧不精确) - 当需要循环且有 `source_start` 时,先裁剪出 `source_start` 到视频结尾的片段,再循环裁剪后的文件(避免 `stream_loop` 从视频 0s 开始循环) - 裁剪临时文件在 `finally` 中自动清理 #### 1.3 `workflow.py` 支持 `custom_assignments` - **多素材模式**: `custom_assignments` 存在时,直接使用用户分配(仍运行 Whisper 生成字幕),每个 `prepare_segment` 调用传入 `source_start` - **单素材模式**: `custom_assignments` 有 1 条且 `source_start > 0` 时,先截取片段再传入 LatentSync - **向后兼容**: `custom_assignments` 为 `None` 时完全走旧路径 --- ### 二、前端新增组件 #### 2.1 `useTimelineEditor.ts` — 时间轴段管理 hook ```typescript interface TimelineSegment { id: string; // React key materialId: string; // 素材 ID materialName: string; // 显示名 start: number; // 音频时间轴开始秒数 end: number; // 音频时间轴结束秒数 sourceStart: number; // 源视频截取起点(默认 0) sourceEnd: number; // 源视频截取终点(0 = 到结尾) color: string; // 色块颜色 } ``` 核心方法: - `initSegments()`: selectedMaterials 变化时按数量均分 audioDuration - `resizeSegment(id, newEnd)`: 拖拽右边界,约束每段最小 1s - `setSourceRange(id, sourceStart, sourceEnd)`: 设置截取范围 - `toCustomAssignments()`: 转为后端 `CustomAssignment[]` 格式 #### 2.2 `TimelineEditor.tsx` — 波形 + 色块时间轴 - **wavesurfer.js** 渲染音频波形(仅展示,不播放) - 色块层按比例排列,显示素材名 + 时长 + 截取标记 - 色块间分割线可拖拽(`onPointerDown/Move/Up` 实现连续像素拖拽) - 点击色块打开 ClipTrimmer #### 2.3 `ClipTrimmer.tsx` — 素材截取模态框 - HTML5 `