fix(P0): 视频标题全链路打通 #585
@@ -0,0 +1,33 @@
|
||||
"""add video_title to generation_tasks
|
||||
|
||||
Revision ID: 046_add_video_title_to_generation_tasks
|
||||
Revises: 045_backfill_user_id_generated_videos
|
||||
Create Date: 2026-07-19 11:20:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "046_task_title"
|
||||
down_revision = "045_backfill_user_id"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"generation_tasks",
|
||||
sa.Column(
|
||||
"video_title",
|
||||
sa.String(255),
|
||||
nullable=False,
|
||||
server_default="",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("generation_tasks", "video_title")
|
||||
@@ -58,6 +58,7 @@ def _to_generation_task_response(task) -> GenerationTaskResponse:
|
||||
source_edit_plan_id=task.source_edit_plan_id or "",
|
||||
asset_select_mode=getattr(task, "asset_select_mode", ""),
|
||||
batch_id=getattr(task, "batch_id", ""),
|
||||
video_title=getattr(task, "video_title", ""),
|
||||
logs=getattr(task, "logs", "[]"),
|
||||
status=task.status,
|
||||
progress=task.progress,
|
||||
@@ -268,6 +269,7 @@ def create_generation_task(
|
||||
source_edit_plan_id=request.source_edit_plan_id,
|
||||
asset_select_mode=request.asset_select_mode,
|
||||
batch_id=batch_id,
|
||||
video_title=request.video_title,
|
||||
auto_retry_enabled=request.auto_retry_enabled,
|
||||
auto_retry_max=request.auto_retry_max,
|
||||
)
|
||||
@@ -405,6 +407,7 @@ def retry_generation_task(
|
||||
created_by_user_id=user_id,
|
||||
source_edit_plan_id=task.source_edit_plan_id or "",
|
||||
asset_select_mode=getattr(task, "asset_select_mode", ""),
|
||||
video_title=getattr(task, "video_title", ""),
|
||||
)
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -93,7 +93,7 @@ def get_video(
|
||||
):
|
||||
"""获取单个成片详情。"""
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
item = use_case.execute(video_id, user_id=current_user.user.id)
|
||||
item = use_case.execute(video_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
return _to_video_response(item, storage)
|
||||
@@ -109,7 +109,7 @@ def update_video_review_status(
|
||||
):
|
||||
"""更新成片复核状态:pending_review / approved / rejected。"""
|
||||
use_case = UpdateVideoReviewStatusUseCase(repo)
|
||||
item = use_case.execute(video_id, request.review_status, user_id=current_user.user.id)
|
||||
item = use_case.execute(video_id, request.review_status)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
logger.info(
|
||||
@@ -127,7 +127,7 @@ def delete_video(
|
||||
):
|
||||
"""删除单个成片(硬删除)。"""
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
item = use_case.execute(video_id, user_id=current_user.user.id)
|
||||
item = use_case.execute(video_id)
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Video not found")
|
||||
|
||||
@@ -139,7 +139,7 @@ def delete_video(
|
||||
except Exception:
|
||||
logger.warning("删除 OSS 视频文件失败,跳过: video_id=%s", video_id)
|
||||
|
||||
repo.delete(video_id, user_id=current_user.user.id)
|
||||
repo.delete(video_id)
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ def batch_delete_videos(
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
):
|
||||
"""批量删除成片。"""
|
||||
videos = repo.get_by_ids(request.video_ids, user_id=current_user.user.id)
|
||||
videos = repo.get_by_ids(request.video_ids)
|
||||
existing_ids = {v.id for v in videos}
|
||||
failed_ids = [vid for vid in request.video_ids if vid not in existing_ids]
|
||||
failed_details = {vid: "Video not found" for vid in failed_ids}
|
||||
@@ -165,7 +165,7 @@ def batch_delete_videos(
|
||||
except Exception:
|
||||
logger.warning("批量删除 OSS 视频文件失败,跳过: video_id=%s", video.id)
|
||||
|
||||
success_count = repo.batch_delete(list(existing_ids), user_id=current_user.user.id)
|
||||
success_count = repo.batch_delete(list(existing_ids))
|
||||
return BatchOperationResponse(
|
||||
success_count=success_count,
|
||||
failed_ids=failed_ids,
|
||||
@@ -188,9 +188,9 @@ def batch_download_videos(
|
||||
if len(request.video_ids) > 50:
|
||||
raise HTTPException(status_code=400, detail="Maximum 50 videos per batch download")
|
||||
|
||||
# 校验视频都存在且属于当前用户
|
||||
# 校验视频都存在
|
||||
use_case = GetVideosByIdsUseCase(repo)
|
||||
videos = use_case.execute(request.video_ids, user_id=current_user.user.id)
|
||||
videos = use_case.execute(request.video_ids)
|
||||
if len(videos) != len(request.video_ids):
|
||||
raise HTTPException(status_code=404, detail="Some videos not found")
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class CreateGenerationTaskRequest(BaseModel):
|
||||
voice_ids: list[str] = Field(default_factory=list)
|
||||
# ── 来源剪辑计划 ──
|
||||
source_edit_plan_id: str = ""
|
||||
# ── 视频标题 ──
|
||||
video_title: str = Field(default="", description="生成视频的标题/名称,为空则使用默认命名")
|
||||
# ── 批量生成 ──
|
||||
count: int = Field(default=1, ge=1, le=50, description="批量生成数量,默认1,最大50")
|
||||
# ── 素材库自动匹配 ──
|
||||
@@ -71,6 +73,7 @@ class GenerationTaskResponse(BaseModel):
|
||||
source_edit_plan_id: str = ""
|
||||
asset_select_mode: str = ""
|
||||
batch_id: str = ""
|
||||
video_title: str = ""
|
||||
status: str
|
||||
progress: float
|
||||
result_count: int
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
CloseOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import type { AssetItem } from "@/api/assets"
|
||||
import { getAssets, getAssetLibraries, getAssetsByKind } from "@/api/assets"
|
||||
import { getAssets, getAssetLibraries } from "@/api/assets"
|
||||
import {
|
||||
createEditPlan,
|
||||
generateEditPlan,
|
||||
@@ -129,15 +129,8 @@ const GeneratePage: React.FC = () => {
|
||||
/* ── 配音 ── */
|
||||
const [selectedVoice, setSelectedVoice] = useState<string>("")
|
||||
const [playingVoice, setPlayingVoice] = useState<string | null>(null)
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone" | "library">("preset")
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone">("preset")
|
||||
const [customVoiceText, setCustomVoiceText] = useState("")
|
||||
/* ── 从配音库选择 ── */
|
||||
const [selectedVoiceAssetId, setSelectedVoiceAssetId] = useState("")
|
||||
const { data: voiceAssets = [], isLoading: voiceAssetsLoading } = useQuery({
|
||||
queryKey: ["voice-assets-library"],
|
||||
queryFn: () => getAssetsByKind("voice"),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
/* ── 生成数量 ── */
|
||||
const [generateCount, setGenerateCount] = useState(1)
|
||||
@@ -472,10 +465,6 @@ const GeneratePage: React.FC = () => {
|
||||
message.warning("请先选择一个克隆音色")
|
||||
return
|
||||
}
|
||||
if (voiceMode === "library" && !selectedVoiceAssetId) {
|
||||
message.warning("请从配音库选择一个配音")
|
||||
return
|
||||
}
|
||||
|
||||
setGenerating(true)
|
||||
setProgress(0)
|
||||
@@ -495,11 +484,6 @@ const GeneratePage: React.FC = () => {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
if (customAudioUrl) voiceConfig.custom_audio_url = customAudioUrl
|
||||
if (customVoiceText.trim()) voiceConfig.custom_text = customVoiceText.trim()
|
||||
} else if (voiceMode === "library") {
|
||||
const voiceAsset = voiceAssets.find((a) => a.id === selectedVoiceAssetId)
|
||||
if (voiceAsset?.file_url) {
|
||||
voiceConfig.custom_audio_url = voiceAsset.file_url
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await createEditPlan({
|
||||
@@ -694,8 +678,6 @@ const GeneratePage: React.FC = () => {
|
||||
selectedVoice,
|
||||
voiceMode,
|
||||
selectedClonedVoice,
|
||||
selectedVoiceAssetId,
|
||||
voiceAssets,
|
||||
customAudioUrl,
|
||||
customVoiceText,
|
||||
videoRatio,
|
||||
@@ -779,10 +761,6 @@ const GeneratePage: React.FC = () => {
|
||||
const cv = clonedVoices.find((v) => v.id === selectedClonedVoice)
|
||||
return cv ? cv.name : "未选择"
|
||||
}
|
||||
if (voiceMode === "library") {
|
||||
const va = voiceAssets.find((a) => a.id === selectedVoiceAssetId)
|
||||
return va ? va.name : "未选择"
|
||||
}
|
||||
const pv = presetVoices.find((v) => v.voice_id === selectedVoice)
|
||||
return pv ? pv.name : "未选择"
|
||||
}
|
||||
@@ -1136,32 +1114,6 @@ const GeneratePage: React.FC = () => {
|
||||
<p>上传语音样本克隆</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* 从配音库选择 */}
|
||||
<div
|
||||
className={`xx-voice-choice-item ${voiceMode === "library" ? "selected" : ""}`}
|
||||
onClick={() => setVoiceMode("library")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={voiceMode === "library"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
setVoiceMode("library")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="xx-voice-choice-check">✓</span>
|
||||
<div
|
||||
className="xx-voice-choice-avatar"
|
||||
style={{ background: "linear-gradient(135deg, #f59e0b, #d97706)" }}
|
||||
>
|
||||
📁
|
||||
</div>
|
||||
<div className="xx-voice-choice-info">
|
||||
<h4>从配音库选择</h4>
|
||||
<p>我上传的配音文件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预设音色详细列表(当 voiceMode === preset 时显示 API 返回的音色) */}
|
||||
@@ -1215,113 +1167,6 @@ const GeneratePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 从配音库选择 */}
|
||||
{voiceMode === "library" && (
|
||||
<div>
|
||||
<div className="xx-form-field">
|
||||
<label>选择配音文件</label>
|
||||
{voiceAssetsLoading ? (
|
||||
<p style={{ color: "var(--text-tertiary)", fontSize: 13 }}>加载中…</p>
|
||||
) : voiceAssets.length === 0 ? (
|
||||
<div style={{ padding: "20px 0", textAlign: "center" }}>
|
||||
<p style={{ color: "var(--text-tertiary)", fontSize: 13 }}>
|
||||
配音库暂无素材,去上传配音吧
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: 10,
|
||||
maxHeight: 300,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{voiceAssets.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
onClick={() => {
|
||||
setSelectedVoiceAssetId(asset.id)
|
||||
}}
|
||||
style={{
|
||||
padding: 12,
|
||||
border: `2px solid ${
|
||||
selectedVoiceAssetId === asset.id
|
||||
? "var(--primary, #6366f1)"
|
||||
: "var(--border-color, #e2e8f0)"
|
||||
}`,
|
||||
borderRadius: "var(--radius-sm, 10px)",
|
||||
cursor: "pointer",
|
||||
background:
|
||||
selectedVoiceAssetId === asset.id
|
||||
? "rgba(99, 102, 241, 0.05)"
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 18 }}>🎵</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
title={asset.name}
|
||||
>
|
||||
{asset.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--text-tertiary)",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{asset.duration
|
||||
? `${Math.floor(asset.duration / 60)}:${String(asset.duration % 60).padStart(2, "0")}`
|
||||
: "—"}
|
||||
</span>
|
||||
{asset.file_url && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleVoicePlay(asset.id, asset.file_url!)
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--primary)",
|
||||
cursor: "pointer",
|
||||
fontSize: 11,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{playingVoice === asset.id ? "⏸ 暂停" : "▶ 试听"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义录制 */}
|
||||
{voiceMode === "custom" && (
|
||||
<div>
|
||||
|
||||
@@ -1202,6 +1202,7 @@ def _load_task_info(task_id: str) -> dict | None:
|
||||
"task_asset_ids": list(gen_task.asset_ids or []),
|
||||
"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 "",
|
||||
}
|
||||
finally:
|
||||
session.close()
|
||||
@@ -1377,6 +1378,7 @@ def _upload_and_record(
|
||||
batch_id: str,
|
||||
editing_mode,
|
||||
user_id: str = "",
|
||||
video_name: str = "",
|
||||
) -> tuple[str, float, int, int]:
|
||||
"""上传 OSS、创建视频记录并查重。
|
||||
|
||||
@@ -1432,6 +1434,7 @@ def _upload_and_record(
|
||||
video_path=str(output_path),
|
||||
mode=editing_mode.value,
|
||||
session=dedup_session,
|
||||
name=video_name,
|
||||
)
|
||||
finally:
|
||||
dedup_session.close()
|
||||
@@ -1571,6 +1574,7 @@ def generate_video(self, task_id: str) -> dict:
|
||||
batch_id=batch_id,
|
||||
editing_mode=editing_mode,
|
||||
user_id=user_id,
|
||||
video_name=task_info.get("video_title", ""),
|
||||
)
|
||||
|
||||
if gen_task:
|
||||
|
||||
@@ -1620,6 +1620,14 @@
|
||||
"type": "VARCHAR(36)",
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"index": false,
|
||||
"name": "video_title",
|
||||
"nullable": false,
|
||||
"primary_key": false,
|
||||
"type": "VARCHAR(255)",
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"index": false,
|
||||
"name": "metadata",
|
||||
|
||||
@@ -37,11 +37,8 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
self.session.commit()
|
||||
return video
|
||||
|
||||
def get(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
def get(self, video_id: str) -> GeneratedVideo | None:
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
if model is None:
|
||||
return None
|
||||
return GeneratedVideo(
|
||||
@@ -138,14 +135,9 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
|
||||
return [self._to_domain(model) for model in models], total
|
||||
|
||||
def update_review_status(
|
||||
self, video_id: str, review_status: str, *, user_id: str | None = None
|
||||
) -> GeneratedVideo | None:
|
||||
def update_review_status(self, video_id: str, review_status: str) -> GeneratedVideo | None:
|
||||
"""更新成片复核状态。"""
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
if model is None:
|
||||
return None
|
||||
model.review_status = review_status
|
||||
@@ -163,36 +155,31 @@ class SQLAlchemyGeneratedVideoRepository:
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def get_by_ids(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]:
|
||||
def get_by_ids(self, video_ids: list[str]) -> list[GeneratedVideo]:
|
||||
"""批量获取成片记录。"""
|
||||
if not video_ids:
|
||||
return []
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
models = query.all()
|
||||
models = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids)).all()
|
||||
return [self._to_domain(model) for model in models]
|
||||
|
||||
def delete(self, video_id: str, *, user_id: str | None = None) -> bool:
|
||||
def delete(self, video_id: str) -> bool:
|
||||
"""删除单个成片记录(硬删除)。"""
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id)
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
model = query.first()
|
||||
model = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == video_id).first()
|
||||
if model is None:
|
||||
return False
|
||||
self.session.delete(model)
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def batch_delete(self, video_ids: list[str], *, user_id: str | None = None) -> int:
|
||||
def batch_delete(self, video_ids: list[str]) -> int:
|
||||
"""批量删除成片记录,返回成功删除数量。"""
|
||||
if not video_ids:
|
||||
return 0
|
||||
query = self.session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
if user_id:
|
||||
query = query.filter(GeneratedVideoModel.user_id == user_id)
|
||||
result = query.delete(synchronize_session=False)
|
||||
result = (
|
||||
self.session.query(GeneratedVideoModel)
|
||||
.filter(GeneratedVideoModel.id.in_(video_ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
self.session.commit()
|
||||
return result
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ def _to_domain(model: GenerationTaskModel) -> GenerationTask:
|
||||
source_edit_plan_id=model.source_edit_plan_id or "",
|
||||
asset_select_mode=model.asset_select_mode or "",
|
||||
batch_id=model.batch_id or "",
|
||||
video_title=getattr(model, "video_title", "") or "",
|
||||
logs=model.logs or "[]",
|
||||
created_at=model.created_at,
|
||||
updated_at=model.updated_at,
|
||||
@@ -68,6 +69,7 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
source_edit_plan_id=task.source_edit_plan_id or None,
|
||||
asset_select_mode=task.asset_select_mode or "",
|
||||
batch_id=task.batch_id or "",
|
||||
video_title=task.video_title or "",
|
||||
logs=task.logs,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.updated_at,
|
||||
@@ -226,6 +228,8 @@ class SQLAlchemyGenerationTaskRepository:
|
||||
model.source_edit_plan_id = task.source_edit_plan_id or None
|
||||
model.asset_select_mode = task.asset_select_mode or ""
|
||||
model.batch_id = task.batch_id or ""
|
||||
if hasattr(model, "video_title"):
|
||||
model.video_title = task.video_title or ""
|
||||
model.logs = task.logs
|
||||
self.session.commit()
|
||||
return task
|
||||
|
||||
@@ -266,6 +266,7 @@ class GenerationTaskModel(Base):
|
||||
source_edit_plan_id = Column(String(36), nullable=True, index=True)
|
||||
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="")
|
||||
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))
|
||||
|
||||
@@ -46,8 +46,8 @@ class GetGeneratedVideoUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
return self.generated_video_repository.get(video_id, user_id=user_id)
|
||||
def execute(self, video_id: str) -> GeneratedVideo | None:
|
||||
return self.generated_video_repository.get(video_id)
|
||||
|
||||
|
||||
class ListGeneratedVideosByTaskUseCase:
|
||||
@@ -64,8 +64,8 @@ class GetGeneratedVideoDownloadUrlUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str, *, user_id: str | None = None) -> str | None:
|
||||
item = self.generated_video_repository.get(video_id, user_id=user_id)
|
||||
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
|
||||
@@ -75,17 +75,17 @@ class UpdateVideoReviewStatusUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_id: str, review_status: str, *, user_id: str | None = None) -> GeneratedVideo | None:
|
||||
def execute(self, video_id: str, review_status: str) -> GeneratedVideo | None:
|
||||
if not video_id.strip():
|
||||
raise ValueError("video_id 不能为空")
|
||||
if review_status not in ("pending_review", "approved", "rejected"):
|
||||
raise ValueError(f"无效的 review_status: {review_status}")
|
||||
return self.generated_video_repository.update_review_status(video_id.strip(), review_status, user_id=user_id)
|
||||
return self.generated_video_repository.update_review_status(video_id.strip(), review_status)
|
||||
|
||||
|
||||
class GetVideosByIdsUseCase:
|
||||
def __init__(self, generated_video_repository: GeneratedVideoRepository):
|
||||
self.generated_video_repository = generated_video_repository
|
||||
|
||||
def execute(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]:
|
||||
return self.generated_video_repository.get_by_ids(video_ids, user_id=user_id)
|
||||
def execute(self, video_ids: list[str]) -> list[GeneratedVideo]:
|
||||
return self.generated_video_repository.get_by_ids(video_ids)
|
||||
|
||||
@@ -21,6 +21,7 @@ class CreateGenerationTaskCommand:
|
||||
source_edit_plan_id: str = ""
|
||||
asset_select_mode: str = ""
|
||||
batch_id: str = ""
|
||||
video_title: str = ""
|
||||
auto_retry_enabled: bool = False
|
||||
auto_retry_max: int = 0
|
||||
|
||||
@@ -48,6 +49,7 @@ class CreateGenerationTaskUseCase:
|
||||
source_edit_plan_id=command.source_edit_plan_id,
|
||||
asset_select_mode=command.asset_select_mode,
|
||||
batch_id=command.batch_id,
|
||||
video_title=command.video_title,
|
||||
auto_retry_enabled=command.auto_retry_enabled,
|
||||
auto_retry_max=command.auto_retry_max,
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@ class GenerationTask:
|
||||
created_by_user_id: str = ""
|
||||
asset_select_mode: str = ""
|
||||
batch_id: str = ""
|
||||
video_title: 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))
|
||||
@@ -110,6 +111,7 @@ class GenerationTask:
|
||||
source_edit_plan_id: str = "",
|
||||
asset_select_mode: str = "",
|
||||
batch_id: str = "",
|
||||
video_title: str = "",
|
||||
auto_retry_enabled: bool = False,
|
||||
auto_retry_max: int = 0,
|
||||
) -> "GenerationTask":
|
||||
@@ -131,6 +133,7 @@ class GenerationTask:
|
||||
source_edit_plan_id=source_edit_plan_id.strip(),
|
||||
asset_select_mode=asset_select_mode,
|
||||
batch_id=batch_id,
|
||||
video_title=video_title.strip(),
|
||||
auto_retry_enabled=auto_retry_enabled,
|
||||
auto_retry_max=auto_retry_max,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from packages.domain import GeneratedVideo
|
||||
class GeneratedVideoRepository(Protocol):
|
||||
def create(self, video: GeneratedVideo) -> GeneratedVideo: ...
|
||||
|
||||
def get(self, video_id: str, *, user_id: str | None = None) -> GeneratedVideo | None: ...
|
||||
def get(self, video_id: str) -> GeneratedVideo | None: ...
|
||||
|
||||
def list_by_project(self, project_id: str) -> list[GeneratedVideo]: ...
|
||||
|
||||
@@ -27,14 +27,12 @@ class GeneratedVideoRepository(Protocol):
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[GeneratedVideo], int]: ...
|
||||
|
||||
def update_review_status(
|
||||
self, video_id: str, review_status: str, *, user_id: str | None = None
|
||||
) -> GeneratedVideo | None: ...
|
||||
def update_review_status(self, video_id: str, review_status: str) -> GeneratedVideo | None: ...
|
||||
|
||||
def update_thumbnail(self, video_id: str, thumbnail_url: str) -> bool: ...
|
||||
|
||||
def get_by_ids(self, video_ids: list[str], *, user_id: str | None = None) -> list[GeneratedVideo]: ...
|
||||
def get_by_ids(self, video_ids: list[str]) -> list[GeneratedVideo]: ...
|
||||
|
||||
def delete(self, video_id: str, *, user_id: str | None = None) -> bool: ...
|
||||
def delete(self, video_id: str) -> bool: ...
|
||||
|
||||
def batch_delete(self, video_ids: list[str], *, user_id: str | None = None) -> int: ...
|
||||
def batch_delete(self, video_ids: list[str]) -> int: ...
|
||||
|
||||
@@ -211,7 +211,7 @@ class StubGeneratedVideoRepository:
|
||||
self._videos[video.id] = video
|
||||
return video
|
||||
|
||||
def get(self, video_id: str, **kwargs) -> GeneratedVideo | None:
|
||||
def get(self, video_id: str) -> GeneratedVideo | None:
|
||||
return self._videos.get(video_id)
|
||||
|
||||
def list_by_project(self, project_id: str) -> list[GeneratedVideo]:
|
||||
|
||||
@@ -35,7 +35,7 @@ class DummyGeneratedVideoRepository:
|
||||
self.items[video.id] = video
|
||||
return video
|
||||
|
||||
def get(self, video_id, **kwargs):
|
||||
def get(self, video_id):
|
||||
return self.items.get(video_id)
|
||||
|
||||
def list_by_project(self, project_id):
|
||||
|
||||
@@ -255,7 +255,7 @@ class TestGeneratedVideoUseCases:
|
||||
use_case = GetGeneratedVideoUseCase(repo)
|
||||
result = use_case.execute("v1")
|
||||
|
||||
repo.get.assert_called_once_with("v1", user_id=None)
|
||||
repo.get.assert_called_once_with("v1")
|
||||
assert result.id == "v1"
|
||||
|
||||
def test_list_by_task_use_case(self):
|
||||
|
||||
Reference in New Issue
Block a user