From 66dd02e03aa08423de463c200d887ef5c4aa2a51 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 23 Jul 2026 10:22:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(#632):=20=E4=B8=80=E9=94=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=BE=93=E5=87=BA=E5=88=86=E8=BE=A8=E7=8E=87=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generation_tasks 表新增 resolution 字段(migration 051) - API 请求/响应新增 resolution 参数,格式 WIDTHxHEIGHT(如 1080x1920) - 渲染优先级:用户指定 > 模板配置 > 默认 1280x720 - 领域模型 + Command + Repository 全链路透传 - 重试任务保留原分辨率 - 125 个相关单测全绿 Closes #632 --- .../051_generation_task_resolution.py | 53 +++++++++++++++++++ apps/api/app/api/routes/generation_tasks.py | 3 ++ apps/api/app/schemas/generation_task.py | 6 +++ apps/worker/worker_app/tasks/generation.py | 17 +++--- .../generation_task_repository.py | 4 ++ packages/adapters/sqlalchemy_impl/models.py | 1 + packages/application/generation_tasks.py | 2 + packages/domain/generation_task.py | 3 ++ tests/unit/test_generation_task_domain.py | 4 ++ 9 files changed, 87 insertions(+), 6 deletions(-) create mode 100755 alembic/versions/051_generation_task_resolution.py mode change 100644 => 100755 tests/unit/test_generation_task_domain.py diff --git a/alembic/versions/051_generation_task_resolution.py b/alembic/versions/051_generation_task_resolution.py new file mode 100755 index 000000000..cad8b6d93 --- /dev/null +++ b/alembic/versions/051_generation_task_resolution.py @@ -0,0 +1,53 @@ +"""#632 - 一键生成输出分辨率可配置 + +Revision ID: 051 +Revises: 050 +Create Date: 2026-07-23 + +Changes: +1. generation_tasks 表新增 resolution 字段,存储用户指定的输出分辨率(如 "1280x720") +2. 为空时使用默认值(1280x720) +""" + +import sqlalchemy as sa + +from alembic import context, op + +revision = "051_generation_task_resolution" +down_revision = "050_video_shares" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + if context.get_context().dialect.name == "postgresql": + # 检查列是否已存在(幂等) + result = conn.execute( + sa.text( + "SELECT column_name FROM information_schema.columns " + "WHERE table_name = 'generation_tasks' AND column_name = 'resolution'" + ) + ) + if result.scalar() is not None: + return + + op.add_column( + "generation_tasks", + sa.Column("resolution", sa.String(20), nullable=False, server_default=""), + ) + + +def downgrade() -> None: + conn = op.get_bind() + if context.get_context().dialect.name == "postgresql": + result = conn.execute( + sa.text( + "SELECT column_name FROM information_schema.columns " + "WHERE table_name = 'generation_tasks' AND column_name = 'resolution'" + ) + ) + if result.scalar() is None: + return + + op.drop_column("generation_tasks", "resolution") diff --git a/apps/api/app/api/routes/generation_tasks.py b/apps/api/app/api/routes/generation_tasks.py index a7c990734..cfc395d0b 100755 --- a/apps/api/app/api/routes/generation_tasks.py +++ b/apps/api/app/api/routes/generation_tasks.py @@ -59,6 +59,7 @@ def _to_generation_task_response(task) -> GenerationTaskResponse: asset_select_mode=getattr(task, "asset_select_mode", ""), batch_id=getattr(task, "batch_id", ""), video_title=getattr(task, "video_title", ""), + resolution=getattr(task, "resolution", ""), logs=getattr(task, "logs", "[]"), status=task.status, progress=task.progress, @@ -270,6 +271,7 @@ def create_generation_task( asset_select_mode=request.asset_select_mode, batch_id=batch_id, video_title=request.video_title, + resolution=request.resolution, auto_retry_enabled=request.auto_retry_enabled, auto_retry_max=request.auto_retry_max, ) @@ -408,6 +410,7 @@ def retry_generation_task( source_edit_plan_id=task.source_edit_plan_id or "", asset_select_mode=getattr(task, "asset_select_mode", ""), video_title=getattr(task, "video_title", ""), + resolution=getattr(task, "resolution", ""), ) ) try: diff --git a/apps/api/app/schemas/generation_task.py b/apps/api/app/schemas/generation_task.py index 671269094..8c2a7460d 100755 --- a/apps/api/app/schemas/generation_task.py +++ b/apps/api/app/schemas/generation_task.py @@ -46,6 +46,11 @@ class CreateGenerationTaskRequest(BaseModel): le=5, description="最大自动重试次数,0表示不自动重试,最大5次", ) + # ── 输出分辨率 ── + resolution: str = Field( + default="", + description="输出分辨率,格式为 WIDTHxHEIGHT,如 1280x720、1080x1920。为空使用默认 1280x720", + ) @model_validator(mode="after") def _check_at_least_one_mode(self) -> "CreateGenerationTaskRequest": @@ -74,6 +79,7 @@ class GenerationTaskResponse(BaseModel): asset_select_mode: str = "" batch_id: str = "" video_title: str = "" + resolution: str = "" status: str progress: float result_count: int diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index 0612dfa5b..6b1e1bd37 100755 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -1110,6 +1110,7 @@ def _load_task_info(task_id: str) -> dict | None: "batch_id": getattr(gen_task, "batch_id", "") or "", "user_id": getattr(gen_task, "created_by_user_id", "") or "", "video_title": getattr(gen_task, "video_title", "") or "", + "resolution": getattr(gen_task, "resolution", "") or "", } finally: session.close() @@ -1168,6 +1169,7 @@ def _render_video( user_id: str, temp_path: Path, output_name: str, + resolution: str = "", ) -> tuple[Path, float]: """渲染视频(含配音混音)。 @@ -1199,15 +1201,17 @@ def _render_video( list(template_config.keys()), ) - # 确保输出分辨率配置存在(一键生成默认横屏 1280x720) - # RenderAdapter 从 plan.config.export.resolution 读取, - # 如果模板没有配置则用默认值,这里显式设置保持和旧逻辑一致 + # 确保输出分辨率配置存在 + # 优先级:用户指定 > 模板配置 > 默认 1280x720 plan_cfg = virtual_plan.config or {} export_cfg = plan_cfg.get("export", {}) or {} - if not export_cfg.get("resolution"): + if resolution: + # 用户在 API 调用时指定的分辨率优先级最高 + export_cfg["resolution"] = resolution + elif not export_cfg.get("resolution"): export_cfg["resolution"] = f"{OUTPUT_WIDTH}x{OUTPUT_HEIGHT}" - plan_cfg["export"] = export_cfg - virtual_plan.config = plan_cfg + plan_cfg["export"] = export_cfg + virtual_plan.config = plan_cfg total_duration = sum(c.duration for c in virtual_clips) logger.info( @@ -1456,6 +1460,7 @@ def generate_video(self, task_id: str) -> dict: user_id=user_id, temp_path=temp_path, output_name=output_name, + resolution=task_info.get("resolution", ""), ) if gen_task: diff --git a/packages/adapters/sqlalchemy_impl/generation_task_repository.py b/packages/adapters/sqlalchemy_impl/generation_task_repository.py index 78ba701e1..dd347453a 100755 --- a/packages/adapters/sqlalchemy_impl/generation_task_repository.py +++ b/packages/adapters/sqlalchemy_impl/generation_task_repository.py @@ -34,6 +34,7 @@ def _to_domain(model: GenerationTaskModel) -> GenerationTask: asset_select_mode=model.asset_select_mode or "", batch_id=model.batch_id or "", video_title=getattr(model, "video_title", "") or "", + resolution=getattr(model, "resolution", "") or "", logs=model.logs or "[]", created_at=model.created_at, updated_at=model.updated_at, @@ -70,6 +71,7 @@ class SQLAlchemyGenerationTaskRepository: asset_select_mode=task.asset_select_mode or "", batch_id=task.batch_id or "", video_title=task.video_title or "", + resolution=task.resolution or "", logs=task.logs, created_at=task.created_at, updated_at=task.updated_at, @@ -230,6 +232,8 @@ class SQLAlchemyGenerationTaskRepository: model.batch_id = task.batch_id or "" if hasattr(model, "video_title"): model.video_title = task.video_title or "" + if hasattr(model, "resolution"): + model.resolution = task.resolution or "" model.logs = task.logs self.session.commit() return task diff --git a/packages/adapters/sqlalchemy_impl/models.py b/packages/adapters/sqlalchemy_impl/models.py index 8ed664673..df3857b6c 100755 --- a/packages/adapters/sqlalchemy_impl/models.py +++ b/packages/adapters/sqlalchemy_impl/models.py @@ -291,6 +291,7 @@ class GenerationTaskModel(Base): asset_select_mode = Column(String(20), nullable=False, default="") batch_id = Column(String(36), nullable=False, default="", index=True) video_title = Column(String(255), nullable=False, default="") + resolution = Column(String(20), nullable=False, default="") extra_meta = Column("metadata", JSON, nullable=False, default=dict) logs = Column(Text, nullable=False, default="[]", server_default="[]") created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) diff --git a/packages/application/generation_tasks.py b/packages/application/generation_tasks.py index e70940d63..598460c47 100755 --- a/packages/application/generation_tasks.py +++ b/packages/application/generation_tasks.py @@ -22,6 +22,7 @@ class CreateGenerationTaskCommand: asset_select_mode: str = "" batch_id: str = "" video_title: str = "" + resolution: str = "" auto_retry_enabled: bool = False auto_retry_max: int = 0 @@ -50,6 +51,7 @@ class CreateGenerationTaskUseCase: asset_select_mode=command.asset_select_mode, batch_id=command.batch_id, video_title=command.video_title, + resolution=command.resolution, auto_retry_enabled=command.auto_retry_enabled, auto_retry_max=command.auto_retry_max, ) diff --git a/packages/domain/generation_task.py b/packages/domain/generation_task.py index 2eba0ca53..a973c4f72 100755 --- a/packages/domain/generation_task.py +++ b/packages/domain/generation_task.py @@ -91,6 +91,7 @@ class GenerationTask: asset_select_mode: str = "" batch_id: str = "" video_title: str = "" + resolution: str = "" logs: str = "[]" created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -112,6 +113,7 @@ class GenerationTask: asset_select_mode: str = "", batch_id: str = "", video_title: str = "", + resolution: str = "", auto_retry_enabled: bool = False, auto_retry_max: int = 0, ) -> "GenerationTask": @@ -134,6 +136,7 @@ class GenerationTask: asset_select_mode=asset_select_mode, batch_id=batch_id, video_title=video_title.strip(), + resolution=resolution.strip(), auto_retry_enabled=auto_retry_enabled, auto_retry_max=auto_retry_max, ) diff --git a/tests/unit/test_generation_task_domain.py b/tests/unit/test_generation_task_domain.py old mode 100644 new mode 100755 index 1b0248a74..09a614e78 --- a/tests/unit/test_generation_task_domain.py +++ b/tests/unit/test_generation_task_domain.py @@ -73,6 +73,7 @@ class TestGenerationTaskCreate: asset_select_mode="smart", batch_id="batch-001", video_title="测试视频", + resolution="1080x1920", auto_retry_enabled=True, auto_retry_max=3, ) @@ -87,6 +88,7 @@ class TestGenerationTaskCreate: assert task.asset_select_mode == "smart" assert task.batch_id == "batch-001" assert task.video_title == "测试视频" + assert task.resolution == "1080x1920" assert task.auto_retry_enabled is True assert task.auto_retry_max == 3 @@ -134,12 +136,14 @@ class TestGenerationTaskCreate: strategy_id=" strat-789 ", template_id=" tmpl-001 ", video_title=" 测试视频 ", + resolution=" 1080x1920 ", ) assert task.project_id == "proj-123" assert task.asset_library_id == "lib-456" assert task.strategy_id == "strat-789" assert task.template_id == "tmpl-001" assert task.video_title == "测试视频" + assert task.resolution == "1080x1920" def test_create_default_empty_lists(self): """测试 None 列表默认化为空列表""" -- 2.54.0