From bd078438c0b29e84c19380226bf9b6678aa8ea86 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Thu, 18 Jun 2026 20:33:14 +0800 Subject: [PATCH] feat(phase7): add generated video download endpoint --- apps/api/app/api/routes/generated_videos.py | 24 +++++++++++++++++-- apps/api/app/schemas/generated_video.py | 5 ++++ docs/PHASE7-PROGRESS.md | 12 ++++++---- packages/application/generated_videos.py | 11 +++++++++ tests/integration/test_generation_pipeline.py | 23 +++++++++++++++++- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/apps/api/app/api/routes/generated_videos.py b/apps/api/app/api/routes/generated_videos.py index e88caeffe..b4391dbc9 100644 --- a/apps/api/app/api/routes/generated_videos.py +++ b/apps/api/app/api/routes/generated_videos.py @@ -1,9 +1,17 @@ from fastapi import APIRouter, Depends, HTTPException from app.dependencies import get_generated_video_repository -from app.schemas.generated_video import GeneratedVideoResponse, ListGeneratedVideosResponse +from app.schemas.generated_video import ( + GeneratedVideoDownloadUrlResponse, + GeneratedVideoResponse, + ListGeneratedVideosResponse, +) from packages.adapters.sqlalchemy_impl import SQLAlchemyGeneratedVideoRepository -from packages.application import GetGeneratedVideoUseCase, ListGeneratedVideosUseCase +from packages.application import ( + GetGeneratedVideoDownloadUrlUseCase, + GetGeneratedVideoUseCase, + ListGeneratedVideosUseCase, +) router = APIRouter() @@ -59,3 +67,15 @@ def get_generated_video( height=item.height, fps=item.fps, ) + + +@router.get("/{video_id}/download-url", response_model=GeneratedVideoDownloadUrlResponse) +def get_generated_video_download_url( + video_id: str, + generated_video_repository: SQLAlchemyGeneratedVideoRepository = Depends(get_generated_video_repository), +) -> GeneratedVideoDownloadUrlResponse: + use_case = GetGeneratedVideoDownloadUrlUseCase(generated_video_repository) + download_url = use_case.execute(video_id) + if download_url is None: + raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found") + return GeneratedVideoDownloadUrlResponse(video_id=video_id, download_url=download_url) diff --git a/apps/api/app/schemas/generated_video.py b/apps/api/app/schemas/generated_video.py index dfe02be2b..c09f7fdc5 100644 --- a/apps/api/app/schemas/generated_video.py +++ b/apps/api/app/schemas/generated_video.py @@ -16,5 +16,10 @@ class GeneratedVideoResponse(BaseModel): fps: float +class GeneratedVideoDownloadUrlResponse(BaseModel): + video_id: str + download_url: str + + class ListGeneratedVideosResponse(BaseModel): items: list[GeneratedVideoResponse] diff --git a/docs/PHASE7-PROGRESS.md b/docs/PHASE7-PROGRESS.md index 58035b778..5df38b7fa 100644 --- a/docs/PHASE7-PROGRESS.md +++ b/docs/PHASE7-PROGRESS.md @@ -2,7 +2,7 @@ **Phase**: Phase 7 - 核心视频剪辑业务 **状态**: 🔄 进行中 -**最后更新**: 2026-06-18 20:11 GMT+8 +**最后更新**: 2026-06-18 20:31 GMT+8 --- @@ -50,6 +50,7 @@ - [x] GeneratedVideo 主线骨架已落地 - [x] 生成任务创建后自动触发 worker - [x] 生成结果最小闭环测试已落地 +- [x] 生成结果下载地址接口已落地 - [ ] 前端主链路联调完成 ### 4. 本轮已完成的具体验证 @@ -58,7 +59,7 @@ - [x] `tests/integration/test_upload_pipeline.py` 通过 - [x] `tests/integration/test_classification_pipeline.py` 通过 - [x] `tests/integration/test_projects.py` 通过 -- [x] `tests/integration/test_generation_pipeline.py` 通过(含生成结果最小闭环) +- [x] `tests/integration/test_generation_pipeline.py` 通过(含生成结果最小闭环、下载地址查询) - [x] 素材与生成主线相关目录编译检查通过 --- @@ -97,6 +98,7 @@ - [x] ClassificationJob 与 Asset 元数据更新链路第一轮打通 - [x] GenerationTask / GeneratedVideo 主线骨架已补齐 - [x] API / Adapter / Worker 的生成结果流第一轮联调 +- [x] GeneratedVideo 查询 / 下载地址第一轮打通 - [ ] 测试补齐与回归验证 ### Step 3:保留 Agent 体系设计,等待 runtime 修复后再恢复实跑 @@ -107,7 +109,7 @@ ## 五、当前判断 -**当前 Phase 7 已完成素材前半主链打通,并把生成链推进到“最小可运行闭环”,整体仍保持在既定规则内推进。** +**当前 Phase 7 已完成素材前半主链打通,并把生成链推进到“最小可运行闭环 + 结果查询/下载接口可用”,整体仍保持在既定规则内推进。** 当前执行策略是: - 暂停 Agent 实跑 @@ -125,7 +127,7 @@ - 答:Phase 7 - 核心视频剪辑业务 2. **当前 Phase 主要在做什么?** - - 答:已切入 Phase 7 第一批业务开发,当前已完成素材前半主链收口,并把生成链推进到最小可运行闭环,正在持续提交与 CI/CD 验证 + - 答:已切入 Phase 7 第一批业务开发,当前已完成素材前半主链收口,并把生成链推进到最小可运行闭环与结果查询/下载可用,正在持续提交与 CI/CD 验证 3. **当前最重要的阻塞点是什么?** - 答:OpenClaw 子 Agent runtime 暂不稳定,因此暂停 Agent 实跑;另外数据库字段命名仍有历史包袱,但已通过映射兼容,不阻断主线开发 @@ -152,4 +154,4 @@ --- -**状态结论**:Phase 7 未跑偏,已暂停 Agent 实跑并切回主会话直开;当前素材前半主链已打通,生成链已进入最小可运行闭环,现继续通过提交与 CI/CD 验证推进。 +**状态结论**:Phase 7 未跑偏,已暂停 Agent 实跑并切回主会话直开;当前素材前半主链已打通,生成链已进入最小可运行闭环且结果查询/下载可用,现继续通过提交与 CI/CD 验证推进。 diff --git a/packages/application/generated_videos.py b/packages/application/generated_videos.py index 17e1a31ef..4e638346d 100644 --- a/packages/application/generated_videos.py +++ b/packages/application/generated_videos.py @@ -30,3 +30,14 @@ class ListGeneratedVideosByTaskUseCase: if not generation_task_id.strip(): raise ValueError("generation_task_id 不能为空") return self.generated_video_repository.list_by_generation_task(generation_task_id.strip()) + + +class GetGeneratedVideoDownloadUrlUseCase: + def __init__(self, generated_video_repository: GeneratedVideoRepository): + self.generated_video_repository = generated_video_repository + + def execute(self, video_id: str) -> str | None: + item = self.generated_video_repository.get(video_id) + if item is None: + return None + return item.file_url diff --git a/tests/integration/test_generation_pipeline.py b/tests/integration/test_generation_pipeline.py index ef40ecf1c..91d9d2bf5 100644 --- a/tests/integration/test_generation_pipeline.py +++ b/tests/integration/test_generation_pipeline.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone -from packages.application import CreateGenerationTaskCommand, CreateGenerationTaskUseCase +from packages.application import CreateGenerationTaskCommand, CreateGenerationTaskUseCase, GetGeneratedVideoDownloadUrlUseCase from packages.domain import GeneratedVideo, GenerationTaskStatus @@ -121,3 +121,24 @@ def test_generation_pipeline_smoke(): assert len(videos) == 1 assert videos[0].project_id == "proj-1" assert videos[0].generation_task_id == task.id + + +def test_get_generated_video_download_url(): + video_repo = DummyGeneratedVideoRepository() + video = GeneratedVideo.create( + workspace_id="ws-1", + project_id="proj-1", + generation_task_id="task-1", + name="task-1.mp4", + file_url="https://example.invalid/generated/task-1.mp4", + file_size=2048, + duration=12.5, + width=1920, + height=1080, + fps=25.0, + ) + video_repo.create(video) + + use_case = GetGeneratedVideoDownloadUrlUseCase(video_repo) + assert use_case.execute(video.id) == "https://example.invalid/generated/task-1.mp4" + assert use_case.execute("missing") is None