legco_ai_assistant/backend/app/routers/video.py

134 lines
4.2 KiB
Python

import logging
import time
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()
upload_start = time.monotonic()
logger.info("upload-started filename=%s content_type=%s", filename, file.content_type)
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)
upload_duration = time.monotonic() - upload_start
logger.info(
"upload-completed video_id=%s filename=%s size=%d duration=%.2fs",
video_id,
filename,
total_size,
upload_duration,
)
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.",
)
transcribe_start = time.monotonic()
logger.info("transcribe-started video_id=%s language=%s", video_id, language)
service = _get_video_service()
wav_path = await service.extract_audio(video_id)
try:
audio_bytes = wav_path.read_bytes()
logger.debug("audio-extracted video_id=%s wav_size=%d", video_id, len(audio_bytes))
asr = ASRClient(settings)
text = asr.transcribe_full(audio_bytes, language=language)
except Exception as e:
logger.error("transcribe-failed video_id=%s error=%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)
transcribe_duration = time.monotonic() - transcribe_start
logger.info(
"transcribe-completed video_id=%s text_len=%d duration=%.2fs",
video_id,
len(text),
transcribe_duration,
)
return FullTranscriptResponse(
text=text,
language=language,
duration_seconds=None,
)