From 19a6fa3d5a9f8013d92c4a4c5908fdc3853566fa Mon Sep 17 00:00:00 2001 From: saas-backend-agent Date: Mon, 7 Sep 2026 07:02:08 +0800 Subject: [PATCH] =?UTF-8?q?test(#1749):=20=E6=97=A7=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=80=82=E9=85=8D=E6=96=B0=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=EF=BC=88=E7=9B=AE=E6=A0=87=E6=AE=B5=E9=95=BF=E4=B8=BA=E5=87=86?= =?UTF-8?q?/=E9=80=90=E5=A4=84=E8=BD=AC=E5=9C=BA=E9=87=8D=E5=8F=A0/voice?= =?UTF-8?q?=5Fids=20=E7=A7=BB=E9=99=A4=EF=BC=89+=20filter=5Fcomplex=20trim?= =?UTF-8?q?=20=E5=8F=A3=E5=BE=84=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - unified_render_service filter_complex Step1 trim 时长改为 effective-freeze(与直通路径同口径,tpad 补冻结) - test_duration_compensation / render_audio_utils / render_audio_pure / 两处 render_layer_utils:min(duration,actual) 断言改为目标段长为准 - estimate_total_duration 测试:cut 零重叠、非 cut 逐处扣转场(FakeClip 加 transition_effect/duration) - test_generation_tasks/preview:voice_ids kwarg/字段移除,配音改 voice_library_id --- .../unified_render_service.py | 10 +++-- tests/unit/domain/test_render_layer_utils.py | 41 +++++++++++++++---- tests/unit/test_duration_compensation.py | 31 +++++++------- tests/unit/test_generation_preview.py | 10 ++--- tests/unit/test_generation_tasks.py | 4 +- tests/unit/test_render_audio_pure.py | 4 +- tests/unit/test_render_audio_utils.py | 4 +- tests/unit/test_render_layer_utils.py | 22 +++++++++- 8 files changed, 87 insertions(+), 39 deletions(-) diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index 61d86b54d..d13ee3bf2 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -1736,12 +1736,15 @@ class UnifiedRenderService: # trim — 裁剪到指定区间,精确到帧 effective_duration = UnifiedRenderService._clip_effective_duration(clip) trim_start = getattr(clip, "start_time", 0) or 0 + # #1749 冻结时素材内截取时长 = 目标段长 − freeze(末帧冻结由 tpad 铺满,与直通路径同口径) + freeze_seconds = float((clip.config or {}).get("_freeze_seconds", 0.0) or 0.0) + trim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration - if effective_duration > 0: + if trim_dur > 0: if trim_start > 0: - filters.append(f"trim=start={trim_start:.3f}:duration={effective_duration:.3f}") + filters.append(f"trim=start={trim_start:.3f}:duration={trim_dur:.3f}") else: - filters.append(f"trim=duration={effective_duration:.3f}") + filters.append(f"trim=duration={trim_dur:.3f}") filters.append("setpts=PTS-STARTPTS") # 调速 — 基于 setpts 改变播放速度 @@ -1793,7 +1796,6 @@ class UnifiedRenderService: filters.append("setpts=PTS-STARTPTS") filters.append(f"fps={self.output_fps}") # #1749 末帧冻结:素材短于目标段长时克隆末帧铺满(禁慢放) - freeze_seconds = float((clip.config or {}).get("_freeze_seconds", 0.0) or 0.0) if freeze_seconds > 0: filters.append(f"tpad=stop_mode=clone:stop_duration={freeze_seconds:.3f}") diff --git a/tests/unit/domain/test_render_layer_utils.py b/tests/unit/domain/test_render_layer_utils.py index 3b9ba9090..c0976cf7a 100755 --- a/tests/unit/domain/test_render_layer_utils.py +++ b/tests/unit/domain/test_render_layer_utils.py @@ -135,8 +135,8 @@ class TestClipEffectiveDuration: assert clip_effective_duration(3.0, 5.0) == 3.0 def test_duration_greater_than_actual(self): - """duration > actual,取actual.""" - assert clip_effective_duration(10.0, 5.0) == 5.0 + """#1749:duration > actual 仍取目标段长 duration(素材短走末帧冻结,不钳制).""" + assert clip_effective_duration(10.0, 5.0) == 10.0 def test_duration_equal_to_actual(self): assert clip_effective_duration(5.0, 5.0) == 5.0 @@ -203,8 +203,8 @@ class TestClipAdjustedDuration: assert clip_adjusted_duration(4.0, 10.0, 0.5) == 8.0 def test_uses_effective_duration(self): - """duration>actual时取actual,再调速.""" - assert clip_adjusted_duration(10.0, 4.0, 2.0) == 2.0 + """#1749:effective=目标段长 10.0(不再钳 actual),2 倍速 → 10/2 = 5.0.""" + assert clip_adjusted_duration(10.0, 4.0, 2.0) == 5.0 def test_default_speed(self): assert clip_adjusted_duration(5.0, 10.0) == 5.0 @@ -229,6 +229,9 @@ class FakeClip: duration: float = 0.0 actual_duration: float = 0.0 playback_speed: float = 1.0 + # #1749:逐处转场口径——cut 零重叠,非 cut 才扣 transition_duration + transition_effect: str = "cut" + transition_duration: float = 0.0 @dataclass @@ -257,6 +260,19 @@ class TestEstimateTotalDuration: assert estimate_total_duration(layers) == 5.0 def test_with_transition_duration(self): + layers = [ + FakeLayer( + role="main", + clips=[ + FakeClip(duration=5.0), + FakeClip(duration=5.0, transition_effect="fade", transition_duration=0.5), + ], + ), + ] + # #1749:总10s - 1个非cut转场 * 0.5s = 9.5s(cut 零重叠) + assert estimate_total_duration(layers, transition_duration=0.5) == 9.5 + + def test_cut_clips_have_zero_overlap(self): layers = [ FakeLayer( role="main", @@ -266,8 +282,8 @@ class TestEstimateTotalDuration: ], ), ] - # 总10s - 1个转场 * 0.5s = 9.5s - assert estimate_total_duration(layers, transition_duration=0.5) == 9.5 + # #1749:cut 转场零重叠 → 总时长 10s,即使传了全局 transition_duration + assert estimate_total_duration(layers, transition_duration=0.5) == 10.0 def test_transition_with_many_clips(self): layers = [ @@ -276,7 +292,18 @@ class TestEstimateTotalDuration: clips=[FakeClip(duration=3.0) for _ in range(5)], ), ] - # 5个3s = 15s,4个转场 * 0.5s = 2s,总13s + # #1749:全 cut 零重叠 → 5个3s = 15s + assert estimate_total_duration(layers, transition_duration=0.5) == 15.0 + + def test_non_cut_transitions_with_many_clips(self): + layers = [ + FakeLayer( + role="main", + clips=[FakeClip(duration=3.0, transition_effect="fade", transition_duration=0.5) + for _ in range(5)], + ), + ] + # 5个3s = 15s,4个非cut转场 * 0.5s = 2s,总13s(#1749 逐处重叠) assert estimate_total_duration(layers, transition_duration=0.5) == 13.0 def test_prefers_main_over_broll(self): diff --git a/tests/unit/test_duration_compensation.py b/tests/unit/test_duration_compensation.py index abd4c08da..61caab764 100644 --- a/tests/unit/test_duration_compensation.py +++ b/tests/unit/test_duration_compensation.py @@ -10,8 +10,11 @@ from apps.worker.video_processing.unified_render_service import ResolvedClip, Un class TestClipEffectiveDurationWithSpeedCompensation: """Test _clip_effective_duration handles speed < 1 correctly.""" - def test_normal_speed_returns_min(self): - """When speed=1.0, effective_duration = min(duration, actual_duration).""" + def test_normal_speed_returns_target_duration(self): + """#1749:speed=1.0 时 effective_duration 始终为目标段长 duration。 + + 素材短于段长不再慢放补偿,差值由末帧冻结 tpad / 音频 apad 铺满。 + """ clip = ResolvedClip( clip_id="c1", asset_id="a1", @@ -23,11 +26,10 @@ class TestClipEffectiveDurationWithSpeedCompensation: actual_duration=3.0, # shorter than configured playback_speed=1.0, ) - # Without speed compensation, effective = min(4, 3) = 3 - assert UnifiedRenderService._clip_effective_duration(clip) == 3.0 + assert UnifiedRenderService._clip_effective_duration(clip) == 4.0 - def test_compensated_speed_returns_configured_duration(self): - """When speed < 1 (compensated), effective_duration = configured duration.""" + def test_explicit_speed_does_not_change_effective_duration(self): + """#1749:effective_duration 只认目标段长,与 playback_speed 无关(慢放补偿已删除)。""" clip = ResolvedClip( clip_id="c1", asset_id="a1", @@ -37,9 +39,8 @@ class TestClipEffectiveDurationWithSpeedCompensation: start_time=0.0, duration=4.0, actual_duration=3.0, # shorter than configured - playback_speed=0.75, # compensated: 3/4 = 0.75 + playback_speed=0.75, ) - # With speed compensation, effective = configured duration = 4.0 assert UnifiedRenderService._clip_effective_duration(clip) == 4.0 def test_zero_actual_duration_returns_configured(self): @@ -76,10 +77,10 @@ class TestClipEffectiveDurationWithSpeedCompensation: class TestClipAdjustedDurationWithSpeedCompensation: """Test _clip_adjusted_duration accounts for compensated speed.""" - def test_adjusted_duration_with_compensation(self): - """Adjusted duration = min(duration, actual) / speed. - With compensation: min(4,3)/0.75 = 3/0.75 = 4.0 - This equals the configured duration, which is the goal. + def test_adjusted_duration_with_explicit_speed(self): + """#1749:adjusted duration = 目标段长 / speed(用户显式调速仍生效;短素材不再自动慢放)。 + + 4.0 / 0.75 ≈ 5.333。 """ clip = ResolvedClip( clip_id="c1", @@ -90,8 +91,8 @@ class TestClipAdjustedDurationWithSpeedCompensation: start_time=0.0, duration=4.0, actual_duration=3.0, - playback_speed=0.75, # compensated + playback_speed=0.75, ) adjusted = UnifiedRenderService._clip_adjusted_duration(clip) - # min(4,3)/0.75 = 3/0.75 = 4.0 (matches configured duration) - assert abs(adjusted - 4.0) < 0.01 + # 4.0 / 0.75 = 5.333... + assert abs(adjusted - (4.0 / 0.75)) < 0.01 diff --git a/tests/unit/test_generation_preview.py b/tests/unit/test_generation_preview.py index 69a3444bc..8522e2f54 100644 --- a/tests/unit/test_generation_preview.py +++ b/tests/unit/test_generation_preview.py @@ -159,7 +159,6 @@ class TestCreatePreviewGenerationTaskRequest: assert req.template_id == "tmpl_123" assert req.asset_ids == ["asset_1", "asset_2"] assert req.title_ids == [] - assert req.voice_ids == [] assert req.video_title == "" assert req.duration == 0.0 assert req.bgm_config == {} @@ -175,15 +174,16 @@ class TestCreatePreviewGenerationTaskRequest: assert req.template_id == "tmpl_123" assert req.title_ids == ["title_1"] - def test_valid_request_with_voice_ids_only(self): - """有效请求:template_id + voice_ids""" + def test_valid_request_with_voice_library_id_and_asset_ids(self): + """有效请求:template_id + asset_ids + 统一配音 voice_library_id(#1749 替换旧 voice_ids 字段)""" from app.schemas.generation_task import CreatePreviewGenerationTaskRequest req = CreatePreviewGenerationTaskRequest( template_id="tmpl_123", - voice_ids=["voice_1"], + asset_ids=["asset_1"], + voice_library_id="voice_1", ) - assert req.voice_ids == ["voice_1"] + assert req.voice_library_id == "voice_1" def test_missing_template_id_raises(self): """缺少 template_id 报错""" diff --git a/tests/unit/test_generation_tasks.py b/tests/unit/test_generation_tasks.py index be060c94d..ad33cc4c3 100755 --- a/tests/unit/test_generation_tasks.py +++ b/tests/unit/test_generation_tasks.py @@ -77,13 +77,13 @@ class TestCreateGenerationTaskUseCase: project_id="proj_001", asset_ids=["asset_1", "asset_2", "asset_3"], title_ids=["title_1", "title_2"], - voice_ids=["voice_1"], ) result = use_case.execute(command) assert len(result.asset_ids) == 3 assert len(result.title_ids) == 2 - assert len(result.voice_ids) == 1 + # #1749:voice_ids 已从 Command 移除(配音走 voice_library_id(s)),落库固定空列表 + assert list(getattr(result, "voice_ids", []) or []) == [] def test_create_task_with_auto_retry(self, mock_repo): """创建带自动重试配置的任务""" diff --git a/tests/unit/test_render_audio_pure.py b/tests/unit/test_render_audio_pure.py index 336291a74..b4d3d1d73 100755 --- a/tests/unit/test_render_audio_pure.py +++ b/tests/unit/test_render_audio_pure.py @@ -62,9 +62,9 @@ class TestClipEffectiveDuration: assert clip_effective_duration(clip) == pytest.approx(10.0) def test_duration_greater_than_actual(self): - """duration > actual → 返回actual(不能超过素材时长).""" + """#1749:duration > actual → 返回目标段长 duration(素材短于段长走末帧冻结/补静音,不慢放不截配音).""" clip = _make_clip(duration=50.0, actual_duration=30.0) - assert clip_effective_duration(clip) == pytest.approx(30.0) + assert clip_effective_duration(clip) == pytest.approx(50.0) def test_duration_equals_actual(self): """duration == actual → 返回该值.""" diff --git a/tests/unit/test_render_audio_utils.py b/tests/unit/test_render_audio_utils.py index dd24e7daa..3d4dc24fe 100644 --- a/tests/unit/test_render_audio_utils.py +++ b/tests/unit/test_render_audio_utils.py @@ -24,9 +24,9 @@ class TestClipEffectiveDuration: assert clip_effective_duration(clip) == 5.0 def test_duration_specified_and_actual_shorter(self): - """指定了 duration,但实际时长更短 → 取实际时长""" + """#1749:指定了 duration 但实际时长更短 → 仍取目标段长 duration(素材短的部分由末帧冻结 tpad/音频 apad 铺满)""" clip = SimpleNamespace(duration=10.0, actual_duration=5.0) - assert clip_effective_duration(clip) == 5.0 + assert clip_effective_duration(clip) == 10.0 def test_duration_specified_actual_zero(self): """指定了 duration,但实际时长为 0 → 取 duration""" diff --git a/tests/unit/test_render_layer_utils.py b/tests/unit/test_render_layer_utils.py index 65f8d2eb0..922f83a1f 100755 --- a/tests/unit/test_render_layer_utils.py +++ b/tests/unit/test_render_layer_utils.py @@ -28,6 +28,9 @@ class FakeClip: duration: float = 0.0 actual_duration: float = 0.0 playback_speed: Any = 1.0 + # #1749:逐处转场口径——cut 零重叠,非 cut 才扣 transition_duration + transition_effect: str = "cut" + transition_duration: float = 0.0 @dataclass @@ -134,7 +137,8 @@ class TestClipEffectiveDuration: assert clip_effective_duration(5.0) == 5.0 def test_explicit_duration_with_shorter_actual(self): - assert clip_effective_duration(5.0, 3.0) == 3.0 + """#1749:duration>actual 仍返回目标段长(短素材末帧冻结铺满,不钳制)。""" + assert clip_effective_duration(5.0, 3.0) == 5.0 def test_explicit_duration_with_longer_actual(self): assert clip_effective_duration(5.0, 10.0) == 5.0 @@ -254,7 +258,21 @@ class TestEstimateTotalDuration: ], ) ] - # 3 + 2 + 5 - 2 * 0.5 = 9.0 + # #1749:全 cut 零重叠 → 3 + 2 + 5 = 10.0 + assert estimate_total_duration(layers, transition_duration=0.5) == pytest.approx(10.0) + + def test_non_cut_clips_deduct_transition_per_clip(self): + layers = [ + FakeLayer( + role="main", + clips=[ + FakeClip(duration=3.0, transition_effect="fade", transition_duration=0.5), + FakeClip(duration=2.0, transition_effect="fade", transition_duration=0.5), + FakeClip(duration=5.0, transition_effect="fade", transition_duration=0.5), + ], + ), + ] + # #1749:逐处非cut转场重叠 → 3 + 2 + 5 - 2 * 0.5 = 9.0 assert estimate_total_duration(layers, transition_duration=0.5) == pytest.approx(9.0) def test_prefers_main_over_broll(self):