diff --git a/apps/worker/video_processing/processor.py b/apps/worker/video_processing/processor.py index 8b2257280..5b2e3e7ca 100644 --- a/apps/worker/video_processing/processor.py +++ b/apps/worker/video_processing/processor.py @@ -2,6 +2,7 @@ 视频处理核心类 """ +import logging import os import tempfile from dataclasses import dataclass @@ -9,6 +10,8 @@ from typing import List import ffmpeg +logger = logging.getLogger(__name__) + @dataclass class VideoResult: diff --git a/tests/unit/test_unified_render_service.py b/tests/unit/test_unified_render_service.py index b3223fa13..a9dc6d5a2 100755 --- a/tests/unit/test_unified_render_service.py +++ b/tests/unit/test_unified_render_service.py @@ -11,14 +11,22 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest +from video_processing.render_audio import ( + RenderContext, + clip_has_audio, + merge_audio_video, + mix_audio, +) +from video_processing.render_subtitles import ( + _hex_to_ass_color, + _position_to_ass_alignment, + generate_ass_subtitles, +) from video_processing.unified_render_service import ( RenderResult, ResolvedClip, UnifiedRenderService, - _hex_to_ass_color, - _position_to_ass_alignment, _resolve_layer_role, - generate_ass_subtitles, ) # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -101,6 +109,11 @@ def _patch_path_exists(): return patch("pathlib.Path.exists", return_value=True) +def _make_ctx() -> RenderContext: + """创建测试用 RenderContext。""" + return RenderContext(work_dir=Path("/tmp/test_render"), plan_id="plan_001") + + # ── 测试 _resolve_layer_role ───────────────────────────────────────────────── @@ -572,7 +585,7 @@ class TestPassThrough: patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_render_pass_through") as mock_pass, patch.object(svc, "_execute_ffmpeg") as mock_exec, - patch.object(svc, "_mix_audio", return_value=None), + patch("video_processing.unified_render_service.mix_audio", return_value=None), patch("shutil.copy2"), patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)), ): @@ -599,7 +612,7 @@ class TestPassThrough: patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_render_pass_through") as mock_pass, patch.object(svc, "_execute_ffmpeg") as mock_exec, - patch.object(svc, "_mix_audio", return_value=None), + patch("video_processing.unified_render_service.mix_audio", return_value=None), patch("shutil.copy2"), patch.object(svc, "_probe_output", return_value=(5.5, 2048, 1280, 720)), ): @@ -819,7 +832,7 @@ class TestRender: _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_render_pass_through") as mock_pass, - patch.object(svc, "_mix_audio", return_value=None), + patch("video_processing.unified_render_service.mix_audio", return_value=None), patch("shutil.copy2"), patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)), ): @@ -848,7 +861,7 @@ class TestRender: _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_execute_ffmpeg") as mock_exec, - patch.object(svc, "_mix_audio", return_value=None), + patch("video_processing.unified_render_service.mix_audio", return_value=None), patch("shutil.copy2"), patch.object(svc, "_probe_output", return_value=(5.5, 2048, 1280, 720)), ): @@ -929,10 +942,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None assert result.name == "audio_plan_001.aac" @@ -957,10 +971,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 4.5) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 4.5) assert result is not None mock_run.assert_called_once() @@ -991,10 +1006,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1018,7 +1034,8 @@ class TestAudioMixing: # 没有素材的clip会被跳过,layers为空 resolved = svc._resolve_clips() layers = svc._group_clips_into_layers(resolved) - result = svc._mix_audio(layers, 3.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 3.0) assert result is None @@ -1037,10 +1054,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1066,10 +1084,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1088,10 +1107,11 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1108,11 +1128,12 @@ class TestAudioMixing: 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, + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) + ctx = _make_ctx() # video_duration 只有 3.0,小于 clip 的 10.0 - result = svc._mix_audio(layers, 3.0) + result = mix_audio(ctx, layers, 3.0) assert result is not None mock_run.assert_called_once() @@ -1125,13 +1146,13 @@ class TestAudioMixing: def test_merge_audio_video(self): """合并音视频命令正确。""" - svc = _make_service([], {}) video_path = Path("/tmp/video.mp4") audio_path = Path("/tmp/audio.aac") output_path = Path("/tmp/output.mp4") + ctx = _make_ctx() - with patch("video_processing.unified_render_service.run_ffmpeg") as mock_run: - svc._merge_audio_video(video_path, audio_path, output_path) + with patch("video_processing.render_audio.run_ffmpeg") as mock_run: + merge_audio_video(ctx, video_path, audio_path, output_path) mock_run.assert_called_once() cmd = mock_run.call_args[0][0] @@ -1156,8 +1177,8 @@ class TestAudioMixing: _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_execute_ffmpeg"), - patch.object(svc, "_mix_audio", return_value=Path("/tmp/audio.aac")) as mock_mix, - patch.object(svc, "_merge_audio_video") as mock_merge, + patch("video_processing.unified_render_service.mix_audio", return_value=Path("/tmp/audio.aac")) as mock_mix, + patch("video_processing.unified_render_service.merge_audio_video") as mock_merge, patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)), ): result = svc.render() @@ -1182,7 +1203,7 @@ class TestAudioMixing: _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_execute_ffmpeg"), - patch.object(svc, "_mix_audio", return_value=None), + patch("video_processing.unified_render_service.mix_audio", return_value=None), patch("shutil.copy2") as mock_copy, patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)), ): @@ -1201,8 +1222,8 @@ class TestAudioMixing: _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), patch.object(svc, "_render_pass_through", return_value=True) as mock_pt, - patch.object(svc, "_mix_audio") as mock_mix, - patch.object(svc, "_merge_audio_video") as mock_merge, + patch("video_processing.unified_render_service.mix_audio") as mock_mix, + patch("video_processing.unified_render_service.merge_audio_video") as mock_merge, patch("shutil.copy2") as mock_copy, patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)), ): @@ -1266,11 +1287,12 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.ffmpeg_utils.probe_has_audio", return_value=False), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.probe_has_audio", return_value=False), + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is None # 没有音频流时不应调用 FFmpeg @@ -1295,11 +1317,12 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.ffmpeg_utils.probe_has_audio", side_effect=fake_has_audio), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.probe_has_audio", side_effect=fake_has_audio), + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1333,11 +1356,12 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.ffmpeg_utils.probe_has_audio", side_effect=fake_has_audio), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.probe_has_audio", side_effect=fake_has_audio), + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is not None mock_run.assert_called_once() @@ -1367,11 +1391,12 @@ class TestAudioMixing: with ( _patch_path_exists(), patch("video_processing.unified_render_service.probe_duration", return_value=5.0), - patch("video_processing.ffmpeg_utils.probe_has_audio", return_value=False), - patch("video_processing.unified_render_service.run_ffmpeg") as mock_run, + patch("video_processing.render_audio.probe_has_audio", return_value=False), + patch("video_processing.render_audio.run_ffmpeg") as mock_run, ): layers = svc._group_clips_into_layers(svc._resolve_clips()) - result = svc._mix_audio(layers, 5.0) + ctx = _make_ctx() + result = mix_audio(ctx, layers, 5.0) assert result is None mock_run.assert_not_called() @@ -1389,11 +1414,12 @@ class TestAudioMixing: resolved = svc._resolve_clips() clip = resolved[0] - with patch("video_processing.ffmpeg_utils.probe_has_audio", return_value=True) as mock_probe: + with patch("video_processing.render_audio.probe_has_audio", return_value=True) as mock_probe: # 调用 3 次 - r1 = svc._clip_has_audio(clip) - r2 = svc._clip_has_audio(clip) - r3 = svc._clip_has_audio(clip) + ctx = _make_ctx() + r1 = clip_has_audio(ctx, clip) + r2 = clip_has_audio(ctx, clip) + r3 = clip_has_audio(ctx, clip) assert r1 is True and r2 is True and r3 is True # 实际只探测了 1 次