test(#1749): 旧行为测试适配新口径(目标段长为准/逐处转场重叠/voice_ids 移除)+ filter_complex trim 口径统一

- 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
This commit is contained in:
saas-backend-agent
2026-09-07 07:02:08 +08:00
parent c9fc7a8cce
commit 19a6fa3d5a
8 changed files with 87 additions and 39 deletions
@@ -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}")
+34 -7
View File
@@ -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):
+16 -15
View File
@@ -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
+5 -5
View File
@@ -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 报错"""
+2 -2
View File
@@ -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):
"""创建带自动重试配置的任务"""
+2 -2
View File
@@ -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 → 返回该值."""
+2 -2
View File
@@ -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"""
+20 -2
View File
@@ -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):