feat(phase7): prefer presigned generated video downloads
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.storage import MinIOService, get_minio_service
|
||||
from app.dependencies import get_generated_video_repository
|
||||
from app.schemas.generated_video import (
|
||||
GeneratedVideoDownloadUrlResponse,
|
||||
@@ -73,9 +74,11 @@ def get_generated_video(
|
||||
def get_generated_video_download_url(
|
||||
video_id: str,
|
||||
generated_video_repository: SQLAlchemyGeneratedVideoRepository = Depends(get_generated_video_repository),
|
||||
storage_service: MinIOService = Depends(get_minio_service),
|
||||
) -> GeneratedVideoDownloadUrlResponse:
|
||||
use_case = GetGeneratedVideoDownloadUrlUseCase(generated_video_repository)
|
||||
download_url = use_case.execute(video_id)
|
||||
if download_url is None:
|
||||
file_url = use_case.execute(video_id)
|
||||
if file_url is None:
|
||||
raise HTTPException(status_code=404, detail=f"GeneratedVideo {video_id} not found")
|
||||
download_url = storage_service.get_download_url(file_url)
|
||||
return GeneratedVideoDownloadUrlResponse(video_id=video_id, download_url=download_url)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""MinIO storage service for file uploads."""
|
||||
from datetime import timedelta
|
||||
from typing import BinaryIO
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
|
||||
from minio import Minio
|
||||
@@ -67,6 +69,25 @@ class MinIOService:
|
||||
def get_url(self, storage_key: str) -> str:
|
||||
return f"{self.public_url}/{self.bucket_name}/{storage_key}"
|
||||
|
||||
def get_download_url(self, storage_key_or_url: str, expires_seconds: int = 3600) -> str:
|
||||
storage_key = self._normalize_storage_key(storage_key_or_url)
|
||||
try:
|
||||
return self.client.presigned_get_object(
|
||||
self.bucket_name,
|
||||
storage_key,
|
||||
expires=timedelta(seconds=expires_seconds),
|
||||
)
|
||||
except S3Error:
|
||||
return self.get_url(storage_key)
|
||||
|
||||
def _normalize_storage_key(self, storage_key_or_url: str) -> str:
|
||||
prefix = f"/{self.bucket_name}/"
|
||||
if storage_key_or_url.startswith("http://") or storage_key_or_url.startswith("https://"):
|
||||
parsed = urlparse(storage_key_or_url)
|
||||
if prefix in parsed.path:
|
||||
return parsed.path.split(prefix, 1)[1]
|
||||
return storage_key_or_url.lstrip("/")
|
||||
|
||||
def delete_file(self, storage_key: str):
|
||||
try:
|
||||
self.client.remove_object(self.bucket_name, storage_key)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Phase**: Phase 7 - 核心视频剪辑业务
|
||||
**状态**: 🔄 进行中
|
||||
**最后更新**: 2026-06-18 20:31 GMT+8
|
||||
**最后更新**: 2026-06-18 20:48 GMT+8
|
||||
|
||||
---
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
- [x] 生成任务创建后自动触发 worker
|
||||
- [x] 生成结果最小闭环测试已落地
|
||||
- [x] 生成结果下载地址接口已落地
|
||||
- [x] 下载地址已升级为 MinIO 预签名优先策略
|
||||
- [ ] 前端主链路联调完成
|
||||
|
||||
### 4. 本轮已完成的具体验证
|
||||
@@ -59,7 +60,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` 通过(含生成结果最小闭环、下载地址查询、下载源 URL 稳定性)
|
||||
- [x] 素材与生成主线相关目录编译检查通过
|
||||
|
||||
---
|
||||
@@ -99,6 +100,7 @@
|
||||
- [x] GenerationTask / GeneratedVideo 主线骨架已补齐
|
||||
- [x] API / Adapter / Worker 的生成结果流第一轮联调
|
||||
- [x] GeneratedVideo 查询 / 下载地址第一轮打通
|
||||
- [x] 下载地址已接入 MinIO 预签名优先策略
|
||||
- [ ] 测试补齐与回归验证
|
||||
|
||||
### Step 3:保留 Agent 体系设计,等待 runtime 修复后再恢复实跑
|
||||
@@ -109,7 +111,7 @@
|
||||
|
||||
## 五、当前判断
|
||||
|
||||
**当前 Phase 7 已完成素材前半主链打通,并把生成链推进到“最小可运行闭环 + 结果查询/下载接口可用”,整体仍保持在既定规则内推进。**
|
||||
**当前 Phase 7 已完成素材前半主链打通,并把生成链推进到“最小可运行闭环 + 结果查询/下载接口可用(预签名优先)”,整体仍保持在既定规则内推进。**
|
||||
|
||||
当前执行策略是:
|
||||
- 暂停 Agent 实跑
|
||||
@@ -127,7 +129,7 @@
|
||||
- 答:Phase 7 - 核心视频剪辑业务
|
||||
|
||||
2. **当前 Phase 主要在做什么?**
|
||||
- 答:已切入 Phase 7 第一批业务开发,当前已完成素材前半主链收口,并把生成链推进到最小可运行闭环与结果查询/下载可用,正在持续提交与 CI/CD 验证
|
||||
- 答:已切入 Phase 7 第一批业务开发,当前已完成素材前半主链收口,并把生成链推进到最小可运行闭环与结果查询/下载可用(预签名优先),正在持续提交与 CI/CD 验证
|
||||
|
||||
3. **当前最重要的阻塞点是什么?**
|
||||
- 答:OpenClaw 子 Agent runtime 暂不稳定,因此暂停 Agent 实跑;另外数据库字段命名仍有历史包袱,但已通过映射兼容,不阻断主线开发
|
||||
@@ -154,4 +156,4 @@
|
||||
|
||||
---
|
||||
|
||||
**状态结论**:Phase 7 未跑偏,已暂停 Agent 实跑并切回主会话直开;当前素材前半主链已打通,生成链已进入最小可运行闭环且结果查询/下载可用,现继续通过提交与 CI/CD 验证推进。
|
||||
**状态结论**:Phase 7 未跑偏,已暂停 Agent 实跑并切回主会话直开;当前素材前半主链已打通,生成链已进入最小可运行闭环且结果查询/下载可用(预签名优先),现继续通过提交与 CI/CD 验证推进。
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
from .asset_libraries import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
|
||||
from .assets import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
|
||||
from .classification_jobs import SubmitClassificationJobCommand, SubmitClassificationJobUseCase
|
||||
from .generated_videos import GetGeneratedVideoUseCase, ListGeneratedVideosByTaskUseCase, ListGeneratedVideosUseCase
|
||||
from .generated_videos import (
|
||||
GetGeneratedVideoDownloadUrlUseCase,
|
||||
GetGeneratedVideoUseCase,
|
||||
ListGeneratedVideosByTaskUseCase,
|
||||
ListGeneratedVideosUseCase,
|
||||
)
|
||||
from .generation_tasks import CreateGenerationTaskCommand, CreateGenerationTaskUseCase, GetGenerationTaskUseCase
|
||||
from .ingest_jobs import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from .projects import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
|
||||
@@ -17,6 +22,7 @@ __all__ = [
|
||||
"CreateGenerationTaskUseCase",
|
||||
"CreateProjectCommand",
|
||||
"CreateProjectUseCase",
|
||||
"GetGeneratedVideoDownloadUrlUseCase",
|
||||
"GetGeneratedVideoUseCase",
|
||||
"GetGenerationTaskUseCase",
|
||||
"ListAssetLibrariesUseCase",
|
||||
|
||||
@@ -142,3 +142,23 @@ def test_get_generated_video_download_url():
|
||||
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
|
||||
|
||||
|
||||
def test_generated_video_download_source_url_is_stable():
|
||||
video_repo = DummyGeneratedVideoRepository()
|
||||
video = GeneratedVideo.create(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
generation_task_id="task-2",
|
||||
name="task-2.mp4",
|
||||
file_url="http://localhost:9000/xiaoxia-assets/generated/task-2.mp4",
|
||||
file_size=1024,
|
||||
duration=8.0,
|
||||
width=1280,
|
||||
height=720,
|
||||
fps=25.0,
|
||||
)
|
||||
video_repo.create(video)
|
||||
|
||||
use_case = GetGeneratedVideoDownloadUrlUseCase(video_repo)
|
||||
assert use_case.execute(video.id).endswith("/generated/task-2.mp4")
|
||||
|
||||
Reference in New Issue
Block a user