import logging import uuid from pathlib import Path import aiofiles from fastapi import APIRouter, UploadFile, File, HTTPException from fastapi.responses import FileResponse from app.models.video import VideoUploadResponse, FullTranscriptResponse from app.services.video_service import VideoService from app.services.asr_client import ASRClient logger = logging.getLogger(__name__) router = APIRouter(tags=["video"]) def _get_video_service() -> VideoService: from app.core.config import get_settings s = get_settings() return VideoService( upload_dir=s.video_upload_dir, max_size_mb=s.max_video_size_mb, supported_formats=s.supported_video_formats, ) @router.post("/video/upload", response_model=VideoUploadResponse) async def upload_video(file: UploadFile = File(...)): service = _get_video_service() filename = file.filename or "unknown" ext = Path(filename).suffix.lower() total_size = 0 video_id = uuid.uuid4().hex[:12] dest_path = service.upload_dir / f"{video_id}{ext}" try: async with aiofiles.open(dest_path, "wb") as out: while chunk := await file.read(1024 * 1024): total_size += len(chunk) if total_size > service.max_size_bytes: raise HTTPException( status_code=413, detail=f"File exceeds {service.max_size_bytes // 1024 // 1024}MB limit", ) await out.write(chunk) except HTTPException: dest_path.unlink(missing_ok=True) raise except Exception: dest_path.unlink(missing_ok=True) raise HTTPException(status_code=500, detail="Upload failed") service.validate_video(filename, file.content_type, total_size) logger.info("Video uploaded: id=%s filename=%s size=%d", video_id, filename, total_size) return VideoUploadResponse( video_id=video_id, filename=filename, size_bytes=total_size, url=f"/api/v1/video/{video_id}", ) @router.get("/video/{video_id}") async def serve_video(video_id: str): service = _get_video_service() video_path = service.get_video_path(video_id) ext = video_path.suffix.lower() media_types = { ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime", ".avi": "video/x-msvideo", ".mkv": "video/x-matroska", } return FileResponse(str(video_path), media_type=media_types.get(ext, "application/octet-stream")) @router.post("/video/{video_id}/transcribe", response_model=FullTranscriptResponse) async def transcribe_video(video_id: str, language: str = "yue"): from app.core.config import get_settings settings = get_settings() if not settings.dashscope_api_key: raise HTTPException( status_code=500, detail="DASHSCOPE_API_KEY is not configured. Set it in .env to enable transcription.", ) service = _get_video_service() wav_path = await service.extract_audio(video_id) try: audio_bytes = wav_path.read_bytes() asr = ASRClient(settings) text = asr.transcribe_full(audio_bytes, language=language) except Exception as e: logger.error("Transcription failed for video_id=%s: %s", video_id, e) raise HTTPException(status_code=500, detail=f"Transcription failed: {str(e)}") finally: if wav_path.exists(): wav_path.unlink(missing_ok=True) return FullTranscriptResponse( text=text, language=language, duration_seconds=None, )