fix(title): 标题/字幕字号按 video_width/720 等比缩放,修复成片标题比预览过小 #2023

Merged
auto-approve-bot merged 4 commits from fix/title-fontsize-scaling into develop 2026-09-23 23:17:13 +08:00
Owner

问题

用户反馈最终生成视频中标题字体明显小于模板预览/选择时的字号。

根因

前端 titleCanvas.ts 已明确 titleConfig.size 的语义是「720p 基准宽度下的 px 字号」,按 scale = videoWidth / 720 缩放渲染到预览 Canvas;但后端三个渲染路径直接使用原始 size 值:

  • packages/domain/ass_subtitle_builder.py(主 ASS 路径)
  • packages/domain/video_filter_builder.py::build_title_drawtext_filter(drawtext 降级路径)
  • apps/worker/video_processing/subtitle_generator.py::generate_ass_from_timeline(ASR 时间轴字幕)

在 1080×1920 竖屏/1920×1080 横屏等非 720p 输出下,标题字号/描边/阴影/边距未随视频分辨率等比放大。

此外 subtitle_generator.py 存在 min(int(size), 36) 的上限钳位,任何分辨率下标题字号都被强行压到 ≤36,进一步放大问题。

修复

以 720p 为基准,按 video_width / 720 等比缩放所有长度类参数:

主 ASS 路径 (ass_subtitle_builder.py)

  • 新增 _scale_len(value, video_width):整数入返回 int,浮点入保留 float(支持 1.5 等细描边)
  • 新增内部 _scale_cfg(cfg, defaults):先 setdefault 填 size 默认值(title=36、subtitle=24),再统一缩放 size/font_size/margin_top/bg_padding/bg_radius/line_overrides[*].size;pos_x/pos_y 为百分比(0-100) 不缩放
  • 描边宽(默认 2)、阴影 blur(默认 4)、阴影 offset_x/y(默认 2)在字段提取处缩放一次,避免双重缩放
  • 边距常量 TITLE_MARGIN_TOP/BOTTOM/SIDE 缩放为局部变量
  • subtitle outline_width(1.0) 缩放为 float

drawtext 降级路径 (video_filter_builder.py)

  • 新增 _scale_title_len(value, output_width)
  • font_size / border_width / shadowx / shadowy / top|bottom y-offset(50px) 统一缩放

ASR 字幕路径 (subtitle_generator.py)

  • 复用 _scale_len
  • subtitle: font_size 默认 24、outline_width 默认 1.5、margin_v/l/r 默认 60/40/40 全部缩放
  • title: 移除 min(..., 36) 上限钳位;stroke/shadow/margin 统一缩放

不缩放的字段:颜色/字体/对齐/粗体/斜体等枚举/布尔;pos_x/pos_y 百分比;AI 数字人 WYSIWYG PNG overlay 路径(前端 Canvas 按 videoWidth 直接绘制,后端仅 overlay=0:0 叠加,无需后端缩放)。

测试

  • 既有固定值断言的单测:video_width 改为 720(基准下缩放比=1,断言值不变)
  • 新增 TestTitleFontsizeScaling / TestDrawtextFontsizeScaling 共 16 个测试:720p 不变、1080p 1.5×、1920p 8/3×、描边宽/阴影偏移/边距/位置边距/line_overrides size
  • 修正 test_subtitle_generator.py::test_720p_resolution 中 PlayResX/Y 与 video_width/height 互换的断言笔误
  • 全量单测:15978 passed, 2 failed(test_1970_musetalk_server.py 两个 ffmpeg timeout/FileNotFound flaky,与本次改动无关,在 develop HEAD 上可复现),28 skipped

影响面

所有带标题/字幕的视频生成任务,在 1080p/1080×1920/1920×1080/1920×1920/4K 分辨率下,标题和字幕字号、描边宽度、阴影偏移、边距会按 video_width/720 等比放大,效果与前端预览一致。720p 输出保持一字节不变。

