01e057c939
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Waiting to run
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Waiting to run
CI/CD Pipeline / PR Build Web Image (push) Waiting to run
CI/CD Pipeline / PR Build Worker Image (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Build Production API Image (push) Waiting to run
CI/CD Pipeline / Build Production Web Image (push) Waiting to run
CI/CD Pipeline / Build Production Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
319 lines
10 KiB
Python
Executable File
319 lines
10 KiB
Python
Executable File
"""片段管理路由.
|
|
|
|
端点:
|
|
- GET /clips 片段列表
|
|
- POST /clips 创建片段
|
|
- GET /clips/{clip_id} 片段详情
|
|
- PUT /clips/{clip_id} 更新片段
|
|
- DELETE /clips/{clip_id} 删除片段
|
|
- POST /clips/{clip_id}/split 分割片段
|
|
- POST /clips/merge 合并片段
|
|
- POST /clips/reorder 重排片段
|
|
- POST /clips/batch-delete 批量删除
|
|
- POST /clips/from-assets 从素材创建片段
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.services.edit_plan_service import EditPlanService
|
|
from app.services.edit_template_service import EditTemplateService
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
from .dependencies import get_draft_plan_id, get_editor_services
|
|
from .schemas import (
|
|
ClipBatchDeleteRequest,
|
|
ClipBatchDeleteResponse,
|
|
ClipReorderRequest,
|
|
ClipReorderResponse,
|
|
ClipsFromAssetsRequest,
|
|
ClipsFromAssetsResponse,
|
|
EditorClipCreateRequest,
|
|
EditorClipListResponse,
|
|
EditorClipResponse,
|
|
EditorClipUpdateRequest,
|
|
MergeClipsRequest,
|
|
SplitClipRequest,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["Template Editor"])
|
|
|
|
|
|
def _clip_to_response(clip) -> EditorClipResponse:
|
|
"""统一构造片段响应"""
|
|
return EditorClipResponse(
|
|
id=clip.id,
|
|
plan_id=clip.plan_id,
|
|
clip_type=clip.clip_type.value
|
|
if hasattr(clip.clip_type, "value")
|
|
else str(clip.clip_type),
|
|
order=clip.order,
|
|
duration=clip.duration,
|
|
text_content=clip.text_content or "",
|
|
transition_effect=clip.transition_effect.value
|
|
if hasattr(clip.transition_effect, "value")
|
|
else str(clip.transition_effect),
|
|
playback_speed=clip.playback_speed or 1.0,
|
|
config=clip.config or {},
|
|
)
|
|
|
|
|
|
@router.get("/clips", response_model=EditorClipListResponse)
|
|
def list_draft_clips(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
skip: int = Query(default=0, ge=0),
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""获取草稿的片段列表"""
|
|
_, plan_svc = services
|
|
clips = plan_svc.list_clips(plan_id, skip=skip, limit=limit)
|
|
total = plan_svc.count_clips(plan_id)
|
|
return EditorClipListResponse(
|
|
items=[_clip_to_response(c) for c in clips],
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.post("/clips", response_model=EditorClipResponse, status_code=status.HTTP_201_CREATED)
|
|
def create_draft_clip(
|
|
template_id: str,
|
|
req: EditorClipCreateRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""在草稿中创建新片段"""
|
|
_, plan_svc = services
|
|
try:
|
|
clip = plan_svc.create_clip(
|
|
plan_id,
|
|
clip_type=req.clip_type,
|
|
order=req.order,
|
|
duration=req.duration,
|
|
text_content=req.text_content,
|
|
transition_effect=req.transition_effect,
|
|
config=req.config,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
return _clip_to_response(clip)
|
|
|
|
|
|
@router.put("/clips/{clip_id}", response_model=EditorClipResponse)
|
|
def update_draft_clip(
|
|
template_id: str,
|
|
clip_id: str,
|
|
req: EditorClipUpdateRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""更新草稿中的片段"""
|
|
_, plan_svc = services
|
|
try:
|
|
clip = plan_svc.update_clip(
|
|
clip_id,
|
|
order=req.order,
|
|
duration=req.duration,
|
|
text_content=req.text_content,
|
|
transition_effect=req.transition_effect,
|
|
playback_speed=req.playback_speed,
|
|
config=req.config,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
return _clip_to_response(clip)
|
|
|
|
|
|
@router.delete("/clips/{clip_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_draft_clip(
|
|
template_id: str,
|
|
clip_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""删除草稿中的片段"""
|
|
_, plan_svc = services
|
|
success = plan_svc.delete_clip(clip_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="片段不存在")
|
|
return None
|
|
|
|
|
|
@router.get("/clips/{clip_id}", response_model=EditorClipResponse)
|
|
def get_draft_clip_detail(
|
|
template_id: str,
|
|
clip_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""获取草稿中的片段详情"""
|
|
_, plan_svc = services
|
|
clip = plan_svc.get_clip(clip_id)
|
|
if clip is None:
|
|
raise HTTPException(status_code=404, detail="片段不存在")
|
|
if clip.plan_id != plan_id:
|
|
raise HTTPException(status_code=404, detail="片段不存在")
|
|
return _clip_to_response(clip)
|
|
|
|
|
|
@router.post("/clips/{clip_id}/split", response_model=dict[str, Any], status_code=status.HTTP_200_OK)
|
|
def split_draft_clip(
|
|
template_id: str,
|
|
clip_id: str,
|
|
body: SplitClipRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""将一个片段从指定时间点分割为两个片段"""
|
|
_, plan_svc = services
|
|
clip = plan_svc.get_clip(clip_id)
|
|
if clip is None or clip.plan_id != plan_id:
|
|
raise HTTPException(status_code=404, detail="片段不存在")
|
|
try:
|
|
result = plan_svc.split_clip(clip_id, body.split_time)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
|
|
) from exc
|
|
left = result["left_clip"]
|
|
right = result["right_clip"]
|
|
return {
|
|
"left_clip": {
|
|
"id": left.id,
|
|
"plan_id": left.plan_id,
|
|
"clip_type": left.clip_type,
|
|
"order": left.order,
|
|
"duration": left.duration,
|
|
"start_time": left.start_time,
|
|
},
|
|
"right_clip": {
|
|
"id": right.id,
|
|
"plan_id": right.plan_id,
|
|
"clip_type": right.clip_type,
|
|
"order": right.order,
|
|
"duration": right.duration,
|
|
"start_time": right.start_time,
|
|
},
|
|
}
|
|
|
|
|
|
@router.post("/clips/merge", response_model=dict[str, Any], status_code=status.HTTP_200_OK)
|
|
def merge_draft_clips(
|
|
template_id: str,
|
|
body: MergeClipsRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
):
|
|
"""将多个连续的同类型片段合并为一个片段"""
|
|
_, plan_svc = services
|
|
for cid in body.clip_ids:
|
|
clip = plan_svc.get_clip(cid)
|
|
if clip is None or clip.plan_id != plan_id:
|
|
raise HTTPException(status_code=404, detail=f"片段不存在: {cid}")
|
|
try:
|
|
merged = plan_svc.merge_clips(body.clip_ids)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
|
|
) from exc
|
|
return {
|
|
"id": merged.id,
|
|
"plan_id": merged.plan_id,
|
|
"clip_type": merged.clip_type,
|
|
"order": merged.order,
|
|
"duration": merged.duration,
|
|
"text_content": merged.text_content,
|
|
}
|
|
|
|
|
|
@router.post("/clips/reorder", response_model=ClipReorderResponse)
|
|
def reorder_editor_clips(
|
|
template_id: str,
|
|
body: ClipReorderRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
) -> ClipReorderResponse:
|
|
"""批量重排片段顺序"""
|
|
_, plan_svc = services
|
|
count = 0
|
|
for item in body.items:
|
|
try:
|
|
plan_svc.update_clip(item.clip_id, order=item.new_order)
|
|
count += 1
|
|
except ValueError:
|
|
pass
|
|
|
|
return ClipReorderResponse(updated_count=count, plan_id=plan_id)
|
|
|
|
|
|
@router.post("/clips/batch-delete", response_model=ClipBatchDeleteResponse)
|
|
def batch_delete_editor_clips(
|
|
template_id: str,
|
|
body: ClipBatchDeleteRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
) -> ClipBatchDeleteResponse:
|
|
"""批量删除片段"""
|
|
_, plan_svc = services
|
|
deleted = 0
|
|
for clip_id in body.clip_ids:
|
|
if plan_svc.delete_clip(clip_id):
|
|
deleted += 1
|
|
|
|
return ClipBatchDeleteResponse(deleted_count=deleted, plan_id=plan_id)
|
|
|
|
|
|
@router.post("/clips/from-assets", response_model=ClipsFromAssetsResponse)
|
|
def create_clips_from_assets_editor(
|
|
template_id: str,
|
|
body: ClipsFromAssetsRequest,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
current_user: AuthenticatedUser = Depends(get_current_user),
|
|
) -> ClipsFromAssetsResponse:
|
|
"""从素材批量创建片段"""
|
|
_, plan_svc = services
|
|
clips = []
|
|
for i, asset_id in enumerate(body.asset_ids):
|
|
try:
|
|
clip = plan_svc.create_clip(
|
|
plan_id,
|
|
clip_type="main",
|
|
order=body.start_order + i if hasattr(body, "start_order") else i,
|
|
duration=5.0,
|
|
asset_id=asset_id,
|
|
)
|
|
clips.append(clip)
|
|
except ValueError:
|
|
pass
|
|
|
|
logger.info(
|
|
"模板编辑器从素材创建片段: template_id=%s plan_id=%s count=%d by user=%s",
|
|
template_id,
|
|
plan_id,
|
|
len(clips),
|
|
current_user.user.id,
|
|
)
|
|
|
|
return ClipsFromAssetsResponse(
|
|
created_count=len(clips),
|
|
plan_id=plan_id,
|
|
clip_ids=[c.id for c in clips],
|
|
)
|