8f1e03ba4c
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m2s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 766h36m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 766h36m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 766h36m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 766h36m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 766h36m45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 766h36m19s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 766h36m45s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 766h36m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 767h10m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 767h10m15s
根因:封面抽帧发生在预览视频渲染时,但用户选标题在渲染之后, 导致封面天然没有标题。 修复方案(彻底方案): 1. 调整步骤时序:前端将标题步骤移到预览之前 2. 预览 API 接收 title_config(标题文本+样式),渲染时烧录进视频 3. 封面抽帧从已带标题的视频中取帧,自然包含标题 后端改动: - generation_task.py schema: 新增 title_config dict 字段 - generation_tasks.py command: 新增 title_config 字段 - generation_preview.py: 序列化 title_config 为 JSON 存入 custom_title - generation.py worker: 解析 JSON 格式 custom_title,合并标题文本+样式到 plan.config["title"] - generation_cover.py: 移除 PR#1375 的 drawtext 临时方案(不再需要) - 删除 test_cover_title_overlay.py,新增 test_preview_title_config.py(12测试) 向后兼容: - title_config 全部可选,不传时行为和之前一致 - custom_title 支持 JSON(新)和纯文本(旧)两种格式
191 lines
6.3 KiB
Python
191 lines
6.3 KiB
Python
"""Tests for preview title_config feature.
|
|
|
|
验证预览 API 的 title_config 字段和 Worker 的标题配置解析逻辑。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
|
|
class TestPreviewTitleConfigSchema:
|
|
"""测试 CreatePreviewGenerationTaskRequest 的 title_config 字段."""
|
|
|
|
def test_title_config_default_empty(self):
|
|
"""title_config 默认为空 dict."""
|
|
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
|
|
|
|
req = CreatePreviewGenerationTaskRequest(
|
|
template_id="test_template",
|
|
asset_ids=["asset1"],
|
|
)
|
|
assert req.title_config == {}
|
|
|
|
def test_title_config_with_text(self):
|
|
"""传入标题文本."""
|
|
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
|
|
|
|
req = CreatePreviewGenerationTaskRequest(
|
|
template_id="test_template",
|
|
asset_ids=["asset1"],
|
|
title_config={"text": "测试标题"},
|
|
)
|
|
assert req.title_config["text"] == "测试标题"
|
|
|
|
def test_title_config_with_full_style(self):
|
|
"""传入完整标题样式配置."""
|
|
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
|
|
|
|
config = {
|
|
"text": "我的视频标题",
|
|
"font": "思源黑体",
|
|
"font_size": 48,
|
|
"font_color": "#ffffff",
|
|
"position": "top",
|
|
"bold": True,
|
|
"stroke": 2,
|
|
"shadow": True,
|
|
}
|
|
req = CreatePreviewGenerationTaskRequest(
|
|
template_id="test_template",
|
|
asset_ids=["asset1"],
|
|
title_config=config,
|
|
)
|
|
assert req.title_config["text"] == "我的视频标题"
|
|
assert req.title_config["font_size"] == 48
|
|
assert req.title_config["position"] == "top"
|
|
|
|
|
|
class TestCommandTitleConfig:
|
|
"""测试 CreateGenerationTaskCommand 的 title_config 字段."""
|
|
|
|
def test_command_has_title_config(self):
|
|
"""Command 包含 title_config 字段."""
|
|
from packages.application.generation_tasks import CreateGenerationTaskCommand
|
|
|
|
cmd = CreateGenerationTaskCommand(
|
|
title_config={"text": "hello", "font_size": 32},
|
|
)
|
|
assert cmd.title_config["text"] == "hello"
|
|
assert cmd.title_config["font_size"] == 32
|
|
|
|
def test_command_title_config_default_empty(self):
|
|
"""Command 的 title_config 默认为空 dict."""
|
|
from packages.application.generation_tasks import CreateGenerationTaskCommand
|
|
|
|
cmd = CreateGenerationTaskCommand()
|
|
assert cmd.title_config == {}
|
|
|
|
|
|
class TestWorkerTitleConfigParsing:
|
|
"""测试 Worker 渲染时的标题配置解析逻辑."""
|
|
|
|
def test_json_format_parsing(self):
|
|
"""JSON 格式的 custom_title 能正确解析."""
|
|
config = {"text": "测试标题", "font_size": 48, "font_color": "#ff0000"}
|
|
custom_title = json.dumps(config, ensure_ascii=False)
|
|
|
|
ct_stripped = custom_title.strip()
|
|
parsed = None
|
|
if ct_stripped.startswith("{"):
|
|
try:
|
|
parsed = json.loads(ct_stripped)
|
|
except (json.JSONDecodeError, ValueError):
|
|
parsed = None
|
|
|
|
assert parsed is not None
|
|
assert parsed["text"] == "测试标题"
|
|
assert parsed["font_size"] == 48
|
|
|
|
def test_plain_text_fallback(self):
|
|
"""纯文本的 custom_title 不触发 JSON 解析."""
|
|
custom_title = "简单的标题文字"
|
|
|
|
ct_stripped = custom_title.strip()
|
|
parsed = None
|
|
if ct_stripped.startswith("{"):
|
|
try:
|
|
parsed = json.loads(ct_stripped)
|
|
except (json.JSONDecodeError, ValueError):
|
|
parsed = None
|
|
|
|
assert parsed is None
|
|
|
|
def test_invalid_json_fallback(self):
|
|
"""无效 JSON 的 custom_title 降级为纯文本."""
|
|
custom_title = "{invalid json"
|
|
|
|
ct_stripped = custom_title.strip()
|
|
parsed = None
|
|
if ct_stripped.startswith("{"):
|
|
try:
|
|
parsed = json.loads(ct_stripped)
|
|
except (json.JSONDecodeError, ValueError):
|
|
parsed = None
|
|
|
|
assert parsed is None
|
|
|
|
def test_json_without_text_skipped(self):
|
|
"""JSON 格式但缺少 text 字段时,跳过标题注入."""
|
|
config = {"font_size": 48}
|
|
custom_title = json.dumps(config, ensure_ascii=False)
|
|
|
|
ct_stripped = custom_title.strip()
|
|
parsed = json.loads(ct_stripped)
|
|
title_text = (parsed.get("text") or "").strip()
|
|
|
|
assert title_text == ""
|
|
|
|
def test_style_key_mapping(self):
|
|
"""前端字段名正确映射到 ASS 字段名."""
|
|
config = {
|
|
"text": "标题",
|
|
"font_size": 48,
|
|
"font_color": "#ffffff",
|
|
"font_preset": "思源黑体",
|
|
}
|
|
|
|
style_keys = ["font", "font_size", "font_color", "position",
|
|
"bold", "stroke", "shadow", "font_preset"]
|
|
title_cfg = {}
|
|
for key in style_keys:
|
|
if key in config and config[key] is not None:
|
|
mapped_key = {
|
|
"font_size": "size",
|
|
"font_color": "color",
|
|
"font_preset": "font",
|
|
}.get(key, key)
|
|
title_cfg[mapped_key] = config[key]
|
|
|
|
assert title_cfg["size"] == 48
|
|
assert title_cfg["color"] == "#ffffff"
|
|
assert title_cfg["font"] == "思源黑体"
|
|
|
|
|
|
class TestPreviewRouteTitleConfigPassing:
|
|
"""测试预览路由正确序列化 title_config 到 custom_title."""
|
|
|
|
def test_title_config_serialization(self):
|
|
"""title_config 序列化为 JSON 字符串."""
|
|
title_config = {
|
|
"text": "我的标题",
|
|
"font_size": 32,
|
|
"font_color": "#d4a843",
|
|
}
|
|
serialized = json.dumps(title_config, ensure_ascii=False)
|
|
|
|
parsed = json.loads(serialized)
|
|
assert parsed["text"] == "我的标题"
|
|
assert parsed["font_size"] == 32
|
|
|
|
def test_empty_title_config_produces_empty_string(self):
|
|
"""空 title_config 时 custom_title 为空字符串."""
|
|
title_config = {}
|
|
title_text = (title_config.get("text") or "").strip()
|
|
custom_title_value = ""
|
|
if title_text:
|
|
custom_title_value = json.dumps(title_config, ensure_ascii=False)
|
|
|
|
assert custom_title_value == ""
|