项目文件夹

文件
wehub-resource-sync 41b710f9c7
CI / Backend tests (push) Failing after 1s
CI / Frontend checks (push) Failing after 0s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

389 行
14 KiB
Python

import logging
from pathlib import Path
from typing import List, Dict, Optional
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
from fastapi.responses import FileResponse
from pydantic import BaseModel
from ...utils.subtitle_processor import SubtitleProcessor
from ...utils.video_editor import VideoEditor
from ...core.path_utils import get_data_directory, get_projects_directory
from ...core.database import get_db
from ...services.project_service import ProjectService
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
router = APIRouter()
# 请求和响应模型
class SubtitleEditRequest(BaseModel):
project_id: str
clip_id: str
deleted_segments: List[str]
class SubtitleEditResponse(BaseModel):
success: bool
message: str
edited_video_path: Optional[str] = None
deleted_duration: Optional[float] = None
final_duration: Optional[float] = None
class SubtitleDataResponse(BaseModel):
segments: List[Dict]
total_duration: float
word_count: int
segment_count: int
class EditPreviewRequest(BaseModel):
project_id: str
clip_id: str
deleted_segments: List[str]
# 依赖注入函数
def get_subtitle_processor() -> SubtitleProcessor:
return SubtitleProcessor()
def get_video_editor() -> VideoEditor:
# 字幕编辑器使用全局路径作为后备(不影响主流水线)
from ...core.shared_config import CLIPS_DIR, COLLECTIONS_DIR
return VideoEditor(clips_dir=str(CLIPS_DIR), collections_dir=str(COLLECTIONS_DIR))
def get_project_service(db: Session = Depends(get_db)) -> ProjectService:
"""Dependency to get project service."""
return ProjectService(db)
@router.get("/{project_id}/clips/{clip_id}/subtitles")
async def get_clip_subtitles(
project_id: str,
clip_id: str,
subtitle_processor: SubtitleProcessor = Depends(get_subtitle_processor),
project_service: ProjectService = Depends(get_project_service)
):
"""获取片段的字粒度字幕数据"""
try:
# 获取项目信息
project = project_service.get(project_id)
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
# 获取片段信息
from ...models.clip import Clip
clip = project_service.db.query(Clip).filter(Clip.id == clip_id, Clip.project_id == project_id).first()
if not clip:
raise HTTPException(status_code=404, detail="片段不存在")
# 查找原始SRT文件
projects_dir = get_projects_directory()
project_dir = projects_dir / project_id
srt_file = project_dir / "raw" / "input.srt"
if not srt_file.exists():
raise HTTPException(status_code=404, detail="字幕文件不存在")
# 解析字幕数据
subtitle_data = subtitle_processor.parse_srt_to_word_level(srt_file)
# 过滤出属于当前片段的时间范围
# 如果start_time和end_time是整数(秒),直接使用
if isinstance(clip.start_time, int):
clip_start = clip.start_time
else:
clip_start = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.start_time)
)
if isinstance(clip.end_time, int):
clip_end = clip.end_time
else:
clip_end = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.end_time)
)
# 过滤字幕段
clip_subtitles = [
seg for seg in subtitle_data
if seg['startTime'] >= clip_start and seg['endTime'] <= clip_end
]
# 调整时间戳为相对于片段的
for seg in clip_subtitles:
seg['startTime'] -= clip_start
seg['endTime'] -= clip_start
for word in seg['words']:
word['startTime'] -= clip_start
word['endTime'] -= clip_start
# 获取统计信息
stats = subtitle_processor.get_subtitle_statistics(clip_subtitles)
return SubtitleDataResponse(
segments=clip_subtitles,
total_duration=stats['totalDuration'],
word_count=stats['wordCount'],
segment_count=stats['segmentCount']
)
except Exception as e:
import traceback
logger.error(f"获取字幕数据失败: {e}")
logger.error(f"错误详情: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"获取字幕数据失败: {str(e)}")
@router.post("/{project_id}/clips/{clip_id}/edit")
async def edit_clip_by_subtitles(
project_id: str,
clip_id: str,
request: SubtitleEditRequest,
subtitle_processor: SubtitleProcessor = Depends(get_subtitle_processor),
video_editor: VideoEditor = Depends(get_video_editor),
project_service: ProjectService = Depends(get_project_service)
):
"""基于字幕删除编辑视频片段"""
try:
# 获取项目信息
project = project_service.get(project_id)
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
# 获取片段信息
from ...models.clip import Clip
clip = project_service.db.query(Clip).filter(Clip.id == clip_id, Clip.project_id == project_id).first()
if not clip:
raise HTTPException(status_code=404, detail="片段不存在")
# 获取原始视频和字幕文件路径
projects_dir = get_projects_directory()
project_dir = projects_dir / project_id
# 查找原始视频文件
video_files = list(project_dir.glob("raw/*.mp4"))
if not video_files:
raise HTTPException(status_code=404, detail="原始视频文件不存在")
original_video = video_files[0]
# 查找字幕文件
srt_file = project_dir / "raw" / "input.srt"
if not srt_file.exists():
raise HTTPException(status_code=404, detail="字幕文件不存在")
# 解析字幕数据
subtitle_data = subtitle_processor.parse_srt_to_word_level(srt_file)
# 过滤出属于当前片段的时间范围
# 如果start_time和end_time是整数(秒),直接使用
if isinstance(clip.start_time, int):
clip_start = clip.start_time
else:
clip_start = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.start_time)
)
if isinstance(clip.end_time, int):
clip_end = clip.end_time
else:
clip_end = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.end_time)
)
clip_subtitles = [
seg for seg in subtitle_data
if seg['startTime'] >= clip_start and seg['endTime'] <= clip_end
]
# 验证编辑操作
validation = video_editor.validate_edit_operations(
clip_subtitles, request.deleted_segments
)
if not validation['valid']:
raise HTTPException(status_code=400, detail=validation['error'])
# 创建输出目录
output_dir = project_dir / "edited_clips"
output_dir.mkdir(exist_ok=True)
# 生成编辑后的视频文件名
edited_video_name = f"{clip_id}_edited.mp4"
edited_video_path = output_dir / edited_video_name
# 执行视频编辑
edit_result = video_editor.edit_video_by_subtitle_deletion(
original_video,
clip_subtitles,
request.deleted_segments,
edited_video_path
)
if not edit_result['success']:
raise HTTPException(status_code=500, detail=f"视频编辑失败: {edit_result['error']}")
# 导出编辑后的字幕文件
edited_srt_path = output_dir / f"{clip_id}_edited.srt"
subtitle_processor.export_edited_srt(
clip_subtitles,
request.deleted_segments,
edited_srt_path
)
return SubtitleEditResponse(
success=True,
message="视频编辑成功",
edited_video_path=str(edited_video_path),
deleted_duration=edit_result['totalDeletedDuration'],
final_duration=edit_result['finalDuration']
)
except HTTPException:
raise
except Exception as e:
logger.error(f"编辑视频片段失败: {e}")
raise HTTPException(status_code=500, detail=f"编辑视频片段失败: {str(e)}")
@router.get("/{project_id}/clips/{clip_id}/edited-video")
async def get_edited_video(
project_id: str,
clip_id: str,
project_service: ProjectService = Depends(get_project_service)
):
"""获取编辑后的视频文件"""
try:
# 检查项目是否存在
project = await project_service.get_project(project_id)
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
# 查找编辑后的视频文件
projects_dir = get_projects_directory()
edited_video_path = projects_dir / project_id / "edited_clips" / f"{clip_id}_edited.mp4"
if not edited_video_path.exists():
raise HTTPException(status_code=404, detail="编辑后的视频文件不存在")
# 返回视频文件
return FileResponse(
path=str(edited_video_path),
media_type="video/mp4",
filename=f"{clip_id}_edited.mp4"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取编辑后的视频失败: {e}")
raise HTTPException(status_code=500, detail=f"获取编辑后的视频失败: {str(e)}")
@router.post("/{project_id}/clips/{clip_id}/preview")
async def create_edit_preview(
project_id: str,
clip_id: str,
request: EditPreviewRequest,
subtitle_processor: SubtitleProcessor = Depends(get_subtitle_processor),
video_editor: VideoEditor = Depends(get_video_editor),
project_service: ProjectService = Depends(get_project_service)
):
"""创建编辑预览片段"""
try:
# 获取项目信息
project = project_service.get(project_id)
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
# 获取片段信息
from ...models.clip import Clip
clip = project_service.db.query(Clip).filter(Clip.id == clip_id, Clip.project_id == project_id).first()
if not clip:
raise HTTPException(status_code=404, detail="片段不存在")
# 获取原始视频和字幕文件路径
projects_dir = get_projects_directory()
project_dir = projects_dir / project_id
video_files = list(project_dir.glob("raw/*.mp4"))
if not video_files:
raise HTTPException(status_code=404, detail="原始视频文件不存在")
original_video = video_files[0]
srt_file = project_dir / "raw" / "input.srt"
if not srt_file.exists():
raise HTTPException(status_code=404, detail="字幕文件不存在")
# 解析字幕数据
subtitle_data = subtitle_processor.parse_srt_to_word_level(srt_file)
# 过滤出属于当前片段的时间范围
# 如果start_time和end_time是整数(秒),直接使用
if isinstance(clip.start_time, int):
clip_start = clip.start_time
else:
clip_start = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.start_time)
)
if isinstance(clip.end_time, int):
clip_end = clip.end_time
else:
clip_end = subtitle_processor._srt_time_to_seconds(
subtitle_processor._seconds_to_srt_time_object(clip.end_time)
)
clip_subtitles = [
seg for seg in subtitle_data
if seg['startTime'] >= clip_start and seg['endTime'] <= clip_end
]
# 创建预览目录
preview_dir = project_dir / "edit_previews" / clip_id
preview_dir.mkdir(parents=True, exist_ok=True)
# 创建预览片段
preview_files = video_editor.create_preview_clips(
original_video,
clip_subtitles,
request.deleted_segments,
preview_dir
)
return {
"success": True,
"preview_files": [str(f) for f in preview_files],
"count": len(preview_files)
}
except Exception as e:
logger.error(f"创建编辑预览失败: {e}")
raise HTTPException(status_code=500, detail=f"创建编辑预览失败: {str(e)}")
@router.get("/{project_id}/clips/{clip_id}/preview/{segment_id}")
async def get_preview_segment(
project_id: str,
clip_id: str,
segment_id: str,
project_service: ProjectService = Depends(get_project_service)
):
"""获取预览片段文件"""
try:
# 检查项目是否存在
project = project_service.get(project_id)
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
# 查找预览文件
projects_dir = get_projects_directory()
preview_file = projects_dir / project_id / "edit_previews" / clip_id / f"preview_{segment_id}.mp4"
if not preview_file.exists():
raise HTTPException(status_code=404, detail="预览文件不存在")
# 返回预览文件
return FileResponse(
path=str(preview_file),
media_type="video/mp4",
filename=f"preview_{segment_id}.mp4"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"获取预览文件失败: {e}")
raise HTTPException(status_code=500, detail=f"获取预览文件失败: {str(e)}")