zhouxiaoka--autoclip
389 行
14 KiB
Python
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)}")
|