## 问题 用户反馈最终生成视频中标题字体明显小于模板预览/选择时的字号。 ## 根因 前端 `titleCanvas.ts` 已明确 `titleConfig.size` 的语义是「720p 基准宽度下的 px 字号」,按 `scale = videoWidth / 720` 缩放渲染到预览 Canvas;但后端三个渲染路径直接使用原始 size 值: - `packages/domain/ass_subtitle_builder.py`(主 ASS 路径) - `packages/domain/video_filter_builder.py::build_title_drawtext_filter`(drawtext 降级路径) - `apps/worker/video_processing/subtitle_generator.py::generate_ass_from_timeline`(ASR 时间轴字幕) 在 1080×1920 竖屏/1920×1080 横屏等非 720p 输出下,标题字号/描边/阴影/边距未随视频分辨率等比放大。 此外 `subtitle_generator.py` 存在 `min(int(size), 36)` 的上限钳位,任何分辨率下标题字号都被强行压到 ≤36,进一步放大问题。 ## 修复 以 720p 为基准,按 `video_width / 720` 等比缩放所有长度类参数: **主 ASS 路径 (`ass_subtitle_builder.py`)** - 新增 `_scale_len(value, video_width)`:整数入返回 int,浮点入保留 float(支持 1.5 等细描边) - 新增内部 `_scale_cfg(cfg, defaults)`:先 setdefault 填 size 默认值(title=36、subtitle=24),再统一缩放 `size/font_size/margin_top/bg_padding/bg_radius/line_overrides[*].size`;`pos_x/pos_y` 为百分比(0-100) 不缩放 - 描边宽(默认 2)、阴影 blur(默认 4)、阴影 offset_x/y(默认 2)在字段提取处缩放一次,避免双重缩放 - 边距常量 TITLE_MARGIN_TOP/BOTTOM/SIDE 缩放为局部变量 - subtitle outline_width(1.0) 缩放为 float **drawtext 降级路径 (`video_filter_builder.py`)** - 新增 `_scale_title_len(value, output_width)` - `font_size / border_width / shadowx / shadowy / top|bottom y-offset(50px)` 统一缩放 **ASR 字幕路径 (`subtitle_generator.py`)** - 复用 `_scale_len` - subtitle: font_size 默认 24、outline_width 默认 1.5、margin_v/l/r 默认 60/40/40 全部缩放 - title: **移除 `min(..., 36)` 上限钳位**;stroke/shadow/margin 统一缩放 **不缩放的字段**:颜色/字体/对齐/粗体/斜体等枚举/布尔;pos_x/pos_y 百分比;AI 数字人 WYSIWYG PNG overlay 路径(前端 Canvas 按 videoWidth 直接绘制,后端仅 overlay=0:0 叠加,无需后端缩放)。 ## 测试 - 既有固定值断言的单测:video_width 改为 720(基准下缩放比=1,断言值不变) - 新增 `TestTitleFontsizeScaling` / `TestDrawtextFontsizeScaling` 共 16 个测试:720p 不变、1080p 1.5×、1920p 8/3×、描边宽/阴影偏移/边距/位置边距/line_overrides size - 修正 `test_subtitle_generator.py::test_720p_resolution` 中 PlayResX/Y 与 video_width/height 互换的断言笔误 - 全量单测:**15978 passed**, 2 failed(`test_1970_musetalk_server.py` 两个 ffmpeg timeout/FileNotFound flaky,与本次改动无关,在 develop HEAD 上可复现),28 skipped ## 影响面 所有带标题/字幕的视频生成任务,在 1080p/1080×1920/1920×1080/1920×1920/4K 分辨率下,标题和字幕字号、描边宽度、阴影偏移、边距会按 video_width/720 等比放大,效果与前端预览一致。720p 输出保持一字节不变。

🚀 预览环境已部署

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

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

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

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

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #2023 | | 预览链接 | [https://pr-2023.preview.xiaoxiajianji.com](https://pr-2023.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
xiaoxia added 4 commits 2026-09-23 23:01:51 +08:00
## 问题
用户反馈最终生成视频中标题字体比模板预览/选择时看到的小很多。
前端 titleCanvas.ts 已明确 titleConfig.size 的语义是「720p 基准宽度下的 px 字号」,
按 scale = videoWidth/720 缩放渲染到预览 Canvas;但后端三个渲染路径
(ass_subtitle_builder / drawtext 降级 / subtitle_generator ASR)直接使用原始 size 值,
在 1080×1920 竖屏/1920×1080 横屏等非 720p 输出下,标题字号未随分辨率等比放大,
导致成片标题明显小于前端预览。

