diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index ad614e2c3..66c9e4c72 100755 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -1363,23 +1363,39 @@ class UnifiedRenderService: # 单 clip 层,直接使用预处理标签 layer_output_labels[layer.role] = layer_labels[0] else: - # 多 clip 层,用 TransitionEngine 构建转场链 out_label = f"{layer.role}_merged" - # 计算该层使用的转场时长(取首个非零值,否则用默认) - layer_dur = 0.0 - for d in layer_transition_durations: - if d > 0: - layer_dur = d - break - xfade_filter, _ = self._transition_engine.build_xfade_chain( - clip_durations=layer_durations, - clip_video_labels=layer_labels, - transitions=layer_transitions, - transition_duration=layer_dur if layer_dur > 0 else None, - output_label=out_label, + # 判断是否全部为硬切:是则用 concat filter,否则用 xfade 转场链 + all_cut = all( + t is None or t == "" or str(t).lower() == "cut" + for t in layer_transitions[1:] # 第一个 clip 的转场忽略 ) - if xfade_filter: - filter_parts.append(xfade_filter) + if all_cut: + # 全硬切:用 concat filter,性能远优于 xfade + concat_inputs = "".join(f"[{label}]" for label in layer_labels) + filter_parts.append( + f"{concat_inputs}concat=n={len(layer_labels)}:v=1:a=0[{out_label}]" + ) + logger.info( + "[unified-render] layer=%s clips=%d using concat (all hard-cut)", + layer.role, + len(layer_labels), + ) + else: + # 有转场效果:用 TransitionEngine 构建 xfade 链 + layer_dur = 0.0 + for d in layer_transition_durations: + if d > 0: + layer_dur = d + break + xfade_filter, _ = self._transition_engine.build_xfade_chain( + clip_durations=layer_durations, + clip_video_labels=layer_labels, + transitions=layer_transitions, + transition_duration=layer_dur if layer_dur > 0 else None, + output_label=out_label, + ) + if xfade_filter: + filter_parts.append(xfade_filter) layer_output_labels[layer.role] = out_label # Step 3: 合成各层 diff --git a/tests/unit/test_unified_render_service.py b/tests/unit/test_unified_render_service.py index ad78b8da3..3c85a35a0 100755 --- a/tests/unit/test_unified_render_service.py +++ b/tests/unit/test_unified_render_service.py @@ -45,6 +45,7 @@ class FakeClip: start_time: float = 0.0 duration: float = 0.0 transition_effect: str = "cut" + transition_duration: float = 0.0 status: str = "ready" config: dict[str, Any] = field(default_factory=dict) @@ -65,6 +66,7 @@ def _make_clip( asset_id: str = "", duration: float = 0.0, transition_effect: str = "cut", + transition_duration: float = 0.0, config: dict[str, Any] | None = None, ) -> FakeClip: return FakeClip( @@ -74,6 +76,7 @@ def _make_clip( asset_id=asset_id or f"asset_{clip_id}.mp4", duration=duration, transition_effect=transition_effect, + transition_duration=transition_duration, config=config or {}, ) @@ -332,7 +335,7 @@ class TestBuildFilterComplex: assert "[final_video]" in fc def test_single_layer_multi_clips(self): - """多个 main clips → xfade 串联。""" + """多个 main clips(默认硬切)→ concat 串联。""" clips = [ _make_clip("c1", "main", order=0, duration=3.0), _make_clip("c2", "main", order=1, duration=3.0), @@ -343,6 +346,35 @@ class TestBuildFilterComplex: } svc = _make_service(clips, asset_paths) + with _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0): + resolved = svc._resolve_clips() + layers = svc._group_clips_into_layers(resolved) + fc, input_args = svc._build_filter_complex(layers) + + assert input_args.count("-i") == 2 + # 全硬切场景走 concat filter(性能远优于 xfade) + assert "concat=n=2:v=1:a=0" in fc + assert "[final_video]" in fc + + def test_single_layer_multi_clips_with_transition(self): + """多个 main clips 带转场效果 → xfade 串联。""" + clips = [ + _make_clip("c1", "main", order=0, duration=3.0), + _make_clip( + "c2", + "main", + order=1, + duration=3.0, + transition_effect="fade", + transition_duration=0.5, + ), + ] + asset_paths = { + "asset_c1.mp4": Path("/tmp/asset_c1.mp4"), + "asset_c2.mp4": Path("/tmp/asset_c2.mp4"), + } + svc = _make_service(clips, asset_paths) + with _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0): resolved = svc._resolve_clips() layers = svc._group_clips_into_layers(resolved) @@ -380,7 +412,7 @@ class TestBuildFilterComplex: """ clips = [ _make_clip("c1", "main", order=0, duration=3.0), - _make_clip("c2", "main", order=1, duration=5.0), + _make_clip("c2", "main", order=1, duration=5.0, transition_effect="fade", transition_duration=0.5), ] asset_paths = { "asset_c1.mp4": Path("/tmp/asset_c1.mp4"),