fix(worker): align gpu-direct title defaults (size/margin/bold) with CPU vfb path
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
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 API 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 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 / PR Build Worker Image (pull_request) Successful in 2m26s
AI Code Review / AI Code Review (pull_request) Successful in 6m55s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 9m28s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 9m32s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 8m40s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 11m15s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 11m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m46s
CI/CD Pipeline / Validate - Security (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
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
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 API 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 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 / PR Build Worker Image (pull_request) Successful in 2m26s
AI Code Review / AI Code Review (pull_request) Successful in 6m55s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 9m28s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 9m32s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 8m40s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 11m15s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 11m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m46s
CI/CD Pipeline / Validate - Security (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 #2095 fixed title baseline positioning and width-scaling but didn't fully align default constants with the CPU video_filter_builder path, causing GPU rendered titles to appear slightly smaller / higher / bolder-differently than the CPU/ASS preview the template was authored against. This shows up in both single and batch GPU renders since every variant shares the same gpu_direct pipeline. Root causes in PR #2095 defaults: - Default title size 36@720p vs config_schemas DEFAULT 48 / vfb default 48 - Top/bottom margin 40@720p (PAD16+margin24) vs vfb _scale_title_len(50) - Subtitle bottom margin 60@720p vs vfb 50 - Faux bold used same-color 1px border (white-on-white invisible for default white text; red-on-red for colored) vs vfb black 2px (intentional choice per #2001 to avoid double-print/halo artifact) - margin_top was treated as absolute y offset, now correctly added on top of base margin (matches vfb additive semantics) - subtitle stroke/bold block referenced uninitialized s_borderw (ruff F821) — added proper init + stroke parsing consistent with title block Changes: - TITLE_DEFAULT_MARGIN_TOP/BOTTOM = 50 (was 24+PAD=40) - SUBTITLE_DEFAULT_MARGIN_BOTTOM = 50 (was 60) - TITLE_FAUX_BOLD_WIDTH = 2, border color = #000000 (was 1, same as text) - default title size 48@720p (was 36) - margin_top from cfg added on top of base 50 (additive, same as vfb) - subtitle: init s_borderw/s_border_color, parse stroke dict before bold check - bottom position: y = h - th - margin_bottom (correct baseline; previously used margin_top variable which was misleading but numerically equivalent before margin split; now uses explicit margin_bottom) Tests updated to new expected scaled values (1280x720 scale=1.778: fontsize 85, y=89, bold borderw=4 black; 1080x1920 vertical: fontsize 72, y=75; margin_top=100 user offset => y=267). Added test_title_bold_false_disables_faux_bold. Batch investigation notes (separate findings, not bugs in this PR): - per-variant plan config is correctly deep-copied via clone_plan_for_variant (config=dict(source.config or {})); title text per-variant via titles[] override; voice per-variant independent download with #1749 strict guards; bgm merged via merge_bgm_config — batch passthrough chain is correct. - Worker generation concurrency = 2 (compose.yml default); USER_PENDING_LIMIT=3, 6 tasks will enqueue in two waves (first 3 → then next 3 as workers free up); GLOBAL_PENDING_LIMIT allows it. This is expected behavior, not a bug. - Legacy key mismatch: subtitle_render_engine.py reads plan_config['subtitle_config'] (snake_case) but normalize_plan_config writes 'subtitle' short key. This file is not imported by unified_render_service (which uses render_subtitles.py with explicit subtitle_config= parameter), so no runtime impact; left for separate cleanup. - Frontend usePlanConfigLoader.ts reads config.title_config snake_case while backend writes 'title' short key — frontend-only issue for subsequent edit sessions, out of backend scope.
This commit is contained in:
@@ -30,11 +30,16 @@ EDGE_CROP_MAX_PCT = 0.05
|
||||
# 均以 720p 为基准(见前端 titleCanvas.ts 注释 scale=videoWidth/720,types.ts "px @720p"),
|
||||
# 非 720p 输出时按 video_width / TITLE_SIZE_REF_WIDTH 等比缩放,保证成片位置与前端预览一致。
|
||||
TITLE_SIZE_REF_WIDTH = 720
|
||||
# 与前端 titleCanvas.ts 对齐:top 时文本 top-edge 距视频顶 = PAD(16@720p) + margin_top(默认 24@720p)
|
||||
TITLE_PAD_TOP = 16
|
||||
TITLE_DEFAULT_MARGIN_TOP = 24
|
||||
TITLE_DEFAULT_MARGIN_BOTTOM = 24
|
||||
SUBTITLE_DEFAULT_MARGIN_BOTTOM = 60 # 字幕距底边距(720p 基准,与前端字幕面板默认对齐)
|
||||
# 与 video_filter_builder.build_title_drawtext_filter(CPU 路径)和 ass_subtitle_builder 对齐:
|
||||
# - top/bottom 默认 margin 50@720p(vfb 用 _scale_title_len(50, w),即 y=50 / y=h-th-50)
|
||||
# - margin_top 字段:前端编辑器 marginTop 滑块,叠加在默认 margin 之上(#2095 支持)
|
||||
# - PAD 概念仅用于前端 Canvas 预览;ffmpeg drawtext y 是 baseline,无 font metrics 可用,
|
||||
# 直接用统一 50@720p baseline 位置即可保持三端(GPU/CPU/前端视觉)一致。
|
||||
TITLE_DEFAULT_MARGIN_TOP = 50 # top 位置 baseline 默认距顶 50@720p(与 vfb/CPU 路径一致)
|
||||
TITLE_DEFAULT_MARGIN_BOTTOM = 50 # bottom 位置 baseline 默认距底 50@720p
|
||||
SUBTITLE_DEFAULT_MARGIN_BOTTOM = 50 # 字幕距底边距 50@720p(与 vfb 一致)
|
||||
TITLE_MARGIN_TOP_FROM_CFG_DEFAULT = 24 # 前端 marginTop 滑块默认值(用户未传时叠加 0)
|
||||
TITLE_FAUX_BOLD_WIDTH = 2 # 仿粗黑色描边宽度(与 vfb 一致,2@720p 黑色细描边)
|
||||
|
||||
|
||||
def _scale_title_len(value, video_width: int):
|
||||
@@ -507,7 +512,7 @@ def build_direct_render(
|
||||
except (TypeError, ValueError):
|
||||
t_size_720 = 0
|
||||
if t_size_720 <= 0:
|
||||
t_size_720 = 36 # 与 config_schemas 默认 48 接近;AI Avatar 前端默认 48,剪辑前端默认 28,取中
|
||||
t_size_720 = 48 # 与 config_schemas.DEFAULT_EDIT_PLAN_CONFIG.title.size=48 及 vfb 默认一致
|
||||
t_size = _scale_title_len(t_size_720, output_width)
|
||||
# stroke/shadow 长度字段也需 720p→输出分辨率缩放
|
||||
t_color = str(t_cfg.get("color", t_cfg.get("font_color", "#ffffff")))
|
||||
@@ -525,14 +530,22 @@ def build_direct_render(
|
||||
# baseline 需再下移约 0.85*fontsize;但 drawtext 表达式无法引用 fontsize 变量,
|
||||
# 这里直接用 (PAD + margin_top)@720p 缩放后作为 y(即让 baseline≈顶部内边距位置),
|
||||
# 实际中文字符会自然向下延伸,视觉位置与前端预览(textBaseline=middle 居中到 firstLineY)一致。
|
||||
_t_margin_top_raw = t_cfg.get("margin_top", TITLE_DEFAULT_MARGIN_TOP)
|
||||
# margin_top:前端滑块值(默认 24@720p),叠加在默认 50@720p 基线之上
|
||||
_t_user_margin_top = t_cfg.get("margin_top")
|
||||
try:
|
||||
_t_margin_top_720 = int(_t_margin_top_raw)
|
||||
_t_user_margin_top_720 = int(_t_user_margin_top) if _t_user_margin_top is not None else 0
|
||||
except (TypeError, ValueError):
|
||||
_t_margin_top_720 = TITLE_DEFAULT_MARGIN_TOP
|
||||
t_margin_top = _scale_title_len(TITLE_PAD_TOP + _t_margin_top_720, output_width)
|
||||
# bottom margin(标题放在 bottom 时也支持)
|
||||
t_margin_bottom = _scale_title_len(TITLE_PAD_TOP + TITLE_DEFAULT_MARGIN_BOTTOM, output_width)
|
||||
_t_user_margin_top_720 = 0
|
||||
t_margin_top_720 = TITLE_DEFAULT_MARGIN_TOP + _t_user_margin_top_720
|
||||
t_margin_top = _scale_title_len(t_margin_top_720, output_width)
|
||||
# bottom margin(标题放在 bottom 时):用户 margin_bottom 透传,默认 50@720p
|
||||
_t_user_margin_bottom = t_cfg.get("margin_bottom")
|
||||
try:
|
||||
_t_user_margin_bottom_720 = int(_t_user_margin_bottom) if _t_user_margin_bottom is not None else 0
|
||||
except (TypeError, ValueError):
|
||||
_t_user_margin_bottom_720 = 0
|
||||
t_margin_bottom_720 = TITLE_DEFAULT_MARGIN_BOTTOM + _t_user_margin_bottom_720
|
||||
t_margin_bottom = _scale_title_len(t_margin_bottom_720, output_width)
|
||||
t_borderw = 0
|
||||
t_border_color = "#000000"
|
||||
t_box = False
|
||||
@@ -566,11 +579,11 @@ def build_direct_render(
|
||||
t_shadow_x = _scale_title_len(t_shadow_x_720, output_width)
|
||||
t_shadow_y = _scale_title_len(t_shadow_y_720, output_width)
|
||||
# bold/italic:drawtext 原生无粗斜体选项;通过同色描边模拟粗体
|
||||
t_bold = bool(t_cfg.get("bold", True)) # 与 ASS 路径默认 bold=True 对齐
|
||||
t_bold = bool(t_cfg.get("bold", True)) # 与 ASS/vfb 路径默认 bold=True 对齐
|
||||
if t_bold and t_borderw < 1:
|
||||
# 同色描边宽度 1@720p,按宽度缩放保证各分辨率视觉一致
|
||||
t_borderw = max(1, _scale_title_len(1, output_width))
|
||||
t_border_color = t_color # 用文字色描边模拟加粗
|
||||
# 粗体未配用户描边时:黑色细描边 2@720p(与 vfb 一致,避免同色描边导致重影)
|
||||
t_borderw = _scale_title_len(TITLE_FAUX_BOLD_WIDTH, output_width)
|
||||
t_border_color = "#000000" # 黑色细描边模拟粗体
|
||||
|
||||
# ── 解析 subtitle_config ──
|
||||
s_cfg = dict(subtitle_config) if isinstance(subtitle_config, dict) else {}
|
||||
@@ -595,8 +608,21 @@ def build_direct_render(
|
||||
s_pos_x, s_pos_y = None, None
|
||||
s_margin_top = _scale_title_len(60, output_width) # subtitle top (not commonly used)
|
||||
s_margin_bottom = _scale_title_len(SUBTITLE_DEFAULT_MARGIN_BOTTOM, output_width)
|
||||
s_borderw = max(1, _scale_title_len(2, output_width)) # 字幕默认描边保证可读性
|
||||
# subtitle stroke/bold:先解析用户 stroke,再按 bold 默认补描边
|
||||
s_borderw = 0
|
||||
s_border_color = "#000000"
|
||||
_s_stroke = s_cfg.get("stroke")
|
||||
if isinstance(_s_stroke, dict) and _s_stroke.get("enabled", False):
|
||||
try:
|
||||
s_borderw = _scale_title_len(int(float(_s_stroke.get("width", 2))), output_width)
|
||||
except (TypeError, ValueError):
|
||||
s_borderw = 0
|
||||
s_border_color = str(_s_stroke.get("color", "#000000"))
|
||||
s_bold = bool(s_cfg.get("bold", False))
|
||||
if s_bold and s_borderw < 1:
|
||||
# 粗体默认黑色细描边 2@720p(与 title/CPU vfb 一致)
|
||||
s_borderw = _scale_title_len(TITLE_FAUX_BOLD_WIDTH, output_width)
|
||||
s_border_color = "#000000"
|
||||
|
||||
# 静态字幕:static_subtitle_text 非空时构造全片长 segment(0 → total_duration)
|
||||
static_text = (static_subtitle_text or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user