406 lines
20 KiB
Markdown
406 lines
20 KiB
Markdown
## Remotion 缓存修复 + 编码流水线质量优化 + 唇形同步容错 + 统一下拉交互 (Day 30)
|
||
|
||
### 概述
|
||
|
||
本轮最终合并为五大方面:(1) Remotion bundle 缓存导致标题/字幕丢失的严重 Bug;(2) 全面优化 LatentSync + MuseTalk 双引擎编码流水线,消除冗余有损编码;(3) 增强 LatentSync 的鲁棒性,允许素材中部分帧检测不到人脸时继续推理而非中断任务;(4) 唇形模型选择全链路透传(默认/快速/高级);(5) 首页与发布页选择器统一为 SelectPopover 交互,并修复遮挡、定位与预览层级问题。
|
||
|
||
---
|
||
|
||
## ✅ 改动内容
|
||
|
||
### 1. Remotion Bundle 缓存 404 修复(严重 Bug)
|
||
|
||
- **问题**: 生成的视频没有标题和字幕,Remotion 渲染失败后静默回退到 FFmpeg(无文字叠加能力)
|
||
- **根因**: Remotion 的 bundle 缓存机制只在首次打包时复制 `publicDir`(视频/字体所在目录)。代码稳定后缓存持续命中,新生成的视频和字体文件不在旧缓存的 `public/` 目录 → Remotion HTTP server 返回 404 → 渲染失败
|
||
- **尝试**: 先用 `fs.symlinkSync` 符号链接,但 Remotion 内部 HTTP server 不支持跟随符号链接
|
||
- **最终方案**: 使用 `fs.linkSync` 硬链接(同文件系统零拷贝,对应用完全透明),跨文件系统时自动回退为 `fs.copyFileSync`
|
||
|
||
**文件**: `remotion/render.ts`
|
||
|
||
```typescript
|
||
function ensureInCachedPublic(cachedPublicDir, srcAbsPath, fileName) {
|
||
// 检查是否已存在且为同一 inode
|
||
// 优先硬链接(零拷贝),跨文件系统回退为复制
|
||
try {
|
||
fs.linkSync(srcAbsPath, cachedPath);
|
||
} catch {
|
||
fs.copyFileSync(srcAbsPath, cachedPath);
|
||
}
|
||
}
|
||
```
|
||
|
||
使用缓存 bundle 时,自动将当前渲染所需的文件(视频 + 字体)硬链接到缓存的 `public/` 目录:
|
||
- 视频文件(`videoFileName`)
|
||
- 字体文件(从 `subtitleStyle` / `titleStyle` / `secondaryTitleStyle` 的 `font_file` 字段提取)
|
||
|
||
---
|
||
|
||
### 2. 视频编码流水线质量优化
|
||
|
||
对完整流水线做全面审查,发现从素材上传到最终输出,视频最多经历 **5-6 次有损重编码**,而官方 LatentSync demo 只有 1-2 次。
|
||
|
||
#### 优化前编码链路
|
||
|
||
| # | 阶段 | CRF | 问题 |
|
||
|---|------|-----|------|
|
||
| 1 | 方向归一化 | 23 | 条件触发 |
|
||
| 2 | `prepare_segment` 缩放+时长 | 23 | 必经,质量偏低 |
|
||
| 3 | LatentSync `read_video` FPS 转换 | 18 | **即使已是 25fps 也重编码** |
|
||
| 4 | LatentSync `imageio` 写帧 | 13 | 模型输出 |
|
||
| 5 | LatentSync final mux | 18 | **CRF13 刚写完立刻 CRF18 重编码** |
|
||
| 6 | compose | copy | Day29 已优化 |
|
||
| 7 | 多素材 concat | 23 | **段参数已统一,不需要重编码** |
|
||
| 8 | Remotion 渲染 | ~18 | 必经(叠加文字) |
|
||
|
||
#### 优化措施
|
||
|
||
##### 2a. LatentSync `read_video` 跳过冗余 FPS 重编码
|
||
|
||
**文件**: `models/LatentSync/latentsync/utils/util.py`
|
||
|
||
- 原代码无条件执行 `ffmpeg -r 25 -crf 18`,即使输入视频已是 25fps
|
||
- 新增 FPS 检测:`abs(current_fps - 25.0) < 0.5` 时直接使用原文件
|
||
- 我们的 `prepare_segment` 已统一输出 25fps,此步完全多余
|
||
|
||
```python
|
||
cap = cv2.VideoCapture(video_path)
|
||
current_fps = cap.get(cv2.CAP_PROP_FPS)
|
||
cap.release()
|
||
|
||
if abs(current_fps - 25.0) < 0.5:
|
||
print(f"Video already at {current_fps:.1f}fps, skipping FPS conversion")
|
||
target_video_path = video_path
|
||
else:
|
||
# 仅非 25fps 时才重编码
|
||
command = f"ffmpeg ... -r 25 -crf 18 ..."
|
||
```
|
||
|
||
##### 2b. LatentSync final mux 流复制替代重编码
|
||
|
||
**文件**: `models/LatentSync/latentsync/pipelines/lipsync_pipeline.py`
|
||
|
||
- 原代码:`imageio` 以 CRF 13 高质量写完帧后,final mux 又用 `libx264 -crf 18` 完整重编码
|
||
- 修复:改为 `-c:v copy` 流复制,仅 mux 音频轨,视频零损失
|
||
|
||
```diff
|
||
- ffmpeg ... -c:v libx264 -crf 18 -c:a aac -q:v 0 -q:a 0
|
||
+ ffmpeg ... -c:v copy -c:a aac -q:a 0
|
||
```
|
||
|
||
##### 2c. `prepare_segment` + `normalize_orientation` CRF 23 → 18
|
||
|
||
**文件**: `backend/app/services/video_service.py`
|
||
|
||
- `normalize_orientation`:CRF 23 → 18
|
||
- `prepare_segment` trim 临时文件:CRF 23 → 18
|
||
- `prepare_segment` 主命令:CRF 23 → 18
|
||
- CRF 18 是"高质量"级别,与 LatentSync 内部标准一致
|
||
|
||
##### 2d. 多素材 concat 流复制
|
||
|
||
**文件**: `backend/app/services/video_service.py`
|
||
|
||
- 原代码用 `libx264 -crf 23` 重编码拼接
|
||
- 所有段已由 `prepare_segment` 统一为相同分辨率/帧率/编码参数
|
||
- 改为 `-c:v copy` 流复制,消除一次完整重编码
|
||
|
||
```diff
|
||
- -vsync cfr -r 25 -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p
|
||
+ -c:v copy
|
||
```
|
||
|
||
#### 优化后编码链路
|
||
|
||
| # | 阶段 | CRF | 状态 |
|
||
|---|------|-----|------|
|
||
| 1 | 方向归一化 | **18** | 提质(条件触发) |
|
||
| 2 | `prepare_segment` | **18** | 提质(必经) |
|
||
| 3 | ~~LatentSync FPS 转换~~ | - | **已消除** |
|
||
| 4 | LatentSync 模型输出 | 13 | 不变(不可避免) |
|
||
| 5 | ~~LatentSync final mux~~ | - | **已消除(copy)** |
|
||
| 6 | compose | copy | 不变 |
|
||
| 7 | ~~多素材 concat~~ | - | **已消除(copy)** |
|
||
| 8 | Remotion 渲染 | ~18 | 不变(不可避免) |
|
||
|
||
**总计:5-6 次有损编码 → 3 次**(prepare_segment → LatentSync 模型输出 → Remotion),质量损失减少近一半。
|
||
|
||
---
|
||
|
||
## 📁 修改文件清单
|
||
|
||
| 文件 | 改动 |
|
||
|------|------|
|
||
| `remotion/render.ts` | bundle 缓存使用时硬链接视频+字体到 public 目录 |
|
||
| `models/LatentSync/latentsync/utils/util.py` | `read_video` 检测 FPS,25fps 时跳过重编码 |
|
||
| `models/LatentSync/latentsync/pipelines/lipsync_pipeline.py` | final mux `-c:v copy`;无脸帧容错(affine_transform + restore_video) |
|
||
| `backend/app/services/video_service.py` | `normalize_orientation` CRF 23→18;`prepare_segment` CRF 23→18;`concat_videos` `-c:v copy` |
|
||
| `backend/app/modules/videos/workflow.py` | 单素材 LatentSync 异常时回退原视频 |
|
||
|
||
---
|
||
|
||
### 3. LatentSync 无脸帧容错
|
||
|
||
- **问题**: 素材中如果有部分帧检测不到人脸(转头、遮挡、空镜头),`affine_transform` 会抛异常导致整个推理任务失败
|
||
- **改动**:
|
||
- `affine_transform_video`: 单帧异常时 catch 住,用最近有效帧的 face/box/affine_matrix 填充(保证 tensor batch 维度完整),全部帧无脸时仍 raise
|
||
- `restore_video`: 新增 `valid_face_flags` 参数,无脸帧直接保留原画面(不做嘴型替换)
|
||
- `loop_video`: `valid_face_flags` 跟随循环和翻转
|
||
- `workflow.py`: 单素材路径 `lipsync.generate()` 整体异常时 copy 原视频继续流程,任务不会失败
|
||
|
||
---
|
||
|
||
### 4. MuseTalk 编码链路优化
|
||
|
||
#### 4a. FFmpeg rawvideo 管道直编码(消除中间有损文件)
|
||
|
||
**文件**: `models/MuseTalk/scripts/server.py`
|
||
|
||
- **原流程**: UNet 推理帧 → `cv2.VideoWriter(mp4v)` 写中间文件(有损) → FFmpeg 重编码+音频 mux(又一次有损)
|
||
- **新流程**: UNet 推理帧 → FFmpeg rawvideo stdin 管道 → 一次 libx264 编码+音频 mux
|
||
|
||
```python
|
||
ffmpeg_cmd = [
|
||
"ffmpeg", "-y", "-v", "warning",
|
||
"-f", "rawvideo", "-pix_fmt", "bgr24",
|
||
"-s", f"{w}x{h}", "-r", str(fps),
|
||
"-i", "-", # stdin 管道输入
|
||
"-i", audio_path,
|
||
"-c:v", "libx264", "-preset", ENCODE_PRESET, "-crf", str(ENCODE_CRF),
|
||
"-pix_fmt", "yuv420p",
|
||
"-c:a", "copy", "-shortest",
|
||
output_vid_path,
|
||
]
|
||
ffmpeg_proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, ...)
|
||
# 每帧直接 pipe_in.write(frame.tobytes())
|
||
```
|
||
|
||
关键实现细节:
|
||
- `-pix_fmt bgr24` 匹配 OpenCV 原生帧格式,零转换开销
|
||
- `np.ascontiguousarray` 确保帧内存连续
|
||
- `BrokenPipeError` 捕获 + return code 检查覆盖异常路径
|
||
- `pipe_in.close()` 在 `ffmpeg_proc.wait()` 之前,正确发送 EOF
|
||
- 合成 fallback(resize 失败、mask 失败、blending 失败)均通过 `_write_pipe_frame` 输出原帧
|
||
|
||
#### 4b. MuseTalk 参数环境变量化 + 质量优先档
|
||
|
||
**文件**: `models/MuseTalk/scripts/server.py` + `backend/.env`
|
||
|
||
所有推理与编码参数从硬编码改为 `.env` 可配置,当前使用"质量优先"档:
|
||
|
||
| 参数 | 原默认值 | 质量优先值 | 作用 |
|
||
|------|----------|-----------|------|
|
||
| `MUSETALK_DETECT_EVERY` | 5 | **2** | 人脸检测频率 ↑2.5x,画面跟踪更稳 |
|
||
| `MUSETALK_BLEND_CACHE_EVERY` | 5 | **2** | mask 更新更频,面部边缘融合更干净 |
|
||
| `MUSETALK_EXTRA_MARGIN` | 15 | **14** | 下巴区域微调 |
|
||
| `MUSETALK_BLEND_MODE` | auto | **jaw** | v1.5 显式 jaw 模式 |
|
||
| `MUSETALK_ENCODE_CRF` | 18 | **14** | 接近视觉无损(输出还要进 Remotion 再编码) |
|
||
| `MUSETALK_ENCODE_PRESET` | medium | **slow** | 同 CRF 下压缩效率更高 |
|
||
| `MUSETALK_AUDIO_PADDING` | 2/2 | 2/2 | 不变 |
|
||
| `MUSETALK_FACEPARSING_CHEEK` | 90/90 | 90/90 | 不变 |
|
||
|
||
新增可配置参数完整列表:`DETECT_EVERY`、`BLEND_CACHE_EVERY`、`AUDIO_PADDING_LEFT/RIGHT`、`EXTRA_MARGIN`、`DELAY_FRAME`、`BLEND_MODE`、`FACEPARSING_LEFT/RIGHT_CHEEK_WIDTH`、`ENCODE_CRF`、`ENCODE_PRESET`。
|
||
|
||
---
|
||
|
||
### 5. Workflow 异步防阻塞 + compose 跳过优化
|
||
|
||
#### 5a. 阻塞调用线程池化
|
||
|
||
**文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
workflow 中多处同步 FFmpeg 调用会阻塞 asyncio 事件循环,导致其他 API 请求(健康检查、任务状态查询)无法响应。新增通用辅助函数 `_run_blocking()`,将所有阻塞调用统一走线程池:
|
||
|
||
```python
|
||
async def _run_blocking(func, *args):
|
||
"""在线程池执行阻塞函数,避免卡住事件循环。"""
|
||
loop = asyncio.get_running_loop()
|
||
return await loop.run_in_executor(None, func, *args)
|
||
```
|
||
|
||
已改造的阻塞调用点:
|
||
|
||
| 调用 | 位置 | 说明 |
|
||
|------|------|------|
|
||
| `video.normalize_orientation()` | 单素材旋转归一化 | FFmpeg 旋转/转码 |
|
||
| `video.prepare_segment()` | 多素材片段准备 | FFmpeg 缩放+时长裁剪,配合 `asyncio.gather` 多段并行 |
|
||
| `video.concat_videos()` | 多素材拼接 | FFmpeg concat |
|
||
| `video.prepare_segment()` | 单素材 prepare | FFmpeg 缩放+时长裁剪 |
|
||
| `video.mix_audio()` | BGM 混音 | FFmpeg 音频混合 |
|
||
| `video._get_duration()` | 音频/视频时长探测 (3处) | ffprobe 子进程 |
|
||
|
||
#### 5b. `prepare_segment` 同分辨率跳过 scale
|
||
|
||
**文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
原来无论素材分辨率是否已匹配目标,都强制传 `target_resolution` 给 `prepare_segment`,触发 scale filter + libx264 重编码。优化后逐素材比对分辨率:
|
||
|
||
- **多素材**: 逐段判断,分辨率匹配的传 `None`(`prepare_target_res = None if res == base_res else base_res`),走 `-c:v copy` 分支
|
||
- **单素材**: 先 `get_resolution` 比对,匹配则传 `None`
|
||
|
||
当分辨率匹配且无截取、不需要循环、不需要变帧率时,`prepare_segment` 内部走 `-c:v copy`,完全零损编码。
|
||
|
||
#### 5c. `_get_duration()` 线程池化
|
||
|
||
**文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
3 处 `video._get_duration()` 同步 ffprobe 调用改为 `await _run_blocking(video._get_duration, ...)`,避免阻塞事件循环。
|
||
|
||
#### 5d. compose 循环场景 CRF 统一
|
||
|
||
**文件**: `backend/app/services/video_service.py`
|
||
|
||
`compose()` 在视频需要循环时的编码从 CRF 23 提升到 CRF 18,与全流水线质量标准统一。
|
||
|
||
#### 5e. 多素材片段校验
|
||
|
||
**文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
多素材 `prepare_segment` 完成后新增片段数量一致性校验,避免空片段进入 concat 导致异常。
|
||
|
||
#### 5f. compose() 内部防阻塞
|
||
|
||
**文件**: `backend/app/services/video_service.py`
|
||
|
||
`compose()` 改为 `async def`,内部的 `_get_duration()` 和 `_run_ffmpeg()` 都通过 `loop.run_in_executor` 在线程池执行。
|
||
|
||
#### 5g. 无需二次 compose 直接透传
|
||
|
||
**文件**: `backend/app/modules/videos/workflow.py`
|
||
|
||
当没有 BGM 时(`final_audio_path == audio_path`),LatentSync/MuseTalk 输出已包含正确音轨,跳过多余的 compose 步骤:
|
||
|
||
```python
|
||
needs_audio_compose = str(final_audio_path) != str(audio_path)
|
||
```
|
||
|
||
- **Remotion 路径**: 音频没变则跳过 pre-compose,直接用 lipsync 输出进 Remotion
|
||
- **非 Remotion 路径**: 音频没变则 `shutil.copy` 直接透传 lipsync 输出,不再走 compose
|
||
|
||
---
|
||
|
||
### 6. 唇形模型选择全链路
|
||
|
||
前端“生成视频”按钮右侧新增模型选择,下拉值全链路透传到后端路由与推理服务。
|
||
|
||
#### 模型选项
|
||
|
||
| 选项 | 值 | 路由逻辑 |
|
||
|------|------|------|
|
||
| 默认模型 | `default` | 保持阈值路由(`LIPSYNC_DURATION_THRESHOLD`,当前建议 100s) |
|
||
| 快速模型 | `fast` | 强制 MuseTalk,不可用时回退 LatentSync |
|
||
| 高级模型 | `advanced` | 强制 LatentSync |
|
||
|
||
#### 最终 UI 形态
|
||
|
||
- 模型按钮由原生 `<select>` 升级为统一 `SelectPopover`
|
||
- 触发器文案改为业务语义(`默认模型 / 快速模型 / 高级模型` + `按时长智能路由 / 速度优先 / 质量优先`)
|
||
- 选择状态持久化到 `useHomePersistence`(`lipsyncModelMode`)
|
||
|
||
#### 数据流
|
||
|
||
```
|
||
前端 SelectPopover → setLipsyncModelMode("fast") → localStorage 持久化
|
||
↓
|
||
用户点击"生成视频" → handleGenerate()
|
||
→ payload.lipsync_model = lipsyncModelMode
|
||
→ POST /api/videos/generate { ..., lipsync_model: "fast" }
|
||
→ workflow: req.lipsync_model 透传给 lipsync.generate(model_mode=...)
|
||
→ lipsync_service.generate(): 按 model_mode 路由
|
||
→ fast: 强制 MuseTalk → 回退 LatentSync
|
||
→ advanced: 强制 LatentSync
|
||
→ default: 阈值策略
|
||
```
|
||
|
||
---
|
||
|
||
### 7. 首页/发布页统一下拉交互(SelectPopover)
|
||
|
||
#### 7a. 统一改造范围
|
||
|
||
首页与发布页的业务选择项统一迁移到 `SelectPopover`:
|
||
|
||
- 首页:音色、参考音频、配音列表、素材选择、BGM 选择、作品选择、标题显示模式、标题/副标题/字幕样式、时间轴画面比例、唇形模型
|
||
- 发布页:选择发布作品(搜索 + 预览)
|
||
|
||
例外:`ScriptEditor` 的“历史文案 / AI多语言”按产品要求恢复为原有轻量菜单,不强制统一。
|
||
|
||
#### 7b. 关键交互修复
|
||
|
||
- **遮挡修复**:桌面端面板改为 `Portal + fixed`,脱离局部 stacking context,彻底解决被卡片遮挡
|
||
- **上拉/下拉自适应**:底部空间不足时自动上拉,避免菜单显示不全
|
||
- **同宽展示**:面板宽度与触发器保持一致
|
||
- **风格统一**:面板背景加实(高不透明度),滚动条隐藏但可滚动
|
||
- **已选定位**:再次打开下拉时自动滚动到已选项(`data-popover-selected="true"`)
|
||
- **预览协同**:
|
||
- 下拉内点“预览”不强制关闭,支持连续预览
|
||
- 视频预览弹窗层级高于下拉,避免被遮挡
|
||
- 预览弹窗打开时,下拉不会因外部点击/Esc被误关闭;关闭预览后仍可继续操作
|
||
|
||
#### 7c. BGM 面板收敛
|
||
|
||
- BGM 改为与“发布作品”同款选择器(搜索 + 列表 + 试听 + 选中态)
|
||
- 按产品要求移除首页 BGM 音量滑杆
|
||
- 生成请求统一使用固定 `bgm_volume=0.2`
|
||
|
||
---
|
||
|
||
## 📁 总修改文件清单
|
||
|
||
| 文件 | 改动 |
|
||
|------|------|
|
||
| `remotion/render.ts` | bundle 缓存使用时硬链接视频+字体到 public 目录 |
|
||
| `models/LatentSync/latentsync/utils/util.py` | `read_video` 检测 FPS,25fps 时跳过重编码 |
|
||
| `models/LatentSync/latentsync/pipelines/lipsync_pipeline.py` | final mux `-c:v copy`;无脸帧容错 |
|
||
| `backend/app/services/video_service.py` | CRF 23→18;`concat_videos` copy;`compose()` 异步化 + 循环 CRF 18 |
|
||
| `backend/app/modules/videos/workflow.py` | 线程池化;同分辨率跳过 scale;compose 跳过;片段校验;模型选择透传 |
|
||
| `backend/app/modules/videos/schemas.py` | 新增 `lipsync_model` 字段 |
|
||
| `backend/app/services/lipsync_service.py` | `generate()` 新增 `model_mode` 三路分支路由 |
|
||
| `models/MuseTalk/scripts/server.py` | FFmpeg rawvideo 管道;参数环境变量化 |
|
||
| `backend/.env` | MuseTalk 推理/融合/编码参数可配;路由阈值与质量档调优 |
|
||
| `frontend/src/shared/ui/SelectPopover.tsx` | 新增统一选择器:Portal+fixed、防遮挡、上拉/下拉自适应、同宽、隐藏滚动条、已选定位、预览协同 |
|
||
| `frontend/src/features/home/ui/HomePage.tsx` | 配音卡层级修复;传递统一下拉状态 |
|
||
| `frontend/src/features/home/model/useHomeController.ts` | `lipsyncModelMode` 透传;BGM 固定 `bgm_volume=0.2` |
|
||
| `frontend/src/features/home/model/useHomePersistence.ts` | 模型模式等新增字段持久化 |
|
||
| `frontend/src/features/home/ui/GenerateActionBar.tsx` | 模型选择改为 SelectPopover(速度/质量语义文案) |
|
||
| `frontend/src/features/home/ui/VoiceSelector.tsx` | 音色选择统一为 SelectPopover(音色名+语言) |
|
||
| `frontend/src/features/home/ui/RefAudioPanel.tsx` | 参考音频选择统一为 SelectPopover(含试听/重命名/删除/重识别) |
|
||
| `frontend/src/features/home/ui/GeneratedAudiosPanel.tsx` | 配音列表、语速、语气统一为 SelectPopover |
|
||
| `frontend/src/features/home/ui/MaterialSelector.tsx` | 素材选择改为发布页同款下拉(搜索/多选/预览/重命名/删除) |
|
||
| `frontend/src/features/home/ui/BgmPanel.tsx` | BGM 选择改为发布页同款下拉(搜索+试听),移除音量滑杆 |
|
||
| `frontend/src/features/home/ui/HistoryList.tsx` | 首页作品选择改为下拉(搜索+删除+选中态) |
|
||
| `frontend/src/features/home/ui/TitleSubtitlePanel.tsx` | 标题显示模式与样式选择统一为 SelectPopover |
|
||
| `frontend/src/features/home/ui/TimelineEditor.tsx` | 画面比例选择统一为 SelectPopover(单行按钮) |
|
||
| `frontend/src/features/publish/ui/PublishPage.tsx` | 发布作品选择改为 SelectPopover;预览时下拉保持打开 |
|
||
| `frontend/src/components/VideoPreviewModal.tsx` | 提升层级并添加预览标记,与下拉联动 |
|
||
| `frontend/src/features/home/ui/ScriptEditor.tsx` | 历史文案/AI多语言恢复原轻量菜单(产品例外) |
|
||
| `Docs/FRONTEND_DEV.md` | 新增 SelectPopover 规范、预览层级规范、持久化字段修订 |
|
||
|
||
---
|
||
|
||
## 🔍 验证
|
||
|
||
1. **标题字幕恢复**: 生成视频应有标题和逐字高亮字幕(Remotion 渲染成功,非 FFmpeg 回退)
|
||
2. **Remotion 日志**: 应出现 `Hardlinked into cached bundle:` 或 `Copied into cached bundle:` 而非 404
|
||
3. **LatentSync FPS 跳过**: 日志应出现 `Video already at 25.0fps, skipping FPS conversion`
|
||
4. **LatentSync mux**: FFmpeg 日志中 final mux 应为 `-c:v copy`
|
||
5. **画质对比**: 同一素材+音频,优化后生成的视频嘴型区域(尤其牙齿)应比优化前更清晰
|
||
6. **多素材拼接**: concat 步骤应为流复制,耗时从秒级降到毫秒级
|
||
7. **无脸帧容错**: 包含转头/遮挡帧的素材不再导致任务失败,无脸帧保留原画面
|
||
8. **MuseTalk 管道编码**: 日志中不应出现中间 mp4v 文件,合成阶段直接管道写入
|
||
9. **MuseTalk 质量参数**: `curl localhost:8011/health` 确认服务在线,生成视频嘴型边缘更清晰
|
||
10. **事件循环不阻塞**: 生成视频期间,`/api/tasks/{id}` 等接口应正常响应,不出现超时
|
||
11. **compose 跳过**: 无 BGM 时日志应出现 `Audio unchanged, skip pre-Remotion compose`
|
||
12. **同分辨率跳过 scale**: 素材已是目标分辨率时,`prepare_segment` 应走 `-c:v copy`(日志中无 scale filter)
|
||
13. **compose 循环 CRF**: 循环场景编码应为 CRF 18(非 23)
|
||
14. **模型选择 UI**: 生成按钮右侧应出现默认模型/快速模型/高级模型下拉
|
||
15. **模型选择持久化**: 切换模型后刷新页面,下拉应恢复上次选择
|
||
16. **快速模型路由**: 选择"快速模型"时,后端日志应出现 `强制快速模型:MuseTalk`
|
||
17. **高级模型路由**: 选择"高级模型"时,后端日志应出现 `强制高级模型:LatentSync`
|
||
18. **默认模型不变**: 选择"默认模型"时行为与改动前完全一致(阈值路由)
|
||
19. **统一下拉样式**: 首页/发布页业务选择项均为同款 SelectPopover(触发器 + 面板 + 选中态)
|
||
20. **上拉自适应**: 页面底部打开下拉时应自动上拉,不出现被截断
|
||
21. **已选定位**: 任意下拉再次打开时应自动定位到已选项,而非列表顶端
|
||
22. **预览层级**: 视频预览弹窗应始终覆盖在下拉之上,不被菜单遮挡
|
||
23. **连续预览**: 下拉内点击预览后菜单保持打开,关闭预览后可继续点击其他预览项
|
||
24. **BGM 行为**: 首页 BGM 不再显示音量滑杆,生成请求固定 `bgm_volume=0.2`
|