feat: 字幕管理 - 增删改查 + 批量导入(基于片段config) (#394)
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Production (push) Has been cancelled
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1468h39m0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI Build & Deploy Pipeline / Build Staging API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI Build & Deploy Pipeline / Staging E2E Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production API Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been cancelled
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been cancelled
CI Build & Deploy Pipeline / Deploy Production (push) Has been cancelled
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1468h39m0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
feat: 字幕管理 - 增删改查 + 批量导入(基于片段config) (#394)
This commit was merged in pull request #394.
This commit is contained in:
@@ -522,6 +522,245 @@ def copy_plan(
|
||||
return _to_response(new_plan)
|
||||
|
||||
|
||||
# ── 字幕管理 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class SubtitleCreateRequest(BaseModel):
|
||||
"""添加字幕请求体"""
|
||||
|
||||
start: float = Field(..., ge=0, description="开始时间(秒)")
|
||||
end: float = Field(..., gt=0, description="结束时间(秒)")
|
||||
text: str = Field(..., min_length=1, max_length=500, description="字幕文本")
|
||||
style: Optional[dict[str, Any]] = Field(default=None, description="字幕样式")
|
||||
|
||||
|
||||
class SubtitleUpdateRequest(BaseModel):
|
||||
"""更新字幕请求体"""
|
||||
|
||||
start: Optional[float] = Field(default=None, ge=0, description="开始时间(秒)")
|
||||
end: Optional[float] = Field(default=None, gt=0, description="结束时间(秒)")
|
||||
text: Optional[str] = Field(default=None, min_length=1, max_length=500, description="字幕文本")
|
||||
style: Optional[dict[str, Any]] = Field(default=None, description="字幕样式")
|
||||
|
||||
|
||||
class SubtitleBatchUpdateRequest(BaseModel):
|
||||
"""批量更新字幕请求体"""
|
||||
|
||||
subtitles: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description="字幕列表(全量替换),每条包含 start/end/text,可选 id/style",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/clips/{clip_id}/subtitles",
|
||||
response_model=list[dict[str, Any]],
|
||||
summary="获取片段的所有字幕",
|
||||
)
|
||||
def list_subtitles(
|
||||
clip_id: str,
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
project_repo=Depends(get_project_repository),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""获取指定片段的所有字幕,按时间排序。"""
|
||||
service = EditPlanService(db)
|
||||
|
||||
clip = service.get_clip(clip_id)
|
||||
if clip is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"片段不存在: {clip_id}",
|
||||
)
|
||||
plan = service.get_plan(clip.plan_id)
|
||||
if plan and plan.project_id:
|
||||
check_project_access(project_repo, current_user, plan.project_id)
|
||||
|
||||
return service.list_subtitles(clip_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/clips/{clip_id}/subtitles",
|
||||
response_model=dict[str, Any],
|
||||
summary="添加一条字幕",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def add_subtitle(
|
||||
clip_id: str,
|
||||
body: SubtitleCreateRequest,
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
project_repo=Depends(get_project_repository),
|
||||
) -> dict[str, Any]:
|
||||
"""给片段添加一条字幕。"""
|
||||
service = EditPlanService(db)
|
||||
|
||||
clip = service.get_clip(clip_id)
|
||||
if clip is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"片段不存在: {clip_id}",
|
||||
)
|
||||
plan = service.get_plan(clip.plan_id)
|
||||
if plan and plan.project_id:
|
||||
check_project_access(project_repo, current_user, plan.project_id)
|
||||
|
||||
try:
|
||||
subtitle = service.add_subtitle(
|
||||
clip_id,
|
||||
start=body.start,
|
||||
end=body.end,
|
||||
text=body.text,
|
||||
style=body.style,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
) from e
|
||||
|
||||
logger.info("添加字幕: clip_id=%s by user=%s", clip_id, current_user.user.id)
|
||||
return subtitle
|
||||
|
||||
|
||||
@router.put(
|
||||
"/clips/{clip_id}/subtitles/{subtitle_id}",
|
||||
response_model=dict[str, Any],
|
||||
summary="更新一条字幕",
|
||||
)
|
||||
def update_subtitle(
|
||||
clip_id: str,
|
||||
subtitle_id: str,
|
||||
body: SubtitleUpdateRequest,
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
project_repo=Depends(get_project_repository),
|
||||
) -> dict[str, Any]:
|
||||
"""更新一条字幕的时间、文本或样式。"""
|
||||
service = EditPlanService(db)
|
||||
|
||||
clip = service.get_clip(clip_id)
|
||||
if clip is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"片段不存在: {clip_id}",
|
||||
)
|
||||
plan = service.get_plan(clip.plan_id)
|
||||
if plan and plan.project_id:
|
||||
check_project_access(project_repo, current_user, plan.project_id)
|
||||
|
||||
try:
|
||||
subtitle = service.update_subtitle(
|
||||
clip_id,
|
||||
subtitle_id,
|
||||
start=body.start,
|
||||
end=body.end,
|
||||
text=body.text,
|
||||
style=body.style,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
"更新字幕: clip_id=%s subtitle_id=%s by user=%s",
|
||||
clip_id,
|
||||
subtitle_id,
|
||||
current_user.user.id,
|
||||
)
|
||||
return subtitle
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/clips/{clip_id}/subtitles/{subtitle_id}",
|
||||
summary="删除一条字幕",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
response_model=None,
|
||||
response_class=Response,
|
||||
)
|
||||
def delete_subtitle(
|
||||
clip_id: str,
|
||||
subtitle_id: str,
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
project_repo=Depends(get_project_repository),
|
||||
) -> None:
|
||||
"""删除一条字幕。"""
|
||||
service = EditPlanService(db)
|
||||
|
||||
clip = service.get_clip(clip_id)
|
||||
if clip is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"片段不存在: {clip_id}",
|
||||
)
|
||||
plan = service.get_plan(clip.plan_id)
|
||||
if plan and plan.project_id:
|
||||
check_project_access(project_repo, current_user, plan.project_id)
|
||||
|
||||
deleted = service.delete_subtitle(clip_id, subtitle_id)
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"字幕不存在: {subtitle_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"删除字幕: clip_id=%s subtitle_id=%s by user=%s",
|
||||
clip_id,
|
||||
subtitle_id,
|
||||
current_user.user.id,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/clips/{clip_id}/subtitles",
|
||||
response_model=list[dict[str, Any]],
|
||||
summary="批量更新字幕(全量替换)",
|
||||
)
|
||||
def batch_update_subtitles(
|
||||
clip_id: str,
|
||||
body: SubtitleBatchUpdateRequest,
|
||||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||||
db: Session = Depends(get_db_session),
|
||||
project_repo=Depends(get_project_repository),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""批量更新片段的所有字幕(全量替换)。
|
||||
|
||||
用于批量编辑、SRT导入、ASR结果导入等场景。
|
||||
每条字幕包含 start/end/text,已有 id 则保留,否则生成新 id。
|
||||
"""
|
||||
service = EditPlanService(db)
|
||||
|
||||
clip = service.get_clip(clip_id)
|
||||
if clip is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"片段不存在: {clip_id}",
|
||||
)
|
||||
plan = service.get_plan(clip.plan_id)
|
||||
if plan and plan.project_id:
|
||||
check_project_access(project_repo, current_user, plan.project_id)
|
||||
|
||||
try:
|
||||
subtitles = service.batch_update_subtitles(clip_id, body.subtitles)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
"批量更新字幕: clip_id=%s count=%d by user=%s",
|
||||
clip_id,
|
||||
len(subtitles),
|
||||
current_user.user.id,
|
||||
)
|
||||
return subtitles
|
||||
|
||||
|
||||
# ── BGM 背景音乐 ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user