6925a0821c
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build 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 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 / PR Build API Image (pull_request) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m1s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 2m22s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m46s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 3m20s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m20s
AI Code Review / AI Code Review (pull_request) Successful in 6m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 2s
CI/CD Pipeline / Deploy 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 3m52s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
133 lines
5.1 KiB
Python
133 lines
5.1 KiB
Python
"""#1749 配音对齐行为测试(重写)。
|
||
|
||
旧行为(已删除):渲染期全局等比裁剪片段 / 全局慢放补偿配音时长。
|
||
新行为(定稿):
|
||
- 段长由 voice_duration_planner 在选片阶段精确分配,成片总时长=配音;
|
||
- 渲染期 _align_clips_to_voice_duration 仅保留 ±5% 守卫日志,**不修改任何 clip**
|
||
(不裁剪、不慢放);
|
||
- 素材短于段长 → freeze(config['_freeze_seconds'])+ 目标段长始终为准,
|
||
由 tpad/apad 铺满。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from video_processing.unified_render_service import RenderLayer, ResolvedClip, UnifiedRenderService
|
||
|
||
|
||
def _make_clip(
|
||
clip_id: str,
|
||
duration: float,
|
||
actual_duration: float = 0.0,
|
||
playback_speed: float = 1.0,
|
||
config: dict | None = None,
|
||
transition_effect: str = "cut",
|
||
) -> ResolvedClip:
|
||
return ResolvedClip(
|
||
clip_id=clip_id,
|
||
asset_id=f"asset_{clip_id}",
|
||
local_path=Path(f"/tmp/{clip_id}.mp4"),
|
||
clip_type="main",
|
||
order=0,
|
||
duration=duration,
|
||
actual_duration=actual_duration or duration,
|
||
playback_speed=playback_speed,
|
||
config=config or {},
|
||
transition_effect=transition_effect,
|
||
transition_duration=0.0,
|
||
)
|
||
|
||
|
||
def _make_layer(role: str, clips: list[ResolvedClip]) -> RenderLayer:
|
||
return RenderLayer(role=role, clips=clips, z_index=0)
|
||
|
||
|
||
def _make_service() -> UnifiedRenderService:
|
||
plan = MagicMock()
|
||
plan.id = "test_plan"
|
||
plan.config = {}
|
||
with patch.object(UnifiedRenderService, "__init__", lambda self, **kwargs: None):
|
||
service = UnifiedRenderService.__new__(UnifiedRenderService)
|
||
service.plan = plan
|
||
service.voiceover_audio_path = None
|
||
service.transition_duration = 0.0
|
||
return service
|
||
|
||
|
||
def test_align_does_not_trim_clips_when_clips_longer():
|
||
"""片段合计比配音长:旧逻辑裁剪,新逻辑不动。"""
|
||
service = _make_service()
|
||
clips = [_make_clip("c1", 10.0), _make_clip("c2", 10.0)]
|
||
layers = [_make_layer("main", clips)]
|
||
service._align_clips_to_voice_duration(layers, voice_duration=10.0)
|
||
assert clips[0].duration == 10.0
|
||
assert clips[1].duration == 10.0
|
||
|
||
|
||
def test_align_does_not_slow_down_when_clips_shorter():
|
||
"""片段合计比配音短:旧逻辑慢放,新逻辑不动。"""
|
||
service = _make_service()
|
||
clips = [_make_clip("c1", 5.0, actual_duration=5.0), _make_clip("c2", 5.0, actual_duration=5.0)]
|
||
layers = [_make_layer("main", clips)]
|
||
service._align_clips_to_voice_duration(layers, voice_duration=20.0)
|
||
assert clips[0].playback_speed == 1.0
|
||
assert clips[1].playback_speed == 1.0
|
||
assert clips[0].duration == 5.0
|
||
|
||
|
||
def test_align_zero_voice_noop():
|
||
service = _make_service()
|
||
clips = [_make_clip("c1", 10.0)]
|
||
layers = [_make_layer("main", clips)]
|
||
service._align_clips_to_voice_duration(layers, voice_duration=0.0)
|
||
assert clips[0].duration == 10.0
|
||
|
||
|
||
def test_effective_duration_uses_target_duration_not_actual():
|
||
"""素材短于目标段长:effective_duration = 目标段长(冻结铺满),不被 actual 钳制。"""
|
||
clip = _make_clip("c1", duration=10.0, actual_duration=6.2)
|
||
eff = UnifiedRenderService._clip_effective_duration(clip)
|
||
assert eff == 10.0
|
||
|
||
|
||
def test_freeze_marked_in_config_respects_target_duration():
|
||
"""freeze 秒数 = 目标段长 − 素材内可用时长;config 标记供 tpad/apad 读取。"""
|
||
# 模拟 _resolve_clips 单段路径的 freeze 计算
|
||
target = 10.0
|
||
actual = 6.2
|
||
start = 0.0
|
||
avail = max(0.0, actual - start)
|
||
freeze = round(target - avail, 3) if avail < target - 0.05 else 0.0
|
||
assert freeze == 3.8
|
||
clip = _make_clip("c1", duration=target, actual_duration=actual, config={"_freeze_seconds": freeze})
|
||
assert clip.config["_freeze_seconds"] == 3.8
|
||
|
||
|
||
def test_estimate_total_duration_matches_planner_cut():
|
||
"""cut 零重叠:总时长 = Σ段长。"""
|
||
service = _make_service()
|
||
clips = [
|
||
_make_clip("c1", 7.333, actual_duration=30.0, transition_effect="cut"),
|
||
_make_clip("c2", 7.333, actual_duration=30.0, transition_effect="cut"),
|
||
_make_clip("c3", 7.334, actual_duration=30.0, transition_effect="cut"),
|
||
]
|
||
clips[0].order, clips[1].order, clips[2].order = 0, 1, 2
|
||
total = service._estimate_total_duration([_make_layer("main", clips)])
|
||
assert abs(total - 22.0) < 0.1
|
||
|
||
|
||
def test_estimate_total_duration_deducts_xfade_overlap():
|
||
"""xfade 转场:逐处扣减重叠(与 planner 同口径)。"""
|
||
service = _make_service()
|
||
clips = [
|
||
_make_clip("c1", 4.333, actual_duration=30.0, transition_effect="cut"),
|
||
_make_clip("c2", 4.333, actual_duration=30.0, transition_effect="xfade"),
|
||
_make_clip("c3", 4.334, actual_duration=30.0, transition_effect="xfade"),
|
||
]
|
||
clips[0].transition_duration, clips[1].transition_duration, clips[2].transition_duration = 0.0, 0.5, 0.5
|
||
clips[0].order, clips[1].order, clips[2].order = 0, 1, 2
|
||
total = service._estimate_total_duration([_make_layer("main", clips)])
|
||
assert abs(total - 12.0) < 0.1
|