"""#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