fix(cover): 封面标题叠加 — 选标题后用 ffmpeg drawtext 在封面图上叠加文字 #1375

Merged
xiaoxia merged 2 commits from fix/cover-title-overlay into develop 2026-08-15 16:00:16 +08:00
Owner

根因

封面帧在预览视频渲染时(Step 3)就被抽帧保存为 cover_url,但用户是在渲染之后才选标题(Step 4)。所以 cover_url 天然没标题。

修复方案

轻量方案:在封面 API 返回 cover_url 前,检查 plan.config["title"]["text"] 是否有标题。如果有:

  1. 下载现有封面图片
  2. 用 ffmpeg drawtext 叠加标题文字(白色+黑色描边+阴影,居中偏下)
  3. 上传到 OSS 返回新 URL

叠加失败时降级使用原 cover_url。

改动

  • apps/api/app/api/routes/generation_cover.py — 新增 _overlay_title_on_cover_image() + _escape_drawtext_text() + 路由中检查标题并叠加
  • tests/unit/test_cover_title_overlay.py — 12 个新测试

测试结果

  • 新增 12 个测试全通过
  • 全量 13603 passed, 12 skipped
## 根因 封面帧在预览视频渲染时(Step 3)就被抽帧保存为 cover_url,但用户是在渲染之后才选标题(Step 4)。所以 cover_url 天然没标题。 ## 修复方案 **轻量方案**:在封面 API 返回 cover_url 前,检查 plan.config["title"]["text"] 是否有标题。如果有: 1. 下载现有封面图片 2. 用 ffmpeg drawtext 叠加标题文字(白色+黑色描边+阴影,居中偏下) 3. 上传到 OSS 返回新 URL 叠加失败时降级使用原 cover_url。 ## 改动 - `apps/api/app/api/routes/generation_cover.py` — 新增 `_overlay_title_on_cover_image()` + `_escape_drawtext_text()` + 路由中检查标题并叠加 - `tests/unit/test_cover_title_overlay.py` — 12 个新测试 ## 测试结果 - 新增 12 个测试全通过 - 全量 13603 passed, 12 skipped
xiaoxia added 1 commit 2026-08-15 15:43:11 +08:00
fix(cover): overlay title text on cover image after frame extraction
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 / Deploy Staging (Watchtower auto-deploy) (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
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m22s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m37s
AI Code Review / AI Code Review (pull_request) Successful in 2m3s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
1a2f34e508
Root cause: cover frame is extracted during preview video rendering (Step 3),
but user selects title AFTER rendering (Step 4). The cover frame naturally
has no title because it was captured before the title was chosen.

Fix: In generation_cover.py, after finding cover_url_from_task, check if
plan.config['title']['text'] has a title. If yes, download the cover image,
use ffmpeg drawtext to overlay the title (white text + black shadow, centered
near bottom), upload to OSS, and return the new URL. On failure, fallback
to original cover_url.

Changes:
- Add _overlay_title_on_cover_image() helper in generation_cover.py
- Add _escape_drawtext_text() for ffmpeg special character escaping
- Modify cover route to check for title and overlay if present
- 12 new unit tests (test_cover_title_overlay.py)
- All 13603 tests pass

Fixes: cover title missing issue in staging

🚀 预览环境已部署

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

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

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

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

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1375 | | 预览链接 | [https://pr-1375.preview.xiaoxiajianji.com](https://pr-1375.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
auto-approve-bot added 1 commit 2026-08-15 15:46:09 +08:00
style: auto-format with black + isort + prettier [skip ci-format-check]
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 / Deploy Staging (Watchtower auto-deploy) (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
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 42s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m45s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m23s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m13s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API 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 / Integration Tests (pull_request) Successful in 1m28s
AI Code Review / AI Code Review (pull_request) Successful in 5m43s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 32s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 45s
d48d652465
Collaborator

代码审查结果 - PR #1375

⚠️ 问题(2个需要修改)

  1. apps/api/app/api/routes/generation_cover.py _overlay_title_on_cover_image 函数:存在服务端请求伪造(SSRF)安全风险

    • 代码直接使用 requests.get(cover_image_url) 下载图片,未对 cover_image_url 进行域名或协议白名单校验。如果该 URL 来源不可信(如用户可控的 plan.config),攻击者可利用此接口探测内网服务或读取本地文件(如 file:///etc/passwd)。
    • 后果:可能导致内网信息泄露或被攻击。
    • 建议:必须校验 cover_image_url 的合法性,仅允许访问可信的 CDN 或 OSS 域名。
  2. apps/api/app/api/routes/generation_cover.py _overlay_title_on_cover_image 函数:存在阻塞异步事件循环的性能问题

    • 该文件位于 app/api/routes 下,通常运行在 FastAPI 异步环境中。代码中使用了同步的 requests.getsubprocess.run,且设置了长达 30 秒的超时。
    • 后果:在并发请求较高时,同步阻塞操作会占用工作线程,导致服务吞吐量急剧下降甚至假死。
    • 建议:应使用异步 HTTP 库(如 httpx)或通过 asyncio.to_thread / loop.run_in_executor 将阻塞调用移至线程池中执行。

💡 建议(2个可选)

  1. apps/api/app/api/routes/generation_cover.py 第 86 行:十六进制颜色格式处理可能存在逻辑缺陷。

    • 代码使用 font_color.replace("#", "0x") 处理颜色。如果输入是简写格式(如 #fff),结果为 0xfff,FFmpeg 的 drawtext 滤镜通常要求完整的 6 位十六进制(0xffffff),否则可能无法正确渲染颜色。
    • 建议:增加颜色格式校验和补全逻辑,确保传入 FFmpeg 的颜色值格式正确。
  2. apps/api/app/api/routes/generation_cover.py 第 135 行:异常捕获过于宽泛。

    • except Exception: 会捕获所有异常,包括 KeyboardInterruptSystemExit(虽然在此处概率低),且掩盖了具体的配置错误(如 font_size 类型转换错误)。
    • 建议:虽然降级策略是合理的,但建议明确捕获 (requests.RequestException, subprocess.TimeoutExpired, ValueError) 等预期异常,避免掩盖意外的程序错误。

格式检查通过 | 逻辑审查需修改 | ⚠️ 建议关注性能


🤖 由 AI 代码审查机器人自动生成 | 2026-08-15 07:51:52 | 模型:

## 代码审查结果 - PR #1375 ### ⚠️ 问题(2个需要修改) 1. **apps/api/app/api/routes/generation_cover.py `_overlay_title_on_cover_image` 函数**:存在**服务端请求伪造(SSRF)安全风险**。 - 代码直接使用 `requests.get(cover_image_url)` 下载图片,未对 `cover_image_url` 进行域名或协议白名单校验。如果该 URL 来源不可信(如用户可控的 `plan.config`),攻击者可利用此接口探测内网服务或读取本地文件(如 `file:///etc/passwd`)。 - **后果**:可能导致内网信息泄露或被攻击。 - **建议**:必须校验 `cover_image_url` 的合法性,仅允许访问可信的 CDN 或 OSS 域名。 2. **apps/api/app/api/routes/generation_cover.py `_overlay_title_on_cover_image` 函数**:存在**阻塞异步事件循环的性能问题**。 - 该文件位于 `app/api/routes` 下,通常运行在 FastAPI 异步环境中。代码中使用了同步的 `requests.get` 和 `subprocess.run`,且设置了长达 30 秒的超时。 - **后果**:在并发请求较高时,同步阻塞操作会占用工作线程,导致服务吞吐量急剧下降甚至假死。 - **建议**:应使用异步 HTTP 库(如 `httpx`)或通过 `asyncio.to_thread` / `loop.run_in_executor` 将阻塞调用移至线程池中执行。 ### 💡 建议(2个可选) 1. **apps/api/app/api/routes/generation_cover.py 第 86 行**:十六进制颜色格式处理可能存在逻辑缺陷。 - 代码使用 `font_color.replace("#", "0x")` 处理颜色。如果输入是简写格式(如 `#fff`),结果为 `0xfff`,FFmpeg 的 `drawtext` 滤镜通常要求完整的 6 位十六进制(`0xffffff`),否则可能无法正确渲染颜色。 - **建议**:增加颜色格式校验和补全逻辑,确保传入 FFmpeg 的颜色值格式正确。 2. **apps/api/app/api/routes/generation_cover.py 第 135 行**:异常捕获过于宽泛。 - `except Exception:` 会捕获所有异常,包括 `KeyboardInterrupt` 或 `SystemExit`(虽然在此处概率低),且掩盖了具体的配置错误(如 `font_size` 类型转换错误)。 - **建议**:虽然降级策略是合理的,但建议明确捕获 `(requests.RequestException, subprocess.TimeoutExpired, ValueError)` 等预期异常,避免掩盖意外的程序错误。 --- ✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-08-15 07:51:52 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
xiaoxia merged commit a9f6d7e712 into develop 2026-08-15 16:00:16 +08:00

🗑️ 预览环境已清理

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

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

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