Files
xiaoxia-saas/tests/unit/test_xfade_per_transition_1766.py
T
xiaoxia 44224cfaf6
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
CI/CD Pipeline / Build Staging API Image (push) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 18s
CI/CD Pipeline / Validate - Style (push) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m7s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m56s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m33s
CI/CD Pipeline / Unit Tests (push) Failing after 9m24s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 196h56m42s
CI/CD Pipeline / Deploy Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Web Image (push) Failing after 196h56m42s
CI/CD Pipeline / Build Production API Image (push) Failing after 196h56m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 197h1m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 197h4m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 197h4m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 197h4m56s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 197h5m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 197h5m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 197h5m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 197h5m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 197h6m1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 197h5m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 197h6m4s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 197h6m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 197h6m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 197h6m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 197h6m7s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 197h6m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 197h6m17s
CI/CD Pipeline / CI Gate (push) Failing after 197h31m25s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 197h39m53s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 197h39m58s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 197h40m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 197h40m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 197h40m51s
CI/CD Pipeline / PR Build Web Image (push) Failing after 197h40m51s
feat: 转场位置与类型随机化 (#1766) (#1786)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 10:17:35 +08:00

262 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""#1766 xfade_builder 逐转场时长与位置微调测试.
覆盖:
- build_xfade_filter_chain:transition_durations 参数(逐转场独立时长)
- build_xfade_filter_chain:jitters 参数(位置微调偏移)
- 向后兼容:不传新参数时行为不变
- TransitionEngine.build_xfade_chain:透传新参数
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
for sub in ("packages", ""):
p = str(REPO_ROOT / sub) if sub else str(REPO_ROOT)
if p not in sys.path:
sys.path.insert(0, p)
from packages.domain.xfade_builder import ( # noqa: E402
DEFAULT_TRANSITION_DURATION,
build_xfade_filter_chain,
)
class TestBackwardCompatibility:
"""向后兼容测试:不传新参数时行为不变。"""
def test_single_transition_duration(self):
"""全局 transition_duration 仍有效。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
)
assert "xfade" in result
assert "duration=0.500" in result
assert dur > 0
def test_default_transition_duration(self):
"""不传 transition_duration 时使用默认值 0.5。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
)
assert "duration=0.500" in result
class TestPerTransitionDurations:
"""逐转场独立时长测试。"""
def test_different_durations_per_transition(self):
"""每个转场使用不同的时长。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0, 5.0],
clip_video_labels=["v0", "v1", "v2"],
transitions=["cut", "fade", "dissolve"],
transition_durations=[0.3, 0.8], # 第 1 个转场 0.3s,第 2 个 0.8s
)
assert "duration=0.300" in result
assert "duration=0.800" in result
def test_partial_durations_fallback_to_global(self):
"""transition_durations 长度不足时 fallback 到 transition_duration。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0, 5.0],
clip_video_labels=["v0", "v1", "v2"],
transitions=["cut", "fade", "dissolve"],
transition_duration=0.5,
transition_durations=[0.3], # 只有第一个,第二个 fallback 到 0.5
)
assert "duration=0.300" in result
assert "duration=0.500" in result
def test_empty_durations_uses_global(self):
"""transition_durations=[] 时使用全局 transition_duration。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.6,
transition_durations=[],
)
assert "duration=0.600" in result
def test_none_durations_uses_global(self):
"""transition_durations=None 时使用全局 transition_duration。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.6,
transition_durations=None,
)
assert "duration=0.600" in result
def test_duration_clamped_by_clip_length(self):
"""转场时长不能超过相邻片段时长。"""
result, dur = build_xfade_filter_chain(
clip_durations=[2.0, 2.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_durations=[1.5], # 1.5s > 2.0s * 0.4,会被钳制
)
# 钳制到可用范围内
assert "duration=" in result
def test_total_duration_reflects_per_transition(self):
"""总时长反映逐转场的重叠量。"""
# 使用 0.3s 转场
_, dur_short = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_durations=[0.3],
)
# 使用 0.8s 转场
_, dur_long = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_durations=[0.8],
)
# 更长转场 → 更多重叠 → 总时长更短
assert dur_long < dur_short
class TestJitters:
"""位置微调 jitter 测试。"""
def test_positive_jitter_delays_transition(self):
"""正 jitter 推迟转场(offset 增大)。"""
result_no_jitter, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
)
result_with_jitter, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
jitters=[0.3], # 正 jitter:推迟转场
)
# 提取 offset 值
import re
offset_no = float(re.search(r"offset=([\d.]+)", result_no_jitter).group(1))
offset_yes = float(re.search(r"offset=([\d.]+)", result_with_jitter).group(1))
assert offset_yes > offset_no
def test_negative_jitter_advances_transition(self):
"""负 jitter 提前转场(offset 减小)。"""
result_no_jitter, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
)
result_with_jitter, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
jitters=[-0.3], # 负 jitter:提前转场
)
import re
offset_no = float(re.search(r"offset=([\d.]+)", result_no_jitter).group(1))
offset_yes = float(re.search(r"offset=([\d.]+)", result_with_jitter).group(1))
assert offset_yes < offset_no
def test_jitter_clamped_to_valid_range(self):
"""jitter 不会使 offset 超出有效范围(>=0)。"""
result, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
jitters=[-100.0], # 极大负 jitter
)
import re
offset = float(re.search(r"offset=([\d.]+)", result).group(1))
assert offset >= 0.0
def test_per_transition_jitters(self):
"""每个转场可以有独立的 jitter。"""
result, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0, 5.0],
clip_video_labels=["v0", "v1", "v2"],
transitions=["cut", "fade", "dissolve"],
transition_duration=0.5,
jitters=[0.2, -0.1],
)
import re
offsets = [float(m.group(1)) for m in re.finditer(r"offset=([\d.]+)", result)]
assert len(offsets) == 2
def test_empty_jitters_no_effect(self):
"""jitters=[] 等同于无 jitter。"""
result_no, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
)
result_empty, _ = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "fade"],
transition_duration=0.5,
jitters=[],
)
assert result_no == result_empty
class TestEdgeCases:
"""边界情况测试。"""
def test_single_clip_with_durations(self):
"""单片段传入 transition_durations 不报错。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0],
clip_video_labels=["v0"],
transitions=["cut"],
transition_durations=[0.5],
)
assert "copy" in result
assert dur == 5.0
def test_empty_clips(self):
"""空片段列表返回空字符串。"""
result, dur = build_xfade_filter_chain(
clip_durations=[],
clip_video_labels=[],
transitions=[],
transition_durations=[],
jitters=[],
)
assert result == ""
assert dur == 0.0
def test_all_cut_transitions(self):
"""全硬切场景(不进入 xfade,由调用方处理 concat)。"""
result, dur = build_xfade_filter_chain(
clip_durations=[5.0, 5.0],
clip_video_labels=["v0", "v1"],
transitions=["cut", "cut"],
transition_durations=[0.0, 0.0],
)
# cut 转场仍然会生成 xfade 滤镜(因为底层不区分 cut)
# 调用方(unified_render_service)负责检测全硬切并走 concat
assert dur > 0