Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66630befa9 | |||
| ace7e0d7d1 |
@@ -163,6 +163,36 @@ def _compose_with_unified_engine(task, job_service, job, plan_id: str, db) -> di
|
||||
job_service.fail_job(job_id, error_msg[:500])
|
||||
raise RuntimeError(result.error_message)
|
||||
|
||||
# 生成封面(如果配置启用)
|
||||
cover_url = None
|
||||
try:
|
||||
from video_processing.cover_generator import generate_cover_from_plan
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
|
||||
SQLAlchemyEditPlanRepository as EditPlanRepository,
|
||||
)
|
||||
|
||||
# 获取 plan 对象
|
||||
plan_repo = EditPlanRepository(db)
|
||||
plan = plan_repo.get(plan_id)
|
||||
|
||||
if plan and result.output_path:
|
||||
# 检查 cover_config
|
||||
cover_config = (plan.config or {}).get("cover_config")
|
||||
if cover_config and cover_config.get("enabled", False):
|
||||
from pathlib import Path
|
||||
|
||||
output_dir = Path(result.output_path).parent
|
||||
cover_path = generate_cover_from_plan(plan, result.output_path, output_dir)
|
||||
if cover_path:
|
||||
# 生成 cover_url(相对路径或上传到存储)
|
||||
cover_url = f"/covers/{plan_id}.jpg"
|
||||
logger.info("封面生成成功: plan_id=%s cover_path=%s", plan_id, cover_path)
|
||||
else:
|
||||
logger.info("封面生成未启用: plan_id=%s", plan_id)
|
||||
except Exception as e:
|
||||
logger.warning("封面生成失败(不影响视频合成): plan_id=%s error=%s", plan_id, e)
|
||||
|
||||
# 更新 Job 状态为完成
|
||||
result_data = {
|
||||
"plan_id": plan_id,
|
||||
@@ -175,6 +205,7 @@ def _compose_with_unified_engine(task, job_service, job, plan_id: str, db) -> di
|
||||
"width": result.width,
|
||||
"height": result.height,
|
||||
"file_size": result.file_size,
|
||||
"cover_url": cover_url,
|
||||
}
|
||||
job_service.complete_job(job_id, result=result_data)
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"""测试 compose_video 任务中封面生成集成."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestComposeVideoCoverIntegration:
|
||||
"""测试视频合成任务中的封面生成集成."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_job_service(self):
|
||||
"""模拟 JobService."""
|
||||
service = MagicMock()
|
||||
service.get_job.return_value = MagicMock(
|
||||
id="job_123",
|
||||
payload={"plan_id": "plan_456"},
|
||||
)
|
||||
return service
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db(self):
|
||||
"""模拟数据库会话."""
|
||||
return MagicMock()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_render_result(self):
|
||||
"""模拟渲染结果."""
|
||||
result = MagicMock()
|
||||
result.success = True
|
||||
result.output_path = Path("/tmp/output/video_123.mp4")
|
||||
result.output_url = "https://example.com/video_123.mp4"
|
||||
result.duration = 30.0
|
||||
result.clip_count = 5
|
||||
result.width = 1080
|
||||
result.height = 1920
|
||||
result.file_size = 1024000
|
||||
return result
|
||||
|
||||
@pytest.fixture
|
||||
def mock_plan_with_cover_enabled(self):
|
||||
"""模拟启用封面的 plan."""
|
||||
plan = MagicMock()
|
||||
plan.id = "plan_456"
|
||||
plan.config = {
|
||||
"cover_config": {
|
||||
"enabled": True,
|
||||
"mode": "smart",
|
||||
}
|
||||
}
|
||||
return plan
|
||||
|
||||
@pytest.fixture
|
||||
def mock_plan_with_cover_disabled(self):
|
||||
"""模拟禁用封面的 plan."""
|
||||
plan = MagicMock()
|
||||
plan.id = "plan_456"
|
||||
plan.config = {
|
||||
"cover_config": {
|
||||
"enabled": False,
|
||||
}
|
||||
}
|
||||
return plan
|
||||
|
||||
@pytest.fixture
|
||||
def mock_plan_without_cover_config(self):
|
||||
"""模拟没有 cover_config 的 plan."""
|
||||
plan = MagicMock()
|
||||
plan.id = "plan_456"
|
||||
plan.config = {}
|
||||
return plan
|
||||
|
||||
def test_cover_generation_called_when_enabled(
|
||||
self,
|
||||
mock_job_service,
|
||||
mock_db,
|
||||
mock_render_result,
|
||||
mock_plan_with_cover_enabled,
|
||||
):
|
||||
"""测试封面生成在启用时被调用."""
|
||||
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
||||
|
||||
# 模拟 RenderAdapter
|
||||
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
||||
adapter_instance = MagicMock()
|
||||
adapter_instance.render_plan.return_value = mock_render_result
|
||||
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
||||
MockAdapter.return_value = adapter_instance
|
||||
|
||||
# 模拟 EditPlanRepository
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
||||
) as MockPlanRepo:
|
||||
plan_repo_instance = MagicMock()
|
||||
plan_repo_instance.get.return_value = mock_plan_with_cover_enabled
|
||||
MockPlanRepo.return_value = plan_repo_instance
|
||||
|
||||
# 模拟 generate_cover_from_plan
|
||||
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
||||
mock_gen_cover.return_value = Path("/tmp/output/cover_plan_456.jpg")
|
||||
|
||||
# 执行
|
||||
task = MagicMock()
|
||||
result = _compose_with_unified_engine(
|
||||
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
||||
)
|
||||
|
||||
# 验证封面生成被调用
|
||||
mock_gen_cover.assert_called_once()
|
||||
call_args = mock_gen_cover.call_args
|
||||
assert call_args[0][0] == mock_plan_with_cover_enabled # plan
|
||||
assert call_args[0][1] == mock_render_result.output_path # video_path
|
||||
assert call_args[0][2] == mock_render_result.output_path.parent # output_dir
|
||||
|
||||
# 验证结果包含 cover_url
|
||||
assert "cover_url" in result["result"]
|
||||
assert result["result"]["cover_url"] == "/covers/plan_456.jpg"
|
||||
|
||||
def test_cover_generation_skipped_when_disabled(
|
||||
self,
|
||||
mock_job_service,
|
||||
mock_db,
|
||||
mock_render_result,
|
||||
mock_plan_with_cover_disabled,
|
||||
):
|
||||
"""测试封面生成在禁用时被跳过."""
|
||||
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
||||
|
||||
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
||||
adapter_instance = MagicMock()
|
||||
adapter_instance.render_plan.return_value = mock_render_result
|
||||
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
||||
MockAdapter.return_value = adapter_instance
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
||||
) as MockPlanRepo:
|
||||
plan_repo_instance = MagicMock()
|
||||
plan_repo_instance.get.return_value = mock_plan_with_cover_disabled
|
||||
MockPlanRepo.return_value = plan_repo_instance
|
||||
|
||||
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
||||
task = MagicMock()
|
||||
result = _compose_with_unified_engine(
|
||||
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
||||
)
|
||||
|
||||
# 验证封面生成未被调用
|
||||
mock_gen_cover.assert_not_called()
|
||||
|
||||
# 验证结果中 cover_url 为 None
|
||||
assert "cover_url" in result["result"]
|
||||
assert result["result"]["cover_url"] is None
|
||||
|
||||
def test_cover_generation_skipped_when_no_config(
|
||||
self,
|
||||
mock_job_service,
|
||||
mock_db,
|
||||
mock_render_result,
|
||||
mock_plan_without_cover_config,
|
||||
):
|
||||
"""测试没有 cover_config 时封面生成被跳过."""
|
||||
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
||||
|
||||
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
||||
adapter_instance = MagicMock()
|
||||
adapter_instance.render_plan.return_value = mock_render_result
|
||||
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
||||
MockAdapter.return_value = adapter_instance
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
||||
) as MockPlanRepo:
|
||||
plan_repo_instance = MagicMock()
|
||||
plan_repo_instance.get.return_value = mock_plan_without_cover_config
|
||||
MockPlanRepo.return_value = plan_repo_instance
|
||||
|
||||
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
||||
task = MagicMock()
|
||||
result = _compose_with_unified_engine(
|
||||
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
||||
)
|
||||
|
||||
# 验证封面生成未被调用
|
||||
mock_gen_cover.assert_not_called()
|
||||
|
||||
# 验证结果中 cover_url 为 None
|
||||
assert "cover_url" in result["result"]
|
||||
assert result["result"]["cover_url"] is None
|
||||
|
||||
def test_cover_generation_failure_does_not_break_video(
|
||||
self,
|
||||
mock_job_service,
|
||||
mock_db,
|
||||
mock_render_result,
|
||||
mock_plan_with_cover_enabled,
|
||||
):
|
||||
"""测试封面生成失败不影响视频合成."""
|
||||
from worker_app.tasks.compose_video import _compose_with_unified_engine
|
||||
|
||||
with patch("video_processing.render_adapter.RenderAdapter") as MockAdapter:
|
||||
adapter_instance = MagicMock()
|
||||
adapter_instance.render_plan.return_value = mock_render_result
|
||||
adapter_instance.validate_plan.return_value = (True, [], [], 5, 5)
|
||||
MockAdapter.return_value = adapter_instance
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.edit_plan_repository.SQLAlchemyEditPlanRepository"
|
||||
) as MockPlanRepo:
|
||||
plan_repo_instance = MagicMock()
|
||||
plan_repo_instance.get.return_value = mock_plan_with_cover_enabled
|
||||
MockPlanRepo.return_value = plan_repo_instance
|
||||
|
||||
with patch("video_processing.cover_generator.generate_cover_from_plan") as mock_gen_cover:
|
||||
# 模拟封面生成抛出异常
|
||||
mock_gen_cover.side_effect = Exception("FFmpeg failed")
|
||||
|
||||
task = MagicMock()
|
||||
result = _compose_with_unified_engine(
|
||||
task, mock_job_service, mock_job_service.get_job(), "plan_456", mock_db
|
||||
)
|
||||
|
||||
# 验证视频合成仍然成功
|
||||
assert result["status"] == "completed"
|
||||
assert "result" in result
|
||||
assert result["result"]["output_url"] == mock_render_result.output_url
|
||||
|
||||
# 验证结果中 cover_url 为 None
|
||||
assert result["result"]["cover_url"] is None
|
||||
Reference in New Issue
Block a user