fix(cover): Worker本地ffmpeg抽帧 + E2源素材封面标题叠加 #1462

Merged
xiaoxia merged 1 commits from fix/cover-ffmpeg-title-overlay into develop 2026-08-23 10:43:40 +08:00
Owner

问题

  1. Worker 之前用 MediaKit 网络抽帧写 cover_url,因状态字符串 bug(#1461 已修)一直失败;在 API 请求里同步抽帧也容易超时挂死。
  2. 封面从源素材抽取时没有标题(源素材本身没烧录标题)。

修复

1. Worker 统一走本地 ffmpeg 抽帧(apps/worker/worker_app/tasks/generation.py)

  • _render_video 返回值从 (output_path, render_duration) 改为 (output_path, render_duration, cover_candidates)
  • RenderAdapter 渲染完成后已用本地 ffmpeg 从 output_path 抽出 cover_candidates(3 帧,已上传 OSS),直接复用
  • 第一帧写入 gen_task.cover_url,完整列表写入 gen_task.metadata["cover_candidates"]
  • 删除步骤 4.5 的 MediaKit 抽帧代码(不再依赖网络调用,不阻塞)

2. 标题文字叠加(packages/shared/title_overlay.py + thumbnail_generator.py)

  • 新增共享模块 packages/shared/title_overlay.py:Pillow 绘制白色文字 + 黑色描边/阴影,支持 CJK 字体和自动换行,API/Worker 共用
  • thumbnail_generator.extract_and_upload_cover_framestitle_text 参数真正生效
  • render_adapter.py 从已渲染视频抽帧时传空 title_text(标题已通过 ASS 烧录进画面,避免二次叠加重影)

3. 封面 API E2 兜底叠加标题(generation_cover.py)

  • E2 从源素材抽帧后,从 plan.config.title.text 读取标题并叠加到帧图
  • _persist_cover_frame 增加 title_text 参数,下载帧后用 Pillow 叠加再转存 OSS
  • A/B/C/D 路径命中的封面来自已渲染视频(带标题),无需再叠加

4. API 基础镜像依赖(infra/docker/api-base.Dockerfile + requirements-base.txt)

  • 增加 Pillow==10.4.0 和 fonts-noto-cjk / fontconfig(E2 标题叠加依赖)

测试

  • 新增 test_title_overlay.py(5 个:空标题跳过、绘制、位置、自动换行、显式换行)
  • 新增 E2 标题透传测试:plan.config.title.text 会作为 title_text 传给 _persist_cover_frame
  • 更新 test_1294_preview_voice_injection.py 适配 3 元组返回值
  • 相关测试 51 passed

验收

  1. staging 生成预览(Step4 先输入标题),预览视频可见标题烧录
  2. 预览完成后 gen_task.cover_url 非空,封面图带标题
  3. 点"自动生成封面"返回带标题封面
  4. 不设标题直接生成预览不报错(返回无标题纯帧)
## 问题 1. Worker 之前用 MediaKit 网络抽帧写 cover_url,因状态字符串 bug(#1461 已修)一直失败;在 API 请求里同步抽帧也容易超时挂死。 2. 封面从源素材抽取时没有标题(源素材本身没烧录标题)。 ## 修复 ### 1. Worker 统一走本地 ffmpeg 抽帧(apps/worker/worker_app/tasks/generation.py) - `_render_video` 返回值从 `(output_path, render_duration)` 改为 `(output_path, render_duration, cover_candidates)` - RenderAdapter 渲染完成后已用本地 ffmpeg 从 output_path 抽出 cover_candidates(3 帧,已上传 OSS),直接复用 - 第一帧写入 `gen_task.cover_url`,完整列表写入 `gen_task.metadata["cover_candidates"]` - 删除步骤 4.5 的 MediaKit 抽帧代码(不再依赖网络调用,不阻塞) ### 2. 标题文字叠加(packages/shared/title_overlay.py + thumbnail_generator.py) - 新增共享模块 `packages/shared/title_overlay.py`:Pillow 绘制白色文字 + 黑色描边/阴影,支持 CJK 字体和自动换行,API/Worker 共用 - `thumbnail_generator.extract_and_upload_cover_frames` 的 `title_text` 参数真正生效 - `render_adapter.py` 从已渲染视频抽帧时传空 `title_text`(标题已通过 ASS 烧录进画面,避免二次叠加重影) ### 3. 封面 API E2 兜底叠加标题(generation_cover.py) - E2 从源素材抽帧后,从 `plan.config.title.text` 读取标题并叠加到帧图 - `_persist_cover_frame` 增加 `title_text` 参数,下载帧后用 Pillow 叠加再转存 OSS - A/B/C/D 路径命中的封面来自已渲染视频(带标题),无需再叠加 ### 4. API 基础镜像依赖(infra/docker/api-base.Dockerfile + requirements-base.txt) - 增加 Pillow==10.4.0 和 fonts-noto-cjk / fontconfig(E2 标题叠加依赖) ## 测试 - 新增 `test_title_overlay.py`(5 个:空标题跳过、绘制、位置、自动换行、显式换行) - 新增 E2 标题透传测试:plan.config.title.text 会作为 title_text 传给 `_persist_cover_frame` - 更新 `test_1294_preview_voice_injection.py` 适配 3 元组返回值 - 相关测试 51 passed ## 验收 1. staging 生成预览(Step4 先输入标题),预览视频可见标题烧录 2. 预览完成后 `gen_task.cover_url` 非空,封面图带标题 3. 点"自动生成封面"返回带标题封面 4. 不设标题直接生成预览不报错(返回无标题纯帧)
xiaoxia added 1 commit 2026-08-23 10:29:15 +08:00
fix(cover): worker local ffmpeg cover frames + title overlay for source-asset fallback
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Successful in 4m45s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m23s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m39s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 15s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 52s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m31s
f6b50b49ec
问题:
1. 之前 Worker 用 MediaKit 网络抽帧写 cover_url,因状态字符串 bug(#1461 已修)
   一直失败;且在 API 请求内同步抽帧易超时。
2. 封面从源素材抽取时无标题(源素材未烧录标题)。

修复:
1. Worker 渲染完成后直接复用 RenderAdapter 已用本地 ffmpeg 抽出的 cover_candidates:
   - _render_video 返回值改为 (output_path, render_duration, cover_candidates)
   - 第一帧写入 gen_task.cover_url,完整列表写入 metadata.cover_candidates
   - 删除步骤 4.5 的 MediaKit 抽帧代码(不依赖网络,不阻塞)
2. 标题叠加:
   - 新增 packages/shared/title_overlay.py,Pillow 绘制白色文字+黑色描边/阴影,
     支持 CJK 字体和自动换行(API/Worker 共用)
   - thumbnail_generator.extract_and_upload_cover_frames 的 title_text 参数真正生效
   - render_adapter 从已渲染视频抽帧时传空 title_text(标题已 ASS 烧录,避免重影)
   - 封面 API E2 从源素材抽帧后,从 plan.config.title.text 读取标题并叠加
3. API 基础镜像增加 Pillow 和 fonts-noto-cjk(E2 标题叠加依赖)

测试:新增 test_title_overlay.py(5)、E2 标题透传测试,更新 1294 解包;
相关 51 passed。

🚀 预览环境已部署

项目 详情
PR号 #1462
预览链接 https://pr-1462.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1462 | | 预览链接 | [https://pr-1462.preview.xiaoxiajianji.com](https://pr-1462.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:否
  • 阻塞级问题数量:0 个

📊 审查概览

  • 整体评价:通过
  • 建议级问题数量:2 个

🔴 阻塞级问题(必须修复)

💡 改进建议(不阻塞合并)

  1. [packages/shared/title_overlay.py: 98] 资源管理优化

    • 具体内容:img = Image.open(image_path).convert("RGB") 建议使用 with Image.open(image_path) as img: 上下文管理器,并在 with 块内进行 convert 和操作。虽然函数结束时会触发 GC,但显式关闭文件句柄是更佳实践,尤其在高并发场景下能避免文件句柄耗尽风险。
  2. [packages/shared/title_overlay.py: 28] 性能优化建议

    • 具体内容:find_title_font 函数每次调用都会遍历文件系统检查字体路径是否存在。由于字体路径在容器生命周期内是固定的,建议使用 lru_cache 或全局变量缓存第一次找到的字体对象,避免重复的 IO 开销。

良好实践

  • 防御性编程:在 generation_cover.py 中对 plan.config 的多层嵌套取值做了非常完善的空值和类型检查(or {}, isinstance 判断),有效避免了 NPE 风险。
  • 异常降级处理:在 _persist_cover_framegenerate_video 中,对标题叠加和封面持久化等非关键路径操作使用了 try-except 包裹,失败时记录 Warning 但不中断主流程,保证了系统的鲁棒性。
  • 数据一致性generation.py 中更新 metadata 时使用了 dict(_cover_model.metadata or {}) 创建副本,避免了直接修改引用可能带来的副作用,符合 SQLAlchemy JSON 字段的最佳实践。

格式检查通过 | 逻辑审查通过 | 性能良好


🤖 由 AI 代码审查机器人自动生成 | 2026-08-23 02:34:06 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:否 - 阻塞级问题数量:0 个 ### 📊 审查概览 - 整体评价:通过 - 建议级问题数量:2 个 ### 🔴 阻塞级问题(必须修复) 无 ### 💡 改进建议(不阻塞合并) 1. **[packages/shared/title_overlay.py: 98] 资源管理优化** - 具体内容:`img = Image.open(image_path).convert("RGB")` 建议使用 `with Image.open(image_path) as img:` 上下文管理器,并在 `with` 块内进行 `convert` 和操作。虽然函数结束时会触发 GC,但显式关闭文件句柄是更佳实践,尤其在高并发场景下能避免文件句柄耗尽风险。 2. **[packages/shared/title_overlay.py: 28] 性能优化建议** - 具体内容:`find_title_font` 函数每次调用都会遍历文件系统检查字体路径是否存在。由于字体路径在容器生命周期内是固定的,建议使用 `lru_cache` 或全局变量缓存第一次找到的字体对象,避免重复的 IO 开销。 ### ✅ 良好实践 - **防御性编程**:在 `generation_cover.py` 中对 `plan.config` 的多层嵌套取值做了非常完善的空值和类型检查(`or {}`, `isinstance` 判断),有效避免了 NPE 风险。 - **异常降级处理**:在 `_persist_cover_frame` 和 `generate_video` 中,对标题叠加和封面持久化等非关键路径操作使用了 `try-except` 包裹,失败时记录 Warning 但不中断主流程,保证了系统的鲁棒性。 - **数据一致性**:`generation.py` 中更新 `metadata` 时使用了 `dict(_cover_model.metadata or {})` 创建副本,避免了直接修改引用可能带来的副作用,符合 SQLAlchemy JSON 字段的最佳实践。 --- ✅ 格式检查通过 | ✅ 逻辑审查通过 | ✅ 性能良好 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-08-23 02:34:06 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
auto-approve-bot approved these changes 2026-08-23 10:35:33 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot approved these changes 2026-08-23 10:35:33 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
xiaoxia merged commit 9ac21d37f0 into develop 2026-08-23 10:43:40 +08:00

🗑️ 预览环境已清理

PR #1462 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1462 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.