fix(#463): 直通模式调速失效 - final_duration未考虑调速导致减速场景被截断 #625
@@ -1050,8 +1050,10 @@ class UnifiedRenderService:
|
||||
|
||||
vf_str = ",".join(filters)
|
||||
|
||||
# 最终输出时长:取 clip 有效时长和 video_duration 的较小值
|
||||
final_duration = effective_duration
|
||||
# 最终输出时长:取 clip 调速后有效时长和 video_duration 的较小值
|
||||
# 注意:必须用调速后的时长,否则减速场景(speed<1)会被 -t 截断
|
||||
adjusted_duration = UnifiedRenderService._clip_adjusted_duration(clip)
|
||||
final_duration = adjusted_duration
|
||||
if video_duration > 0 and (final_duration <= 0 or final_duration > video_duration):
|
||||
final_duration = video_duration
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class FakeClip:
|
||||
transition_effect: str = "cut"
|
||||
transition_duration: float = 0.0
|
||||
status: str = "ready"
|
||||
playback_speed: float = 1.0
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -68,6 +69,7 @@ def _make_clip(
|
||||
transition_effect: str = "cut",
|
||||
transition_duration: float = 0.0,
|
||||
config: dict[str, Any] | None = None,
|
||||
playback_speed: float = 1.0,
|
||||
) -> FakeClip:
|
||||
return FakeClip(
|
||||
id=clip_id,
|
||||
@@ -78,6 +80,7 @@ def _make_clip(
|
||||
transition_effect=transition_effect,
|
||||
transition_duration=transition_duration,
|
||||
config=config or {},
|
||||
playback_speed=playback_speed,
|
||||
)
|
||||
|
||||
|
||||
@@ -1925,6 +1928,88 @@ class TestConcatNormalizeVideoResolution:
|
||||
vf_value = cmd[vf_idx + 1]
|
||||
assert "crop=" in vf_value, "background 层应有 crop 滤镜"
|
||||
|
||||
def test_pass_through_speed_up_correct_duration(self):
|
||||
"""直通模式加速(speed=2x):视频+音频均调速,-t 时长为原始的 1/2。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=10.0, playback_speed=2.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
svc = _make_service(clips, asset_paths)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=10.0),
|
||||
patch("video_processing.unified_render_service.run_ffmpeg") as mock_run,
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
svc._render_pass_through(layers, Path("/tmp/out.mp4"))
|
||||
|
||||
assert mock_run.called
|
||||
cmd = mock_run.call_args[0][0]
|
||||
cmd_str = " ".join(cmd)
|
||||
|
||||
# 视频调速:setpts=PTS/2.0
|
||||
assert "setpts=PTS/2.0" in cmd_str, "加速场景应有 setpts=PTS/speed 滤镜"
|
||||
|
||||
# 音频调速:atempo
|
||||
assert "atempo" in cmd_str, "加速场景应有 atempo 音频调速滤镜"
|
||||
|
||||
# 输出时长应为原始 / speed = 10 / 2 = 5 秒
|
||||
t_idx = cmd.index("-t")
|
||||
t_value = float(cmd[t_idx + 1])
|
||||
assert abs(t_value - 5.0) < 0.01, f"加速后 -t 时长应为 5.0s,实际 {t_value}s"
|
||||
|
||||
def test_pass_through_slow_down_correct_duration(self):
|
||||
"""直通模式减速(speed=0.5x):视频+音频均调速,-t 时长为原始的 2 倍(不被截断)。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=10.0, playback_speed=0.5)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
svc = _make_service(clips, asset_paths)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=10.0),
|
||||
patch("video_processing.unified_render_service.run_ffmpeg") as mock_run,
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
svc._render_pass_through(layers, Path("/tmp/out.mp4"))
|
||||
|
||||
assert mock_run.called
|
||||
cmd = mock_run.call_args[0][0]
|
||||
cmd_str = " ".join(cmd)
|
||||
|
||||
# 视频调速:setpts=PTS/0.5
|
||||
assert "setpts=PTS/0.5" in cmd_str, "减速场景应有 setpts=PTS/speed 滤镜"
|
||||
|
||||
# 音频调速:atempo
|
||||
assert "atempo" in cmd_str, "减速场景应有 atempo 音频调速滤镜"
|
||||
|
||||
# 输出时长应为原始 / speed = 10 / 0.5 = 20 秒(减速后视频变长,不应被截断)
|
||||
t_idx = cmd.index("-t")
|
||||
t_value = float(cmd[t_idx + 1])
|
||||
assert abs(t_value - 20.0) < 0.01, f"减速后 -t 时长应为 20.0s,实际 {t_value}s"
|
||||
|
||||
def test_pass_through_speed_with_video_duration_cap(self):
|
||||
"""直通模式调速 + video_duration 截断:取调速后时长与 video_duration 的较小值。"""
|
||||
clips = [_make_clip("c1", "main", order=0, duration=10.0, playback_speed=2.0)]
|
||||
asset_paths = {"asset_c1.mp4": Path("/tmp/asset_c1.mp4")}
|
||||
svc = _make_service(clips, asset_paths)
|
||||
|
||||
with (
|
||||
_patch_path_exists(),
|
||||
patch("video_processing.unified_render_service.probe_duration", return_value=10.0),
|
||||
patch("video_processing.unified_render_service.run_ffmpeg") as mock_run,
|
||||
):
|
||||
resolved = svc._resolve_clips()
|
||||
layers = svc._group_clips_into_layers(resolved)
|
||||
# video_duration=3.0 < 调速后时长 5.0,应取 3.0
|
||||
svc._render_pass_through(layers, Path("/tmp/out.mp4"), video_duration=3.0)
|
||||
|
||||
assert mock_run.called
|
||||
cmd = mock_run.call_args[0][0]
|
||||
t_idx = cmd.index("-t")
|
||||
t_value = float(cmd[t_idx + 1])
|
||||
assert abs(t_value - 3.0) < 0.01, f"video_duration 更小时应取 video_duration,实际 {t_value}s"
|
||||
|
||||
|
||||
class TestConcatNormalizeVideoFps:
|
||||
"""视频帧率归一化:fps 滤镜统一到目标 fps。
|
||||
|
||||
Reference in New Issue
Block a user