此外 apps/worker/video_processing/subtitle_generator.py 中存在
`min(int(title_cfg.get("size", 36)), 36)` 的上限钳位,任何分辨率下标题字号
都会被强行压到 ≤36,进一步放大了问题。

## 修复
三个渲染路径统一以 720p 为基准,按 video_width/720 等比缩放所有长度类参数:

- packages/domain/ass_subtitle_builder.py
  - 新增 _scale_len(value, video_width) 工具:整数输入返回 int,浮点输入保留 float
  - build_ass_content 内新增 _scale_cfg(cfg, defaults) 内部函数:
    先 setdefault 填充 size 默认值(title=36、subtitle=24),再统一缩放
    size/font_size、margin_top/bg_padding/bg_radius、line_overrides[*].size;
    pos_x/pos_y 是百分比(0-100)不缩放
  - 描边宽(默认 2)、阴影 blur(默认 4)、阴影 offset_x/y(默认 2)在字段提取
    处经 _scale_len 缩放,避免双重缩放
  - TITLE_MARGIN_TOP/BOTTOM/SIDE 常量经 _scale_len 成局部 _margin_top/bottom/side
  - subtitle outline_width(1.0)经 _scale_len 缩放(保持 float 以支持 1.5 等小数)

- packages/domain/video_filter_builder.py (build_title_drawtext_filter 降级路径)
  - 新增 _scale_title_len(value, output_width) 工具
  - font_size、border_width(描边)、shadowx/shadowy(阴影偏移)、top/bottom 位置
    y 偏移(50px)统一按 output_width/720 缩放

- apps/worker/video_processing/subtitle_generator.py (ASR 时间轴字幕路径)
  - 复用 ass_subtitle_builder._scale_len
  - subtitle: font_size 默认 24、outline_width 默认 1.5、margin_v/l/r 默认 60/40/40
    全部按 video_width/720 缩放
  - title: 移除 min(..., 36) 上限钳位;s_width/sh_blur/sh_offset、margin_top/side
    统一缩放;_wrap_title_text 传入缩放后的 margin_l/r

## 不缩放的字段
颜色/字体/对齐/粗体/斜体等枚举/布尔值不缩放;
pos_x/pos_y 为百分比(0-100)不缩放;
AI 数字人 WYSIWYG PNG overlay 路径(build_title_overlay_filter)由前端 Canvas 按
videoWidth 直接绘制,后端只 overlay=0:0 叠加,无需后端缩放。

## 测试
- 既有断言固定值的单测:将 video_width 改为 720(720p 基准下缩放比=1,断言值不变)
- 新增 TestTitleFontsizeScaling / TestDrawtextFontsizeScaling 共 16 个测试用例:
  720p 不变、1080p 1.5×、1920p 8/3×、描边宽/阴影偏移/边距/位置边距/逐行覆盖 size
  等场景
- 修正 test_subtitle_generator.py::test_720p_resolution 中 PlayResX/Y 与
  video_width/height 互换的断言错误
- 全量单测:15978 passed, 2 failed(均为 test_1970_musetalk_server.py 中
  ffmpeg 超时/FileNotFound 的 flaky,与本次改动无关,在 develop HEAD 上复现)
  28 skipped
test: fix ruff E741 ambiguous variable name 'l' → 'ln'
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m4s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
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 / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m31s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m5s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 4m29s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 4m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m32s
AI Code Review / AI Code Review (pull_request) Successful in 6m58s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 14m37s
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 / CI Gate (pull_request) Successful in 1s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 8m48s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m8s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m39s
c068a5c46e
xiaoxia force-pushed fix/title-fontsize-scaling from 87468d78c5 to c068a5c46e 2026-09-23 23:01:51 +08:00 Compare
auto-approve-bot merged commit 4a695c6eb6 into develop 2026-09-23 23:17:13 +08:00
auto-approve-bot deleted branch fix/title-fontsize-scaling 2026-09-23 23:17:15 +08:00

🗑️ 预览环境已清理

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

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

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