|
|
|
@@ -0,0 +1,166 @@
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
from app.api.routes.permissions import require_workspace_member
|
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
|
|
|
from app.dependencies import get_asset_repository, get_db_session, get_project_repository, get_workspace_member_repository
|
|
|
|
|
from app.schemas.edit_plan import CreateEditPlanRequest, EditPlanClipResponse, EditPlanResponse, EditTemplateResponse
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel, EditPlanModel, EditTemplateModel
|
|
|
|
|
from packages.domain import AssetStatus
|
|
|
|
|
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/projects/{project_id}/edit-plans", tags=["剪辑计划"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_project(project_id: str, workspace_id: str, user: AuthenticatedUser, project_repository, member_repository):
|
|
|
|
|
project = project_repository.find_by_id(project_id)
|
|
|
|
|
if project is None or project.workspace_id != workspace_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
|
require_workspace_member(workspace_id, user, member_repository)
|
|
|
|
|
return project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _default_template(session: Session, workspace_id: str, project_id: str, user_id: str) -> EditTemplateModel:
|
|
|
|
|
template = session.query(EditTemplateModel).filter(
|
|
|
|
|
EditTemplateModel.project_id == project_id,
|
|
|
|
|
EditTemplateModel.is_active.is_(True),
|
|
|
|
|
).order_by(EditTemplateModel.created_at.asc()).first()
|
|
|
|
|
if template is not None:
|
|
|
|
|
return template
|
|
|
|
|
template = EditTemplateModel(
|
|
|
|
|
id=uuid4().hex,
|
|
|
|
|
workspace_id=workspace_id,
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
name="基础节奏模板",
|
|
|
|
|
description="自动选择可用视频素材,按上传顺序生成三段式剪辑计划。",
|
|
|
|
|
target_duration=30,
|
|
|
|
|
clip_count=3,
|
|
|
|
|
created_by_user_id=user_id,
|
|
|
|
|
)
|
|
|
|
|
session.add(template)
|
|
|
|
|
session.commit()
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _to_template_response(template: EditTemplateModel) -> EditTemplateResponse:
|
|
|
|
|
return EditTemplateResponse(
|
|
|
|
|
id=template.id,
|
|
|
|
|
workspace_id=template.workspace_id,
|
|
|
|
|
project_id=template.project_id,
|
|
|
|
|
name=template.name,
|
|
|
|
|
description=template.description,
|
|
|
|
|
target_duration=float(template.target_duration or 0),
|
|
|
|
|
clip_count=int(template.clip_count or 0),
|
|
|
|
|
is_active=bool(template.is_active),
|
|
|
|
|
created_at=template.created_at,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _to_plan_response(plan: EditPlanModel, clips: list[EditPlanClipModel], asset_names: dict[str, str]) -> EditPlanResponse:
|
|
|
|
|
return EditPlanResponse(
|
|
|
|
|
id=plan.id,
|
|
|
|
|
workspace_id=plan.workspace_id,
|
|
|
|
|
project_id=plan.project_id,
|
|
|
|
|
template_id=plan.template_id,
|
|
|
|
|
asset_library_id=plan.asset_library_id,
|
|
|
|
|
title_id=plan.title_id,
|
|
|
|
|
status=plan.status,
|
|
|
|
|
summary=plan.summary,
|
|
|
|
|
clips=[EditPlanClipResponse(
|
|
|
|
|
id=clip.id,
|
|
|
|
|
asset_id=clip.asset_id,
|
|
|
|
|
asset_name=asset_names.get(clip.asset_id, clip.asset_id),
|
|
|
|
|
sequence=clip.sequence,
|
|
|
|
|
start_time=float(clip.start_time or 0),
|
|
|
|
|
duration=float(clip.duration or 0),
|
|
|
|
|
reason=clip.reason,
|
|
|
|
|
) for clip in clips],
|
|
|
|
|
created_at=plan.created_at,
|
|
|
|
|
updated_at=plan.updated_at,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/templates", response_model=list[EditTemplateResponse])
|
|
|
|
|
def list_edit_templates(
|
|
|
|
|
project_id: str,
|
|
|
|
|
workspace_id: str,
|
|
|
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
|
|
|
project_repository=Depends(get_project_repository),
|
|
|
|
|
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
|
|
|
|
session: Session = Depends(get_db_session),
|
|
|
|
|
) -> list[EditTemplateResponse]:
|
|
|
|
|
_ensure_project(project_id, workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
|
|
|
|
template = _default_template(session, workspace_id, project_id, authenticated_user.user.id)
|
|
|
|
|
templates = session.query(EditTemplateModel).filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True)).all()
|
|
|
|
|
return [_to_template_response(item) for item in templates or [template]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=EditPlanResponse)
|
|
|
|
|
def create_edit_plan(
|
|
|
|
|
project_id: str,
|
|
|
|
|
request: CreateEditPlanRequest,
|
|
|
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
|
|
|
project_repository=Depends(get_project_repository),
|
|
|
|
|
asset_repository=Depends(get_asset_repository),
|
|
|
|
|
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
|
|
|
|
session: Session = Depends(get_db_session),
|
|
|
|
|
) -> EditPlanResponse:
|
|
|
|
|
_ensure_project(project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
|
|
|
|
template = session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first() if request.template_id else None
|
|
|
|
|
if template is None:
|
|
|
|
|
template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id)
|
|
|
|
|
assets = [asset for asset in asset_repository.list_by_library(request.asset_library_id) if asset.status == AssetStatus.READY and asset.mime_type.startswith("video/")]
|
|
|
|
|
if not assets:
|
|
|
|
|
raise HTTPException(status_code=422, detail="素材库暂无可用于剪辑计划的视频素材")
|
|
|
|
|
selected = sorted(assets, key=lambda asset: (-(asset.quality_score or 0), asset.created_at))[: max(1, int(template.clip_count or 3))]
|
|
|
|
|
plan = EditPlanModel(
|
|
|
|
|
id=uuid4().hex,
|
|
|
|
|
workspace_id=request.workspace_id,
|
|
|
|
|
project_id=project_id,
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
asset_library_id=request.asset_library_id,
|
|
|
|
|
title_id=request.title_id,
|
|
|
|
|
status="draft",
|
|
|
|
|
summary=f"按《{template.name}》自动选择 {len(selected)} 段素材,预计生成约 {int(template.target_duration or 30)} 秒成片。",
|
|
|
|
|
created_by_user_id=authenticated_user.user.id,
|
|
|
|
|
updated_at=datetime.now(timezone.utc),
|
|
|
|
|
)
|
|
|
|
|
session.add(plan)
|
|
|
|
|
clips: list[EditPlanClipModel] = []
|
|
|
|
|
clip_duration = max(1, float(template.target_duration or 30) / len(selected))
|
|
|
|
|
for index, asset in enumerate(selected, start=1):
|
|
|
|
|
clip = EditPlanClipModel(
|
|
|
|
|
id=uuid4().hex,
|
|
|
|
|
edit_plan_id=plan.id,
|
|
|
|
|
asset_id=asset.id,
|
|
|
|
|
sequence=index,
|
|
|
|
|
start_time=0,
|
|
|
|
|
duration=min(float(asset.duration or clip_duration), clip_duration),
|
|
|
|
|
reason="优先选择已就绪、质量分较高的视频素材。",
|
|
|
|
|
)
|
|
|
|
|
session.add(clip)
|
|
|
|
|
clips.append(clip)
|
|
|
|
|
session.commit()
|
|
|
|
|
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in selected})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{plan_id}", response_model=EditPlanResponse)
|
|
|
|
|
def get_edit_plan(
|
|
|
|
|
project_id: str,
|
|
|
|
|
plan_id: str,
|
|
|
|
|
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
|
|
|
|
project_repository=Depends(get_project_repository),
|
|
|
|
|
asset_repository=Depends(get_asset_repository),
|
|
|
|
|
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
|
|
|
|
session: Session = Depends(get_db_session),
|
|
|
|
|
) -> EditPlanResponse:
|
|
|
|
|
plan = session.query(EditPlanModel).filter(EditPlanModel.id == plan_id, EditPlanModel.project_id == project_id).first()
|
|
|
|
|
if plan is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Edit plan not found")
|
|
|
|
|
_ensure_project(project_id, plan.workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
|
|
|
|
clips = session.query(EditPlanClipModel).filter(EditPlanClipModel.edit_plan_id == plan.id).order_by(EditPlanClipModel.sequence.asc()).all()
|
|
|
|
|
assets = asset_repository.list_by_library(plan.asset_library_id)
|
|
|
|
|
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in assets})
|