""" 视频 API 端点 """ from fastapi import APIRouter, HTTPException, status, Header, UploadFile, File, Form, Query from pydantic import BaseModel from typing import Optional, Any from datetime import datetime import uuid from app.api.v1.endpoints.auth import get_current_user from app.services.video_auditor import VideoFileValidator, VideoAuditor router = APIRouter() # 最大文件大小 100MB MAX_FILE_SIZE = 100 * 1024 * 1024 # 模拟视频存储 VIDEOS: dict[str, dict] = { "video_001": { "video_id": "video_001", "task_id": "task_001", "brief_id": "brief_001", "title": "测试视频", "status": "completed", "owner_id": "user_creator_001", "processing_time_ms": 12000, "violations": [ { "violation_id": "vio_001", "type": "forbidden_word", "content": "最好的", "severity": "high", "timestamp_start": 5.0, "timestamp_end": 5.5, "source": "ai", }, { "violation_id": "vio_002", "type": "competitor_logo", "content": "检测到竞品 Logo", "severity": "medium", "timestamp_start": 10.0, "timestamp_end": 12.0, "source": "ai", }, ], "brief_compliance": { "selling_point_coverage": {"coverage_rate": 0.8}, "duration_check": {"product_visible": {"status": "passed"}}, }, "created_at": datetime.now().isoformat(), }, "video_processing": { "video_id": "video_processing", "task_id": "task_001", "status": "processing", "progress": 45, "owner_id": "user_creator_001", "created_at": datetime.now().isoformat(), }, "video_own": { "video_id": "video_own", "task_id": "task_001", "status": "pending_review", "owner_id": "user_creator_001", "violations": [], "created_at": datetime.now().isoformat(), }, "video_assigned": { "video_id": "video_assigned", "task_id": "task_001", "status": "pending_review", "owner_id": "user_creator_001", "assigned_agency": "user_agency_001", "violations": [], "created_at": datetime.now().isoformat(), }, } # 模拟违规证据 EVIDENCES: dict[str, dict] = { "vio_001": { "violation_id": "vio_001", "evidence_type": "text", "screenshot_url": "/static/screenshots/vio_001.jpg", "timestamp_start": 5.0, "timestamp_end": 5.5, "content": "最好的", }, } # 模拟上传会话 UPLOAD_SESSIONS: dict[str, dict] = {} class VideoUploadResponse(BaseModel): video_id: str status: str message: str = "" class UploadInitRequest(BaseModel): filename: str file_size: int task_id: str class UploadInitResponse(BaseModel): upload_id: str chunk_size: int = 1024 * 1024 # 1MB class ChunkUploadResponse(BaseModel): received_chunks: int total_chunks: int status: str class VideoListResponse(BaseModel): items: list[dict[str, Any]] total: int page: int page_size: int class ResubmitRequest(BaseModel): modification_note: str = "" modified_sections: list[str] = [] class ResubmitResponse(BaseModel): status: str new_video_id: str class PreviewResponse(BaseModel): preview_url: str start_ms: int end_ms: int @router.post("/upload", response_model=VideoUploadResponse, status_code=status.HTTP_202_ACCEPTED) async def upload_video( file: UploadFile = File(...), task_id: str = Form(...), title: str = Form(""), authorization: Optional[str] = Header(None), ): """上传视频文件""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) user = get_current_user(authorization) # 验证文件格式 content_type = file.content_type or "" file_ext = file.filename.split(".")[-1].lower() if file.filename else "" validator = VideoFileValidator() # 检查格式 if file_ext not in ["mp4", "mov"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Unsupported video format: {file_ext}. Only MP4 and MOV are supported.", ) # 读取文件内容检查大小 content = await file.read() file_size = len(content) if file_size > MAX_FILE_SIZE: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail=f"File too large. Maximum size is 100MB, got {file_size / (1024*1024):.1f}MB", ) # 创建视频记录 video_id = f"video_{uuid.uuid4().hex[:8]}" VIDEOS[video_id] = { "video_id": video_id, "task_id": task_id, "title": title or file.filename, "status": "processing", "owner_id": user["user_id"], "created_at": datetime.now().isoformat(), } return VideoUploadResponse( video_id=video_id, status="processing", message="Video is being processed", ) @router.post("/upload/init", response_model=UploadInitResponse) async def init_resumable_upload( request: UploadInitRequest, authorization: Optional[str] = Header(None), ): """初始化断点续传""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) user = get_current_user(authorization) if request.file_size > MAX_FILE_SIZE: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail=f"File too large. Maximum size is 100MB", ) upload_id = f"upload_{uuid.uuid4().hex[:8]}" chunk_size = 1024 * 1024 # 1MB UPLOAD_SESSIONS[upload_id] = { "upload_id": upload_id, "filename": request.filename, "file_size": request.file_size, "task_id": request.task_id, "user_id": user["user_id"], "received_chunks": [], "total_chunks": (request.file_size + chunk_size - 1) // chunk_size, "created_at": datetime.now().isoformat(), } return UploadInitResponse( upload_id=upload_id, chunk_size=chunk_size, ) @router.post("/upload/{upload_id}/chunk", response_model=ChunkUploadResponse) async def upload_chunk( upload_id: str, chunk: UploadFile = File(...), chunk_index: int = Form(...), authorization: Optional[str] = Header(None), ): """上传分片""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) session = UPLOAD_SESSIONS.get(upload_id) if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Upload session not found", ) # 记录已接收的分片 if chunk_index not in session["received_chunks"]: session["received_chunks"].append(chunk_index) return ChunkUploadResponse( received_chunks=len(session["received_chunks"]), total_chunks=session["total_chunks"], status="uploading" if len(session["received_chunks"]) < session["total_chunks"] else "completed", ) @router.get("/{video_id}/audit") async def get_audit_result( video_id: str, authorization: Optional[str] = Header(None), ): """获取审核结果""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) user = get_current_user(authorization) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) return { "report_id": f"report_{video_id}", "video_id": video_id, "status": video.get("status"), "progress": video.get("progress"), "violations": video.get("violations", []), "brief_compliance": video.get("brief_compliance"), "processing_time_ms": video.get("processing_time_ms"), } @router.get("/{video_id}/violations") async def get_video_violations( video_id: str, authorization: Optional[str] = Header(None), ): """获取视频违规列表""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) return {"violations": video.get("violations", [])} @router.get("/{video_id}/violations/{violation_id}/evidence") async def get_violation_evidence( video_id: str, violation_id: str, authorization: Optional[str] = Header(None), ): """获取违规证据""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) # 查找违规项 violation = next( (v for v in video.get("violations", []) if v["violation_id"] == violation_id), None, ) if not violation: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Violation not found: {violation_id}", ) evidence = EVIDENCES.get(violation_id, { "violation_id": violation_id, "evidence_type": violation.get("type", "unknown"), "screenshot_url": f"/static/screenshots/{violation_id}.jpg", "timestamp_start": violation.get("timestamp_start", 0), "timestamp_end": violation.get("timestamp_end", 0), "content": violation.get("content", ""), }) return evidence @router.get("/{video_id}/preview", response_model=PreviewResponse) async def get_video_preview( video_id: str, start_ms: int = Query(0), end_ms: int = Query(10000), authorization: Optional[str] = Header(None), ): """获取视频预览""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) return PreviewResponse( preview_url=f"/static/videos/{video_id}/preview.mp4?start={start_ms}&end={end_ms}", start_ms=start_ms, end_ms=end_ms, ) @router.post("/{video_id}/resubmit", response_model=ResubmitResponse, status_code=status.HTTP_202_ACCEPTED) async def resubmit_video( video_id: str, request: ResubmitRequest, authorization: Optional[str] = Header(None), ): """重新提交视频""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) user = get_current_user(authorization) video = VIDEOS.get(video_id) if not video: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Video not found: {video_id}", ) # 创建新视频记录 new_video_id = f"video_{uuid.uuid4().hex[:8]}" VIDEOS[new_video_id] = { "video_id": new_video_id, "task_id": video.get("task_id"), "title": video.get("title"), "status": "processing", "owner_id": user["user_id"], "previous_version": video_id, "modification_note": request.modification_note, "modified_sections": request.modified_sections, "created_at": datetime.now().isoformat(), } return ResubmitResponse( status="processing", new_video_id=new_video_id, ) @router.get("", response_model=VideoListResponse) async def list_videos( page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), status: Optional[str] = Query(None), task_id: Optional[str] = Query(None), authorization: Optional[str] = Header(None), ): """获取视频列表""" if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization header required", ) user = get_current_user(authorization) # 过滤视频 filtered = list(VIDEOS.values()) if status: filtered = [v for v in filtered if v.get("status") == status] if task_id: filtered = [v for v in filtered if v.get("task_id") == task_id] # 分页 total = len(filtered) start = (page - 1) * page_size end = start + page_size items = filtered[start:end] return VideoListResponse( items=items, total=total, page=page, page_size=page_size, )