222 lines
11 KiB
Markdown
222 lines
11 KiB
Markdown
## 🔧 多素材生成优化与健壮性加固 (Day 22)
|
||
|
||
### 概述
|
||
对 Day 21 实现的多素材视频生成(多机位)功能进行全面审查,修复 6 个高优先级 Bug、完成 8 项体验优化,并将多素材流水线从"逐段 LatentSync"重构为"先拼接再推理"架构,推理次数从 N 次降为 1 次。
|
||
|
||
---
|
||
|
||
### 一、后端高优 Bug 修复
|
||
|
||
#### 1. `_split_equal()` 素材数 > 字符数边界溢出
|
||
- **问题**: 5 个素材但只有 2 个 Whisper 字符时,边界索引重复,部分素材被跳过
|
||
- **修复**: 加入 `n = min(n, len(all_chars))` 上限保护
|
||
- **文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
#### 2. 多素材 LatentSync 单段失败无 fallback
|
||
- **问题**: 单素材模式下 LatentSync 失败会 fallback 到原始素材,但多素材模式直接抛异常,整个任务失败
|
||
- **修复**: 多素材循环中加 try-except,失败时 fallback 到原始素材片段
|
||
- **文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
#### 3. `num_segments == 0` 时 ZeroDivisionError
|
||
- **问题**: 所有 assignments 被跳过后 `i / num_segments` 触发除零
|
||
- **修复**: 循环前加 `if num_segments == 0` 检查并抛出明确错误
|
||
- **文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
#### 4. `split_audio` 未校验 duration > 0
|
||
- **问题**: `end <= start` 时 FFmpeg 行为异常
|
||
- **修复**: 加入 `if duration <= 0: raise ValueError(...)`
|
||
- **文件**: `backend/app/services/video_service.py`
|
||
|
||
#### 5. Whisper 失败时按时长均分兜底
|
||
- **问题**: Whisper 失败后直接退化为单素材,其他素材被浪费
|
||
- **修复**: 按 `audio_duration / len(material_paths)` 均分,不依赖字符对齐
|
||
- **文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
#### 6. `concat_videos` 空列表未检查
|
||
- **问题**: 传入空 `video_paths` 时 FFmpeg 报错
|
||
- **修复**: 加入 `if not video_paths: raise ValueError(...)`
|
||
- **文件**: `backend/app/services/video_service.py`
|
||
|
||
---
|
||
|
||
### 二、前端优化
|
||
|
||
#### 1. payload 构建非空断言修复
|
||
- `m!.path` → `m?.path` + `.filter(Boolean)`,防止素材被删后 crash
|
||
- **文件**: `frontend/src/features/home/model/useHomeController.ts`
|
||
|
||
#### 2. 生成按钮展示后端进度消息
|
||
- 新增 `message` prop,生成中显示如"(正在处理片段 2/3...)"
|
||
- **文件**: `frontend/src/features/home/ui/GenerateActionBar.tsx`, `HomePage.tsx`
|
||
|
||
#### 3. 新上传素材自动选中
|
||
- 上传成功后对比前后素材列表,新增的 ID 自动追加到 `selectedMaterials`
|
||
- **文件**: `frontend/src/features/home/model/useMaterials.ts`
|
||
|
||
#### 4. Material 接口统一
|
||
- 三处 `interface Material` 重复定义提取到 `shared/types/material.ts`
|
||
- **文件**: `frontend/src/shared/types/material.ts` (新建), `useMaterials.ts`, `useHomeController.ts`, `MaterialSelector.tsx`
|
||
|
||
#### 5. 拖拽排序修复
|
||
- 移除 `DragOverlay`(`backdrop-blur` 创建新 containing block 导致定位错乱)
|
||
- 改为 `useSortable` 原生拖拽 + `CSS.Translate`,拖拽中元素高亮加阴影
|
||
- **文件**: `frontend/src/features/home/ui/MaterialSelector.tsx`
|
||
|
||
#### 6. 素材选择上限 4 个
|
||
- `toggleMaterial` 新增 `MAX_MATERIALS = 4` 限制
|
||
- UI 选满后未选中项变半透明禁用,提示文字改为"可多选,最多4个"
|
||
- **文件**: `useMaterials.ts`, `MaterialSelector.tsx`
|
||
|
||
#### 7. 移动端排序区域响应式
|
||
- 素材列表 `max-h-64` → `max-h-48 sm:max-h-64`
|
||
- **文件**: `MaterialSelector.tsx`
|
||
|
||
#### 8. 多素材耗时提示
|
||
- 选中 ≥2 素材时生成按钮下方显示"多素材模式 (N 个机位),生成耗时较长"
|
||
- **文件**: `GenerateActionBar.tsx`, `HomePage.tsx`
|
||
|
||
---
|
||
|
||
### 三、核心架构重构:先拼接再推理
|
||
|
||
#### V1 (Day 21): 逐段 LatentSync
|
||
```
|
||
素材A → LatentSync(素材A, 音频片段1) → lipsync_A
|
||
素材B → LatentSync(素材B, 音频片段2) → lipsync_B
|
||
FFmpeg concat(lipsync_A, lipsync_B) → 最终视频
|
||
```
|
||
- 缺点:N 个素材 = N 次 LatentSync 推理(每次 ~30s)
|
||
|
||
#### V2 (Day 22): 先拼接再推理
|
||
```
|
||
素材A → prepare_segment(裁剪到3.67s) → prepared_A
|
||
素材B → prepare_segment(裁剪到4.00s) → prepared_B
|
||
FFmpeg concat(prepared_A, prepared_B) → concat_video (7.67s)
|
||
LatentSync(concat_video, 完整音频) → 最终视频
|
||
```
|
||
- 优点:只需 **1 次** LatentSync 推理,时间从 N×30s 降为 1×30s
|
||
|
||
#### 新增 `prepare_segment()` 方法
|
||
```python
|
||
def prepare_segment(self, video_path, target_duration, output_path, target_resolution=None):
|
||
# 素材时长 > 目标: 裁剪 (-t)
|
||
# 素材时长 < 目标: 循环 (-stream_loop) + 裁剪
|
||
# 分辨率一致: -c copy 无损 (不重编码)
|
||
# 分辨率不一致: scale + pad 统一到第一个素材分辨率
|
||
```
|
||
|
||
#### 分辨率处理策略
|
||
- 新增 `get_resolution()` 方法检测各素材分辨率
|
||
- 所有素材分辨率相同时:`-c copy` 无损裁剪(保持原画质)
|
||
- 分辨率不一致时:统一到第一个素材的分辨率,`force_original_aspect_ratio=decrease` + `pad` 居中
|
||
- LatentSync 只处理嘴部 512×512 区域,输出保持原分辨率
|
||
|
||
#### 时间对齐验证
|
||
|
||
| 环节 | 时间基准 | 对齐关系 |
|
||
|------|---------|---------|
|
||
| TTS 音频 | 原始时长 (7.67s) | 基准 |
|
||
| Whisper 字幕 | 基于 TTS 音频 | 时间戳对齐音频 |
|
||
| 均分切分 | assignments 总时长 = 音频时长 | 首段 start=0, 末段 end=audio_duration |
|
||
| prepare 各段 | `-t seg_dur` 精确截断 | 总和 ≈ 音频时长 |
|
||
| LatentSync | concat_video + 完整音频 | 内部 0.5s 容差 |
|
||
| compose | lipsync_video + 音频/BGM | `-shortest` 保证同步 |
|
||
| Remotion | 基于 captions_path 渲染字幕 | 时间戳对齐音频 |
|
||
|
||
---
|
||
|
||
### 涉及文件汇总
|
||
|
||
| 文件 | 变更类型 | 说明 |
|
||
|------|----------|------|
|
||
| `backend/app/modules/videos/workflow.py` | 修改 | 6 个 Bug 修复 + 流水线重构(先拼接再推理)|
|
||
| `backend/app/services/video_service.py` | 修改 | 新增 `prepare_segment()`、`get_resolution()`,`split_audio` 校验,`concat_videos` 空列表检查 |
|
||
| `frontend/src/shared/types/material.ts` | 新建 | 统一 Material 接口 |
|
||
| `frontend/src/features/home/model/useMaterials.ts` | 修改 | 上传自动选中、素材上限 4 个 |
|
||
| `frontend/src/features/home/model/useHomeController.ts` | 修改 | payload 非空断言修复、Material 接口引用 |
|
||
| `frontend/src/features/home/ui/MaterialSelector.tsx` | 修改 | 拖拽修复、上限 4 个 UI、移动端响应式 |
|
||
| `frontend/src/features/home/ui/GenerateActionBar.tsx` | 修改 | 进度消息展示、多素材耗时提示 |
|
||
| `frontend/src/features/home/ui/HomePage.tsx` | 修改 | 传递 message、materialCount prop |
|
||
|
||
---
|
||
|
||
### 四、AI 多语言翻译
|
||
|
||
#### 功能
|
||
在文案编辑区新增「AI多语言」按钮,支持将中文口播文案一键翻译为 9 种语言,并可随时还原原文。
|
||
|
||
#### 支持语言
|
||
英语 English、日语 日本語、韩语 한국어、法语 Français、德语 Deutsch、西班牙语 Español、俄语 Русский、意大利语 Italiano、葡萄牙语 Português
|
||
|
||
#### 实现
|
||
|
||
##### 后端
|
||
- **`backend/app/services/glm_service.py`** — 新增 `translate_text()` 方法,调用智谱 GLM API(temperature=0.3),prompt 要求只返回译文、保持语气风格
|
||
- **`backend/app/modules/ai/router.py`** — 新增 `POST /api/ai/translate` 接口,接收 `{text, target_lang}`,返回 `{translated_text}`
|
||
|
||
##### 前端
|
||
- **`frontend/src/features/home/ui/ScriptEditor.tsx`** — 新增 `LANGUAGES` 列表(9 种语言)、语言下拉菜单(点击外部自动关闭)、翻译中 loading 状态、「还原原文」按钮(翻译过后出现在菜单顶部)
|
||
- **`frontend/src/features/home/model/useHomeController.ts`** — 新增 `handleTranslate`(调用翻译 API、首次翻译保存原文)、`originalText` 状态、`handleRestoreOriginal`(恢复原文)
|
||
|
||
#### 涉及文件
|
||
|
||
| 文件 | 变更 | 说明 |
|
||
|------|------|------|
|
||
| `backend/app/services/glm_service.py` | 修改 | 新增 `translate_text()` 方法 |
|
||
| `backend/app/modules/ai/router.py` | 修改 | 新增 `/api/ai/translate` 接口 |
|
||
| `frontend/src/features/home/ui/ScriptEditor.tsx` | 修改 | 语言菜单 UI、翻译 loading、还原原文按钮 |
|
||
| `frontend/src/features/home/model/useHomeController.ts` | 修改 | `handleTranslate`、`originalText`、`handleRestoreOriginal` |
|
||
|
||
---
|
||
|
||
### 五、TTS 多语言支持
|
||
|
||
#### 背景
|
||
翻译功能实现后,用户可将中文文案翻译为其他语言。但翻译后生成视频时 TTS 仍只支持中文:
|
||
- **EdgeTTS**:声音列表只有 5 个 `zh-CN-*` 中文声音
|
||
- **声音克隆 (Qwen3-TTS)**:`language` 参数硬编码为 `"Chinese"`
|
||
|
||
#### 实现方案
|
||
|
||
##### 1. 前端:语言感知的声音列表
|
||
- `VOICES` 从扁平数组扩展为 `Record<string, VoiceOption[]>`,覆盖 10 种语言(zh-CN / en-US / ja-JP / ko-KR / fr-FR / de-DE / es-ES / ru-RU / it-IT / pt-BR),每种语言 2 个声音(男/女)
|
||
- 新增 `LANG_TO_LOCALE` 映射:翻译目标语言名 → EdgeTTS locale(如 `"English" → "en-US"`)
|
||
- 新增 `textLang` 状态,跟踪当前文案语言,默认 `"zh-CN"`
|
||
|
||
##### 2. 翻译时自动切换声音
|
||
- `handleTranslate` 成功后:根据目标语言设置 `textLang`,EdgeTTS 模式下自动切换 `voice` 为目标语言的默认声音
|
||
- `handleRestoreOriginal` 还原时:重置 `textLang` 为 `"zh-CN"`,恢复中文默认声音
|
||
- `VoiceSelector` 根据 `textLang` 动态显示对应语言的声音列表
|
||
|
||
##### 3. 声音克隆语言透传
|
||
- 前端:新增 `LOCALE_TO_QWEN_LANG` 映射(`zh-CN→"Chinese"`, `en-US→"English"`, 其他→`"Auto"`)
|
||
- 生成请求 payload 加入 `language` 字段(仅声音克隆模式)
|
||
- 后端 `GenerateRequest` schema 新增 `language: str = "Chinese"` 字段
|
||
- `workflow.py`:`language="Chinese"` 硬编码改为 `language=req.language`
|
||
|
||
##### 4. Bug 修复:textLang 持久化
|
||
- **问题**: `voice` 已持久化但 `textLang` 未持久化,刷新页面后 `voice` 恢复为英文声音但 `textLang` 默认回中文,导致 VoiceSelector 显示中文声音列表却选中英文声音,无高亮按钮
|
||
- **修复**: 在 `useHomePersistence` 中加入 `textLang` 的 localStorage 读写
|
||
|
||
#### 数据流
|
||
|
||
```
|
||
用户翻译 "English"
|
||
→ ScriptEditor.onTranslate("English")
|
||
→ LANG_TO_LOCALE["English"] = "en-US"
|
||
→ setTextLang("en-US"), setVoice("en-US-GuyNeural")
|
||
→ VoiceSelector 显示 VOICES["en-US"] = [Guy, Jenny]
|
||
→ 生成时:
|
||
EdgeTTS: payload.voice = "en-US-GuyNeural"
|
||
声音克隆: payload.language = "English" (via getQwenLanguage)
|
||
```
|
||
|
||
#### 涉及文件
|
||
|
||
| 文件 | 变更 | 说明 |
|
||
|------|------|------|
|
||
| `frontend/src/features/home/model/useHomeController.ts` | 修改 | VOICES 多语言 Record、textLang 状态、LANG_TO_LOCALE / LOCALE_TO_QWEN_LANG 映射、翻译自动切换 voice |
|
||
| `frontend/src/features/home/model/useHomePersistence.ts` | 修改 | textLang 持久化读写 |
|
||
| `backend/app/modules/videos/schemas.py` | 修改 | GenerateRequest 加 `language` 字段 |
|
||
| `backend/app/modules/videos/workflow.py` | 修改 | 声音克隆调用处用 `req.language` 替代硬编码 |
|