diff --git a/apps/worker/video_processing/__init__.py b/apps/worker/video_processing/__init__.py index 09de09a36..7cb64d4bc 100644 --- a/apps/worker/video_processing/__init__.py +++ b/apps/worker/video_processing/__init__.py @@ -2,13 +2,10 @@ 视频处理模块 """ -from .processor import VideoProcessor, VideoResult - # 共享工具模块(供 editing_modes / generation / edit_plan_generation 等复用) -from . import ffmpeg_utils -from . import oss_helpers -from . import dedup_helpers -from .unified_render_service import UnifiedRenderService, RenderResult +from . import dedup_helpers, ffmpeg_utils, oss_helpers +from .processor import VideoProcessor, VideoResult +from .unified_render_service import RenderResult, UnifiedRenderService __all__ = [ "VideoProcessor", diff --git a/apps/worker/video_processing/dedup_helpers.py b/apps/worker/video_processing/dedup_helpers.py index 7e51dce1f..61e1ef427 100644 --- a/apps/worker/video_processing/dedup_helpers.py +++ b/apps/worker/video_processing/dedup_helpers.py @@ -91,9 +91,7 @@ def create_video_record_and_dedup( # (b) 批次内查重(仅当有 batch_id 时) if not duplicate_result and batch_id: - duplicate_result = deduplicator.check_batch_duplicate( - fingerprint, batch_id, video_id, session - ) + duplicate_result = deduplicator.check_batch_duplicate(fingerprint, batch_id, video_id, session) if duplicate_result: generated_video.is_duplicate = True diff --git a/apps/worker/video_processing/editing_modes.py b/apps/worker/video_processing/editing_modes.py index 5c25a2d95..36a9fe361 100644 --- a/apps/worker/video_processing/editing_modes.py +++ b/apps/worker/video_processing/editing_modes.py @@ -21,7 +21,7 @@ else: from pathlib import Path from typing import Optional -from video_processing.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg, probe_video_info +from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_video_info, run_ffmpeg logger = logging.getLogger(__name__) diff --git a/apps/worker/video_processing/unified_render_service.py b/apps/worker/video_processing/unified_render_service.py index f7b1ec44b..6b6a15287 100644 --- a/apps/worker/video_processing/unified_render_service.py +++ b/apps/worker/video_processing/unified_render_service.py @@ -28,11 +28,11 @@ from pathlib import Path from typing import Any from video_processing.ffmpeg_utils import ( - FFMPEG_BIN, - DEFAULT_OUTPUT_WIDTH, - DEFAULT_OUTPUT_HEIGHT, DEFAULT_FPS, + DEFAULT_OUTPUT_HEIGHT, + DEFAULT_OUTPUT_WIDTH, DEFAULT_TRANSITION_DURATION, + FFMPEG_BIN, build_xfade_filter_chain, probe_duration, probe_video_info, @@ -273,7 +273,7 @@ class UnifiedRenderService: ) # 按 z_index 排序返回 - layers = sorted(layer_map.values(), key=lambda l: l.z_index) + layers = sorted(layer_map.values(), key=lambda lyr: lyr.z_index) return layers def _build_filter_complex(self, layers: list[RenderLayer]) -> tuple[str, list[str]]: diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index bef354a83..320be17e7 100644 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -30,13 +30,12 @@ OUTPUT_FPS = 25.0 # ── 共享工具模块导入 ────────────────────────────────────────────────────────── +from video_processing.dedup_helpers import create_video_record_and_dedup from video_processing.oss_helpers import ( download_asset, upload_to_oss, ) from video_processing.unified_render_service import UnifiedRenderService -from video_processing.dedup_helpers import create_video_record_and_dedup - # ── Repository imports (延迟导入避免循环依赖) ───────────────────────────────── diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index f9b27065f..9a1c1e3ee 100755 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -85,16 +85,15 @@ def _update_task_status(task_id: str, status_action: str, **kwargs) -> bool: # ── 共享工具模块导入 ────────────────────────────────────────────────────────── -from video_processing.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg, probe_duration +from video_processing.dedup_helpers import create_video_record_and_dedup +from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg from video_processing.oss_helpers import ( download_asset, oss_bucket, upload_to_oss, ) -from video_processing.dedup_helpers import create_video_record_and_dedup from video_processing.unified_render_service import UnifiedRenderService - # ── 虚拟 Plan / Clip(内存中构建,不写数据库) ──────────────────────────────── @@ -156,24 +155,28 @@ def _build_plan_and_clips_from_task( # 1 main + N-1 overlay for i, p in enumerate(downloaded_paths): clip_type = "main" if i == 0 else "overlay" - clips.append(_VirtualClip( - id=f"vc_{i:03d}", - plan_id=task_id, - clip_type=clip_type, - order=i, - asset_id=path_to_asset_id[p], - )) + clips.append( + _VirtualClip( + id=f"vc_{i:03d}", + plan_id=task_id, + clip_type=clip_type, + order=i, + asset_id=path_to_asset_id[p], + ) + ) elif mode == "voice_over": # N 个 main(config.role=b_roll) for i, p in enumerate(downloaded_paths): - clips.append(_VirtualClip( - id=f"vc_{i:03d}", - plan_id=task_id, - clip_type="main", - order=i, - asset_id=path_to_asset_id[p], - config={"role": "b_roll"}, - )) + clips.append( + _VirtualClip( + id=f"vc_{i:03d}", + plan_id=task_id, + clip_type="main", + order=i, + asset_id=path_to_asset_id[p], + config={"role": "b_roll"}, + ) + ) elif mode == "voice_pip": # 1 background + 1 corner_voice + N-2 b_roll for i, p in enumerate(downloaded_paths): @@ -183,23 +186,27 @@ def _build_plan_and_clips_from_task( clip_type = "corner_voice" else: clip_type = "b_roll" - clips.append(_VirtualClip( - id=f"vc_{i:03d}", - plan_id=task_id, - clip_type=clip_type, - order=i, - asset_id=path_to_asset_id[p], - )) + clips.append( + _VirtualClip( + id=f"vc_{i:03d}", + plan_id=task_id, + clip_type=clip_type, + order=i, + asset_id=path_to_asset_id[p], + ) + ) else: # ONE_TAKE (default): N 个 main clips for i, p in enumerate(downloaded_paths): - clips.append(_VirtualClip( - id=f"vc_{i:03d}", - plan_id=task_id, - clip_type="main", - order=i, - asset_id=path_to_asset_id[p], - )) + clips.append( + _VirtualClip( + id=f"vc_{i:03d}", + plan_id=task_id, + clip_type="main", + order=i, + asset_id=path_to_asset_id[p], + ) + ) return plan, clips, asset_path_map @@ -237,15 +244,23 @@ def _mux_audio_track(video_path: Path, audio_path: str, output_path: Path) -> No command = [ FFMPEG_BIN, "-y", - "-i", str(video_path), - "-i", audio_path, - "-c:v", "copy", - "-c:a", "aac", - "-b:a", "192k", + "-i", + str(video_path), + "-i", + audio_path, + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + "192k", "-shortest", - "-map", "0:v:0", - "-map", "1:a:0", - "-movflags", "+faststart", + "-map", + "0:v:0", + "-map", + "1:a:0", + "-movflags", + "+faststart", str(output_path), ] run_ffmpeg(command) @@ -380,9 +395,7 @@ def generate_video(self, task_id: str) -> dict: output_path = temp_path / output_name # 1. 从素材库下载视频素材 - downloaded_videos = _download_library_assets( - asset_library_id, temp_path, asset_ids=task_asset_ids or None - ) + downloaded_videos = _download_library_assets(asset_library_id, temp_path, asset_ids=task_asset_ids or None) # 2. 下载配音(如有) audio_path: str | None = None @@ -485,5 +498,3 @@ def generate_video(self, task_id: str) -> dict: "task_id": task_id, "error": str(error), } - - diff --git a/tests/integration/test_four_mode_rendering.py b/tests/integration/test_four_mode_rendering.py index fd433f6c0..eaa93506d 100644 --- a/tests/integration/test_four_mode_rendering.py +++ b/tests/integration/test_four_mode_rendering.py @@ -14,13 +14,12 @@ import tempfile from pathlib import Path import pytest - -from worker_app.tasks.generation import _build_plan_and_clips_from_task from video_processing.unified_render_service import ( RenderResult, UnifiedRenderService, _resolve_layer_role, ) +from worker_app.tasks.generation import _build_plan_and_clips_from_task pytestmark = pytest.mark.skipif( not shutil.which("ffmpeg"), diff --git a/tests/integration/test_full_pipeline.py b/tests/integration/test_full_pipeline.py index 878a96682..36059c7e9 100644 --- a/tests/integration/test_full_pipeline.py +++ b/tests/integration/test_full_pipeline.py @@ -9,20 +9,19 @@ import shutil import subprocess import tempfile from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest - from video_processing.unified_render_service import ( RenderResult, UnifiedRenderService, ) from worker_app.tasks.generation import ( - _build_plan_and_clips_from_task, - _mux_audio_track, - _create_fallback_clip, - OUTPUT_WIDTH, OUTPUT_HEIGHT, + OUTPUT_WIDTH, + _build_plan_and_clips_from_task, + _create_fallback_clip, + _mux_audio_track, ) pytestmark = pytest.mark.skipif( @@ -215,11 +214,11 @@ class TestFullPipeline: def test_dedup_helper_integration(self): """验证 dedup_helpers.create_video_record_and_dedup 的导入和签名。""" - from video_processing.dedup_helpers import create_video_record_and_dedup - # 只验证函数存在且签名正确(不实际调用,需要数据库) import inspect + from video_processing.dedup_helpers import create_video_record_and_dedup + sig = inspect.signature(create_video_record_and_dedup) params = set(sig.parameters.keys()) expected = { diff --git a/tests/unit/test_unified_render_service.py b/tests/unit/test_unified_render_service.py index 6973de500..4670c132b 100644 --- a/tests/unit/test_unified_render_service.py +++ b/tests/unit/test_unified_render_service.py @@ -8,19 +8,17 @@ from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from typing import Any -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest - from video_processing.unified_render_service import ( - ResolvedClip, RenderLayer, RenderResult, + ResolvedClip, UnifiedRenderService, _resolve_layer_role, ) - # ── Fixtures ────────────────────────────────────────────────────────────────── @@ -167,12 +165,12 @@ class TestGroupClipsIntoLayers: resolved = svc._resolve_clips() layers = svc._group_clips_into_layers(resolved) - roles = {l.role for l in layers} + roles = {lyr.role for lyr in layers} assert "main" in roles assert "overlay" in roles - main_layer = next(l for l in layers if l.role == "main") - overlay_layer = next(l for l in layers if l.role == "overlay") + main_layer = next(lyr for lyr in layers if lyr.role == "main") + overlay_layer = next(lyr for lyr in layers if lyr.role == "overlay") assert len(main_layer.clips) == 1 assert len(overlay_layer.clips) == 2 assert overlay_layer.z_index > main_layer.z_index @@ -206,7 +204,7 @@ class TestGroupClipsIntoLayers: resolved = svc._resolve_clips() layers = svc._group_clips_into_layers(resolved) - roles = {l.role for l in layers} + roles = {lyr.role for lyr in layers} assert roles == {"background", "corner_voice", "broll"} assert len(layers) == 3 @@ -390,10 +388,12 @@ class TestRender: 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=5.0), \ - patch.object(svc, "_execute_ffmpeg") as mock_exec, \ - patch.object(svc, "_probe_output", return_value=(5.0, 1024, 1280, 720)): + with ( + _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, "_probe_output", return_value=(5.0, 1024, 1280, 720)), + ): result = svc.render() assert isinstance(result, RenderResult)