style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m36s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m45s
AI Code Review / AI Code Review (pull_request) Successful in 2m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m12s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m7s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m37s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 30s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 46s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 734h47m24s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 734h47m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 734h47m25s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 734h47m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 734h52m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 734h47m26s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 734h52m32s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 734h52m54s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 734h52m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 734h52m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 735h21m0s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 735h26m6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 735h26m29s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m36s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m45s
AI Code Review / AI Code Review (pull_request) Successful in 2m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m12s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m7s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m37s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 30s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 46s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 734h47m24s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 734h47m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 734h47m25s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 734h47m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 734h52m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 734h47m26s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 734h52m32s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 734h52m54s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 734h52m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 734h52m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 734h52m56s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 735h21m0s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 735h26m6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 735h26m29s
This commit is contained in:
@@ -13,17 +13,17 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from packages.domain.subtitle import SubtitleTimeline
|
||||
from packages.domain.ass_subtitle_builder import (
|
||||
TITLE_MARGIN_SIDE,
|
||||
TITLE_MARGIN_TOP,
|
||||
_wrap_title_text,
|
||||
build_ass_style,
|
||||
hex_to_ass_color,
|
||||
position_to_ass_alignment,
|
||||
escape_ass_text,
|
||||
format_ass_time,
|
||||
_wrap_title_text,
|
||||
TITLE_MARGIN_TOP,
|
||||
TITLE_MARGIN_SIDE,
|
||||
hex_to_ass_color,
|
||||
position_to_ass_alignment,
|
||||
)
|
||||
from packages.domain.subtitle import SubtitleTimeline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -187,14 +187,14 @@ def generate_ass_from_timeline(
|
||||
_stroke_val = title_cfg.get("stroke")
|
||||
if isinstance(_stroke_val, bool):
|
||||
title_cfg["stroke"] = (
|
||||
{"enabled": _stroke_val, "color": "#000000", "width": 2}
|
||||
if _stroke_val else {"enabled": False}
|
||||
{"enabled": _stroke_val, "color": "#000000", "width": 2} if _stroke_val else {"enabled": False}
|
||||
)
|
||||
_shadow_val = title_cfg.get("shadow")
|
||||
if isinstance(_shadow_val, bool):
|
||||
title_cfg["shadow"] = (
|
||||
{"enabled": _shadow_val, "color": "#000000", "blur": 4, "offset_x": 2, "offset_y": 2}
|
||||
if _shadow_val else {"enabled": False}
|
||||
if _shadow_val
|
||||
else {"enabled": False}
|
||||
)
|
||||
|
||||
# 字段名归一化: font_size→size, font_color→color
|
||||
@@ -239,9 +239,7 @@ def generate_ass_from_timeline(
|
||||
if video_duration > 0:
|
||||
t_end_time = format_ass_time(video_duration)
|
||||
else:
|
||||
t_end_time = format_ass_time(
|
||||
(timeline.segments[-1].end + 5.0) if timeline.segments else 60.0
|
||||
)
|
||||
t_end_time = format_ass_time((timeline.segments[-1].end + 5.0) if timeline.segments else 60.0)
|
||||
title_event_line = f"Dialogue: 0,0:00:00.00,{t_end_time},TitleStyle,,0,0,0,,{safe_wrapped}"
|
||||
|
||||
# 组装 ASS 文件
|
||||
|
||||
@@ -4,32 +4,36 @@
|
||||
断点2: _render_video() 解析 custom_title 并注入 virtual_plan.config["title"]
|
||||
断点3: generate_ass_from_timeline() ASR路径也渲染标题
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ── 断点2: _render_video 标题注入 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestRenderVideoCustomTitleInjection:
|
||||
"""验证 _render_video 正确接收并注入 custom_title 到 virtual_plan.config['title']。"""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_custom_title(self):
|
||||
"""模拟前端发送的 custom_title JSON(含 font_size/font_color)。"""
|
||||
return json.dumps({
|
||||
"text": "测试标题",
|
||||
"font": "思源黑体",
|
||||
"font_size": 30,
|
||||
"font_color": "#FF0000",
|
||||
"position": "top",
|
||||
"bold": True,
|
||||
"stroke": True,
|
||||
"shadow": False,
|
||||
}, ensure_ascii=False)
|
||||
return json.dumps(
|
||||
{
|
||||
"text": "测试标题",
|
||||
"font": "思源黑体",
|
||||
"font_size": 30,
|
||||
"font_color": "#FF0000",
|
||||
"position": "top",
|
||||
"bold": True,
|
||||
"stroke": True,
|
||||
"shadow": False,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
def _call_render_video_with_capture(self, custom_title, template_config=None, tmp_path=None):
|
||||
"""调用 _render_video,在 RenderAdapter 处中断并捕获 virtual_plan.config。"""
|
||||
@@ -49,10 +53,12 @@ class TestRenderVideoCustomTitleInjection:
|
||||
captured_config.update(fake_plan.config or {})
|
||||
raise RuntimeError("STOP_HERE")
|
||||
|
||||
with patch("worker_app.tasks.generation._build_plan_and_clips_from_task") as mock_build, \
|
||||
patch("worker_app.tasks.generation._load_template_plan_config", return_value=template_config), \
|
||||
patch("worker_app.tasks.generation.time.monotonic", side_effect=[0.0, 1.0]), \
|
||||
patch("video_processing.render_adapter.RenderAdapter") as mock_adapter_cls:
|
||||
with (
|
||||
patch("worker_app.tasks.generation._build_plan_and_clips_from_task") as mock_build,
|
||||
patch("worker_app.tasks.generation._load_template_plan_config", return_value=template_config),
|
||||
patch("worker_app.tasks.generation.time.monotonic", side_effect=[0.0, 1.0]),
|
||||
patch("video_processing.render_adapter.RenderAdapter") as mock_adapter_cls,
|
||||
):
|
||||
|
||||
mock_build.return_value = (fake_plan, [], {})
|
||||
mock_adapter_cls.side_effect = capture_and_raise
|
||||
@@ -112,14 +118,16 @@ class TestRenderVideoCustomTitleInjection:
|
||||
|
||||
# ── 断点3: generate_ass_from_timeline ASR路径支持标题 ──────────────────────────
|
||||
|
||||
|
||||
class TestGenerateAssFromTimelineWithTitle:
|
||||
"""验证 generate_ass_from_timeline 在有标题时生成包含 TitleStyle 的 ASS。"""
|
||||
|
||||
def test_title_included_in_ass_output(self, tmp_path):
|
||||
"""有 title_text 时,ASS 输出应包含 TitleStyle 和标题事件。"""
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
from video_processing.subtitle_generator import generate_ass_from_timeline
|
||||
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(start=0.0, end=2.0, text="你好世界"),
|
||||
@@ -148,9 +156,10 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
|
||||
def test_no_title_no_title_style(self, tmp_path):
|
||||
"""无标题时,ASS 输出不应包含 TitleStyle。"""
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
from video_processing.subtitle_generator import generate_ass_from_timeline
|
||||
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[
|
||||
SubtitleSegment(start=0.0, end=2.0, text="只有字幕"),
|
||||
@@ -175,12 +184,11 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
|
||||
def test_title_field_normalization_in_ass(self, tmp_path):
|
||||
"""前端字段名 font_size/font_color 应被正确归一化。"""
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
from video_processing.subtitle_generator import generate_ass_from_timeline
|
||||
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[SubtitleSegment(start=0.0, end=2.0, text="test")]
|
||||
)
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
|
||||
timeline = SubtitleTimeline(segments=[SubtitleSegment(start=0.0, end=2.0, text="test")])
|
||||
|
||||
ass_path = tmp_path / "test.ass"
|
||||
result = generate_ass_from_timeline(
|
||||
@@ -192,8 +200,8 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
subtitle_config={},
|
||||
title_text="归一化测试",
|
||||
title_config={
|
||||
"font_size": 30, # 前端字段名
|
||||
"font_color": "#FF0000", # 前端字段名
|
||||
"font_size": 30, # 前端字段名
|
||||
"font_color": "#FF0000", # 前端字段名
|
||||
"position": "top",
|
||||
},
|
||||
)
|
||||
@@ -204,12 +212,11 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
|
||||
def test_title_boolean_stroke_shadow_compat(self, tmp_path):
|
||||
"""boolean stroke/shadow 应被兼容处理。"""
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
from video_processing.subtitle_generator import generate_ass_from_timeline
|
||||
|
||||
timeline = SubtitleTimeline(
|
||||
segments=[SubtitleSegment(start=0.0, end=2.0, text="test")]
|
||||
)
|
||||
from packages.domain.subtitle import SubtitleSegment, SubtitleTimeline
|
||||
|
||||
timeline = SubtitleTimeline(segments=[SubtitleSegment(start=0.0, end=2.0, text="test")])
|
||||
|
||||
ass_path = tmp_path / "test.ass"
|
||||
result = generate_ass_from_timeline(
|
||||
@@ -222,8 +229,8 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
title_text="描边测试",
|
||||
title_config={
|
||||
"size": 36,
|
||||
"stroke": True, # boolean
|
||||
"shadow": False, # boolean
|
||||
"stroke": True, # boolean
|
||||
"shadow": False, # boolean
|
||||
},
|
||||
)
|
||||
|
||||
@@ -234,14 +241,16 @@ class TestGenerateAssFromTimelineWithTitle:
|
||||
|
||||
# ── 断点1: _render_video 签名包含 custom_title ────────────────────────────────
|
||||
|
||||
|
||||
class TestRenderVideoSignature:
|
||||
"""验证 _render_video 函数签名正确。"""
|
||||
|
||||
def test_custom_title_parameter_exists(self):
|
||||
"""_render_video 应有 custom_title 参数,默认空字符串。"""
|
||||
from worker_app.tasks.generation import _render_video
|
||||
import inspect
|
||||
|
||||
from worker_app.tasks.generation import _render_video
|
||||
|
||||
sig = inspect.signature(_render_video)
|
||||
assert "custom_title" in sig.parameters
|
||||
assert sig.parameters["custom_title"].default == ""
|
||||
|
||||
Reference in New Issue
Block a user